dockerdocker-composevscode-devcontainerdevcontainer

devcontainer docker-compose best practice


I have an existing Django project using docker compose and I am trying to add devcontainer support. I have it mostly working except there is still one thing that is bothering me.

In my docker-compose.yml file, I have a service defined called web. I want to expand the docker image created for the web service to add more dev features. I have created the .devcontainer directory with a devcontainer.json file. I also have a docker-compose.yml file in .devcontainer which defines the dev service. I have a Dockerfile in .devcontainer that I want to extend from the web image.

The problem I am running into is when the devcontainer system in vscode tries to build the containers, the web container doesn't exist so building the dev container fails. I have to manually build the web container first so the image exists, and then the dev container build will work.

This is my .devcontainer/devcontainer.json file.

{
    "name": "Existing Docker Compose (Extend)",
    // Update the 'dockerComposeFile' list if you have more compose files or use different names.
    // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
    "dockerComposeFile": [
        "../docker-compose.yml",
        "../docker-compose-development.yml",
        "docker-compose.yml"
    ],
    // The 'service' property is the name of the service for the container that VS Code should
    // use. Update this value and .devcontainer/docker-compose.yml to the real service name.
    "service": "dev",
    // The optional 'workspaceFolder' property is the path VS Code should open by default when
    // connected. This is typically a file mount in .devcontainer/docker-compose.yml
    "workspaceFolder": "/workspace",
    // Features to add to the dev container. More info: https://containers.dev/features.
    // "features": {},
    // Use 'forwardPorts' to make a list of ports inside the container available locally.
    // "forwardPorts": [],
    // Uncomment the next line if you want start specific services in your Docker Compose config.
    // "runServices": [],
    // Uncomment the next line if you want to keep your containers running after VS Code shuts down.
    // "shutdownAction": "none",
    // Uncomment the next line to run commands after the container is created.
    // "postCreateCommand": "bash pip install pre-commit"
    // Configure tool-specific properties.
    // "customizations": {},
    // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
    "remoteUser": "vscode",
    "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && cd sas_applications && poetry install && cd ..",
    "customizations": {
        "vscode": {
            "extensions": [
                "ms-vscode.azure-repos",
                "vscodevim.vim",
                "ms-python.python",
                "ms-python.isort",
                "ms-python.black-formatter"
            ]
        }
    }
}

and my .devcontainer/docker-compose.yml file

services:
  # Update this to the name of the service you want to work with in your docker-compose.yml file
  dev:
    # Uncomment if you want to override the service's Dockerfile to one in the .devcontainer
    # folder. Note that the path of the Dockerfile and context is relative to the *primary*
    # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile"
    # array). The sample below assumes your primary file is in the root of your project.
    #
    build:
      context: .
      dockerfile: .devcontainer/Dockerfile
    # Overrides default command so things don't shut down after the process ends.
    entrypoint: []
    command: /bin/sh -c "while sleep 1000; do :; done"

and .devcontainer/Dockerfile

FROM web
ARG UID=1000
ARG GID=1000
RUN apt install -y pipx zsh
RUN groupadd --gid "${GID}" vscode && useradd -u "${UID}" -g "${GID}" -s /usr/bin/zsh -m vscode
USER vscode
RUN sh -c "$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
RUN sed -i '/plugins=/ s/=.*/=\(git python\)/' ~/.zshrc
RUN pipx install invoke && pipx ensurepath
WORKDIR /workspace/sas_applications
CMD ["sleep", "infinity"]

I have looked into multiple options but can't come up with a solution. I would prefer not duplicating the web dockerfile in the dev dockerfile. I would also prefer not to make the developer run a manual docker-compose build step.

I thought about trying to define the dev service in the base docker-compose.yml file where the web service is defined and make use of the profile feature. However, I don't know how to tell vscode devcontainers to add that profile argument to the build command.

Thanks in advance.


Solution

  • After more research, I have found a decent solution. This solution makes use of multi-stage builds in Docker. This stackoverflow answer gave me the clue that got me pointed in the right direction.

    The first change was to add multi-stage builds to the existing Dockerfile.

    FROM python:3.11 as base
    # Do the original image build instructions
    
    FROM base as dev
    # Do the build steps needed to add any 
    #  development tools or configuration
    

    Then, in the original docker-compose.yml file, set the build target to be the base image.

    services:
      web:
        build:
         ...
         target: base
        ...
    

    Then, in the .devcontainer/docker-compose.yml file, set the build target to be the dev image.

    services:
      web:
        build:
         ...
         target: dev
        ...
    

    Now, when building the images, you can specify which target to build just by changing the docker-compose files included at the command line. To build for deployment, use docker compose -f docker-compose.yml.

    {
        "name": "Existing Docker Compose (Extend)",
        // Update the 'dockerComposeFile' list if you have more compose files or use different names.
        // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
        "dockerComposeFile": [
            "../docker-compose.yml",
            "docker-compose.yml"
        ],
        ...
    

    The .devcontainer/devcontainer.json file will specify to include both docker compose files. vscode will now build the dev container which includes the additional development tools and configuration.