Deploy using Docker and Docker Compose
| File | Purpose |
|---|---|
Dockerfile | Production multi-stage build |
Dockerfile.dev | Development build with hot reload |
docker-compose.yml | Base configuration (volumes, init service) |
docker-compose.override.yml | Development overrides (auto-loaded) |
docker-compose.prod.yml | Production overrides |
docker-compose.showcase.yml | Showcase mode (read-only, no auth) |
.dockerignore | Excludes unnecessary files from Docker context |
.env.example | Environment variable template |
docker compose up
This auto-loads docker-compose.override.yml which uses Dockerfile.dev with hot reload.
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
docker compose -f docker-compose.yml -f docker-compose.showcase.yml up --build
Showcase mode is read-only with no authentication - for public demos.
docker compose --profile init run --rm init-perms
Open http://localhost:3000 in your browser
If you deploy this repo via Coolify → Docker Compose, Coolify runs the same Docker Compose commands you would run locally — but it routes traffic through its proxy (Traefik/Caddy) instead of exposing host ports.
Set these in Coolify if it asks for custom commands (or use them when deploying manually on the server):
Build command
docker compose -f ./docker-compose.yml -f ./docker-compose.prod.yml build app
Start command
docker compose -f ./docker-compose.yml -f ./docker-compose.prod.yml up -d app
Notes:
app service depends on init-perms, so Compose will run the init container automatically.ports: to the host (Coolify docs warn it can reduce features like rolling updates).This app listens on port 3000 inside the container. If Coolify/proxy guesses the wrong upstream port (common default is 80), you’ll get 502 Bad Gateway even though the container is healthy.
In Coolify’s Domains field you can bind the domain to the container port, e.g.:
https://codespider.playdate.events:3000This tells Coolify’s proxy: “route this domain to port 3000 inside the container”.
For a visual explanation, see docs/COOLIFY_PORTS_FLOW.md.
NEXTAUTH_SECRET: must be set (Coolify env var)NEXTAUTH_URL: must be your public URL (no :3000)Example:
NEXTAUTH_URL=https://codespider.playdate.events
GET /api/health always returns 200 (used for container health checks / routing).GET /api/config/validate returns a JSON status object and also returns 200 even when config is incomplete (used by the UI).Start development with hot reload:
docker compose up
This runs pnpm dev inside the container with your repo bind-mounted for fast iteration.
The app is available at http://localhost:3000.
If you see Bind for 0.0.0.0:3000 failed: port is already allocated, stop any existing containers:
docker compose down
docker compose up
To run in the background (logs in Docker Desktop instead of terminal):
docker compose up -d
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
docker compose logs -f app
Or open the container in Docker Desktop and view the Logs tab.
docker compose down
The application supports configuration through environment variables. You can:
GitLab Configuration:
GITLAB_URL=https://gitlab.com GITLAB_TOKEN=your_gitlab_token_here
Claude Configuration:
ANTHROPIC_API_KEY=your_claude_api_key_here # Optional override for the sandbox wrapper path used to run Claude Code non-interactively CLAUDE_CODE_WRAPPER=/usr/local/bin/claude-code-wrapper
This app runs Claude Code via the claude CLI inside the container.
ANTHROPIC_API_KEY (recommended for headless/automation).If you are using a subscription/manual login, set:
CLAUDE_CODE_AUTH_MODE=cli
This forces the app to not pass ANTHROPIC_API_KEY into the claude subprocess even if it is set in the container environment.
Exec into the running container:
docker compose exec app bash
Run an interactive Claude CLI session and follow the prompts (it will typically print a URL + code):
claude --help # Start interactive mode (this is the most version-stable way to complete auth + trust): claude
Exit and restart the app container:
exit
docker compose restart app
Claude Code stores its auth + settings under ~/.claude inside the container.
docker-compose.yml mounts a named volume so your login persists:
/home/nextjs/.claude (the app runs as the nextjs user)/root/.claude (if you exec into the container as root and run claude, it may write here)Important: Claude also writes a ~/.claude.json file in the user's home directory.
The container startup scripts migrate this file into ~/.claude/.claude.json and symlink it back,
so it persists in the same named volume across restarts.
This project currently supports only token/API-key based MCP auth (for example, setting an Authorization: Bearer <token> header on the MCP server entry).
OAuth-based MCP servers are not supported yet (i.e. flows that require a browser login + redirect/callback to exchange codes for tokens). If you need an MCP integration, prefer providers that support static tokens (or manually issued API keys).
If you want a single consistent location, log in as nextjs (recommended):
docker compose exec --user nextjs app bash
cd /app/workspaces
claude
When Claude prompts you to "trust" a directory, do it from the directory you want Claude to operate in.
For this app, that's typically /app/workspaces (or a specific repo under it).
Important nuance: this app typically runs Claude Code in non-interactive mode using -p/--print (and --output-format stream-json).
Per the Claude CLI help, the workspace trust dialog is skipped in -p mode, which is why "headless" commands can appear to work even if you haven't completed the interactive trust/setup flow yet.
If you want to "do it the right way" once and have it persist, run an interactive Claude session (no -p) from /app/workspaces and complete the trust prompt once:
docker compose exec --user nextjs app bash
cd /app/workspaces
run any claude command once to trigger the trust prompt if needed
claude
To "log out" / reset Claude CLI credentials, remove the volume (this deletes the stored login):
docker compose down
docker volume rm workflow_workflow_claude_config
MAX_WORKSPACE_SIZE_MB=500 TEMP_DIR_PREFIX=gitlab-claude- LOG_LEVEL=info ALLOWED_GITLAB_HOSTS=gitlab.com MAX_CONCURRENT_WORKSPACES=3
The Docker setup includes a named volume workflow_workspaces to persist:
Start the application:
docker compose up -d
Stop the application:
docker compose down
View logs:
docker compose logs -f workflow-app
Restart the application:
docker compose restart workflow-app
Update and rebuild:
docker compose up --build -d
Access the container shell:
docker compose exec app bash
Run tests inside container:
docker compose exec app pnpm test
Check application health:
docker compose exec app wget -qO- http://localhost:3000/api/config/validate
Remove containers and networks:
docker compose down
Remove containers, networks, and volumes:
docker compose down -v
Remove all related images:
docker rmi workflow-app workflow-app-dev
Clean up dangling images:
docker image prune
If you're deploying to a single VM (no Swarm), and you have a reverse proxy (Traefik/Caddy/nginx) handling TLS:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
Notes:
3000 to the host. The docker-compose.prod.yml is set up that way by default; your reverse proxy should publish 80/443 and route internally to workflow-app:3000.NEXTAUTH_URL to your public URL (e.g. https://workflow.example.com) in your .env file.Initialize swarm (if not already done):
docker swarm init
Deploy stack:
docker stack deploy -c docker-compose.yml -c docker-compose.prod.yml workflow
Notes:
3000 to the host. Your reverse proxy should publish 80/443 and route internally to the app on port 3000.NEXTAUTH_URL to your public URL (e.g. https://workflow.example.com) in your .env file.You can use the Docker images with Kubernetes. Example deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: workflow-app
spec:
replicas: 3
selector:
matchLabels:
app: workflow-app
template:
metadata:
labels:
app: workflow-app
spec:
containers:
- name: workflow-app
image: workflow-app:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: 'production'
Port already in use:
If you are running the app directly (no reverse proxy), change the port mapping in docker-compose.yml:
ports: - '3001:3000'
If you are running behind a reverse proxy (Traefik/Caddy/nginx), the recommended fix is to not publish the app port at all:
ports: from the app service80/443 and route to workflow-app:3000Permission issues with workspaces:
Fix volume permissions:
docker compose exec app chown -R nextjs:nodejs /app/workspaces
Build failures:
Clean build with no cache:
docker compose build --no-cache
If you want to test webhooks (e.g. n8n callbacks) or access the app from outside your network:
Start the app locally:
docker compose up -d
Expose port 3000:
ngrok http 3000
Use the printed URL (e.g. https://xxxx.ngrok-free.app) as your base URL:
POST https://xxxx.ngrok-free.app/api/askPOST https://xxxx.ngrok-free.app/api/editGET https://xxxx.ngrok-free.app/api/jobs/:jobIdNotes:
NEXTAUTH_URL to the ngrok URL.NEXTAUTH_URL is not required.Memory issues:
Increase Docker memory limit in Docker Desktop settings, or add memory limits to docker-compose.yml:
deploy:
resources:
limits:
memory: 1G
The container includes health checks that verify:
Check health status:
docker compose ps
docker inspect --format='{{.State.Health.Status}}' workflow-appnextjs user for securityAdd monitoring with tools like:
docker stats workflow-appcurl http://localhost:3000/api/config/validateSecrets management - Use Docker secrets or external secret management
Network security - Consider using custom networks
Image scanning - Regularly scan images for vulnerabilities
Updates - Keep base images and dependencies updated
Reverse proxy (Traefik/Caddy) recommended:
Terminate TLS at the reverse proxy and forward traffic to the app over a private network.
Do not publish the app container port to the internet; only the reverse proxy should connect to it.
If you enable API IP allowlisting (ALLOWED_IPS), configure the proxy to overwrite/sanitize X-Forwarded-For / X-Real-IP so clients cannot spoof their IP.
Caddy example:
reverse_proxy workflow-app:3000 {
header_up X-Forwarded-For {remote_host}
header_up X-Real-IP {remote_host}
}
Logging hygiene:
If you're setting up Docker on a fresh Ubuntu VM (20.04+), follow these steps.
Update packages and install dependencies:
sudo apt update && sudo apt upgrade -y sudo apt install -y ca-certificates curl gnupg lsb-release
Add Docker's official GPG key:
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
Set up the Docker repository:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Install Docker:
sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Allow non-root Docker usage:
sudo usermod -aG docker $USER
Log out and back in, or run newgrp docker for the change to take effect.
Verify installation:
docker version
docker run hello-world