Docker Compose facilitates the deployment of Dockerized applications. A series of Docker commands and arguments to start your application become a single command;
docker-compose up. Such an invaluable tool makes deploying in both development and production environments a breeze. However, applications are generally run and configured much differently in production than in development. In this post, we will explore ways to separate your development and production configurations while keeping things DRY and maintaining the simplicity of Docker Compose commands.
# docker-compose.yml version: '3' services: db: image: postgres:11.1 api: build: context: . dockerfile: docker/api/Dockerfile depends_on: - db env_file: - dev.env ports: - "8000:8000" volumes: - .:/code
This is a basic Docker Compose configuration you might use in development for an app with a Postgres database. There are two configurations here that are specific to development that we will need to exclude from production.
env_file allows you to define environment variables in a file that will be set in the container. If your application doesn’t already use environment variables for sensitive information like database credentials, API keys, and debug settings, you will probably want to do that before you deploy your app to production. I like to commit the
dev.env file to source control, so that starting the app in development is still as simple as
docker-compose up, without having to first create the file. It should contain default development configurations.
.:/code bind mount we have defined here is to allow code updates outside the container to automatically update the code inside the container, without rebuilding it. This is essential for development, but not desired for production. For production, it is better to bake the code into the image so that it can be reliably versioned.
Creating a Base Configuration
The first step to getting this configuration production-ready is to extract the development aspects of it. Docker Compose allows you to create a
docker-compose.override.yml file that extends the base configuration in
docker-compose.yml. This file is automatically read when running
docker-compose up. This is where our development-specific settings will go.
# docker-compose.yml version: '3' services: db: image: postgres:11.1 api: build: context: . dockerfile: docker/api/Dockerfile depends_on: - db ports: - "8000:8000"
# docker-compose.override.yml version: '3' services: api: env_file: - dev.env volumes: - .:/code
Creating the Production Configuration
Next, we need to extend the base configuration with production-specific settings. We will do this in a
# docker-compose.prod.yml version: '3' services: api: command: gunicorn myapp.wsgi -b 0.0.0.0:8000 env_file: - prod.env
For our simple example, we may want to run our application with a production-ready server rather than a development server. We can do this with the
command setting, which will override
CMD for a container.
As mentioned previously, we want our sensitive production settings to be extracted into environment variables. For this, we may want to read from a
prod.env file. Unlike
dev.env, this file should not be committed to source control.
Starting our application in development is still as simple as ever.
docker-compose up will read the
docker-compose.yml configuration and extend it with the
docker-compose.override.yml configuration automatically.
Starting the app in production is slightly more verbose. I prefer it this way rather than vice-versa, as this command will be run with much less frequency than in development.
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up uses
-f options to explicitly define which files should be read. This prevents Docker Compose from reading
docker-compose.override.yml by default, and instead extends our base configuration with
docker-compose.prod.yml, as we have specified.
Using Docker Compose for both development and production is the path to effortless deployments without sacrificing security. By extending a base configuration with development and production-specific configurations, we can keep our files DRY.