Run with Docker
The repository ships a multi-stage Dockerfile with development and production targets, orchestrated through docker compose and a Makefile.
Quick start
Section titled “Quick start”make setup # copies .env.example -> .env, creates dirs, creates the caddy networkmake dev # development container with hot reloadmake prod # production container, detachedmake logs # follow container logsThe two stages
Section titled “The two stages”| 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.
FreeCAD is opt-in: WITH_FREECAD
Section titled “FreeCAD is opt-in: WITH_FREECAD”By default the image builds without FreeCAD:
docker compose build # ezdxf tier onlydocker compose build --build-arg WITH_FREECAD=1 # full FreeCAD tierThis 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.
Compose service
Section titled “Compose service”The service exposes port 8000 on the external caddy network and carries caddy-docker-proxy labels:
expose: - "8000"networks: - caddylabels: 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.
Health check
Section titled “Health check”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()"]Running the test suite in the container
Section titled “Running the test suite in the container”The integration tests that need real FreeCAD only run inside the container:
make test-in-dockerSee Run the tests for the full two-tier testing story.