By the end you'll know
- How containers talk to each other, and why
localhostdoesn't work for that - How to let containers find each other by name
- What Docker Compose is, and how to run a whole app from one file
- Why you shouldn't publish your database's port
The problem: apps come in pieces
Real apps are rarely one container. A web app usually has a database, often a cache, sometimes more. Each runs in its own container, and they need to talk to each other.
You might think the app could reach its database at localhost. Lesson 4 showed why not: inside a container, localhost means that container itself. The database is in a different container, so the app knocking on its own door finds nobody home.
What the app needs is a way to say "the database", by name, and have Docker work out where that is.
Networks: containers that know each other's names
A Docker network is a private network that containers can join. On a network you create, Docker runs a small built-in DNS (the system that turns names into addresses, also covered in the Kubernetes Services lesson): every container can reach every other by its container name.
-p does. Most offices only need one outside line, at reception, and that's true of most apps too.Try it: two containers on one network
Create a network, and start Redis on it. Redis is a fast in-memory database, often used as a cache:
docker network create appnet
docker run -d --name cache --network appnet redis:7.2.7-alpine
Now start a second, throwaway container on the same network, and ask it to reach Redis by name. redis-cli is Redis's own command-line tool, and ping asks Redis to answer:
docker run --rm --network appnet redis:7.2.7-alpine redis-cli -h cache ping
PONG. The second container found the first by its name, cache. Notice there's no -p anywhere: containers on the same network can reach each other's ports directly. -p is only for traffic from your laptop.
Now try the same thing without joining the network:
docker run --rm redis:7.2.7-alpine redis-cli -h cache ping
It fails: the name cache can't be found, because this container isn't on appnet. Only containers on the same network share the directory.
Clean up before the next part:
docker rm -f cache
docker network rm appnet
Docker Compose: the whole app in one file
That worked, but imagine typing all of it every time: a network, a database with a volume (Lesson 5), an app with its settings, ports and the right network. Miss one flag and something doesn't connect.
Docker Compose puts all of that in one file, compose.yaml, and starts everything with one command. It's included with Docker Desktop. The file lists services: each one is a container, with everything you'd otherwise type after docker run.
You'll make the app from Lessons 4 and 5 count its visits in Redis.
In your hello-app folder, add one line to requirements.txt, so it reads:
flask==3.1.3
redis==8.1.0
Replace app.py with this version. The new parts connect to Redis and add one to a counter on every visit:
import os
import socket
import redis
from flask import Flask
app = Flask(__name__)
GREETING = os.environ.get("GREETING", "Hello")
cache = redis.Redis(host=os.environ.get("REDIS_HOST", "localhost"), port=6379) # where to find Redis
@app.get("/")
def hello():
visits = cache.incr("visits") # add 1 to the counter called "visits", and return the new total
return f"{GREETING} from {socket.gethostname()}! This page has been visited {visits} times.\n"
Your Dockerfile from Lesson 4 stays exactly as it is. Now save this as compose.yaml in the same folder:
services:
web:
build: . # build the image from the Dockerfile in this folder
ports:
- "8000:8000" # the one outside line: your laptop's 8000 to the app
environment:
GREETING: Hello
REDIS_HOST: cache # find Redis by its service name
depends_on:
- cache # start cache before web
cache:
image: redis:7.2.7-alpine # an existing image, nothing to build
volumes:
- cache-data:/data # Redis saves its data here; keep it in a volume
volumes:
cache-data: # create this named volume
Read it top to bottom and you'll recognize everything: build is docker build, ports is -p, environment is -e, volumes is -v. What you don't see is a network: Compose creates one for every app automatically, and each service is reachable by its service name. That's why REDIS_HOST is just cache.
Try it: run the whole app
From your hello-app folder:
docker compose up -d --build
up creates the network, the volume and both containers; -d runs them in the background, as with docker run; and --build builds the web image first. Check on them, and visit the app a few times:
docker compose ps
curl http://localhost:8000
curl http://localhost:8000
curl http://localhost:8000
visited 1 times, 2 times, 3 times: the app is talking to Redis by name. Logs work like docker logs, by service name:
docker compose logs web
Now stop and remove everything, then start it again:
docker compose down
docker compose up -d
curl http://localhost:8000
visited 4 times. down removed both containers and the network, but not the volume, so Redis's saved count was waiting when it came back. That's Lesson 5's volume, now set up by one line in a file.
To remove the volume too, and start from zero:
docker compose down -v
Don't publish your database's port
Notice that cache has no ports:. The web app reaches it over Compose's network, so there's no reason for anything else to. Publishing a database's port makes it reachable from outside, and databases left open on the internet are one of the most common ways data leaks. Give the outside world a line to the app, and only the app.
In my real setup
My production setup doesn't use Compose. It runs on Kubernetes, which the last lesson of this track maps Compose onto. But the idea is identical. My API reaches its Redis cache by name, civicgrid-redis, never by an IP address. Redis has no outside line at all, and a network rule goes further: only the API's pods are allowed to connect to it, so not even other apps in the same cluster can.
Redis is also the star of a story in Lesson 3: when my API had no Redis address to use, it didn't crash, it quietly counted on its own. That's why the break-it below matters. Make sure you can tell whether your app actually reached its database.
Break it on purpose
Point the app at localhost. In compose.yaml, change REDIS_HOST: cache to REDIS_HOST: localhost. Apply the change, and try the page:
docker compose up -d
curl http://localhost:8000
docker compose logs web
up -d notices the changed setting and recreates web. The page returns Internal Server Error, and near the bottom of the logs:
redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379. Connection refused.
"Connection refused" means the address exists but nothing is listening there. That's the web container's own localhost, where there's no Redis. Inside a container, localhost is always the container itself.
Misspell the service name. Change it to REDIS_HOST: cahce, and do the same again. This time:
redis.exceptions.ConnectionError: Error -2 connecting to cahce:6379. Name or service not known.
"Name or service not known" means the name itself couldn't be found on the network. Those two messages are worth remembering, because they point to different fixes: refused means "right place, nothing listening", usually the wrong host or port; not known means "no such name", usually a typo or a container on a different network.
Set it back to cache and run docker compose up -d once more.
Clean up
docker compose down -v
That removes the containers, the network and the volume. Keep your hello-app folder for Lesson 7.
Check yourself
Your app in one container tries to reach its database at localhost:5432, and the database runs in another container. What happens?
How does a container find another container on a network you created?
In compose.yaml, the app's setting is REDIS_HOST: cache. Where does the name cache come from?
You run docker compose down, then docker compose up -d. The visit counter continues from where it was. Why?
The logs say Connection refused when your app connects to its database. What does that usually mean?
Why does the cache service in compose.yaml have no ports section?
Key terms
- Network: a private network that containers can join. On networks you create, containers reach each other by name.
- DNS: the system that turns names into addresses. Docker runs a small one for each network.
- Docker Compose: a tool that runs a multi-container app from one file.
- compose.yaml: the file that describes the app's services, volumes and settings.
- Service: one container in a Compose app, reachable by its service name.
- docker compose up / down: start everything / stop and remove containers and network.
down -valso removes volumes. - depends_on: start one service before another. It doesn't wait for it to be ready, only started.
Go deeper
- Networking overview (opens in a new tab) in the official Docker docs.
- Docker Compose overview (opens in a new tab) and the Compose quickstart (opens in a new tab), which builds a very similar app.
- The Compose file reference (opens in a new tab) lists every setting.