← Blogs· 2 min read

Tailscale as a Docker Sidecar

Why you should use Tailscale as a Docker sidecar

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).

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 surface.

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.

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 the services share the Tailscale network:

  • We can access the service remotely.
  • We can use Tailscale Grants to control who can connect to which service.
  • We only need to remember the MagicDNS name.

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

esc