Multi-Container Development Environments

Dylan Ratcliffe
July 14, 2022

Devcontainers are a great way of simplifying the management of your local development environment. If you haven’t heard of devcontainers I’d recommend looking up what they are first in whatever format you prefer (blog post, docs or video) as this post is going to assume you've already got a devcontainer set up and dive straight into detail.

It’s great to not have to manage which version of Go you need for which project, or making sure that you never have conflicting Rubygem versions ever again. But many projects have dependencies other than just those required to compile the software. Many microservices for example will need to talk to other services in order to run end-to-end tests. Maybe you need a database running to ensure transactions work as expected, or maybe you’re working on a central controller and you need a few workers running different versions to ensure compatibility. Chances are that if your service is containerised, so are these dependencies. So we need a way to run a devcontainer environment with as many containers as we want, not just the one.

Fortunately we can achieve this with docker compose. Firstly, create a compose file at .devcontainer/docker-compose.yml like so:

version: "3"
  # This runs the devcontainer itself
      context: .
      dockerfile: Dockerfile
        # These args refer to any `ARG` directives in the Dockerfile. This will
        # depend on which image you're using. In this case I'm using the Go
        # image, so the available args are `VARIANT` and `NODE_VERSION`. The
        # documentation for these is generated when you initially set up
        # devcontainers in the Dockerfile itself
        VARIANT: 1-bullseye
        NODE_VERSION: lts/*
    # The container needs access to our code, so we mount the parent directory
    # to /workspace (the directory VSCode is expecting)
      - ..:/workspace:cached

    # Overrides default command so things don't shut down after the process ends.
    command: sleep infinity

    # Runs app on the same network as the database container, allows
    # "forwardPorts" in devcontainer.json function.
      - devcontainer-example

    # Uncomment the next line to use a non-root user for all processes.
    # user: node

    # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. 
    # (Adding the "ports" property to this file will not forward from a Codespace.)

    image: nats:latest
    command: "-DV -m 8222"
    # You'll note that I haven't mapped any ports here. That's because mapping
    # ports inside the docker-compose file maps them from the container to the
    # host. But we'll be doing our work from a container, not from the host.
    # Since this devcontainer is on the same network as this container, we can
    # just connect to it using Docker's DNS e.g.
    #   nats:8222
      - devcontainer-example

    image: dgraph/standalone:latest
    # In this case we are mapping some ports to the host. This might be useful
    # for example to access a management UI from your browser
      - 8080:8080
      - 9080:9080
    restart: on-failure
      - devcontainer-example

  # If we're using many containers we likely want them to be on the same
  # network. I recommend that you change the network name to match the name of
  # the project, since if you're running many devcontainer environments at once
  # with the same network name, they will share the same network and could cause
  # issues

In order to tell devcontainers to use this new docker-compose.yml file, we need to modify our devcontainer.json and replace the “build” setting with docker compose specific properties:

  • dockerComposeFile: Path or an ordered list of paths to Docker Compose files relative to the devcontainer.json file
  • service: The name of the service VS Code should connect to once running.
  • workspaceFolder: The path to a volume mount where the source code can be found in the container

Here is an example:

// For format details, see For config options,
// see the README at:
	"name": "Go & Docker Compose",
    // Docker compose specific settings
	"dockerComposeFile": "docker-compose.yml",
	"service": "devcontainer",
	"workspaceFolder": "/workspace",
    "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ],

	// Configure tool-specific properties.
	"customizations": {
		// Configure properties specific to VS Code.
		"vscode": {
			// Set *default* container specific settings.json values on
			// container create.
			"settings": { 
				"go.toolsManagement.checkForUpdates": "local",
				"go.useLanguageServer": true,
				"go.gopath": "/go"
			// Add the IDs of extensions you want installed when the container
			// is created.
			"extensions": [

	// Use 'forwardPorts' to make a list of ports inside the container available
	// locally. "forwardPorts": [],

	// Use 'postCreateCommand' to run commands after the container is created.
	// "postCreateCommand": "go version",

	// Comment out to connect as root instead. More info:
	"remoteUser": "vscode",
	"features": {
		"github-cli": "latest"

Once this is complete all we need to do is reopen the environment in a container by pressing ⇧⌘P / Ctrl+Shift+P to open the command palette, then select “Remote-Containers: Reopen in Container”.

entering "Remote-Containers: Reopen in Container" into the command pallette

This will not only create the devcontainer, but also start all other required services from the docker-compose.yml file. You can prove this is working by trying to access once of the containers from the terminal:

$ curl --head nats:8222
HTTP/1.1 200 OK
Date: Wed, 13 Jul 2022 13:01:05 GMT
Content-Type: text/html; charset=utf-8

Related Blogs