TT Lab
Get started
Learn Learning paths Courses

Volumes, Networks and Compose

What It Means to Declare a Stack in One File

Continue in TT Lab

One-line summary

A Compose file is a declarative record of the run arguments of several containers. It is not magic; it corresponds one-to-one with docker run flags.

Why this is needed

To start three containers by hand, you have to create a network, create a volume, and attach --network, -v, -e and -p to each in the exact order. There is no problem when one person does it once, but from the moment a second person tries to reproduce the same stack, it becomes hell. Because it is written down nowhere.

This is the problem Compose actually solves. The shape of the stack is written in a single file, and that file is committed to the repository.

How it works

The key correspondences are as follows.

Compose key docker run equivalent
image The image argument
command The command after the image
ports -p
volumes -v / --mount
environment -e
networks --network
depends_on (no equivalent — startup order only)

Two things need to be pointed out here.

First, Compose automatically creates a user-defined network for each project and registers the service names as aliases. That is why it happens that "something that worked fine in Compose does not work when moved to docker run". The cause is not Compose magic but that you did not create the network when moving it by hand.

Second, depends_on guarantees only order, not readiness. A DB container having "started" is different from the DB being "ready to accept connections". So you have to attach a health check condition too, or make the application retry. If you miss this distinction, a flood of connection errors pours out during the first few seconds of every deployment.

What it looks like in the field

A pattern often used in real setups is splitting the network in two. Put only the reverse proxy on the frontend network, put the DB and cache on the backend network, and make only the app a member of both. Then the DB cannot even have its name resolved from the proxy side.

And do not confuse expose with ports. expose does not open a port on the host. It is for documentation. For a service that only containers talk to, it is right not to use ports at all.

Finally, the moment the declaration and reality diverge is the start of an accident. If someone fixes a container by hand, the file and reality split apart, and nobody knows about that split until the next deployment. That is why the habit of comparing the declaration with reality matters.

How to set dependency order properly

depends_on alone is not enough. It means the container has started, not that the service is ready. If the DB container is up but PostgreSQL is still initializing, the application dies with a connection failure.

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s          # 이 동안의 실패는 세지 않는다
  app:
    build: .
    depends_on:
      db:
        condition: service_healthy    # ← 건강해질 때까지 기다린다

start_period matters. Without it, failures during initialization eat up the retry count, and a slow service is judged unhealthy forever.

Even so, the application must know how to reconnect. In production there is no compose, and the DB sometimes restarts. A health check is a development convenience, not a substitute for reconnection logic.

Layering files per environment

Compose files can be layered.

docker compose -f compose.yaml -f compose.dev.yaml up

The later file overrides the earlier one. Put the common parts in compose.yaml, and the development mounts and debug ports in compose.dev.yaml. compose.override.yaml is applied automatically just by its name, so use it for a developer's personal settings (put it in .gitignore).

# compose.dev.yaml — 소스를 마운트해 즉시 반영
services:
  app:
    volumes: ["./src:/app/src"]
    environment: {DEBUG: "1"}
    command: ["python", "-m", "uvicorn", "app:app", "--reload"]

The boundary between compose and Kubernetes

What runs well in compose does not work unchanged in Kubernetes. Here are the things that get in the way when moving.

compose Kubernetes
depends_on None — use an initContainer or retries
DNS by service name The same (the Service name)
volumes: ./src:/app hostPath — avoid in production
restart: always The default behavior (restartPolicy)
ports: 8080:80 Service + Ingress

There are conversion tools such as kompose, but you must not use the result as it is. Resource requests and limits, probes and the security context are all empty. The conversion is a starting point, and you have to fill it in by hand.

What you will do in the next lab

You will declare a stack of two services in a Compose file, start the same stack by hand as well, and directly compare whether the declared values match the values actually running.