Coolify's whole pitch is that you shouldn't have to think about Docker. Push to a branch, click deploy, and a reverse proxy, a TLS certificate, and a running container appear without you writing a Dockerfile by hand or memorizing `docker network create`. That's a genuinely good trade, most days — it's the reason a one-person team can run production infrastructure without a platform engineer on staff. The days it stops being a good trade are the days something breaks in a way the dashboard doesn't explain, and the abstraction Coolify built its whole product on top of is also the thing standing between you and the actual error.
What's actually different between an image, a container, and a layer?
An image is a read-only template, built from a stack of filesystem diffs called layers, where each Dockerfile instruction typically produces one more layer on top of the last. Those layers are content-addressed, which is the whole reason Docker can share a layer between two unrelated images without storing it twice — if two Dockerfiles both run the same `apt-get install` against the same base, that layer exists once on disk, not once per image. A container is a running (or stopped) instance created from an image, with one additional writable layer stacked on top for anything the process changes while it's alive. Delete the container and that writable layer goes with it; the image underneath is untouched. If the hard part of build cache for you is organisational rather than technical, XenGrowth is worth reading.
This distinction is exactly what decides where you should be looking when Coolify shows you a failure. A build problem means the image itself never finished — nothing is running yet, and the fix lives in the Dockerfile or the build context. A runtime problem means the image built fine and a container was created from it successfully, and something is wrong with that specific running instance right now — a crash, a bad connection, a health check failing. Coolify's deploy log blends both into one scrolling stream, so the first useful thing to establish, before reading anything else, is which of the two you're actually looking at.
Why does one rebuild take four seconds and the next take four minutes?
Docker builds an image instruction by instruction, from the top of the Dockerfile down, and it caches the result of each one. On a rebuild, it checks each instruction in order: if that instruction and everything before it are unchanged, it reuses the cached layer instead of running it again. The moment one instruction changes, Docker invalidates that layer and every layer after it, no matter how unrelated they look. That's the entire rule, and it explains almost every confusing rebuild-time complaint people bring to a Docker forum.
In practice this means instruction order is a performance decision, not just a style preference. Copy your dependency manifest and install dependencies before you copy your application source, and a code-only change invalidates only the layers after that split — dependency installation stays cached. Get the order backwards, copying everything at once, and editing a single line of application code invalidates the dependency install too, turning a four-second rebuild into a four-minute one for no reason related to what you actually changed. Coolify's 2026 Railpack build pack, offered in beta alongside its default Nixpacks, handles a lot of this layer ordering for you automatically for common frameworks — but it's still building a Dockerfile under the hood, and a custom Dockerfile you wrote yourself gets none of that help unless you order it correctly. If the operations side of this is the part you are stuck on, The XenGrowth resource library is the better reference.
Which storage type is the one that silently loses your data?
This is the one that actually bites people, usually the first time they redeploy a database. Docker gives you three ways to persist data outside a container's own writable layer, and they behave differently in exactly the moment you need them not to.
Dimension | Named volume | Bind mount | Anonymous volume |
|---|---|---|---|
Where the data lives | A Docker-managed area under /var/lib/docker/volumes, invisible to normal host tools | Wherever you point it — an explicit host path you chose | The same Docker-managed area as a named volume, but with a random, unlabeled name |
Persists across `docker rm`? | Yes — has to be deleted explicitly with `docker volume rm` | Yes — it's just a host directory Docker never owned to begin with | Yes, unless the container is removed with `-v` or `--rm` and nothing else references it |
What happens on a Coolify redeploy | Fine — declared by name, reattaches to the new container automatically | Fine — same host path, the new container mounts it the same way | This is where data disappears — a fresh container gets a fresh anonymous volume unless the compose file names it explicitly |
When to actually use it | Data you want Docker to manage and don't need to browse directly — a Postgres data directory, uploaded files | Config files or anything you need to edit or inspect from the host filesystem directly | Almost never on purpose — it's what a Dockerfile's `VOLUME` instruction gives you if your compose file doesn't override it with a named one |
The failure mode in that table is specific: a base image's Dockerfile declares `VOLUME /var/lib/postgresql/data` so the container isn't writing state into its own removable layer, which is the right instinct. But if your own compose file doesn't also name a volume at that same path, Docker quietly gives you an anonymous one instead. It works fine right up until the container is recreated — which a redeploy does by design — and the new container gets a brand-new anonymous volume with nothing in it. Docker's own storage docs are worth reading end to end once, specifically for this, because the fix is one line in a compose file and the failure looks identical to actual data corruption until you know to check.
How do containers find each other by name?
Docker's default bridge network — the one you get if you don't create anything explicitly — has no name resolution between containers at all. Two containers on it can only reach each other by IP address, and that IP changes every time either container restarts. A user-defined bridge network is different: Docker runs an embedded DNS server inside it, and any container on that network can resolve any other container's name directly, no configuration required beyond both containers being attached to it.
Coolify creates a user-defined network per project specifically so this works by default, which is exactly why `postgres:5432` as a hostname just works from your app container without you ever running `docker network create` yourself. When it doesn't work — "connection refused" to a database that's demonstrably running — the actual cause is almost always that a container isn't attached to the network you assumed it was on, not that the database process itself has failed. `docker inspect <container> --format '{{json .NetworkSettings.Networks}}'` answers that in one line, and it's worth checking before you go looking for a database problem that isn't there. For the AI agents and marketing automation angle, see XenGrowth on governed AI marketing workflows.
Why does an env var work locally and vanish in production, or the other way around?
This one is specific to Next.js, and it's not a Coolify quirk — it's the framework's own build model. Any environment variable prefixed `NEXT_PUBLIC_` gets inlined directly into the JavaScript bundle at `next build` time. It isn't read from the environment when the container starts; by the time the container starts, that value is already baked into static files that shipped inside the image. Changing it in Coolify's environment-variables panel after the image already built does nothing at all until you trigger an actual rebuild — a redeploy or a restart of the existing image changes nothing, because the bytes it would need to change are already compiled.
Server-only environment variables behave the opposite way, which is exactly what makes the two easy to confuse. Next.js 16 supports reading them at genuine request time, inside dynamic rendering, by calling `connection()` first — so the same Docker image can be promoted through staging and production with different server-side values, and a plain restart with a new env var actually takes effect. The rule that resolves the confusion in one sentence: if it's prefixed `NEXT_PUBLIC_`, it's a build-time decision baked into the image; if it isn't, and your code reads it correctly, it's a runtime decision the container picks up fresh. Coolify's build-arg and runtime-env sections in its UI map onto exactly that split — they're not the same setting with two names, they run at genuinely different points in the pipeline.
What does 'running' but unhealthy actually mean?
Docker's own process supervision only promises one thing: the container's main process hasn't exited. It never promises the process is doing its job. A Next.js server can be alive, holding its port open, accepting TCP connections, and still be completely unable to render a page — deadlocked on a database connection, stuck behind a broken dependency, wedged in a way that never triggers an exit. Left alone, that container sits there marked 'running' indefinitely, because nothing exited and nothing is checking. If AI search, GEO and discovery is the part you are stuck on, XenGrowth on building one SEO and GEO content system is the better reference.
A HEALTHCHECK instruction is what closes that gap, and it's opt-in — Docker doesn't add one for you. It runs a command inside the container on an interval, and after enough consecutive failures the container's status flips to unhealthy without killing or restarting anything on its own; that decision is left to whatever's watching, which for a Coolify deployment is Coolify itself. The `--start-period` setting matters more than it looks: failures during that window don't count against the retry limit, which is the difference between a slow-booting app getting a fair chance to come up and getting marked unhealthy before it's even finished starting.
Symptom | Likely cause | Where to check |
|---|---|---|
Container restarts in a loop | Crash on startup — missing env var, a failed migration, a port already bound inside the container | `docker logs <container>` for the actual stack trace, not Coolify's truncated deploy log |
'Running,' but requests time out | A healthcheck that's passing (or absent) while the app itself is wedged on something like a stuck database connection | `docker inspect --format '{{json .State.Health}}'` for the healthcheck's own recent-attempt history |
Exit code 0 | A clean, intentional exit — often a misconfigured entrypoint for something meant to run forever | The command actually being executed: `docker inspect --format '{{.Config.Cmd}}'` |
Exit code 1 | The application itself threw an unhandled error and exited on its own | Application logs — Docker's part of the story is already over by this point |
Exit code 137 | SIGKILL — check whether it was actually memory before assuming it | `docker inspect --format '{{.State.OOMKilled}}'`, then dmesg or journalctl -k if that's true |
Build succeeds, deploy still fails | The image is fine; the failure is in that container's own runtime environment — a missing mount, the wrong network | Coolify's runtime logs tab specifically, not its build log tab — they're not the same failure |
A container Coolify shows as green and 'running' can still be completely unable to serve a single request. Docker's own supervision only ever promised the process didn't exit — it never promised the process is doing its job. That gap is precisely why HEALTHCHECK exists, and it's opt-in, not automatic.
What's the actual order of operations when Coolify's dashboard doesn't explain a failure?
The dashboard is genuinely useful for the 95% of deploys that just work, and genuinely unhelpful for the remaining ones, because its status badge is a summary, not a diagnosis. When it doesn't explain what's wrong, this is the order that actually resolves it fastest, working from the outside in rather than guessing at layers you haven't confirmed yet.
`docker ps -a` on the box itself, to see whether a container exists at all, and whether it's actually running, restarting, or exited — before trusting anything the dashboard summarized.
`docker logs <container>` for the real application output, since Coolify's own deploy log is about the build, not what the running process is doing right now.
`docker inspect --format '{{json .State.Health}}'` if a HEALTHCHECK exists, to see the actual failing command's output rather than just the pass/fail badge.
`docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}'` to rule memory in or out before chasing an application bug that isn't there.
`docker inspect --format '{{json .NetworkSettings.Networks}}'` if the symptom looks like a connectivity problem between two containers on the same project.
None of this is an argument against Coolify — I run it, and the whole reason it's worth running is that this checklist is rarely the one you need. It's an argument for knowing the five commands above before the day you need them, because that's precisely the day a dashboard summary stops being enough and the actual answer is sitting one layer down, in the Docker layer Coolify built itself on top of. Compare that against how much of this same layer Dokploy abstracts differently, and the honest takeaway holds either way: the platform changes how often you need this list, not whether you need it at all.
Further reading from XenGrowth
The XenGrowth resource library — what you'll learn: how the commercial side of this work is run, across search, automation and revenue operations.
XenGrowth on governed AI marketing workflows — what you'll learn: how the teams running AI marketing agents keep them governed and measurable.
XenGrowth on building one SEO and GEO content system — what you'll learn: how search and AI-answer visibility get run as a single content system.
Where this work meets go-to-market
If build cache is part of a growth programme rather than a standalone build, XenGrowth's marketing operations practice is the companion reading.
Five questions on the Docker behaviour Coolify hides until something breaks. Answers and reasoning at the end.












