Skip to content

Deploy with Docker + Caddy

A production deploy uses three pieces: the server image, a .env file, and caddy-docker-proxy labels on the external caddy network.

  • docker-compose.yml
  • .env (copy from .env.production)
  • Dockerfile
  • Makefile

Copy .env.example to .env and fill in the secrets. The Makefile’s make secret-gen target generates OAUTH_SECRET_KEY for you if it’s missing or blank (idempotent — re-runs preserve the existing key).

Terminal window
# Required — server refuses to start without these
OAUTH_SECRET_KEY= # `make secret-gen` fills this in
ADMIN_PASSWORD= # no default; RuntimeError if unset (see below)
DOMAIN=mcp.example.com
COMPOSE_PROJECT_NAME=blender-mcp
# Optional — demo user is opt-in
DEMO_PASSWORD= # only set this if you want a `demo` account created
services:
blender-mcp:
build: .
container_name: blender-mcp
restart: unless-stopped
env_file: .env
volumes:
- ./data:/data
networks:
- caddy
- internal
labels:
caddy: ${DOMAIN}
caddy.reverse_proxy: "{{upstreams 8000}}"
caddy.tls.import: rfc2136_local
networks:
caddy:
external: true
internal:
driver: bridge

The caddy.tls.import: rfc2136_local line pulls in a Caddy snippet that uses RFC 2136 dynamic DNS-01 challenges. Drop the line if you’re using HTTP-01.

  1. Generate the secret + set the admin password:

    Terminal window
    make secret-gen # fills OAUTH_SECRET_KEY in .env if blank
    $EDITOR .env # set ADMIN_PASSWORD (required)
  2. Start the production stack:

    Terminal window
    make prod # builds image, brings up compose with caddy labels
  3. Watch the logs:

    Terminal window
    make logs # or: docker compose logs -f

    Expected first lines:

    blender-mcp | INFO uvicorn:Started server process [1]
    blender-mcp | INFO uvicorn:Uvicorn running on http://0.0.0.0:8000
  4. Verify TLS from another host (the Makefile has a target for this):

    Terminal window
    make health # GETs https://${DOMAIN}/health, pretty-prints JSON
    # or directly:
    curl -fsS https://${DOMAIN}/health

The root Makefile has 14 targets. make help renders them all:

TargetPurpose
prodStart the production stack (FastMCP server behind caddy-docker-proxy)
devStart the dev stack with hot reload
downStop both stacks (prod + dev)
logsTail logs from whichever stack is running
restartGraceful restart without rebuild
buildBuild the production image without starting it
rebuildFull no-cache rebuild of prod image
psShow running services
shellOpen an interactive bash shell in the running prod container
caddy-reloadForce the host Caddy to re-read its config
secret-genGenerate OAUTH_SECRET_KEY into .env only if missing/blank
healthHit the public /health endpoint and pretty-print the response
cleanStop everything and remove project volumes
helpShow all targets
VariableRequired?Purpose
OAUTH_SECRET_KEYyesHMAC signing key for issued tokens. Generate with make secret-gen.
ADMIN_PASSWORDyesAdmin user’s password. No default — server refuses to start without it.
DOMAINyesPublic hostname Caddy serves from.
COMPOSE_PROJECT_NAMEyesCompose project namespace.
DEMO_PASSWORDnoWhen set, creates a demo user with this password. Leave unset in production.
Terminal window
make dev # docker compose up with the dev profile + source mounts + --reload

That target runs uv run uvicorn blender_mcp.oauth_server:app --reload --host 0.0.0.0 inside the container with source volumes mounted from the host.

The branch’s reference deployment runs at https://mcp.l.warehack.ing/mcp/ (note the trailing slash — see Quickstart for why the addon auto-adds it). If you want to skip standing up your own server while exploring the API, you can authenticate against this deployment with the demo credentials and point clients at it directly.

  • The bus state is in-memory only — restarting the container disconnects every persistent client and drops _pending_jobs. Schedule restarts during quiet windows. See Architecture for the rationale.
  • caddy-docker-proxy auto-discovers new containers — no manual Caddyfile edits needed.
  • Pin the image with image: rather than build: once you start tagging releases. CalVer (2026.05.25) keeps “when was this built” obvious from the tag.