Tailscale as A Docker Sidecar
05 February 2025

When working with containers, we often end up with one service per application. So, if Application A needs Postgres, we spin up a dedicated Postgres instance for that application. Let's look at the example below, specifically the db service (the sidecar).

yaml
name: forgejo

services:
  db:
    image: postgres:14
    volumes:
      - ./postgres:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=forgejo
      - POSTGRES_PASSWORD=forgejo
      - POSTGRES_DB=forgejo
    networks:
      - forgejo
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 30s
      timeout: 30s
      retries: 3

  server:
    image: codeberg.org/forgejo/forgejo:13
    container_name: forgejo
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./forgejo:/data
      - /etc/localtime:/etc/localtime:ro
    environment:
      - FORGEJO__database__DB_TYPE=postgres
      - FORGEJO__database__HOST=db:5432
      - FORGEJO__database__NAME=forgejo
      - FORGEJO__database__USER=forgejo
      - FORGEJO__database__PASSWD=forgejo
    ports:
      - "222:22"
      - "3000:3000"
    networks:
      - forgejo

networks:
  forgejo:
    external: false

The Problem

If we want to access the db service, we need to expose the port to the host. Practically, we should expose and bind it to 127.0.0.1 to reduce the attack vector.

yaml
    ports:
      - "127.0.0.1:5432:5432"

However, we end up with several issues:

  1. You must remember which port belongs to which service.
  2. How do you access it remotely? Since the db service is only bound to 127.0.0.1, we cannot access it remotely.
  3. Even if it is safe to bind to 0.0.0.0, how do we ensure db service A is only accessible by User A or Team A, rather than everyone with access to the network?

The Solution

Introducing Tailscale as a Docker sidecar.

yaml
name: forgejo

services:
  db:
    image: postgres:14
    depends_on:
      tailscale-forgejo:
        condition: service_healthy
    volumes:
      - ./postgres:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=forgejo
      - POSTGRES_PASSWORD=forgejo
      - POSTGRES_DB=forgejo
    network_mode: service:tailscale-forgejo
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 30s
      timeout: 30s
      retries: 3

  server:
    image: codeberg.org/forgejo/forgejo:13
    container_name: forgejo
    depends_on:
      db:
        condition: service_healthy
      tailscale-forgejo:
        condition: service_healthy
    volumes:
      - ./forgejo:/data
      - /etc/localtime:/etc/localtime:ro
    environment:
      - FORGEJO__database__DB_TYPE=postgres
      - FORGEJO__database__HOST=db:5432
      - FORGEJO__database__NAME=forgejo
      - FORGEJO__database__USER=forgejo
      - FORGEJO__database__PASSWD=forgejo
    network_mode: service:tailscale-forgejo

  tailscale-forgejo:
    image: tailscale/tailscale:latest
    volumes:
      - ./tailscale:/var/lib/tailscale
    environment:
      - TS_AUTHKEY=tskey-client-notAReal-OAuthClientSecret1Atawk
      - TS_EXTRA_ARGS=--advertise-tags=tag:container
      - TS_STATE_DIR=/var/lib/tailscale
      - TS_USERSPACE=false
      - TS_ENABLE_HEALTH_CHECK=true
    ports:
      - "222:22"
      - "3000:3000"
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:9002/healthz"]
      interval: 5s
      timeout: 3s
      start_period: 20s
      retries: 10
    cap_add:
      - net_admin
    devices:
      - /dev/net/tun:/dev/net/tun
    hostname: tailscale-forgejo

How does it work?

The Tailscale service acts like a normal VPN, other services join the Tailscale service network (check the highlighted part of the code). Since they are on the same network, the localhost of Tailscale is also the localhost of Forgejo and Postgres. This way, we expose the Forgejo ports on the Tailscale service (lines 51–53), and all services can be accessed via the Tailscale IP address.

Since we have joined the service stack to the Tailscale network:

Check the Tailscale official docs for the proper way to set up Tailscale with Docker.

On this page