Port forward with DevContainer and Docker Compose fails



The Problem

You need multiple containers for your development setup and use a docker-compose.yml file to declare the requirements for your services. After starting the setup you see the port for one service in the VSCode ports tab but none of the ports from the other service containers.

If you have no time for background information, jump directly to the working solution.

Example Setup

Let's define a simple example setup to explore the problem space. Accessing services in development differs from the requirements in production.

Example Application Architecture
[Example Application Architecture]

We have a typical node application with two other required services with a docker-compose.yml setup:

1services:
2 service_a:
3 build:
4 context: ../service_a/
5 dockerfile: Dockerfile
6 volumes:
7 - ../service_a:/workspace:cached
8 service_b:
9 build:
10 context: ../service_b/
11 dockerfile: Dockerfile
12 volumes:
13 - ../service_b:/workspace:cached
14 working_dir: /workspace
15 db:
16 image: postgres:latest
17 restart: unless-stopped
18 volumes:
19 - postgres-data:/var/lib/postgresql/data
20 environment:
21 POSTGRES_PASSWORD: postgres
22 POSTGRES_USER: postgres
23 POSTGRES_DB: postgres
24volumes:
25 postgres-data:

VSCode will allow us to add a devcontainer.json setup based on this file which will look like this:

1// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
2// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/javascript-node-postgres
3// Update the VARIANT arg in docker-compose.yml to pick a Node.js version
4{
5 "name": "Node.js & PostgreSQL",
6 "dockerComposeFile": "docker-compose.yml",
7 "service": "service_a",
8 "workspaceFolder": "/workspace",
9
10 // Configure tool-specific properties.
11 "customizations": {
12 // Configure properties specific to VS Code.
13 "vscode": {
14 // Add the IDs of extensions you want installed when the container is created.
15 "extensions": [
16 "dbaeumer.vscode-eslint"
17 ]
18 }
19 },
20
21 // Use 'forwardPorts' to make a list of ports inside the container available locally.
22 // This can be used to network with other containers or with the host.
23 // "forwardPorts": [3000, 5432],
24
25 // Use 'postCreateCommand' to run commands after the container is created.
26 // "postCreateCommand": "yarn install",
27
28 // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
29 "remoteUser": "node"
30}

As part of the assisted setup we will be asked for the main service which will be added as "service": "service_a", to the config.

Let's run this setup with DevContainer: Rebuild and Reopen in Container and look at the options we get.

  1. we see only the files of the service_a in the explorer panel of VS Code:
    VS Code - Remote Container started
    [VS Code - Remote Container started]
  2. starting the node app node index.js will make VS Code detect the port and offers the option to preview the port in the browser:
    node app started / browser preview dialog
    [node app started / browser preview dialog]
  3. switching to the PORTS tab will show the currently forwarded ports:
    VS Code forwarded Ports tab
    [VS Code forwarded Ports tab]

How is everthing related

VS Code to Docker Host Port Mapping
[VS Code to Docker Host Port Mapping]

Based on the current setup:

  • VS Code is only installed in the main container which is service_a
  • all ports are only defined with EXPOSE xxx in the docker files of the services
  • all ports are unbound
  • only the ports from the main container are auto-detected and for devcontainer port forwarding
  • Port from other containers need to be defined with "host:port" in devcontainer.json portFoward directive.

How can this be fixed?

IMPORTANT: host names are only valid with character a-z0-9 and the - dash. All other character e.g. _ underline create unvalid hostname ! Check you docker-compose.yml service names which become hostnames to only contain vaild characters!

(A) devcontainer.json / portFoward

This solution is currently (Nov 2022) not supported by Github Codespaces!

  1. fix the service names in docker-compose.yml. Change service_a to servicea and service_b to serviceb.
  2. in devcontainer.json uncomment the portFoward directive and change to:
    1"forwardPorts": [
    2 3000,
    3 "serviceb:3000",
    4 "db:5432"
    5],
    Use the format "host:port" to specify ports on none main containers.
  3. Optional you can enrich the display of the ports with better lables or define additional settings - e.g. UPC:
    1"portsAttributes": {
    2 "3000": {
    3 "label": "Service A (Main)"
    4 },
    5 "serviceb:3000": {
    6 "label": "Service B"
    7 },
    8 "db:5432": {
    9 "label": "Database Postgress"
    10 }
    11},

Diagram of the port mapping:

VS Code to Docker Host Port Mapping
[VS Code to Docker Host Port Mapping]

The green ports are defined by VS Code which tries to do a 1:1 from the ports of the containers but will increase the port if containers use the same port and will use a different port if the port is used by an other service on the local computer.

You find all relevant code for the example in my Github repository

VS Code with all fowarded ports, database access and previews of both services:

VS Code preview
[VS Code preview]

(B) Ports mapping

This solution does not work with Github Codespaces and any setup where docker is not installed on the local computer!

Use a ports: mapping in the docker-compose.yml file to make the required ports available on the host system e.g.:

1db:
2 ports:
3 - "5432:5432"

This does work with Docker Desktop (MacOS, Windows, Linux) where VS Code and Docker run on the same computer but will fail on setups where docker runs on a different host or in the cloud (vscode.dev). Mapping is not visible in the VS Code ports tab.

(C) IP sharing

WARNING: The IP sharing setup cannot handle container which expose their service on the same port. This needs a fallback to docker compose

Use the IP address of one container db for different containers with the very bad documented option network_mode: service:[service name], which needs to be added to all containers network_mode: service:db except for the service db:

1services:
2 service_a:
3 build:
4 context: ../service_a/
5 dockerfile: Dockerfile
6 volumes:
7 - ../service_a:/workspace:cached
8 network_mode: service:db
9 service_b:
10 build:
11 context: ../service_b/
12 dockerfile: Dockerfile
13 volumes:
14 - ../service_b:/workspace:cached
15 working_dir: /workspace
16 network_mode: service:db
17 db:
18 image: postgres:latest
19 restart: unless-stopped
20 volumes:
21 - postgres-data:/var/lib/postgresql/data
22 environment:
23 POSTGRES_PASSWORD: postgres
24 POSTGRES_USER: postgres
25 POSTGRES_DB: postgres
26volumes:
27 postgres-data:

to make the port always forwarded (optional) add this to your devcontainer.json:

1"forwardPorts": [3001,3002,5432],

Tipp: you can specify the exact attributes of a port and add a description with the parameter portsAttributes.

1"portsAttributes": {
2 "3001": {"label": "Service A (Main)"},
3 "3002": {"label": "Service B"},
4 "5432": {"label": "Database Postgress"}
5},

Diagram of the port mapping:

VS Code to Docker Host Port Mapping
[VS Code to Docker Host Port Mapping]

Github repository (branch c-ip-sharing-example) with example Code & Setup

VS Code with all fowarded ports, database access and previews of both services:

VS Code preview
[VS Code preview]

(D) Other options

  1. TCP Port forwarding - e.g. adding the following script in devcontainer.json to postCreate:
    1socat TCP4-LISTEN:<PORT-TO-FORWARD>,reuseaddr,fork TCP:<SERVICE-NAME>:<PORT-TO-FORWARD> &
    2socat TCP4-LISTEN:3306,reuseaddr,fork TCP:cs-mysql:3306 &
  2. start a container with a http proxy e.g. Tinyproxy and wire up all existing secondary container. This will allow to simulate a production environment with all related production domains.
  3. use a revers proxy as ingress - e.g. Traefik

Did it work for you - do you have a setup which is not covered?

Write a response on Medium