Docker Compose
The repository ships a docker-compose.yml at its root that starts model2vec-serve as a single local container serving two models: minishlab/potion-multilingual-128M (the default model) and minishlab/potion-code-16M-v2. The stack runs the published image ghcr.io/freinold/model2vec-serve:latest (pushed to the GitHub Container Registry on every release), so no build step is required — one command starts the whole stack. This path targets local evaluation, development, and demos; for production, use Helm on Kubernetes.
Prerequisites
- Docker with Compose v2 (
docker compose versionsucceeds). - Network access for the first model download; roughly 2 GB of free disk space for the two default models.
- Port 8080 free on the host (or a different port via
MODEL2VEC_PORT, see Configuration).
Quick start
From the repository root:
docker compose up -d
docker compose ps # wait for STATUS "healthy" (first start downloads models)
docker compose logs -f model2vec-serve # Ctrl+C once you see the startup-complete logsThe first start downloads both models before the service becomes healthy; the health check allows a five-minute start period to cover that. The health status shown by docker compose ps comes from the HEALTHCHECK baked into the image (a curl probe against /health). The host port is bound to 127.0.0.1, so the service is only reachable from your machine — edit the ports mapping in docker-compose.yml if you intentionally want other hosts to reach it.
Note: images published before the release that introduced compose support carry no in-image health check. With such an image the health column simply does not appear in
docker compose ps— everything else behaves the same. Update withdocker compose pull && docker compose up -dto get the health check.
Both models are listed once the stack is healthy:
curl -s http://localhost:8080/v1/models | jq .Expected: a standard OpenAI model list containing both minishlab/potion-multilingual-128M and minishlab/potion-code-16M-v2.
Embeddings from each model (a request without a model field hits the default multilingual model; the code model is selected explicitly):
curl -s http://localhost:8080/v1/embeddings \
-H "Content-Type: application/json" \
-d '{"input":"Hello world"}' | jq '.data[0].embedding | length'
curl -s http://localhost:8080/v1/embeddings \
-H "Content-Type: application/json" \
-d '{"input":"def hello(): pass","model":"minishlab/potion-code-16M-v2"}' \
| jq '.data[0].embedding | length'Expected: a positive integer (the model dimension) in both cases.
The TEI-compatible endpoints behave identically:
curl -s http://localhost:8080/info | jq .
curl -s -X POST http://localhost:8080/tei/potion-code-16M-v2/embed \
-H "Content-Type: application/json" -d '{"inputs":["fn main() {}"]}' | jq '.[0] | length'The operational endpoints are always public and need no authentication:
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/health # 200
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/ready # 200
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/metrics # 200Served models
By default the stack serves exactly two models in one process:
| Model | Role |
|---|---|
minishlab/potion-multilingual-128M | Serves requests that do not name a model (DEFAULT_MODEL) |
minishlab/potion-code-16M-v2 | Selected explicitly via the request's model field or a /tei/{model_id}/... path |
Both the model set and the default are overridable without touching docker-compose.yml:
MODEL— comma-separated Hugging Face model ids (or local model directories) to serve. Setting it replaces the default two-model list entirely.DEFAULT_MODEL— model answering requests without an explicit model. When unset, the firstMODELentry serves default requests, so an overriddenMODELlist never ends up with an out-of-set default. When set, it must be one of the ids inMODEL.
For example, serving a single smaller model:
cp .env.example .env
# in .env: MODEL=minishlab/potion-base-2M and DEFAULT_MODEL=minishlab/potion-base-2M
docker compose up -ddocker compose up -d re-reads .env on each start, so changes apply on the next restart. See Configuration for the .env workflow.
Model cache
Downloaded model artifacts are persisted on the host so restarts are fast and offline restarts work:
- The compose file bind-mounts the host directory
./models(relative to the repository root; the directory is git-ignored and created automatically on first launch) at/modelsinside the container. - The container's
HOMEis fixed to/models, so the Hugging Face cache lands inmodels/.cache/huggingface/hubon the host. This mirrors the Helm chart's persistence pattern, whereHOMEis set to the mount path so the cache lives under the persistent volume — see Persistent model cache. - The cache survives
docker compose down/upcycles and host reboots. Deleting the directory forces a full re-download on the next start. - With a warm cache, a restart skips all downloads and the service becomes healthy in well under the first-start time. Once both models are cached, the stack also restarts without network access.
Override the location with MODEL2VEC_CACHE_DIR (any host path):
docker compose down
MODEL2VEC_CACHE_DIR=/tmp/m2v-cache docker compose up -d
ls /tmp/m2v-cache/.cache/huggingface/hubIf you prefer a Docker-managed named volume over a host bind mount, replace the service's volume entry with a named volume and declare it (the MODEL2VEC_CACHE_DIR override covers host paths only — compose rejects an undeclared volume name):
services:
model2vec-serve:
volumes:
- model-cache:/models
volumes:
model-cache:A named volume is removed with docker compose down -v; the default bind-mount cache is not.
Configuration
The stack is customized through environment variables — never by editing docker-compose.yml. Copy the shipped example file and uncomment what you need:
cp .env.example .envCompose loads .env automatically (the file is git-ignored; only .env.example is committed). Every variable is optional, and unset variables are never injected into the container as empty strings.
| Variable | Scope | Default | Effect when set |
|---|---|---|---|
MODEL2VEC_IMAGE | compose | ghcr.io/freinold/model2vec-serve:latest | Use another image tag or a locally built image |
MODEL2VEC_PORT | compose | 8080 | Host port the service is reachable on (the host side is bound to 127.0.0.1; edit the ports mapping in docker-compose.yml to expose beyond localhost) |
MODEL2VEC_CACHE_DIR | compose | ./models | Host directory for the model cache (host paths only; a named volume requires editing docker-compose.yml) |
MODEL | service | two-model list (see Served models above) | Replace the served model set (comma-separated) |
DEFAULT_MODEL | service | first MODEL entry | Model answering requests without an explicit model; must be one of MODEL |
API_KEY | service | (unset → auth off) | Enables Bearer auth on all embedding and model-list endpoints (/v1/embeddings, /v1/models, /embed, /info, /tei/{model_id}/embed, /tei/{model_id}/info); /health, /ready, and /metrics stay public |
TLS_CERT | service | (unset → plain HTTP) | Path to the PEM TLS certificate; setting it together with TLS_KEY switches the port to HTTPS (single listener) |
TLS_KEY | service | (unset) | Path to the matching unencrypted PEM private key (files must be mounted — see TLS/HTTPS below) |
MODEL_OWNER | service | minishlab (service default) | Owner shown in /v1/models |
MODEL_ALIAS | service | (unset) | KEY=ALIAS pairs for /tei/{model_id}/... paths |
MAX_BATCH_SIZE | service | 256 (service default) | Max inputs per request |
MAX_INPUT_LENGTH | service | 512 (service default) | Max tokens per input |
LOG_LEVEL | service | info (service default) | Log verbosity |
REQUEST_TIMEOUT_SECONDS | service | 30 (service default) | Per-request timeout |
The three MODEL2VEC_* variables are consumed by docker-compose.yml itself (image, host port, cache directory); the remaining variables are passed through to the service verbatim.
Empty vs. unset
Unset optional variables are never injected as empty strings — this matters most for API_KEY. An empty API_KEY would enable authentication with an empty Bearer token; leaving API_KEY unset disables authentication entirely. If you enabled auth and want it off again, remove the API_KEY line from .env (or unset API_KEY in your shell) rather than setting it to an empty value. /health, /ready, and /metrics are always public either way.
TLS/HTTPS
The service can terminate TLS itself, encrypting traffic end to end — for example all the way to the application inside the container. Uncomment TLS_CERT/TLS_KEY in .env and mount the certificate files by adding one volume entry to docker-compose.yml (a compose-file edit, like switching to a named volume):
volumes:
- ${MODEL2VEC_CACHE_DIR:-./models}:/models
- ./tls:/etc/model2vec-serve/tls:roWith both variables set, the port serves HTTPS only (single listener) with identical endpoint behavior over https://; leave them unset for plain HTTP. Notes:
- The certificate file should contain the full chain (leaf plus intermediates).
- PEM formats only; encrypted (passphrase-protected) keys are rejected.
- TLS 1.2 and TLS 1.3 only; older protocol versions are refused.
- Certificates are loaded at startup: rotating the files requires a container restart (
docker compose restart). - Mutual TLS (client certificate verification) is not supported.
Operations
Logs. Follow the combined service logs:
docker compose logs -fStop and teardown. Both keep the model cache:
docker compose stop # stop the container; `docker compose start` resumes it
docker compose down # stop and remove the container; cache persistsTo also reclaim the ~2 GB of downloaded models, delete the cache directory:
docker compose down
rm -rf models/Update the image. Pull the latest published image and recreate the container:
docker compose pull && docker compose up -dRestart behavior. The container uses the unless-stopped restart policy: it comes back automatically after a crash or a host reboot, but stays stopped if you stopped it deliberately. On docker compose stop or down, the container drains for up to 30 seconds (stop_grace_period), matching the default per-request timeout, so in-flight requests can finish.
Choosing a deployment path
- Plain
docker run— quickest way to smoke-test a single model. See Docker for build, run, and image-tagging details. - Docker Compose (this page) — the middle ground for local two-model evaluation, development, and demos: one command, two models, a persisted cache, and
.env-based configuration. - Helm on Kubernetes — the production path, with replicas, autoscaling, ingress, and persistent-volume-backed caches. See Helm.
Troubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
Bind for 0.0.0.0:8080 failed: port is already allocated | Host port 8080 is in use | Set MODEL2VEC_PORT to a free port in .env and run docker compose up -d again |
Container exits at startup; logs mention permission errors on /models | The cache directory is not writable by the container | Point MODEL2VEC_CACHE_DIR at a writable host path |
| Container exits at startup; logs mention download or Hugging Face errors | No network access, or MODEL contains a wrong model id | Restore network access or fix the model id in MODEL — fix the id, not the compose file |
No health column in docker compose ps | The image predates the in-image HEALTHCHECK (published before compose support) | Update with docker compose pull && docker compose up -d |
| Authentication is unexpectedly enabled although no key was configured | An empty API_KEY="" leaked into the environment (for example from a hand-edited compose file or shell) | Use the shipped docker-compose.yml and unset API_KEY entirely instead of setting it empty |
rm -rf models/ fails with Permission denied | The container runs as root, so downloaded cache files are owned by root on the host | Remove the cache with sudo rm -rf models/ |
