Docker¶
Beta
The image¶
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt
# Bake the embedding model in — see below.
ENV HF_HOME=/opt/hf HF_HUB_OFFLINE=1 VENKAI_EMBEDDING_PROVIDER=semantic
RUN HF_HUB_OFFLINE=0 python -c "\
from sentence_transformers import SentenceTransformer; \
SentenceTransformer('all-MiniLM-L6-v2')" && chmod -R a+rX /opt/hf
COPY . ./venkai/
ENV PYTHONUNBUFFERED=1 PYTHONPATH=/app
EXPOSE 8100
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD curl -fsS "http://localhost:8100/api/health" || exit 1
CMD ["uvicorn", "venkai.api.main:app", "--host", "0.0.0.0", "--port", "8100", \
"--workers", "1", "--proxy-headers"]
Three details that matter¶
The model is baked in, and HF_HUB_OFFLINE=1 at runtime. Without this the
first search after every deploy downloads ~80 MB from Hugging Face: the
container needs outbound internet at runtime, the first user pays the latency,
and a Hugging Face outage becomes a Venkai outage. Baking it also pins which
weights ship.
The image defaults to VENKAI_EMBEDDING_PROVIDER=semantic, unlike the
library, which defaults to hashing. The image has the dependency, so it uses
it. Override in .env if you want hashing.
--proxy-headers with --workers 1. Proxy headers are trusted because the
container is only reachable through Caddy. A single worker is honest about what
has been load-tested — scale horizontally rather than raising it blindly.
Files land in /app/venkai/ with PYTHONPATH=/app, so
venkai.api.main:app resolves. Copying to /app directly breaks the import.
Development compose¶
docker-compose.yml — API plus a Caddy serving the static dashboard.
services:
venkai-api:
build: { context: ., dockerfile: Dockerfile }
container_name: venkai-api
restart: unless-stopped
# Port 8100 is NOT published. All access goes through Caddy, so the rate
# limiter and logs see real client IPs instead of a spoofable
# X-Forwarded-For from a directly reachable port. For local debugging,
# temporarily add: ports: ["8100:8100"]
env_file: [.env]
volumes: [venkai-data:/data]
networks: [venkai, external-infra]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8100/api/health"]
interval: 30s
timeout: 5s
retries: 3
venkai-dashboard:
image: caddy:2-alpine
restart: unless-stopped
ports: ["8101:80", "8102:443"]
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./dashboard:/srv/dashboard:ro
- caddy-data:/data
- caddy-config:/config
networks: [venkai]
volumes: { venkai-data: , caddy-data: , caddy-config: }
networks:
venkai: { driver: bridge }
external-infra: { external: true }
external-infra must already exist
It is declared external: true, so compose will not create it:
docker compose up fails before starting anything.
This file expects Postgres to live outside the stack. Use the production compose if you want the database managed for you.
Production compose¶
docker-compose.prod.yml — Postgres, a migration job, the API, and Caddy.
flowchart TB
I["Internet :80 :443"] --> CY["caddy<br/>caddy:2-alpine"]
CY -->|api.venkai.fr| API["api<br/>venkai-api:local"]
CY -->|app.venkai.fr| DASH["static dashboard"]
M["migrate<br/>runs once, then exits"] --> DB
API --> DB[("db<br/>postgres:16-alpine")]
DB --> VOL["venkai-db volume"]
| Service | Image | Role |
|---|---|---|
db |
postgres:16-alpine |
POSTGRES_USER=venkai, POSTGRES_DB=venkai |
migrate |
venkai-api:local |
Runs Alembic once, then exits |
api |
venkai-api:local |
The application |
caddy |
caddy:2-alpine |
TLS, reverse proxy, static dashboard |
The database URL is composed for you:
so POSTGRES_PASSWORD is set once, in .env, and consumed by both db
and api. Set VENKAI_DATABASE_URL yourself only when targeting a database
outside the stack (a managed Postgres, for instance).
Required variables fail fast rather than defaulting:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
VENKAI_JWT_SECRET: ${VENKAI_JWT_SECRET:?set VENKAI_JWT_SECRET in .env}
Compose refuses to start with a clear message instead of booting an instance with an empty signing secret.
cd venkai
cp .env.example .env # fill in the two required secrets
docker compose -f docker-compose.prod.yml up -d --build
Operations¶
docker compose logs -f venkai-api # logs
docker compose ps # health status
docker compose exec venkai-api \
curl -fsS http://localhost:8100/api/health
# rebuild and roll
docker compose -f docker-compose.prod.yml up -d --build
# migrations only
docker compose -f docker-compose.prod.yml run --rm migrate
Known build history¶
docker build failed at pip resolution for a period: uvicorn was pinned to
0.30.0 while mcp 1.27.1 requires >=0.31.1. The stack was documented as
ready before the image had ever been built successfully.
It is fixed. It is recorded here because it is the exact class of failure to check for yourself: build the image before believing a deployment document, including this one.