Docker Networking, Volumes & Health Checks
Make four containers find each other, keep data across a restart, and start in an order that actually works.
- Time
- 50 min
- Level
- Beginner
- Objectives
- 4 objectives
- Cost
- Free
Where this fits in the platform
This lab adds
- Containers that persist data and report their own health
Before you start
You will need
- Docker
- Docker Compose
You do not need these already — the lab environment below provides them.
You will be able to
- Reach one container from another by name
- Tell a bind mount from a volume, and pick correctly
- Use a health check to control startup order
Cost — Free
— Docker on your own machine.
Nothing to pay in the browser. Open the terminal runs this against a simulated cloud — the same API calls and the same commands, with no account and no bill. The figure above applies only if you build it in your own.
The scenario#
The application connects fine on your machine and fails in the pipeline with 'connection refused'. It starts before the database is ready, and everything it wrote last night is gone.
These three problems are the same three you will meet again as Services, PersistentVolumes and readiness probes — which is why this lab sits before Kubernetes.
Hands-on environment
Run this lab in a real terminal, free and in your browser. The environment is temporary and yours alone — break it as much as you like.
Open the terminalOpens in Killercoda, in a new tab — keep this page open for the steps.
Run it on your own machine
Run this lab on your own machine. One command starts the environment, with everything the lab needs already installed:
You will need:
- docker
git clone https://github.com/EgyKode/EgyKode-lab.git
cd EgyKode-lab
./egykode start
./egykode shellYou need Docker and Git installed. Everything else runs inside the environment. The first start downloads it and takes a few minutes; later starts are seconds.
Not sure what you already have? Run: npm run doctor — it checks and changes nothing.
Anything you tick here is your own record. EgyKode cannot see inside that terminal, so the success criteria stay self-assessed even when the environment checks your work for you.
Names, not addresses
Step 1 of 5
What you are proving: You can reach a service by name, with no IP address written down anywhere
Marking this settles success criterion 1.
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: labonly
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 5
cache:
image: redis:7-alpine
app:
image: curlimages/curl
command: ["sh", "-c", "sleep 3600"]
environment:
DB_HOST: db # a name, not an IP
REDIS_HOST: cache
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
volumes:
pgdata:docker compose up -d
docker compose exec app sh -c 'nslookup db; nslookup cache'
docker compose exec app curl -s --max-time 3 telnet://db:5432 && echo reachableCompose puts every service on one network and makes each service name resolvable. Container IPs change on every recreate, so a hardcoded address is broken by design.
What you are proving: You can explain why two containers on separate networks cannot see each other, having proved it
Marking this settles success criterion 4.
docker network create isolated
docker run -d --name lonely --network isolated alpine sleep 3600
docker exec lonely ping -c1 db # fails: different networkA container can only reach what shares a network with it. This is the same model as a Kubernetes NetworkPolicy, learned in thirty seconds.
What you just observed is a network namespace — the kernel giving each container its own interfaces, addresses and ports. Look at it directly:
docker exec lonely ip addr # its own interfaces, not the host's
docker exec lonely hostname # a UTS namespace: its own hostname
docker exec lonely ps aux # a PID namespace: it is PID 1 to itselfThree namespaces, three commands. The isolation is not Docker's — Docker asked the kernel for it, which is why the same primitives reappear under Kubernetes.
What you are proving: You can choose between a volume and a bind mount by what survives a rebuild
Marking this settles success criterion 2.
docker compose exec db psql -U postgres -c "CREATE TABLE t (id int); INSERT INTO t VALUES (1);"
docker compose down # containers destroyed, named volume kept
docker compose up -d
docker compose exec db psql -U postgres -c "SELECT * FROM t;" # still there
docker compose down -v # -v ALSO removes named volumes| Named volume | Bind mount | |
|---|---|---|
| Written as | pgdata:/var/lib/... | ./src:/app |
| Managed by | Docker | You |
Survives down | Yes | Yes (it is your directory) |
Removed by down -v | Yes | No |
| Right for | Databases, state | Source code in development |
down -v is the command that deletes data. It is one character from down,
and it is why the backup lab exists.
What you are proving: You can make a dependent service wait for readiness rather than for a process to exist
Marking this settles success criterion 3.
docker compose down && docker compose up -d
docker compose ps # db shows "healthy", not merely "running"Plain depends_on waits for the container to start, which for PostgreSQL is
several seconds before it accepts connections. That gap is exactly why an app
crashes on first boot in CI and works locally, where the database was already
running.
condition: service_healthy ties startup to the health check instead:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 5
start_period: 10s # grace before failures countThis is a readiness probe. The Kubernetes lab later uses the same idea with different syntax.
What you are proving: You can break each mechanism on purpose and recognise the symptom it produces
This step settles no success criterion on its own.
# wrong service name
docker compose exec app curl -s --max-time 3 telnet://database:5432 || echo "DNS: no such host"
# right host, wrong port
docker compose exec app curl -s --max-time 3 telnet://db:5433 || echo "refused or timeout"Learn the difference now: no such host is name resolution, refused means something answered, timeout means nothing did.
could not resolve host between containers
They are on different networks, or you used the container name instead of the service name. docker network inspect <net> lists members.
Data disappears after down
You used down -v, or the volume is anonymous. Named volumes in the top-level volumes: block persist.
The app still starts too early
depends_on without condition: service_healthy waits only for start. The dependency needs a healthcheck for the condition to mean anything.
The healthcheck never turns healthy
The command runs inside the container — pg_isready must exist there. docker inspect --format '{{json .State.Health}}' <c> shows the output.
Clean up#
Run this even if you did not finish.
docker compose down -v
docker network rm isolated
docker volume prune -fCost of this lab: Free — Docker on your own machine.
Success criteria
0 of 4
The concept behind it
Next up
Lab 10 of 59 on the project path
Previous: Production-Grade Multi-Stage Dockerfile for Django & Gunicorn