3 min read

Docker Compose configs: sometimes the best features aren’t new

Docker Compose configs lets you manage configuration files directly in your Compose setup. Inline, from a file, or via environment variables. Removing the need for copies and making deployments simpler and more reproducible.

Docker Compose files all tend to look the same at first: services, maybe volumes and sometimes networks.
And then there’s configs.

It sits there quietly at the top level, documented, supported, and almost completely ignored (at least by me). For years.

I came across it while extending my Ansible role that builds and deploys Docker Compose stacks. The role already had pre- and post-tasks for copying configuration files to the host, keeping paths in sync, and cleaning up afterwards. Since I usually manage my remote server systems from my client and rarely work directly on the target host, this extra orchestration added even more complexity. That was the moment I realized that Docker Compose might have already solved the problem for me.

As it turns out, that's the case.

Configs
Manage and share configuration data using the configs element in Docker Compose.

Configuration files don't need to exist on the host

A very common Docker Compose pattern looks like this:

services:
  nginx:
    image: nginx
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

This works. It always has.

But it comes with a few annoyances:

  • The config file must exist on the host
  • You have to copy or sync it there (scp, rsync, Ansible, Git checkout, pick your poison)
  • Reproducibility takes a quiet hit

This gets worse once the target host is not your machine:
a VPS, a server, a Raspberry Pi under a desk somewhere, or anything provisioned automatically.

For a long time, my solution was “just manage it with Ansible”. Which works, but feels like bringing a forklift to move a chair.

The overlooked solution: configs

Docker Compose has a top-level element called configs.
It lets you define configuration files inside your Compose setup and mount them into containers, without bind mounts.

There are three main ways to define a config:

  1. Inline content (no file, directly in Compose)
  2. From a file (existing file on disk)
  3. From an environment variable (simple and small configuration)

Let’s see them in practice.

1. Inline content

This approach made me realize how much unnecessary complexity I had added with Ansible. However, this option would fit most of my needs.

configs:
  nginx_inline_config:
    content: |
      server {
          listen 80;
          server_name localhost;
          location / {
              return 200 'Hello from inline config!';
          }
      }

services:
  nginx:
    image: nginx
    ports:
      - "8080:80"
    configs:
      - source: nginx_inline_config
        target: /etc/nginx/nginx.conf

No extra file. No copy steps. One Compose file fully declares the configuration. As you might have guessed, this option works well in most cases, but it becomes confusing when the content is more extensive. In this case, the next one fits well.

2. Config from file

The difference here is subtle to my Ansible solution: the content still comes from a file, but Compose treats it as a managed config rather than a volume bind mount. This is good for reusability, for example, and provides a certain level of clear structure.

configs:
  nginx_file_config:
    file: ./nginx.conf

services:
  nginx:
    image: nginx
    ports:
      - "8080:80"
    configs:
      - source: nginx_file_config
        target: /etc/nginx/nginx.conf

3. Config from environment variable

Sometimes configuration is small or dynamic, and you might want to pull it from an environment variable. Compose supports this too:

export REDIS_CONFIG="appendonly yes"
configs:
  redis_env_config:
    environment: REDIS_CONFIG

services:
  redis:
    image: redis
    configs:
      - source: redis_env_config
        target: /usr/local/etc/redis/redis.conf
    command: ["redis-server", "/usr/local/etc/redis/redis.conf"]

(4.) External configs

Another way to use configs is via "external." In short, this allows you to reuse existing configurations, such as with networks.

configs:
  redis_env_config_external:
    name: redis_env_config
    external: true

services:
  redis:
    image: redis
    configs:
      - source: redis_env_config_external
        target: /usr/local/etc/redis/redis.conf
    command: ["redis-server", "/usr/local/etc/redis/redis.conf"]

The takeaway

A few things to be aware of:

  • Configs are mounted read-only. If your application insists on rewriting its config at startup, this won’t work.
  • Updating a config requires recreating the container. Which is usually fine, but worth knowing.

Once I realized I could define configs inline (or pull them from files or environment variables), all those pre- and post-copy steps in my Ansible roles suddenly seemed unnecessary. The Compose file became a complete declaration of the service: image, ports, and configuration, all in one place.

For new users, it’s a subtle but powerful feature. For experienced Dockerists, it’s one of those “how did I miss this for so long?” moments.

Sometimes the best features aren’t new.