Usually, we use Docker to build a whole application environment in different machines, this can be on an AWS instance, a VM, or even on your localhost.
Of course, you can write a giant Dockerfile to run all the components of your application, database, backend, web server configuration, even a cache service, this is a solution, with some disadvantages:
- Building the image will take so long
- If any component requires an update, the whole image will be rebuilt
- Debugging any component will be a nightmare
Well, the other option is to build many Dockerfiles to actually manage multiple containers, this solution of course solves all the issues above, since we can work with each component individually.
Yet not everything is perfect, now we’ll need to handle many containers manually, run docker run ...
for each component, creating a network to connect all our containers, setup many environment variables, and passing aliases to each container to communicate them is quite another nightmare.
How do we fix this? Maybe creating an init.sh
file with each docker run ...
instruction is a suitable approach, but there is another solution offered by Docker
to handle this kind of situations, it is called docker compose
.
docker compose
Compose is a tool for defining and running multi-container applications. It allows us to define all the services in a single file called docker-compose.yml
file and then start, or stop, all of them with a single command.
Installation
Most of the newer versions of Docker Desktop already comes with docker compose command, although, you can always check the installation instructions at their official GitHub repository.
Post’s code
For this docker compose tutorial, I will use a simple REST API written in Go with a MySQL database. Since the app’s functionality is not in our scope I won’t explain it, yet if you have any question related to the example app, please don’t hesitate to reach me out by email or on Twitter.
You can find all the code used in this post here
docker-compose.yml
In the repository for this post you will find a file named docker-compose.yml
this file will be the “entry point” for our application, let’s take a look into it.
version: "3.7"
services:
db:
build: ./db
platform: linux/x86_64
container_name: todos-db
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=todos
image: db:1.0
volumes:
- type: volume
source: db-volume
target: /data/mysql
app:
depends_on:
- db
build: ./app
container_name: todos-api
environment:
- DB_URI=root:root@tcp(db:3306)/todos?parseTime=true
image: api:1.0
ports:
- "80:8080"
volumes:
db-volume:
name: db-volume
The docker-compose.yml
schema requires the docker compose schema version
at the top of the file, the latest version is 3.8
but any 3
version will work for this project in particular, if you are eager to learn more about the changes between versions, the change-log is published here.
Immediately, we need to define our services, each of them require a name, for example, my database service is named db
this names also works as aliases within the network.
Let’s take a closer look at db service.
db:
build: ./db
platform: linux/x86_64
container_name: todos-db
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=todos
image: db:1.0
volumes:
- type: volume
source: db-volume
target: /data/mysql
Ok, now field by field:
-
build
specifies which Dockerfile to use to create this service, in the project’s repository, we have adb
folder, and within in we have aDockerfile
with some customizations for our database -
platform
is not actually necessary, but a good tip in case you are working with an Apple Silicon Processor. -
container_name
as you might have guessed, specifies the name of the container created, is equivalent todocker run -n
flag -
environment
allows us to specify environment variables used by the container, is equivalent todocker run -e
flag -
image
is for specifying the tag for this image. Is equivalent todocker build -t
flag. -
volumes
is a list of thedocker volumes
being used by this container
Pretty straightforward, right? There are many more fields that can be specified, docker compose
allows customizing each aspect of the containers, this configuration is a pretty simple, but solid, one.
But we have another service, app is for defining my REST API application container, let’s take a look at it.
app:
depends_on:
- db
build: ./app
container_name: todos-api
environment:
- DB_URI=root:root@tcp(db:3306)/todos?parseTime=true
image: api:1.0
ports:
- "80:8080"
And again, let’s explore each field:
-
depends_on
is a quite useful field, it allows us to make our container wait until the service specified here is up, since we depend on a database to store To-dos, it is important to wait until thedb
service is up. -
build
again, receives the context to build a new image, within the app folder we have a Dockerfile to build the API -
container_name
as you might have guessed, again, is for naming our container -
environment
allows us to specify environment variables used in our containers, this time we only use one variable, but as you can see, the URI points todb:3306
remember when I said this names also works as aliases within the network?docker compose
creates a network with each service connected to it; therefore, we can point each service using its alias. -
image
again is for tagging our images -
ports
is a list of the ports exposed by this container and the mapping to the host’s actual ports, is equivalent todocker run -p
flag
docker compose up
Ok, but now what? As I said at the beginning of this post, docker compose
allows us to run all our containers with a single command, this command will build all the images (if they don't exist) for our application and also run all the required containers.
But that’s not it, as I mentioned, it also creates a new docker network
and attach all the containers to it, same thing for all the volumes specified in the volumes field at docker-compose.yml
.
Let’s then run it using the -d
flag to run our containers in the background.
docker compose up -d
If the images don’t exist, all the building log will be displayed; otherwise, the output should be something similar to:
[+] Running 2/0
⠿ Container todos-db Running 0.0s
⠿ Container todos-api Running
As you can see, the *.container_name
field is already reflected here.
Then if you list your images you should see something like:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
api 1.0 b5c4cf711443 49 minutes ago 18.1MB
db 1.0 53c16ac987e5 About an hour ago 517MB
Again, the field *.image
was used to name our images and add the version of the image to it.
But let’s see our containers! The output will be something similar to:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
806258079a07 api:1.0 "./server" 4 minutes ago Up 4 minutes 0.0.0.0:80->8080/tcp todos-api
d01357663cea db:1.0 "docker-entrypoint.s…" 4 minutes ago Up 4 minutes 3306/tcp, 33060/tcp todos-db
Where you can see the port mapping that we used in the app.ports field the name in *.container_name
fields and even the *.image
name.
Finally, let’s take a look at our Docker networks!
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
f8d14f7ddf0b dockercompose_default bridge local
There is a new network, being used for this specific project, and we didn’t need to do nothing! It was automatically created and all the containers are already attached to it.
By the way, you can test the API with these commands:
# Create a new TODO
curl -X POST http://127.0.0.1/todos -H "Content-Type: application/json" -d '{"description": "Write a comment!"}'
# Get TODOs
curl -X GET "http://127.0.0.1/todos"
curl -X GET "http://127.0.0.1/todos/1"
# Mark as complete
curl -X PUT "http://127.0.0.1/todos/1?completed=1"
# Delete a TODO
curl -X DELETE "http://127.0.0.1/todos/1"
Conclusion
docker compose
is a great tool, even it is not the best option to be used in production environments, is really useful to replicate different environments in different hosts, quick, easy to read and easy to share. I encourage you to build your next project using a docker-compose.yml
file!
Note from the OP
Hi! Long time no see, I recently moved so I was very very busy, but I’m back with many ideas and drafts for new posts!
Thank you so much for all your support and reading my content, I’ll really appreciate any feedback or suggestion!
Top comments (0)