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: falseThe 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.
ports:
- "127.0.0.1:5432:5432"However, we end up with several issues:
- You must remember which port belongs to which service.
- How do you access it remotely? Since the
dbservice is only bound to127.0.0.1, we cannot access it remotely. - Even if it is safe to bind to
0.0.0.0, how do we ensuredbservice 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-forgejoHow 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:
- 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 Tailscale MagicDNS.
Check the Tailscale official docs for the proper way to set up Tailscale with Docker.