11 KiB
Compose for development stacks
Dockerfile = great to build one container image.
What if we have multiple containers?
What if some of them require particular docker run parameters?
How do we connect them all together?
... Compose solves these use-cases (and a few more).
Life before Compose
Before we had Compose, we would typically write custom scripts to:
-
build container images,
-
run containers using these images,
-
connect the containers together,
-
rebuild, restart, update these images and containers.
Life with Compose
Compose enables a simple, powerful onboarding workflow:
-
Checkout our code.
-
Run
docker compose up. -
Our app is up and running!
class: pic
Life after Compose
(Or: when do we need something else?)
-
Compose is not an orchestrator
-
It isn't designed to need to run containers on multiple nodes
(it can, however, work with Docker Swarm Mode)
-
Compose isn't ideal if we want to run containers on Kubernetes
-
it uses different concepts (Compose services ≠ Kubernetes services)
-
it needs a Docker Engine (although containerd support might be coming)
-
First rodeo with Compose
-
Write Dockerfiles
-
Describe our stack of containers in a YAML file (the "Compose file")
-
docker compose up(ordocker compose up -dto run in the background) -
Compose pulls and builds the required images, and starts the containers
-
Compose shows the combined logs of all the containers
(if running in the background, use
docker compose logs) -
Hit Ctrl-C to stop the whole stack
(if running in the background, use
docker compose stop)
Iterating
After making changes to our source code, we can:
-
docker compose buildto rebuild container images -
docker compose upto restart the stack with the new images
We can also combine both with docker compose up --build
Compose will be smart, and only recreate the containers that have changed.
When working with interpreted languages:
-
don't rebuild each time
-
leverage a
volumessection instead
Launching Our First Stack with Compose
First step: clone the source code for the app we will be working on.
git clone https://github.com/jpetazzo/trainingwheels
cd trainingwheels
Second step: start the app.
docker compose up
Watch Compose build and run the app.
That Compose stack exposes a web server on port 8000; try connecting to it.
Launching Our First Stack with Compose
We should see a web page like this:
Each time we reload, the counter should increase.
Stopping the app
When we hit Ctrl-C, Compose tries to gracefully terminate all of the containers.
After ten seconds (or if we press ^C again) it will forcibly kill them.
The Compose file
-
Historically: docker-compose.yml or .yaml
-
Recently (kind of): can also be named compose.yml or .yaml
(Since version 1.28.6, March 2021)
Example
Here is the file used in the demo:
.small[
version: "3"
services:
www:
build: www
ports:
- ${PORT-8000}:5000
user: nobody
environment:
DEBUG: 1
command: python counter.py
volumes:
- ./www:/src
redis:
image: redis
]
Compose file structure
A Compose file has multiple sections:
-
servicesis mandatory. Each service corresponds to a container. -
versionis optional (it used to be mandatory). It can be ignored. -
networksis optional and indicates to which networks containers should be connected.
(By default, containers will be connected on a private, per-compose-file network.) -
volumesis optional and can define volumes to be used and/or shared by the containers.
class: extra-details
Compose file versions
-
Version 1 is legacy and shouldn't be used.
(If you see a Compose file without a
servicesblock, it's a legacy v1 file.) -
Version 2 added support for networks and volumes.
-
Version 3 added support for deployment options (scaling, rolling updates, etc).
The Docker documentation has excellent information about the Compose file format if you need to know more about versions.
Containers in Compose file
Each service in the YAML file must contain either build, or image.
-
buildindicates a path containing a Dockerfile. -
imageindicates an image name (local, or on a registry). -
If both are specified, an image will be built from the
builddirectory and namedimage.
The other parameters are optional.
They encode the parameters that you would typically add to docker run.
Sometimes they have several minor improvements.
Container parameters
-
commandindicates what to run (likeCMDin a Dockerfile). -
portstranslates to one (or multiple)-poptions to map ports.
You can specify local ports (i.e.x:yto expose public portx). -
volumestranslates to one (or multiple)-voptions.
You can use relative paths here.
For the full list, check: https://docs.docker.com/compose/compose-file/
Environment variables
-
We can use environment variables in Compose files
(like
$THISor${THAT}) -
We can provide default values, e.g.
${PORT-8000} -
Compose will also automatically load the environment file
.env(it should contain
VAR=value, one per line) -
This is a great way to customize build and run parameters
(base image versions to use, build and run secrets, port numbers...)
Configuring a Compose stack
-
Follow 12-factor app configuration principles
(configure the app through environment variables)
-
Provide (in the repo) a default environment file suitable for development
(no secret or sensitive value)
-
Copy the default environment file to
.envand tweak it(or: provide a script to generate
.envfrom a template)
Running multiple copies of a stack
-
Copy the stack in two different directories, e.g.
frontandfrontcopy -
Compose prefixes images and containers with the directory name:
front_www,front_www_1,front_db_1frontcopy_www,frontcopy_www_1,frontcopy_db_1 -
Alternatively, use
docker compose -p frontcopy(to set the
--project-nameof a stack, which default to the dir name) -
Each copy is isolated from the others (runs on a different network)
Checking stack status
We have ps, docker ps, and similarly, docker compose ps:
$ docker compose ps
Name Command State Ports
----------------------------------------------------------------------------
trainingwheels_redis_1 /entrypoint.sh red Up 6379/tcp
trainingwheels_www_1 python counter.py Up 0.0.0.0:8000->5000/tcp
Shows the status of all the containers of our stack.
Doesn't show the other containers.
Cleaning up (1)
If you have started your application in the background with Compose and
want to stop it easily, you can use the kill command:
$ docker compose kill
Likewise, docker compose rm will let you remove containers (after confirmation):
$ docker compose rm
Going to remove trainingwheels_redis_1, trainingwheels_www_1
Are you sure? [yN] y
Removing trainingwheels_redis_1...
Removing trainingwheels_www_1...
Cleaning up (2)
Alternatively, docker compose down will stop and remove containers.
It will also remove other resources, like networks that were created for the application.
$ docker compose down
Stopping trainingwheels_www_1 ... done
Stopping trainingwheels_redis_1 ... done
Removing trainingwheels_www_1 ... done
Removing trainingwheels_redis_1 ... done
Use docker compose down -v to remove everything including volumes.
Special handling of volumes
-
When an image gets updated, Compose automatically creates a new container
-
The data in the old container is lost...
-
...Except if the container is using a volume
-
Compose will then re-attach that volume to the new container
(and data is then retained across database upgrades)
-
All good database images use volumes
(e.g. all official images)
Gotchas with volumes
-
Unfortunately, Docker volumes don't have labels or metadata
-
Compose tracks volumes thanks to their associated container
-
If the container is deleted, the volume gets orphaned
-
Example:
docker compose down && docker compose up-
the old volume still exists, detached from its container
-
a new volume gets created
-
-
docker compose down -v/--volumesdeletes volumes(but not
docker compose down && docker compose down -v!)
Managing volumes explicitly
Option 1: named volumes
services:
app:
volumes:
- data:/some/path
volumes:
data:
-
Volume will be named
<project>_data -
It won't be orphaned with
docker compose down -
It will correctly be removed with
docker compose down -v
Managing volumes explicitly
Option 2: relative paths
services:
app:
volumes:
- ./data:/some/path
-
Makes it easy to colocate the app and its data
(for migration, backups, disk usage accounting...)
-
Won't be removed by
docker compose down -v
Managing complex stacks
-
Compose provides multiple features to manage complex stacks
(with many containers)
-
-f/--file/$COMPOSE_FILEcan be a list of Compose files(separated by
:and merged together) -
Services can be assigned to one or more profiles
-
--profile/$COMPOSE_PROFILEcan be a list of comma-separated profiles(see Using service profiles in the Compose documentation)
-
These variables can be set in
.env
Dependencies
-
A service can have a
depends_onsection(listing one or more other services)
-
This is used when bringing up individual services
(e.g.
docker compose up blahordocker compose run foo)
⚠️ It doesn't make a service "wait" for another one to be up!
class: extra-details
A bit of history and trivia
-
Compose was initially named "Fig"
-
Compose is one of the only components of Docker written in Python
(almost everything else is in Go)
-
In 2020, Docker introduced "Compose CLI":
-
docker composecommand to deploy Compose stacks to some clouds -
in Go instead of Python
-
progressively getting feature parity with
docker compose -
also provides numerous improvements (e.g. leverages BuildKit by default)
-
???
:EN:- Using compose to describe an environment :EN:- Connecting services together with a Compose file
:FR:- Utiliser Compose pour décrire son environnement :FR:- Écrire un Compose file pour connecter les services entre eux

