I want to share an interesting solution I’ve used to configure my docker-compose setup for multiple environments. While I’m not using Kubernetes nor Swarm here, a pure docker-compose deploy is fine for small projects IMO.

The problem I’ve faced so far is specifying how each YAML block of the docker-compose.yml file should be used for different environments: which values should be merged and which values should be completely rewritten.

Problematic

There’s actually a build-in possibility to use multiple compose files and extend one from the other like so:

$ docker-compose -f docker-compose.yml -f docker-compose.production.yml up -d

But I don’t really like how the things work with this type of inheritence. The YAML blocks are actually merged instead of being rewritten:

For the multi-value options ports, expose, external_links, dns, dns_search, and tmpfs, Compose concatenates both sets of values

Example: Merging instead of overriding

Just suppose that you want to rewrite ports section of the docker-compose.yml file in the docker-compose.production.yml.

docker-compose.yml
...
myawesomeservice:
  ports:
    - "80:8080"

So you’ll redefine it completely and populate it with new values:

docker-compose.production.yml
...
myawesomeservice:
  ports:
    - "443:8080"

But now you’ll suddenly get surprised. Try running the config command to see the resulting configuration set:

$ docker-compose -f docker-compose.yml -f docker-compose.production.yml config

networks:
...
services:
  myawesomeservice:
    ...
    ports:
    - "80:8080"
    - "443:8080"
    ...

The ports section now contains the ports from the both files. That isn’t something we’ve actually supposed to see.

Solution

If you really want to have these sections completely different for both environments, you’ll need to create three files: docker-compose.yml, docker-compose.dev.yml and docker-compose.production.yml and so your configuration will become a bit more complex than.

You can also use docker-compose.override.yml instead of the docker-compose.dev.yml so you can start your local environment with a simple:

$ docker-compose up -d

Cause it is the default name for the configuration overrides:

By default, Compose reads two files, a docker-compose.yml and an optional docker-compose.override.yml file

But you’all still finish with three files and you’ll have to change all of them time to time.

Another Solution With ERB Templates

I have implemented a quick and dirty solution to manage it. Which from the other side does exactly what I wanted to have.

Setup

So. I have created just one file called docker-compose.yml.erb inside of my composed project root directory and added docker-compose.yml and docker-compose.production.yml files to .gitignore to make them not trackable by the source control anymore.

docker-compose.yml.erb
---

<%
  @compose_env = %w[production prod p].include?(ENV['COMPOSE_TEMPLATE_ENV'].downcase) ? 'production' : 'development'

  def production?
    @compose_env == 'production'
  end

  def development?
    @compose_env == 'development'
  end
%>

version: '3'

services:
  myawesomeservice:
    ports:
    <% if development? %>
    - "80:8080"
    <% else %>
    - "443:8080"
    <% end %>
...

And I also have such a section in the Makefile:

Makefile
compile:
	COMPOSE_TEMPLATE_ENV=production erb docker-compose.yml.erb > docker-compose-production.yml
	COMPOSE_TEMPLATE_ENV=development erb docker-compose.yml.erb > docker-compose.yml

Now I can work with the docker-compose.yml.erb template just in the manner we’ve used to do it in the era of Puppet templates :smile:

Usage

Now just run:

$ make compile

to build a separate docker-compose.yml and a docker-compose.production.yml files.

That’s how you get you app running within the development environment now:

$ docker-compose up -d

And for the production environment just use:

$ docker-compose -f docker-compose.production.yml up -d

Conclusion

:exclamation: No complex chaining needed anymore. Only one file needs to be changed to make everything work as expected.

Probably this kind of setup is not ideal and it is not convenient for large projects, it is also not consistent with compose best practices, but I find it pretty suitable for my needs in some cases.