One container is docker run. Two containers that have to find each other is a shell
script nobody wants to read:
docker network create pizza-net
docker volume create mysql-data
docker run -d --name mysql --network pizza-net \
-e MYSQL_ALLOW_EMPTY_PASSWORD=yes -e MYSQL_DATABASE=pizza \
-v mysql-data:/var/lib/mysql -p 3309:3306 mysql:8.4
docker run -d --name api --network pizza-net \
-e SPRING_DATASOURCE_URL='jdbc:mysql://mysql:3306/pizza' \
-p 8086:8085 pizza-api:devThat is not wrong, it is just unmaintainable. Compose is the same thing as a file you can read, diff and commit.
The file
name: pizza
services:
mysql:
image: mysql:8.4
restart: unless-stopped
ports:
- "3309:3306"
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: pizza
...
volumes:
- mysql-data:/var/lib/mysql
...
volumes:
mysql-data:Every key maps to a docker run flag you already know: image,
ports is -p, environment is -e,
volumes is -v, restart is --restart.
What you get for free is the part worth having: a private network with all the services on it, and DNS that resolves service names (lesson 10). Nothing declares that; it is what a project is.
The name at the top
name: pizzaThe project name prefixes the network (pizza_default), the volumes
(pizza_mysql-data) and the container names (pizza-mysql-1). It is how two
stacks coexist without colliding, and how Compose knows which containers a later
down refers to.
Set it explicitly. Left off, Compose uses the directory name — so cloning the repo into a differently-named folder gives you a second project with a second, empty set of volumes, and the database you were using appears to have vanished.
Building, not just pulling
api:
build:
context: ./pizza-springboot-backend
target: runtime
image: pizza-api:dev
restart: unless-stoppedbuild instead of (or alongside) image. context is lesson
5's build context — the directory sent to the builder, and what every COPY path is
relative to. target picks a stage from a multi-stage Dockerfile (lesson 7). There is no
dockerfile: key here because ./Dockerfile relative to the context is the
default.
With both build and image, Compose builds it and tags the result — so
the same file works for local development and produces a named image you could push.
⚠️ docker compose up does not rebuild. If an image already exists,
it is reused, however stale. This is the single most common Compose confusion:
docker compose up -d --build # rebuild changed images, then start
docker compose build --no-cache api # nuclear option for one serviceDo not set container_name
It is tempting and it is a trap. A container name is global to the Docker daemon, so this:
container_name: pizza-mysqlcollides with any other project that had the same idea:
$ docker compose up -d
Error response from daemon: Conflict. The container name "/pizza-mysql" is
already in use by container "ce7502b3be84...". You have to remove (or rename)
that container to be able to reuse that name.Which is exactly what happened when the full-stack file in this track met the demo app's existing
development compose file. Leave it off and Compose names them from the project —
pizza-mysql-1, pizza-api-1 — with no possibility of collision. It also
keeps --scale working, which a fixed name makes impossible.
Address services by service name (docker compose logs api) and the
container names never need to come up.
The commands
docker compose up -d # start everything, detached
docker compose up -d --build # ... rebuilding first
docker compose ps # what is running, and is it healthy
docker compose logs -f api # follow one service
docker compose exec api sh # shell into a running service
docker compose restart api # restart, same container
docker compose down # stop and remove containers + network
docker compose down -v # ... and delete the volumesThree distinctions worth having straight:
restart vs up -d. restart restarts the
same container with the same configuration. up -d notices that the configuration
changed and recreates what needs recreating — which is what you want after editing the file.
stop vs down. stop leaves the containers
in place, so start brings back the same ones. down removes them, and their
logs with them.
run vs exec. exec goes into a running
container. run starts a new, throwaway one from the same service definition — the way
to run a one-off migration or a shell when the service itself will not stay up.
compose.yaml, docker-compose.yml, and the hyphen
All of these still work, and the differences are historical:
docker-compose (with a hyphen) is Compose V1, a separate Python
program. End-of-life since 2023. docker compose (a space) is V2, a
plugin written in Go, and what everything here uses.
The filename: compose.yaml is the name the specification prefers;
docker-compose.yml is the older one and is still found first for compatibility. Both
work. The demo app has one of each, which turned out to be a useful way of keeping two very
different stacks from being confused for one another.
And the version: "3.8" line at the top of every compose file you have ever seen is
obsolete. V2 ignores it and warns about it. Delete it.