Networking, volumes and Compose
Bridge networks, port publishing, volumes for state, and Compose for a service with Postgres and Kafka beside it.
One container is a process; a service is several — the application, its database, a cache, a broker — that must find each other, keep data past a restart, and be started together the same way on every machine. Docker's networks, volumes and Compose are the three answers, and each has one rule that the first outage teaches.
Networks: containers find each other by name
Every container is on a network, and on a user-defined bridge network Docker runs a DNS server that resolves container names to addresses. postgres in a connection string is a hostname, resolved to whatever address that container has today:
jdbc:postgresql://postgres:5432/orders # inside the network: the container's name
jdbc:postgresql://localhost:55432/orders # from the host: the published portTwo things follow. Ports inside a network are not published. The database listens on 5432 and every container on the network can reach it there; the host cannot, unless -p 55432:5432 maps a host port onto it — and that mapping is for the developer's psql, not for the application, which should never reach its own database through the host. And localhost inside a container is the container, so an application configured with localhost:5432 is looking for a database inside itself; the Testcontainers lesson's getHost() and getMappedPort() exist for exactly this. host.docker.internal reaches the host from a container when you genuinely need to.
The default bridge network (the one containers land on with no --network) has no DNS; give every project its own network, and Compose does so for you. A container that must not talk to anything gets --network=none, which is how this repository's runner isolates a learner's submission: no DNS, no route, nothing to exfiltrate to.
Volumes: data that outlives the container
A container's writable layer is deleted with it. A database that writes into that layer loses everything when the container is removed, which is the second thing the first outage teaches. A volume is a directory Docker manages on the host, mounted into the container at a path, and kept until you delete it:
docker volume create orders-pg
docker run -d --name postgres -v orders-pg:/var/lib/postgresql/data postgres:16-alpineA bind mount (-v /home/me/project:/src) mounts a host directory instead, for development — live-reloading sources, a config file — and never for a database in production, where the ownership and permission mismatch between the host's uid and the container's becomes a Saturday-night problem. The USER 10001 from the images lesson has to be able to write the mounted path; --chown on the COPY handles the image, and an init container or an entrypoint chown handles the volume.
Backups are the volume's, not the container's: pg_dump through the running database, or a docker run --volumes-from that tars the directory while the database is stopped. This repository's nightly backup runs pg_dump from a systemd timer and copies the result off the machine; the same job, in a container, is a second container on the same network with the same schedule.
Compose: the whole service in one file
services:
api:
build: .
ports: ["8080:8080"]
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/orders
SPRING_DATASOURCE_USERNAME: app
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
SPRING_PROFILES_ACTIVE: local
depends_on:
postgres: { condition: service_healthy }
kafka: { condition: service_started }
deploy: { resources: { limits: { memory: 1g, cpus: "2" } } }
postgres:
image: postgres:16-alpine
environment: { POSTGRES_USER: app, POSTGRES_PASSWORD: ${DB_PASSWORD}, POSTGRES_DB: orders }
volumes: ["pgdata:/var/lib/postgresql/data"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d orders"]
interval: 5s
retries: 10
kafka:
image: apache/kafka:3.9.0
profiles: ["messaging"]
volumes:
pgdata:docker compose up builds the image, creates the network and the volume, starts the dependencies in order, and streams every container's logs. What each line is for: depends_on with condition: service_healthy waits for the health check, not just the container start — Postgres accepts connections a few seconds after the process starts, and an API that connects at second one crashes; ${DB_PASSWORD} comes from the shell or a .env file beside the compose file, which is gitignored exactly as the application's .env is; deploy.resources.limits are the cgroup limits the last lesson's JVM will read, so the container matches what production gives it; and profiles keeps the broker out of docker compose up until --profile messaging asks for it, so the common case starts fast.
docker compose down stops and removes the containers and the network and keeps the volumes; adding the volumes flag to it removes those too, which is the command for "start the database from nothing" and the one to type carefully.
Secrets, and what the environment is for
Environment variables are the right place for configuration and the wrong place for credentials in production: every process in the container can read them, they appear in docker inspect, and they are copied into any child process. Compose and Swarm have a secrets: section (a file mounted at /run/secrets/<name>), Kubernetes has its Secret objects mounted the same way, and Spring Boot reads either as a property source with spring.config.import=configtree:/run/secrets/. For local development the .env file is fine; the line to hold is that the compose file itself, committed, never contains a value — only a reference.