Writing Compose for DockPod
Applies to Paste & Deploy, 1-Click Deploy and compose-mode Git Deployments. Your compose file is never edited — DockPod applies its own settings through a separate override file that Compose merges at deploy time.
Most compose files work unchanged. The ones that don't almost always fail the same way, and it's worth understanding why before you write one.
The one rule that matters
Don't write traefik.* labels. DockPod writes them for you.
This is the single most common reason a deploy goes green and the domain still doesn't answer — and it catches AI-generated compose files almost every time, because every Docker + Traefik tutorial on the internet teaches the opposite.
DockPod runs one shared Traefik for the whole host. Traefik watches every container on the Docker daemon, so labels in your file create real routers that DockPod never registered. It can't see them, its "domain already in use" check can't see them, and the generic router names such files carry — web, app, api — collide silently between unrelated stacks. One route wins, the other vanishes, and Traefik logs nothing either way.
So a service carrying its own traefik.* labels with no domain registered in DockPod gets switched off at the proxy:
note: ignoring the traefik labels on service "web" — it has no domain in
DockPod, and routes DockPod doesn't know about collide with ones it does.
Add a domain to route it.Add a Custom Domain in DockPod instead. You get the router, HTTPS, the HTTP→HTTPS redirect and the optional www redirect generated for you, under a router name that is unique by construction.
Labels and a domain is the worst case
If a service has both its own traefik.* labels and a DockPod domain, DockPod leaves your labels alone — traefik.enable is per container, so there is no way to drop yours while keeping its own. Both sets of routers go live, and a router name shared with another stack will silently take over one of them. The build log warns, but this one is on you to clean up.
Checklist
Never write these.
| Why | |
|---|---|
traefik.* labels | DockPod generates all routing from the domain you add in its UI. See above. |
A dockpod-proxy network, in any form — above all external: true | The name makes it look like the sanctioned one to join. It is the opposite. See below. |
A traefik network, or any other proxy network of your own | DockPod pins the project's default network and attaches Traefik to it. |
Host port 80 or 443 | Traefik owns both. Docker would refuse the second bind, so the binding is dropped. |
network_mode: host on anything that needs a domain | The container is on no Docker network, so Traefik has no address to route to. Nothing can be attached to fix it. |
Write these instead.
No ports: on the web service at all | It's reached through a DockPod domain over the internal network, on the container's port — never the host's. |
No ports: on internal services either | Containers in a project reach each other by service name: postgres:5432, redis:6379. |
A healthcheck on the web service | DockPod waits on it, so a failed start is reported instead of a route that times out. |
These are safe, and DockPod works around them.
| What happens | |
|---|---|
deploy.resources.limits | Your limits stand. DockPod fills in only the ones you left out. |
| Your own named networks | DockPod re-adds the project's default network alongside them, so domain routing and database links keep working. |
| Published host ports other than 80/443 | Kept exactly as written. Worth it only for a non-HTTP protocol Traefik can't route. |
The dockpod-proxy trap
dockpod-proxy is a real network, Traefik is really attached to it, and the name really does belong to DockPod. Everything about it invites you to join it. Don't — and in particular, never write this:
networks:
dockpod-proxy:
external: trueTwo things go wrong, and the second one is the painful one.
Your app probably doesn't use that network at all. For a service with a real domain, DockPod routes over a dedicated per-app network named dockpod-app-<project> and attaches Traefik to that. dockpod-proxy is reserved for local-only domains — .test, .local and the other reserved suffixes. If your domain is a public one, naming dockpod-proxy adds a dependency your app never had.
And external: true means "this must already exist." DockPod creates dockpod-proxy when Traefik first starts, and Traefik first starts when an app has a domain. On a fresh install the usual order is: create the app, deploy, then add the domain — so the first deploy runs before that network exists, and Compose stops with:
network dockpod-proxy declared as external, but could not be foundThe file is fine on a host where you've deployed before, because the network is already there from an earlier app. It fails on every new host. That is a nasty shape for a bug: it passes every test you run on your own machine and breaks the first time a customer installs from scratch.
Leave networks out and let DockPod attach what it needs.
Giving these rules to an AI
If you're asking Claude or another assistant to write the compose file, paste this in first. It is the checklist above, written for a model rather than a person:
This compose file will be deployed by DockPod, which runs a shared Traefik
reverse proxy. Follow these rules exactly.
DO NOT WRITE:
- Any traefik.* label. DockPod generates all routing labels itself from the
domain added in its UI. A traefik.* label on a service without a DockPod
domain gets forced to traefik.enable=false and the route silently dies.
- A network named dockpod-proxy, in any form, above all `external: true`.
DockPod attaches Traefik to the network it needs by itself. An app with a
real domain is routed over dockpod-app-<project> and never touches
dockpod-proxy; declaring it anyway makes the FIRST deploy on a fresh host
fail, because that network does not exist until Traefik has started.
- Any other traefik/proxy network. DockPod pins the project's default network
and attaches Traefik itself.
- ports: 80 or 443 on the host. Traefik owns both; DockPod strips them.
- network_mode: host on anything that needs a domain. Such a container is on
no Docker network, so Traefik has no address to route to. This cannot be
worked around.
DO WRITE:
- No ports: at all on the web service. It is reached through a DockPod
domain over the internal network, using the CONTAINER port.
- Internal services (db, redis, queue) with no ports: either. Containers in
the project reach each other by service name: postgres:5432, redis:6379.
- A healthcheck on the web service.
SAFE TO WRITE:
- deploy.resources.limits — yours are kept; DockPod only fills in what you
left out.
- Your own named networks — DockPod re-adds the default network alongside
them so routing and database links keep working.
- Published host ports other than 80/443, if you genuinely need a non-HTTP
protocol Traefik cannot route.
The web service must listen on the container port given to DockPod when
adding the domain.The build log is the record
Nothing above happens silently. Every change DockPod makes to the merged result is reported in the deploy's build log, with what to do about it:
note: not publishing host port 80 for service "web" — the reverse proxy owns
80/443 on this host. Add a domain to reach it over HTTPS; its other published
ports are unchanged.
note: web, db declare their own networks — also attaching them to this app's
DockPod network so domain routing and database links work
warning: service "app" sets network_mode, so it is on no Docker network and
Traefik cannot reach it — app.example.com will not route. Remove network_mode
from that service, or publish its port and put your own proxy in front.That second note is the one to read carefully: "this app's DockPod network" means dockpod-app-<project>, which DockPod creates and joins for you. It is not an invitation to declare a network yourself — see the dockpod-proxy trap.
If a domain isn't routing, read the build log before anything else. It almost always names the cause outright.
One case stops the deploy rather than working around it: a host port another app already holds. DockPod names the service, the port, and the container holding it — there's no safe way to guess a different port on your behalf.
What actually fails, and what only looks fine
Worth knowing which mistakes stop a deploy and which let it go green while something quietly doesn't work. The second list is the dangerous one.
DockPod stops the deploy for two things. Both name the cause outright:
| What you see | |
|---|---|
| A host port another container already publishes | service "web" wants host port 8001, which is already published by … — DockPod names the service, the port and the owner. It can't pick a different port for you, so it stops. |
An external: network that doesn't exist | Named, with what to do about it. Before v1.16.4 this wasn't a DockPod check at all — Compose failed with a bare network … declared as external, but could not be found and no hint of where it came from. |
Three more stop it before your compose file is read: no compose file and no Dockerfile in the repo root; a Git provider whose authorisation has lapsed; a clone or pull that fails.
Everything else that fails is Compose itself, relayed to the build log as it comes. In practice it's nearly always one of these:
${VAR:?message} with the variable unset | Stops immediately with your own message. Set it under the app's Environment Variables. |
| A mistyped image tag, or a private registry with no credentials | The pull fails |
depends_on with condition: service_healthy on a healthcheck that never passes | Waits, then fails |
| Invalid YAML, or a Compose key that doesn't exist | Parse error |
The quiet ones
These all deploy green. Nothing is broken enough to fail, and something doesn't work:
| What happens instead | |
|---|---|
traefik.* labels on a service with no DockPod domain | Forced to traefik.enable=false. The route never exists. A note in the build log is the only sign. |
Host port 80 or 443 | Dropped. Note only. |
network_mode: host on a service with a domain | Deploys fine, and the domain never routes — the container is on no Docker network for Traefik to reach. Warning only. |
| Services declaring their own networks | DockPod re-adds the project default alongside. Note only. |
| A bind-mount source path that doesn't exist | Docker creates an empty directory there. The container starts with empty config instead of yours. Nothing warns, because nothing is wrong as far as Docker is concerned — this one is not something DockPod can catch for you. |
If a deploy is green and the site isn't, read the build log first and then check that last row.
Why there's no host port for the web service
A published host port earns its keep only for a protocol Traefik can't route. Everything else is better without one:
- Reachable from the internet — that's what the domain does. Traefik routes over the shared internal network.
- Service-to-service — automatic, by service name, with zero
ports:entries. Two unrelated projects can both run Postgres on container port 5432 forever without colliding, as long as neither publishes it. - Debugging — every container's Console tab drops you into a shell over the Docker API, published port or not.
Skipping ports: is also what lets one compose file serve both local development and a DockPod deploy with no edits.
The Client's Real Address
Your container never talks to the visitor directly — Traefik does. So the TCP peer your app sees is Traefik's address on the app network, something like 172.26.0.4, and that is what every web framework reports as the client address until you tell it otherwise.
The visitor's address arrives in X-Forwarded-For. DockPod's Traefik is configured to trust that header from Cloudflare's published edge ranges and from the private address space, and to discard it from anywhere else — so the header is reliable, and a caller reaching your origin directly cannot forge it.
What's left is for your framework to read it. Every one of them needs to be told which proxies to trust, and none of them trust anything by default:
| Laravel | $middleware->trustProxies(at: [...]) in bootstrap/app.php |
| Symfony | framework.trusted_proxies |
| Django | USE_X_FORWARDED_HOST, plus a middleware for the address itself |
| Express | app.set('trust proxy', [...]) |
| Rails | config.action_dispatch.trusted_proxies |
Two things are worth getting right.
Name the ranges; don't trust everything. A wildcard looks like the easy answer and is the one setting that reintroduces the problem. Frameworks read X-Forwarded-For from the right, returning the first address that isn't a trusted proxy — so when every hop is trusted they return the leftmost entry. Cloudflare appends to that header rather than replacing it, which means a caller can put any address it likes at the front and have it come back as the visitor, logged as the visitor, and counted as the visitor by anything rate limiting on it. Trust loopback, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and — if you're behind Cloudflare — its edge ranges.
Watch when your code reads the address. Frameworks apply trusted-proxy settings in a middleware, so anything that runs before it still sees the raw peer. A request-id or logging middleware installed at the very front of the stack is the usual way this goes wrong: the setting is correct, and the logs still show the proxy.
The same header carries X-Forwarded-Proto, so trusting the proxy is also what stops a TLS-terminated site from generating http:// URLs and breaking signed links.
Related
- Git Deployments → Compose Best Practices — the same ground plus what makes a service safe to scale
- Database Provisioning — for a database managed by DockPod rather than declared in your compose file
- Security — the hardening DockPod applies to every service