Skip to content

Run with Docker

The repository ships a multi-stage Dockerfile with development and production targets, orchestrated through docker compose and a Makefile.

Terminal window
make setup # copies .env.example -> .env, creates dirs, creates the caddy network
make dev # development container with hot reload
make prod # production container, detached
make logs # follow container logs
development production
Dependencies full, including dev group --no-dev
Source volume-mounted read-only, hot reload baked into the image
Server command fastmcp run src/freecad_server.py:app --reload cad-mcp-server console script
User root non-root appuser

Both stages start Xvfb (a virtual display) before the server, because FreeCAD’s GUI-adjacent modules expect a display even headless. Both install dependencies with uv sync --frozen against the lock file for reproducible builds.

By default the image builds without FreeCAD:

Terminal window
docker compose build # ezdxf tier only
docker compose build --build-arg WITH_FREECAD=1 # full FreeCAD tier

This is deliberate. The apt FreeCAD install is heavyweight and fragile in constrained build environments, and the server degrades gracefully without it: the six DXF analysis tools work fully, and FreeCAD-backed tools return a per-call error like "FreeCAD not available" instead of failing at startup. If your workload is civil DXF analysis, the default image is smaller, builds faster, and does everything you need.

The base image is pinned to python:3.11-slim-bookworm because the apt freecad-python3 package targets system Python 3.11 — the venv ABI has to match.

The service exposes port 8000 on the external caddy network and carries caddy-docker-proxy labels:

expose:
- "8000"
networks:
- caddy
labels:
caddy: ${DOMAIN}
caddy.reverse_proxy: "{{upstreams 8000}}"

No ports: mapping — the reverse proxy is the only way in. Set DOMAIN in .env, and the front proxy handles TLS. The HTTP transport is configured with stateless JSON responses specifically so this proxied setup works with remote MCP clients (see Configuration).

Environment variables passed through compose:

environment:
- MAX_FILE_SIZE_MB=${MAX_FILE_SIZE_MB:-100}
- TEMP_DIR=/app/temp
- OUTPUT_DIR=/app/output
- LOG_LEVEL=${LOG_LEVEL:-INFO}

Volumes map ./data, ./output, and ./temp into the container, so files you drop in data/ are visible to tools at /app/data/..., and rendered SVGs and conversions land in output/ on the host.

The compose file defines a TCP socket check against port 8000 — protocol-appropriate for an MCP server, where there’s no conventional HTTP health path:

healthcheck:
test: ["CMD", "python", "-c", "import socket; socket.create_connection(('127.0.0.1', 8000), timeout=3).close()"]

The integration tests that need real FreeCAD only run inside the container:

Terminal window
make test-in-docker

See Run the tests for the full two-tier testing story.