Deploy using Docker and Docker Compose
Dockerfile - Production-optimized multi-stage buildDockerfile.dev - Development build with hot reloadingdocker-compose.yml - Orchestration for production and development.dockerignore - Excludes unnecessary files from Docker contextBuild and run with Docker Compose:
docker compose up -d --build workflow-app
Or build and run manually:
Build the image:
docker build -t workflow-app .
Run the container:
docker run -p 3000:3000 --name workflow-app workflow-app
Access the application: 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 build workflow-app
Start command
docker compose -f ./docker-compose.yml up -d workflow-app
Notes:
workflow-app depends on workflow-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).You have two dev-friendly options:
Hot reload (recommended): workflow-dev (detached)
docker compose up -d --build workflow-dev
This runs pnpm dev inside the container with your repo bind-mounted for fast iteration.
By default, workflow-dev binds to http://localhost:3000.
If you see Bind for 0.0.0.0:3000 failed: port is already allocated, it means something else is already using port 3000 (often workflow-app).
Stop the other service before starting workflow-dev:
docker compose stop workflow-app
docker compose up -d --build workflow-dev
Auto-rebuild on file changes (slower): docker compose watch
The production-like service (workflow-app) has develop.watch configured in docker-compose.yml.
This will rebuild/recreate the container when files change:
docker compose watch workflow-app
If you want the container logs to show in Docker Desktop (not your terminal), run in detached mode:
docker compose up -d --build workflow-dev
Open: http://localhost:3000
docker compose up -d --build workflow-app
Open: http://localhost:3000
Docker Desktop: open the container and view the Logs tab
CLI (optional):
docker compose logs -f workflow-dev
or:
docker compose logs -f workflow-app
docker compose stop workflow-dev
docker compose stop workflow-app
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 exec -it workflow-app bash
or (dev):
docker exec -it workflow-dev 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 workflow-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 exec -it --user nextjs workflow-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:
Git Bash / mintty on Windows: use winpty for proper TTY
winpty docker exec -it --user nextjs workflow-dev bash
cd /app/workspaces
claude
Example (dev or prod):
docker exec -it --user nextjs workflow-dev bash
cd /app/workspaces
run any claude command once to trigger the trust prompt if needed
claude --help
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 exec -it workflow-app bash
or (dev):
docker exec -it workflow-dev bash
Run tests inside container:
docker exec -it workflow-app bash -lc "pnpm test"
Check application health:
docker exec -it workflow-app bash -lc "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 up -d --build workflow-app
Notes:
workflow-app should not publish 3000 to the host. The repo docker-compose.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 production.Initialize swarm (if not already done):
docker swarm init
Deploy stack:
docker stack deploy -c docker-compose.yml workflow
Notes:
workflow-app should not publish 3000 to the host. 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 production.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 workflow-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 (prod or dev):
docker compose up -d workflow-app
or:
docker compose up workflow-dev --build
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