Skip to content

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.* labelsDockPod generates all routing from the domain you add in its UI. See above.
A dockpod-proxy network, in any form — above all external: trueThe 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 ownDockPod pins the project's default network and attaches Traefik to it.
Host port 80 or 443Traefik owns both. Docker would refuse the second bind, so the binding is dropped.
network_mode: host on anything that needs a domainThe 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 allIt's reached through a DockPod domain over the internal network, on the container's port — never the host's.
No ports: on internal services eitherContainers in a project reach each other by service name: postgres:5432, redis:6379.
A healthcheck on the web serviceDockPod 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.limitsYour limits stand. DockPod fills in only the ones you left out.
Your own named networksDockPod re-adds the project's default network alongside them, so domain routing and database links keep working.
Published host ports other than 80/443Kept 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:

yaml
networks:
  dockpod-proxy:
    external: true

Two 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 found

The 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 publishesservice "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 existNamed, 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 unsetStops immediately with your own message. Set it under the app's Environment Variables.
A mistyped image tag, or a private registry with no credentialsThe pull fails
depends_on with condition: service_healthy on a healthcheck that never passesWaits, then fails
Invalid YAML, or a Compose key that doesn't existParse 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 domainForced to traefik.enable=false. The route never exists. A note in the build log is the only sign.
Host port 80 or 443Dropped. Note only.
network_mode: host on a service with a domainDeploys fine, and the domain never routes — the container is on no Docker network for Traefik to reach. Warning only.
Services declaring their own networksDockPod re-adds the project default alongside. Note only.
A bind-mount source path that doesn't existDocker 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
Symfonyframework.trusted_proxies
DjangoUSE_X_FORWARDED_HOST, plus a middleware for the address itself
Expressapp.set('trust proxy', [...])
Railsconfig.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.

Released under a commercial-friendly freemium license.