Docker Compose Deployment
Multi-container deployment with individually distributed services. Each component — API server, frontend, m3-plugin-runner, and Traefik reverse proxy — runs in its own container for independent scaling, logging, and lifecycle management.
Docker Compose is the recommended deployment method for production and active development. For a simpler single-container option, see All-in-One.
Suitable For
- Production deployments
- Active development (with hot reload)
- Teams requiring individual service scaling
- Environments where services need independent monitoring
Components
| Service | Image | Port | Description |
|---|---|---|---|
| api | docker/api.Dockerfile | 3500 | Node.js tRPC API server |
| frontend | docker/frontend.Dockerfile | 80 | nginx serving React SPA |
| m3-plugin-runner | services/m3-plugin-runner/Dockerfile | 8080 | Python container lifecycle manager |
| traefik | traefik:v3.0 | 8880, 8443, 8881 | Reverse proxy with dashboard |
Quick Start
Create docker-compose.yml
Save the following as docker-compose.yml:
services:
api:
image: marieai/m3-forge:latest
container_name: marie-api
environment:
NODE_ENV: production
PORT: 3500
DATABASE_URL: ${DATABASE_URL}
JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET}
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET}
S3_ENDPOINT_URL: ${S3_ENDPOINT_URL}
S3_BUCKET_NAME: ${S3_BUCKET_NAME:-marie}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
S3_REGION: ${S3_REGION:-us-east-1}
S3_ADDRESSING_STYLE: ${S3_ADDRESSING_STYLE:-path}
FRONTEND_URL: ${FRONTEND_URL:-http://localhost}
API_URL: ${API_URL:-http://localhost}
ports:
- "${API_PORT:-3500}:3500"
networks:
- marie-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3500/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
restart: unless-stopped
m3-plugin-runner:
image: marieai/m3-plugin-runner:latest
container_name: marie-m3-plugin-runner
environment:
HOST: 0.0.0.0
PORT: 8080
APPS_DIR: /data/apps
DOMAIN: ${WEBAPP_DOMAIN:-apps.localhost}
DOCKER_NETWORK: marie-network
RUNNER_API_KEY: ${RUNNER_API_KEY:-}
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-http://localhost:${TRAEFIK_HTTP_PORT:-8880}}
DEBUG: ${DEBUG:-false}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- plugin-data:/data/apps
ports:
- "${WEBAPP_RUNNER_PORT:-8090}:8080"
networks:
- marie-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
restart: unless-stopped
traefik:
image: traefik:v3.0
container_name: marie-traefik
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=marie-network"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--api.dashboard=true"
- "--api.insecure=true"
- "--log.level=${TRAEFIK_LOG_LEVEL:-INFO}"
ports:
- "${TRAEFIK_HTTP_PORT:-8880}:80"
- "${TRAEFIK_HTTPS_PORT:-8443}:443"
- "${TRAEFIK_DASHBOARD_PORT:-8881}:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- marie-network
restart: unless-stopped
networks:
marie-network:
name: marie-network
driver: bridge
volumes:
plugin-data:
name: marie-plugin-dataCreate .env
Save the following as .env and fill in the values:
# Required
DATABASE_URL=postgresql://postgres:123456@host.docker.internal:5432/studio
S3_ENDPOINT_URL=http://host.docker.internal:8000
S3_ACCESS_KEY_ID=MARIEACCESSKEY
S3_SECRET_ACCESS_KEY=MARIESECRETACCESSKEY
# Generate with: openssl rand -hex 32
JWT_ACCESS_SECRET=replace-with-generated-secret
JWT_REFRESH_SECRET=replace-with-generated-secret
# Optional
# S3_BUCKET_NAME=marie
# S3_REGION=us-east-1
# WEBAPP_DOMAIN=apps.localhost
# RUNNER_API_KEY=Start the services
docker compose up -dAccess the application
- API: http://localhost:3500
- Traefik dashboard: http://localhost:8881
- Plugin runner: http://localhost:8090
Profiles
Docker Compose profiles control which services start:
| Profile | Command | Services Started |
|---|---|---|
| (default) | docker compose up -d | api, m3-plugin-runner, traefik |
production | docker compose --profile production up -d | + nginx frontend |
dev | docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile dev up -d | + Vite dev server with HMR |
Development Mode
Development mode adds hot reload for all services:
# Start with dev overrides
docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile dev up -dWhat changes in dev mode:
| Service | Production | Development |
|---|---|---|
| API | Pre-built api.Dockerfile | api.dev.Dockerfile with source mount + pnpm dev |
| Frontend | nginx serving static build | Vite dev server at :5173 with HMR |
| Plugin Runner | Standard startup | Source mount + uvicorn --reload |
Production Mode
# Build and start all production services
docker compose --profile production up -d --buildArchitecture
Customizing Ports
Override default ports via environment variables in .env:
| Variable | Default | Service |
|---|---|---|
API_PORT | 3500 | API server |
FRONTEND_PORT | 80 | Frontend (nginx, production) |
FRONTEND_DEV_PORT | 5173 | Frontend (Vite, development) |
WEBAPP_RUNNER_PORT | 8090 | Plugin runner |
TRAEFIK_HTTP_PORT | 8880 | Traefik HTTP |
TRAEFIK_HTTPS_PORT | 8443 | Traefik HTTPS |
TRAEFIK_DASHBOARD_PORT | 8881 | Traefik dashboard |
Volumes
| Volume | Purpose |
|---|---|
marie-plugin-data | Plugin container data and deployed applications |
Development mode adds isolated node_modules volumes to avoid conflicts with host:
| Volume | Purpose |
|---|---|
api-node-modules | API dependencies (dev only) |
frontend-node-modules | Root dependencies (dev only) |
frontend-ui-node-modules | UI dependencies (dev only) |
Scaling Services
Scale individual services with Docker Compose:
# Run 3 API server instances (requires load balancer)
docker compose up -d --scale api=3When scaling the API, configure Traefik or an external load balancer to distribute requests. The m3-plugin-runner should remain a single instance as it manages Docker containers on the host.
Commands Reference
| Command | Description |
|---|---|
docker compose up -d | Start core services |
docker compose --profile production up -d --build | Start all services in production mode |
docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile dev up -d | Start in development mode |
docker compose down | Stop all services |
docker compose logs -f | View all logs |
docker compose logs -f api | View API logs |
docker compose logs -f m3-plugin-runner | View m3-plugin-runner logs |
docker compose build | Rebuild all images |
docker compose ps | List running containers |
docker compose down -v | Stop and remove all containers and volumes |
Environment Variable Reference
| Variable | Default | Description |
|---|---|---|
DATABASE_URL | (required) | PostgreSQL connection string |
S3_ENDPOINT_URL | (required) | S3-compatible endpoint |
S3_ACCESS_KEY_ID | (required) | S3 access key |
S3_SECRET_ACCESS_KEY | (required) | S3 secret key |
JWT_ACCESS_SECRET | (required) | JWT access token secret |
JWT_REFRESH_SECRET | (required) | JWT refresh token secret |
NODE_ENV | production | Node.js environment |
S3_BUCKET_NAME | marie | S3 bucket name |
S3_REGION | us-east-1 | S3 region |
S3_ADDRESSING_STYLE | path | S3 addressing style |
FRONTEND_URL | http://localhost:5173 | Frontend URL for CORS |
API_URL | http://localhost:5173 | API base URL |
WEBAPP_DOMAIN | apps.localhost | Domain for webapp subdomains |
RUNNER_API_KEY | (none) | Plugin-runner authentication key |
PUBLIC_BASE_URL | http://localhost:8880 | Public base URL for webapps |
TRAEFIK_LOG_LEVEL | INFO | Traefik log verbosity |
DEBUG | false | Enable debug mode |
Production Considerations
HTTPS with Traefik
For production HTTPS, configure Traefik with Let’s Encrypt:
# Add to traefik command in docker-compose.yml
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.email=your@email.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"Database Backups
Regularly back up your PostgreSQL database:
pg_dump $DATABASE_URL > backup-$(date +%Y%m%d).sqlResource Limits
Add resource limits to prevent any single service from consuming all available resources:
services:
api:
deploy:
resources:
limits:
memory: 1G
cpus: "1.0"Troubleshooting
Database connection refused
# Verify DATABASE_URL is correct
docker compose exec api node -e "console.log(process.env.DATABASE_URL)"
# Test connection from inside the container
docker compose exec api npx prisma db pull \
--schema packages/@marie/db/prisma/schema.prismaTraefik not routing to webapps
- Check the Traefik dashboard at http://localhost:8881
- Verify webapps are on the
marie-network:
docker network inspect marie-network- Check webapp container labels:
docker ps --filter "network=marie-network" --format "table {{.Names}}\t{{.Labels}}"Frontend shows blank page
# Check frontend build
docker compose logs frontend
# Verify API is reachable
curl http://localhost:3500/healthPlugin runner can’t create containers
# Verify Docker socket is mounted
docker compose exec m3-plugin-runner ls -la /var/run/docker.sock
# Check m3-plugin-runner logs
docker compose logs m3-plugin-runnerHot reload not working in dev mode
Ensure you’re using the dev compose override:
# Correct command
docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile dev up -d
# Verify source volumes are mounted
docker compose exec api ls -la /app/packages