Simplify web application deployment with Docker Compose
Modern web applications have a lot of moving parts. They have SPA front-ends, back-end application servers, databases, in-memory caches, search services, etc.
Docker has simplified the setup of these individual moving parts (read my intro guide on Docker), but managing the entire stack itself can be a complex endeavor.
Setting up even one instance of your web application can involve configuring and connecting all of these various components together.
It’s a pain and very error-prone!
Enter Docker Compose
Docker Compose lets you specify a set of instructions to run docker containers for portions of your web application.
The great thing about Docker Compose is that it will handle the launching and connecting of the various components in the order they need to be launched in, preventing the need to write incredibly complicated docker run
commands.
A single docker-compose up
command can launch an entire web application stack, coordinating connections between multiple docker containers.
.docker-compose.yml
It all starts with this. This is the configuration file you use to tell Docker Compose how your application is built. Let’s take a look at this .docker-compose.yml
file for my personal project, Stockerize:
Stockerize is a Ruby on Rails application — it has a Ruby on Rails server component and a database component.
version: '2'
services:
web:
depends_on:
- db
build: './source/stockerize'
ports:
- "5000:8001"
command: >
bash -c "rm -f tmp/pids/server.pid &&
bundle exec rake db:migrate &&
foreman start"
env_file:
- web.env
db:
image: postgres
volumes:
- ./mounts/db_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: <PG_USER>
POSTGRES_PASSWORD: <PG_PASSWORD>
POSTGRES_DB: stockerize_prod
version
version: '2'
This line specifies what version of the configuration it is. As of this post, there are 3 versions available.
services
services:
web:
# All other stuff
db:
# All other stuff
This is where you list the individual components that this docker compose file will be in charge of starting and connecting. You can name them whatever you want.
In my config I’ve specified two services: web
and db
.
web
web is the alias I gave to the Ruby on Rails component of this stack. I could have just as easily called it rails
or potato
or app
.
depends_on
depends_on:
- db
My Ruby on Rails web server needs to be able to access the database service.
This line tells docker-compose to start up the service I called db
first and to make the db
service available to the web
service via hostname. By doing this, the web
container can connect to the db
container via the db
hostname alias without knowing the IP address ahead of time.
build
build: './source/stockerize'
If you are building the image on the same server as docker-compose, you can specify the build context here. This is the directory that will be delivered to docker build
when build is run.
You could alternatively use image
and specify an image instead.
ports
ports:
- "5000:8001"
Services can expose ports to the host machine, allowing the host to access the services running on the container’s ports or route external connections to the container’s ports. The mapping doesn’t have to be a direct mapping either — you can change the port that is exposed.
The command above expose container port 5000, where Rails is running, as port 8001.
command
command: >
bash -c "rm -f tmp/pids/server.pid &&
bundle exec rake db:migrate &&
foreman start"
You can use this to specify the command to run in the container once it starts. I run a bash command that migrates the database and starts the Rails server, but it will be different for every service.
env_file
env_file:
- web.env
There’s certain fields you may want to keep environment-specific. The env_file
lets you specify an external text file that contains these variables, and makes it available as environment variables in the container.
An .env
file might look like below:
RAILS_ENV=production
DATABASE_URL=postgres://pguser:pgpassword@db:5432/db_name
db
My Rails web application needs a database to run, so this service definition specifies the database to use. Note that db
is what I chose to call it — I could have called it database
or data
or potato
.
image
image: postgres
Because I’m not actually building a database image, I’ll instead specify the pre-made image to use. It’ll be default look in the Docker Hub repository for the image named postgres
, download, and pull it.
volumes
volumes:
- ./mounts/db_data:/var/lib/postgresql/data
Containers don’t keep their data between restarts, which isn’t very useful if you’re trying to persist data permanently for a database. volumes
solves this problem by letting you map data volumes to the host that persist between resets.
The above configuration specifies to mount the host directory ./mounts/db_data
to the container directory that stores the database data /var/lib/postgresql/data
.
environment
environment:
POSTGRES_USER: <PG_USER>
POSTGRES_PASSWORD: <PG_PASSWORD>
POSTGRES_DB: stockerize_prod
Instead of specifying a .env
file, you can also specify environment variables directly into the docker-compose.yml
config. These three environment variables in particular are used by the postgres
image to set up the database.
Running Docker Compose
Now that you’ve written your docker-compose.yml
configuration, it’s time to run it!
You can use docker-compose up -d
to start the services in the background.
If you need to stop the services, you can use docker-compose down
.
If you need to rebuild the images, you can use docker-compose build
.
Docker Compose will run through the configuration and link all the containers together, and then you’ll have yourself a running instance of your application! You can use NGINX to route external requests to the appropriate ports.
Did you find this story helpful? Please Clap to show your support!
If you didn’t find it helpful, please let me know why with a Comment!