Skip to content

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 version succeeds).
  • 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:

bash
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 logs

The 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 with docker compose pull && docker compose up -d to get the health check.

Both models are listed once the stack is healthy:

bash
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):

bash
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:

bash
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:

bash
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  # 200

Served models ​

By default the stack serves exactly two models in one process:

ModelRole
minishlab/potion-multilingual-128MServes requests that do not name a model (DEFAULT_MODEL)
minishlab/potion-code-16M-v2Selected 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 first MODEL entry serves default requests, so an overridden MODEL list never ends up with an out-of-set default. When set, it must be one of the ids in MODEL.

For example, serving a single smaller model:

bash
cp .env.example .env
# in .env: MODEL=minishlab/potion-base-2M and DEFAULT_MODEL=minishlab/potion-base-2M
docker compose up -d

docker 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 /models inside the container.
  • The container's HOME is fixed to /models, so the Hugging Face cache lands in models/.cache/huggingface/hub on the host. This mirrors the Helm chart's persistence pattern, where HOME is set to the mount path so the cache lives under the persistent volume — see Persistent model cache.
  • The cache survives docker compose down/up cycles 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):

bash
docker compose down
MODEL2VEC_CACHE_DIR=/tmp/m2v-cache docker compose up -d
ls /tmp/m2v-cache/.cache/huggingface/hub

If 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):

yaml
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:

bash
cp .env.example .env

Compose 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.

VariableScopeDefaultEffect when set
MODEL2VEC_IMAGEcomposeghcr.io/freinold/model2vec-serve:latestUse another image tag or a locally built image
MODEL2VEC_PORTcompose8080Host 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_DIRcompose./modelsHost directory for the model cache (host paths only; a named volume requires editing docker-compose.yml)
MODELservicetwo-model list (see Served models above)Replace the served model set (comma-separated)
DEFAULT_MODELservicefirst MODEL entryModel answering requests without an explicit model; must be one of MODEL
API_KEYservice(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_CERTservice(unset → plain HTTP)Path to the PEM TLS certificate; setting it together with TLS_KEY switches the port to HTTPS (single listener)
TLS_KEYservice(unset)Path to the matching unencrypted PEM private key (files must be mounted — see TLS/HTTPS below)
MODEL_OWNERserviceminishlab (service default)Owner shown in /v1/models
MODEL_ALIASservice(unset)KEY=ALIAS pairs for /tei/{model_id}/... paths
MAX_BATCH_SIZEservice256 (service default)Max inputs per request
MAX_INPUT_LENGTHservice512 (service default)Max tokens per input
LOG_LEVELserviceinfo (service default)Log verbosity
REQUEST_TIMEOUT_SECONDSservice30 (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):

yaml
    volumes:
      - ${MODEL2VEC_CACHE_DIR:-./models}:/models
      - ./tls:/etc/model2vec-serve/tls:ro

With 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:

bash
docker compose logs -f

Stop and teardown. Both keep the model cache:

bash
docker compose stop   # stop the container; `docker compose start` resumes it
docker compose down   # stop and remove the container; cache persists

To also reclaim the ~2 GB of downloaded models, delete the cache directory:

bash
docker compose down
rm -rf models/

Update the image. Pull the latest published image and recreate the container:

bash
docker compose pull && docker compose up -d

Restart 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 ​

SymptomCauseResolution
Bind for 0.0.0.0:8080 failed: port is already allocatedHost port 8080 is in useSet MODEL2VEC_PORT to a free port in .env and run docker compose up -d again
Container exits at startup; logs mention permission errors on /modelsThe cache directory is not writable by the containerPoint MODEL2VEC_CACHE_DIR at a writable host path
Container exits at startup; logs mention download or Hugging Face errorsNo network access, or MODEL contains a wrong model idRestore network access or fix the model id in MODEL — fix the id, not the compose file
No health column in docker compose psThe 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 configuredAn 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 deniedThe container runs as root, so downloaded cache files are owned by root on the hostRemove the cache with sudo rm -rf models/

Powered by model2vec