Dash0 Raises $110M Series B at $1B Valuation

  • 10 min read

What's The Difference Between links and depends_on in Docker Compose

You're reading an older compose.yaml, or following a tutorial that hasn't aged well, and you hit a service with both links and depends_on declared. They look like they're doing roughly the same thing, pointing one service at another, so you're not sure whether you need both, one, or neither.

The short version: links is a legacy feature that did two unrelated jobs at once, wiring up network connectivity between containers and forcing a startup order. Modern Compose handles the connectivity part automatically through its default network, and depends_on covers the ordering part on its own. In a current project you almost never want links. This article walks through what each one does, how depends_on behaves with and without health check conditions, and why Compose networking has made links redundant.

links connected one service's container to another's and gave it a resolvable hostname. You could keep the service name or assign an alias with the SERVICE:ALIAS syntax:

yaml
12345678
services:
web:
image: nginx
links:
- db
- db:database
db:
image: postgres

Here the web container could reach the database at db or at database. Under the hood, links added entries to the container's /etc/hosts file so those names resolved to the right IP. As a side effect, it also created a startup dependency, so db started before web, the same ordering you'd now write explicitly with depends_on.

That hostname injection mattered on Docker's original default bridge network, where containers had no automatic name resolution and could only find each other by IP unless you linked them. links was the workaround. Docker's own reference now flags it as a legacy option, kept around for backward compatibility rather than for new compose files. Compose resolves service names by itself now, which is the whole reason links faded out.

What depends_on actually controls

depends_on does one thing, and it is not networking. It controls the order in which Compose starts and stops services. It never touches hostnames, /etc/hosts, or the Domain Name System (DNS). In the short form, you list the services a given service depends on:

yaml
12345678910
services:
web:
image: nginx
depends_on:
- db
- redis
db:
image: postgres
redis:
image: redis

When you run docker compose up, Compose creates db and redis before web, and tears them down in the reverse order. So far so good. The trap is what "depends on" means by default: Compose waits only until the dependency's container has started, not until the application inside it is ready to accept connections.

That distinction bites people constantly. A Postgres container reports as started within a fraction of a second, but the database itself needs longer before it accepts queries. With a plain depends_on: [db], your web service launches the moment the db container is up, tries to connect, and crashes because Postgres isn't accepting connections yet. Compose did exactly what you asked. It started db first. It just never promised the database would be usable by the time web came looking.

depends_on with health check conditions

To wait for actual readiness, switch to the long form and attach a condition. Compose supports three:

  • service_started is the default, equivalent to the short form. It only waits for the container to start.
  • service_healthy waits until the dependency passes its health check.
  • service_completed_successfully waits until the dependency exits with code 0, which suits one-off jobs like database migrations.

service_healthy is the one that solves the Postgres race above, but it has a requirement: the dependency needs a healthcheck so Compose knows what "ready" means. Without one, there's no signal to wait on. Here's the database defining readiness through pg_isready, with the app waiting on it:

yaml
12345678910111213
services:
web:
image: my-app
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5

Now web starts only after db reports healthy, not merely started. The service_completed_successfully condition covers a different shape of problem. If you run schema migrations as their own service, you want the app to wait for that job to finish cleanly before it boots:

yaml
123456789
services:
app:
image: my-app
depends_on:
migrate:
condition: service_completed_successfully
migrate:
image: my-app
command: ["./run-migrations.sh"]

You can also mark a dependency as optional with required: false, which tells Compose to warn instead of fail when the dependency isn't available. None of this has any equivalent in links, which only ever knew "start this one first" with no concept of health or completion.

The reason you can drop links entirely is that Compose sets up networking for you. Every project gets a default network, and each service joins it automatically. Docker runs an embedded DNS server on that network, so any service can reach any other by its service name. You never touch /etc/hosts, and you never declare links.

Concretely, given this file, the web service can connect to the database at the hostname db straight away:

yaml
1234567
services:
web:
image: my-app
environment:
DATABASE_HOST: db
db:
image: postgres:16

The name db comes from the key under services:, and Compose's DNS resolves it to the database container's current IP. That is exactly the connectivity links used to provide, except it happens by default. If a service needs to answer to additional names, you use network aliases under the networks key rather than reaching back for the legacy links syntax.

So the two halves of the old links behavior have gone their separate ways. Connectivity is automatic through the default network, and ordering is something you ask for explicitly with depends_on. Neither one needs links.

Common pitfalls

depends_on does not wait for readiness unless you tell it to. This is the single most common surprise. The default service_started condition checks only that the container process launched. If your dependent service connects to something on boot, use condition: service_healthy and give the dependency a real health check. Anything less and you're relying on timing luck.

depends_on is ignored in some deployment paths. It applies to docker compose up on a single host. It has no effect when you deploy a compose file to Swarm with docker stack deploy, and it does not coordinate across separate compose projects. If you're depending on startup order outside a single local stack, the application itself needs retry-and-reconnect logic, which is good practice regardless.

Don't reintroduce links for hostnames you already have. Because the default network already resolves service names, adding links to "make db reachable" duplicates something Compose does for free, and it drags in the implicit start-order side effect that you should be expressing with depends_on instead. Reach for network aliases if you genuinely need an extra name.

Final thoughts

The clean mental model is that links is a legacy two-in-one that Compose has since unbundled. Connectivity comes from the default network and its DNS, and ordering comes from depends_on, with health check conditions when you need to wait for a service to be genuinely ready rather than merely running. New compose files should use depends_on and service-name DNS, and treat links as something you only encounter while modernizing old configuration.

Getting the startup order right in the compose file is one thing. Knowing whether your services are actually healthy and talking to each other once they're running is another, and it doesn't show up in compose.yaml. You can check a container's resource usage or read its Compose logs one service at a time, but that stops scaling the moment your stack grows past a handful of containers. Dash0's infrastructure monitoring tracks container health and network activity alongside real-time logs and distributed traces, so you can see which dependency a service is waiting on, and where a connection is actually failing, instead of inferring it from a crash loop. Start a free trial to see your containers, logs, and traces in one view. No credit card required.