dokploy-docker-compose
Dokploy Docker Compose Deployment Skill
Overview
Dokploy is a self-hosted deployment platform that manages Docker containers on a VPS. It supports two compose modes (Docker Compose and Docker Stack) and requires specific patterns to work reliably.
Quick decision tree:
- Single-repo app with
Dockerfile? → Application (Git) deployment - Pre-built images or multi-service stack? → Compose deployment
- Need a public HTTPS domain? → Domains tab (Dokploy auto-injects Traefik labels)
- Need isolated networking per-app? → Enable Isolated Deployments in UI
- Config files needed without SSH? → File Mounts in Advanced → Mounts
- Complex stack needing generated configs + binary downloads? → Init Containers pattern
Part 1: Service Types
Compose (Docker Compose mode)
Best for: multi-service stacks using pre-built images. Standard docker compose up behavior.
Key constraints vs running locally:
- Each deploy via AutoDeploy does a
git clonewhich clears the repository directory — any relative path like./config.yamlwill be empty or missing after the first push - Never use
container_name:on services — it breaks Dokploy's logs, metrics, and monitoring features - Relative bind mounts (
./data) work only on first deploy; use../files/or named volumes instead
Stack (Docker Swarm mode)
Docker Stack mode for orchestrated deployments. Key differences from Compose mode:
- No
build:directive — must use pre-built images from a registry - Traefik labels must go under
deploy.labels, not directly underlabels - Add
--with-registry-authflag in Advanced → Command if using private registries with replicas
Application (Git) Deployment
Best for: source code you build from a Dockerfile.
Steps: Create Service → Application → Source: Git → Build Type: Dockerfile → configure Volumes and Ports in Advanced.
Part 2: Volume Strategy
⚠️ Critical Dokploy rule: Absolute host paths (e.g., /opt/myapp/) are cleaned up during deployments. Never use them. Use the ../files/ folder or named volumes instead.
Named Volumes (Best for databases and large data)
services:
db:
image: postgres:16
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
Docker manages these. Supports automated backups via Dokploy's Volume Backups feature. Best for databases, large datasets.
../files/ Bind Mounts (Best for config files and small data)
Dokploy provides a persistent ../files/ folder that survives deployments:
volumes:
- ../files/my-config:/etc/myapp/config ✅ persists across deploys
- ../files/my-database:/var/lib/mysql ✅ persists across deploys
- ./config.yaml:/app/config.yaml ❌ wiped on each AutoDeploy (git clone)
- /opt/myapp/data:/app/data ❌ absolute paths are cleaned up by Dokploy
No direct host file access needed. Best for config files, small datasets. No backup support.
Host path on the VPS: ../files/ maps to a files/ folder inside Dokploy's project directory on the host. To find the exact path after first deploy, SSH in and run:
# Step 1: find your container name (Dokploy names them <stack>-<service>-1)
docker ps --format "table {{.Names}}\t{{.Image}}" | grep <service-keyword>
# Step 2: inspect that container to see where ../files/ maps on the host
docker inspect <container-name> | grep -A2 '"Source"'
Or check the Docker tab in the Dokploy UI — the container name is listed there directly.
File Mounts (Dokploy UI — for pasting config content directly)
In Advanced → Mounts → Add Mount → File Mount:
- Content: paste the file text
- File Path: just the filename (e.g.,
config.yaml) - Mount Path: full container path (e.g.,
/app/config/config.yaml)
✅ Best for: config files you want to manage entirely in Dokploy UI ❌ Not for: binary files, files needing runtime generation, files > a few KB
Init Container Pattern (for complex stacks needing generated configs)
When a stack needs multiple config files written at runtime, or binary downloads, use an alpine init container that writes files into a shared named volume before the main service starts:
services:
my-conf:
image: alpine:3.20
command:
- /bin/sh
- -lc
- |
cat > /config/app.yaml <<'YAML'
key: value
YAML
apk add --no-cache wget && wget -O /scripts/tool "https://..."
volumes:
- app-config:/config
- app-scripts:/scripts
restart: "no" # MUST exit after running
app:
depends_on:
my-conf:
condition: service_completed_successfully
volumes:
- app-config:/etc/app/
- app-scripts:/usr/local/scripts/
Shell escaping: Use $$ inside compose heredocs — $$(uname -s) not $(uname -s).
See references/init-container-patterns.md for complete examples.
Choosing the Right Volume Method
| Need | Method |
|---|---|
| Database data | Named volume |
| Automated backups to S3 | Named volume (only option) |
| Config file — manage in UI | File Mount |
| Config file — large or complex | ../files/ bind mount |
| Generated configs + binaries | Init container → named volume |
| ❌ Do NOT use | Absolute host paths, ./ relative paths |
Part 3: Environment Variables
How Dokploy handles env vars
Variables set in the Environment tab are written to a .env file in the same directory as
docker-compose.yml. They are NOT automatically injected into containers. You must explicitly
load them in your compose YAML using one of two approaches:
Option A — Load all variables (simpler for many vars):
services:
app:
env_file:
- .env # loads every variable from Dokploy's Environment tab
Option B — Select specific variables:
services:
app:
environment:
- JWT_SECRET=${JWT_SECRET}
- DATABASE_URL=${DATABASE_URL}
- PORT=3000 # hardcoded values also work
⚠️ If you use environment: with explicit variables, only those listed variables are passed.
Any variable set in the UI but not listed here will be silently ignored.
Recommendation: For stacks with many secrets (LibreChat, etc.) use env_file: - .env on each
service that needs them. For stacks where services need different subsets, use explicit ${VAR} refs.
Generate secure values
openssl rand -hex 32 # 64-char hex string (for keys like CREDS_KEY)
openssl rand -hex 16 # 32-char hex string (for IVs like CREDS_IV)
Part 4: Domains & Traefik Routing
Dokploy uses Traefik as a reverse proxy. There are two ways to expose services via a domain.
Method 1: Dokploy Domains UI (Recommended — since v0.7.0)
Go to your service → Domains tab → Add Domain. Dokploy automatically injects the correct Traefik labels into your compose file at deploy time. You never touch the labels manually.
- Use
expose:(notports:) in your compose YAML when routing via domain - Use Preview Compose button to see the final compose file with auto-injected labels
services:
app:
image: myapp:latest
expose:
- 3000 # container-only; Traefik routes via domain, no host port needed
Method 2: Manual Traefik Labels (Advanced)
For fine-grained control, add labels directly in your compose file. Requires connecting to
dokploy-network (the shared Traefik network):
services:
frontend:
image: myapp:latest
expose:
- 3000
networks:
- dokploy-network
labels:
- traefik.enable=true
- traefik.http.routers.my-unique-name.rule=Host(`app.yourdomain.com`)
- traefik.http.routers.my-unique-name.entrypoints=websecure
- traefik.http.routers.my-unique-name.tls.certResolver=letsencrypt
- traefik.http.services.my-unique-name.loadbalancer.server.port=3000
networks:
dokploy-network:
external: true
For Docker Stack — labels go under deploy.labels:
services:
frontend:
image: myapp:latest
deploy:
labels:
- traefik.enable=true
- traefik.http.routers.my-app.rule=Host(`app.yourdomain.com`)
- traefik.http.services.my-app.loadbalancer.server.port=3000
networks:
- dokploy-network
Isolated Deployments
Enable this in the Dokploy UI (toggle on the Compose service page) to give your stack its own
private network instead of sharing dokploy-network. This is:
- Required when running multiple instances of the same app (e.g., two WordPress sites)
- Safer — services aren't visible across other stacks
- Automatic — Dokploy handles all network creation and Traefik wiring
When Isolated Deployments is enabled, you don't need to add dokploy-network manually.
All open-source templates shipped with Dokploy have this enabled by default.
⚠️ If NOT using Isolated Deployments, only the domain-targeted service gets dokploy-network
added automatically. Other services in the stack won't be on that network unless you add it manually.
Part 5: Networking & DNS (EAI_AGAIN Fix)
The most common multi-service error:
getaddrinfo EAI_AGAIN mongodb
Container can't resolve another service's hostname. Causes: race condition or DNS confusion.
Fix 1 — Healthchecks + proper depends_on (prevents race conditions):
services:
api:
depends_on:
mongodb:
condition: service_healthy # waits until healthy, not just started
mongodb:
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 10s
retries: 5
start_period: 10s
Fix 2 — Explicit custom network (guarantees DNS between services):
networks:
app-net:
driver: bridge
services:
api:
networks: [app-net]
mongodb:
networks: [app-net]
⚠️ Avoid using container_name: as a DNS workaround — it breaks Dokploy monitoring features.
Use service names for inter-container communication instead.
Part 6: Healthchecks
Add a healthcheck to every dependency service (databases, caches, queues). Pair it with depends_on: condition: service_healthy on services that need them — this prevents race conditions where a service starts before its dependency is ready.
Example:
services:
postgres:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
app:
depends_on:
postgres:
condition: service_healthy
Part 7: Updating Deployed Services
Manual update (public repos you don't own)
- Go to Deployments tab → Click Deploy
- If Docker cache is stale: look for "Redeploy without Cache" or clear Build Cache in System Settings
Automatic updates via Webhook
- Copy Webhook URL from Deployments tab
- Add it to your GitHub/GitLab/Gitea repo Settings → Webhooks
- Every push to the branch triggers an automatic deploy
Part 8: Common Errors & Fixes
| Error | Cause | Fix |
|---|---|---|
getaddrinfo EAI_AGAIN <hostname> |
Race condition / DNS | Add healthcheck + condition: service_healthy; use custom network |
| Config file empty after redeploy | ./ relative path wiped by git clone |
Use ../files/ bind mount or File Mount in UI |
JwtStrategy requires a secret or key |
JWT env var not injected | Add env_file: - .env or explicit - JWT_SECRET=${JWT_SECRET}; Redeploy (not Restart) |
ENOENT: no such file or directory |
Config file not mounted | Use File Mount in Advanced → Mounts, or ../files/ bind mount |
permission denied on volume |
Old volume from different user | Rename volume (e.g., data-v2) to force fresh creation |
Container is unhealthy |
Wrong healthcheck for image | Match healthcheck command to image type; see Part 6 |
| Logs/metrics missing in Dokploy | container_name: set on service |
Remove container_name: — it breaks Dokploy's monitoring |
| Services can't see each other after adding domain | Not on same network | Use Isolated Deployments, or manually add dokploy-network to all services |
Part 9: Reference Files
references/sop-compose-deployment.md— step-by-step SOP for any multi-service Compose stackreferences/sop-git-deployment.md— deploying from Git with Dockerfilereferences/librechat-example.md— LibreChat: JWT secrets, MongoDB DNS, librechat.yaml mountingreferences/signoz-example.md— SigNoz: init containers, ClickHouse, Zookeeper migrationreferences/init-container-patterns.md— init container patterns with heredocs and binary downloadsreferences/healthchecks.md— healthcheck snippets for common services
Quick Checklist: Adapting any docker-compose.yml for Dokploy
- Remove all
container_name:— breaks Dokploy logs and metrics - Replace
./relative bind mounts with../files/or named volumes - No absolute host paths (
/opt/...) — they get cleaned up on deploy - Remove
user: "${UID}:${GID}"— causes permission errors in Dokploy - Remove
env_file: - ./.env— file won't exist; useenv_file: - .env(no./) or${VAR}syntax - Add
env_file: - .envOR explicit${VAR}refs for every secret — env vars aren't auto-injected - Add healthchecks to all database/dependency services
- Update
depends_onto usecondition: service_healthyinstead of just service name - For domains: use Dokploy's Domains tab (Method 1) unless you need custom Traefik config
- Use
expose:(notports:) for services routed via Traefik domain - Declare all named volumes at the bottom of the file
More from trfi/skills
cv-resume
Generate professional, ATS-optimized CVs and resumes as polished HTML files. Use this skill whenever the user asks to create, write, build, update, or improve a CV, resume, curriculum vitae, or job application document — even if they just say "make me a resume" or "help with my CV". Also trigger when the user provides career info, work history, or asks to format their experience for job applications. This skill produces visually stunning, eye-catching HTML that is simultaneously ATS-friendly — the best of both worlds.
17writing-skill
>
2