Dockerizing Everything: Our Deployment Pipeline
I started containerizing the Sorren.ai prototypes so they could move cleanly from my workstation to the future Proxmox server: Docker Compose, health checks, and fewer one-off setup rituals.
Saturday, late morning, and I am containerizing the pieces that keep turning into setup rituals.
The pieces: the Companion API experiments, the database, the memory workers, and the web frontend. At this stage they were still a mix of workstation projects, test services, and "it works on the dev box" assumptions. That is fine for learning. It stops being fine when the plan is to move onto a real Proxmox server later.
The answer is containers. The answer is always containers.
The Principle: Reproducibility
Every environment — my workstation now, the future Proxmox VMs later, and maybe a cloud host if we need one — should run the exact same image. Not "similar configurations." The same bytes. If it works in dev, it works in staging, it works in production. Configuration differences live in environment variables, not in the image.
This is the point of Docker: the death of "works on my machine." If the container starts locally, it starts everywhere.
Multi-Stage Builds
Multi-stage builds separate the build environment from the runtime. A naive Dockerfile copies the entire project in, installs all dev dependencies, ships the bloated result. Multi-stage avoids that:
# Build stage
FROM python:3.12-slim AS builder
WORKDIR /app
RUN pip install --no-cache-dir uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
COPY . .
RUN uv build
# Runtime stage
FROM python:3.12-slim AS runtime
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/dist/*.whl .
RUN pip install *.whl && rm -rf *.whl
RUN useradd -m -u 1000 appuser
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["uvicorn", "sorren.api:app", "--host", "0.0.0.0", "--port", "8000"]
Key choices:
- Separate build and runtime. The final image doesn't have
uv, doesn't have the source tree, doesn't have dev dependencies. It has the installed wheel and nothing else. - Non-root user. The process runs as an unprivileged application user, not root. If something goes wrong, it goes wrong with limited permissions.
- Health check baked in. Docker (and any orchestrator) knows whether the service is actually serving, not just whether the process is alive.
.dockerignoreexists. No.git, nonode_modules, no local.envfiles sneaking into the build context.
The resulting image is small, starts fast, and behaves identically everywhere.
Docker Compose for Local Dev
For development, everything runs via Docker Compose. One file describes the entire stack:
# docker-compose.yml
services:
api:
build: ./services/api
ports: ["8000:8000"]
environment:
- DATABASE_URL=postgresql://<app-user>:<app-secret>@db:5432/<app-db>
- INFERENCE_URL=http://inference:8000/v1
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
inference:
condition: service_healthy
restart: unless-stopped
db:
image: ankane/pgvector:latest
volumes: ["pgdata:/var/lib/postgresql/data"]
environment:
- POSTGRES_USER=<app-user>
- POSTGRES_DB=<app-db>
# database secret is injected locally, not shown in examples
healthcheck:
test: ["CMD", "pg_isready", "-U", "<app-user>"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
inference:
image: vllm/vllm-openai:latest
runtime: nvidia
volumes: ["./models:/root/.cache/huggingface"]
command: >
--model bge-large-en-v1.5
--task embed
deploy:
resources:
reservations:
devices:
- driver: nvidia
capabilities: [gpu]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 15s
timeout: 10s
retries: 3
restart: unless-stopped
web:
build: ./services/web
ports: ["3000:3000"]
environment:
- NEXT_PUBLIC_API_URL=http://localhost:8000
depends_on:
- api
restart: unless-stopped
volumes:
pgdata:
A new developer — or me, on a fresh machine — should be able to clone the repo, create a .env with the passwords, run docker compose up, and have the test stack running. The API talks to the database and inference server over the Compose network. The database is pgvector-enabled out of the box. The inference server has the GPU. The web frontend talks to the API.
No manual setup. No "install PostgreSQL and enable this extension." No "run these three commands in different terminals." One command, full stack, reproducible.
Health Checks and Dependency Ordering
Notice depends_on with condition: service_healthy. Without it, the API starts before the database is ready, fails to connect, crashes, restarts — and you spend ten minutes reading connection errors.
With health checks defined on each service, Compose waits. The API doesn't start until the database passes pg_isready. The web frontend doesn't start until the API responds on /health. The stack comes up in order, cleanly. Define health checks on everything. Make startup ordering explicit. Don't rely on timing and luck.
Secrets
Secrets — database passwords, API keys, and provider tokens — live in a local .env file that is .gitignored. Compose reads it automatically. For production, the same variables come from the host environment or a secrets manager. The containers never change; only the environment does.
# .env (never committed)
APP_DB_SECRET=...
MODEL_PROVIDER_TOKEN=...
FRONTIER_MODEL_API_KEY=...
I keep a .env.example in the repo with dummy values, so the required variables are documented without leaking anything real.
The CI/CD Road
Local reproducibility is the foundation. Next is CI/CD — automated builds on every push, image publishing to a registry, deployment without a human SSHing into anything.
We're not there yet. Builds are still manual: docker compose build, push to registry, pull on the server. It works, but it's the kind of process that invites mistakes. The goal is a pipeline where a merge to main triggers a build, runs tests, deploys — without my intervention. The containers make that possible. Every step operates on the same artifact. The image is the contract.
Where This Leaves Us
The important Sorren.ai prototypes are moving into containers. I am not pretending the production backend is finished. The point is that when the Proxmox server is ready, I do not want to migrate a pile of hand-built snowflakes. I want version-controlled compose files, documented environment variables, and rebuildable services.
Next milestones: CI/CD automation, image scanning, automated rollbacks, and eventually orchestration beyond a single host. Docker Swarm or Kubernetes, when we need it. For now, Compose on the workstation and test boxes is enough. Later, Compose on a beefy Proxmox VM may still be enough, and "enough" is the right amount of complexity.
Saturday well spent. The containers are running. Time to close the laptop.