In the previous article about docker(Part 1) we learnt what docker is and saw it in action by dockerizing an application that consists of Rails, Vue.js and Flask apps. We created a docker network and attached all three containers to it.
Running such a multi-container Docker application is much easier with Compose and in this article we’ll use it to run the same setup, plus learn how to utilize it for local development.
What is Docker Compose?
Compose is a tool for defining and running multi-container Docker applications.
To use it you need to create a .yml file, then define and configure all of your services there. In our case, we have Rails, Vue.js and Flask services.
Then instead of running multiple run
commands:
docker run rails_container ...
docker run vue_container ...
docker run flask container ...
you can start all services with a single command:
docker-compose up
Use Docker Compose to run your Rails, Vue.js and Flask apps
Prerequisites
- Ruby 2.6.2
- Minimum of 10 GB of free disk space
- Docker Desktop
Firstly, clone the git repository that we used in the previous tutorial about docker. We’ll use it as a base and make it runnble via docker-compose
Before adding the YAML file, let’s also add postgres as our database, so that the setup feels more like a real project:
cd todos-api && echo "gem 'pg', '~> 0.18.4'" >> Gemfile
bundle install
and modify config/database.yml
of the Rails app accordingly:
development:
adapter: postgresql
encoding: unicode
pool: 5
database: development
host: postgres9.6
password: ''
username: postgres
port: 5432
Now let’s create the docker-compose.yml file in the root folder containing all services:
And here’s what docker-compose.yml should contain. Go ahead and copy it:
version: '3.8'
services:
postgres9.6:
image: postgres:9.6
environment:
- POSTGRES_HOST_AUTH_METHOD=trust
todos-synchronizer:
build:
context: todos-synchronizer/.
depends_on:
- postgres9.6
todos-api:
build:
context: todos-api/.
environment:
- SYNCHRONIZER_URL=http://todos-synchronizer:5000
ports:
- "127.0.0.1:3005:3000"
depends_on:
- todos-synchronizer
todos-app:
build:
context: todos-app/.
ports:
- "127.0.0.1:8080:8080"
depends_on:
- todos-api
We’ve defined 4 services here:
- postgres9.6
- todos-synchronizer
- todos-api
- todos-app
When running the whole multi-container application docker-compose creates a network and the name of each service serves as a hostname (within the network).
Hence we’ve set http://todos-synchronizer:5000
as environment variable for the todos-api service and specified postgres9.6
as the database host in database.yml
build: indicates the path where the Dockerfile of the service resides, so that its image can be build if needed.
depends_on: indicates which other services should be running before the service is started
ports: and environment: are equivalent to docker run -p … -e … options
Now let’s start our app:
docker-compose up -d
with -d
we start the the services in the background
and create the database:
docker-compose exec todos-api rails db:setup
alternatively you can also use:
docker ps # get the todos-api container id
docker exec -it {container_id} rails db:setup
With docker exec
you can execute commands in a running container. Another commonly used one is:
docker exec -it {container_id} bash
which connects you to the container similarly to ssh
Congrats! Now you should be able to add todos on localhost:8080 🚀
With docker-compose ps
you can list containers:
To stop the the application and remove containers:
docker-compose down
Other useful docker-compose
commands:
# run single service
docker-compose run {service_name_here}
# view output from a service container
docker-compose logs {service_name_here}
# build or rebuild image for a service
docker-compose build {service_name_here}
At this point the app should be working fine and you should be able to turn it on and off as you please 😉
There’s one problem though! Creating new containers for your application will wipe out your data. Don’t panic - volumes to the rescue.
What are Docker Volumes?
The official definition reads Volumes are the preferred mechanism for persisting data generated by and used by Docker containers. Simply put that means you’re able to save the data, generated by containers, outside of them. You can also think of volumes as the ‘bridge’ that connect containers to the outside world.
For our Todos app that means we’ll be able to save todos whether we run the app in new containers or not. Let’s add a volume to the postgres9.6 service in docker-compose.yml
:
...
postgres9.6:
image: postgres:9.6
environment:
- POSTGRES_HOST_AUTH_METHOD=trust
volumes:
- ./todos-api/tmp/db:/var/lib/postgresql/data
...
/var/lib/postgresql/data
is the default location for the database files which postgres uses to store data. You can map it to any suitable directory from the host system(host system is the system or the machine that runs/hosts the containers). Now the postgres data files will be mirrored to ./todos-api/tmp/db
and will be kept in sync.
Great, now your todos will persist reagadless of how many times you turn the app on and off 😉
A question that might appear is: Okay, if a volume is the way to persist data, where was it stored when we hadn't specified a volume 🤔
Great question!
If a volume isn’t defined postgres creates its own internal annonymous volume. When removing containers volumes are not removed automatically, so you probably want to remove the dangling volumes and clear some disk space:
# list docker volumes
docker volume ls
# remove one volume
docker volume rm {volume_name_here}
# or remove all unused local volumes
docker volume prune
Use docker compose for local development
Thanks to volumes we can also develop applications more easily. We can map the folders containing the source code within containers to the source code folders in the host system. That way any change we make locally will reflect on the service containers, too. Let’s extend todos-api
and todos-app
in docker-compose.yml:
todos-api:
...
volumes:
- ./todos-api/.:/todosapi
todos-app:
...
volumes:
- ./todos-app/.:/todosapp
To enable hot reloading for the Rails service we also need to disable a setting in development.rb:
# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
ActiveSupport::EventedFileUpdateChecker
somehow blocks picking up new changes that are synchronized over the volume, so we need to comment that line, then rebuild and rerun the app:
# if compose app is running
docker-compose down
# rebuild todos-api image
docker-compose build todos-api
# rerun the app
docker-compose up -d
Congrats again! Open localhost:8080 and see your code changes going live 🚀
Important! Have in mind that is a simple setup, depending on the exact development needs, other optimizations can also be applied relaying to cache, node_modules, gems, etc.
Clean up
docker-compose down
docker system prune --volumes
Thank you for sticking with me again. I hope you learnt something new today that will come in handy at some point 😊
Comments