Docker Compose is a great tool for development but I’ve never found a good example of how to use it for an ideal development workflow. Containers are immutable and thus by default are not an ideal development environment because development is fundamentally about changing things and then running the change. Most Docker Compose blogs talk about mounting your code repository as a volume which gets you around the immutability problem and mapping a port, but they don’t go much past that. There are many more little tricks you can do to make your development experience better. Here I gathered all the tricks I’ve used to make Docker Compose a great development workflow.

Create a Development Container

Your development container will probably diverge from your production and CI containers so you can handle having two diverging containers by either having a multi-staged build or having a separate Dockerfile for development. I normally fall into having a separate Dockerfile because your production and CI containers are often optimized for caching and being efficient to ship the container around. Trying to fit a development workflow into that can become clumsy and difficult to do cleanly. The downside is that you may duplicate some Dockerfile code between the two files. However, this should be small since the development container is often very few Dockerfile lines.

If you do use the staged build approach, use the target attribute in your Docker Compose file.

Sync User Permissions

It’s good practice to not run as root but also for your development container it’s nice to sync up the container user with your local user. This ensures your files mounted outside the container will have the correct permissions. To do this:

1. Add some build args to your Dockerfile

ARG userid
ARG groupid

2. Create a user in your Dockerfile with sudo access

# User setup
RUN addgroup mygroup --gid $groupid
RUN useradd -ms /bin/bash -u $userid -g $groupid myuser
RUN echo 'myuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers

3. When building, pass in your build args

docker-compose build --build-arg userid=$(id -u) --build-arg groupid=$(id -g)

Share Secrets

There is a secrets attribute you can use in Docker Compose. However, since this is for development we don’t need to secure a production environment. I’ve found it easier to just mount some shared files and directories. Below is an example of some “secrets” I like to share.

services:
  myapp:
    volumes:
      - .:/opt/myapp
      - $HOME/.git:/home/myuser/.git
      - $HOME/.ssh:/home/myuser/.ssh
      - $HOME/.netrc:/home/myuser/.netrc
      - $HOME/.pypirc:/home/myuser/.pypirc

Share Git and SSH

See above. If you share your .git and .ssh directories with your development container you can execute git commands in your container without needing to jump in and out. This makes your development workflow much more enjoyable with fewer terminals open or fewer keystrokes.

Install Some Development Tools

Especially if you use a separate Dockerfile and thus don’t need to worry about the container size, install some extra development tools. Some apt packages I like to install: procps, nano, vim, sudo, htop, tmux, redis-tools, autojump, openssh-client, wget, unzip

And one of my other favorite tools: thefuck

Share Shell Environments

Since you have a development container you can now really easy make a shared development environment setup. I like to include a shared .bashrc in my repository and then in the Dockerfile copy that to the user’s home directory.

COPY .bashrc /home/myuser/

Remember Your Command History

Since containers are immutable, they will lose your command history on rebuild. You can solve this by linking in a .bash_history file in your Dockerfile.

RUN ln -s /opt/myapp/.bash_history /home/myuser/.bash_history

Full Setup

Below is the full setup, Dockerfile.dev, compose.yaml, and .gitignore additions.

Dockerfile.dev

FROM ubuntu:bionic

ARG userid
ARG groupid

WORKDIR /build

# apt layer
RUN apt update
RUN apt install -y python3.8 python3.8-venv libpython3.8

# dev tools
RUN apt install -y procps nano vim sudo htop tmux redis-tools autojump openssh-client wget unzip

# pip, pdm setup
RUN python3.8 -m pip install pip --upgrade
RUN pip3.8 install --upgrade pip
RUN pip3.8 install pdm thefuck

# User setup
RUN addgroup mygroup --gid $groupid
RUN useradd -ms /bin/bash -u $userid -g $groupid myuser
RUN echo 'myuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers

WORKDIR /opt/myapp
USER myuser

COPY .bashrc /home/myuser/
RUN ln -s /opt/myapp/.bash_history /home/myuser/.bash_history

compose.yaml

version: "3.9"

services:
    myapp:
        network_mode: host
        build:
            context: .
            dockerfile: Dockerfile.dev
        command: /bin/bash
        environment:
            - DEV_CONTAINER=1
            - PYTHONUNBUFFERED=1
            - XDG_DATA_HOME=/opt/myapp/.local/share
        volumes:
            - .:/opt/myapp
            - $HOME/.git:/home/myuser/.git
            - $HOME/.ssh:/home/myuser/.ssh
            - $HOME/.netrc:/home/myuser/.netrc
            - $HOME/.pypirc:/home/myuser/.pypirc
        ports:
            - 8888:8888
            - 8000:8000
            - 8080:8080
            - 8081:8081
            - 3000:3000
            - 5000:5000
        shm_size: 4gb
        depends_on:
            - redis

    redis:
        image: 'bitnami/redis:latest'
        ports:
            - 6379:6379
        environment:
            - ALLOW_EMPTY_PASSWORD=yes

.gitignore additions

.bash_history
.local