Headscale docker-compose config with PostgreSQL

This config is intended for larger installations than our sqlite based standard config. It tends to be slightly easier to back up correctly and will be faster for larger workloads. However, it will consume more RAM especially for low-workload installations and you have two docker containers to worry about during maintenance (though they are managed using a single docker-compose instance). I do not recommend using a shared postgres server although this is certainly possible.

First, create a random password using

echo POSTGRES_PASSWORD=$(pwgen 30 1) > .env

The docker-compose.yml looks like this:

services:
  headscale:
    image: headscale/headscale:latest
    volumes:
      - ./config:/etc/headscale/
      - ./data:/var/lib/headscale
    ports:
      - 27896:8080
    command: headscale serve
    restart: unless-stopped
    depends_on:
      - postgres
  postgres:
    image: postgres
    restart: unless-stopped
    volumes:
      - ./pg_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=headscale
      - POSTGRES_USER=headscale

Now we create the default headscale config:

mkdir -p ./config
curl https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml -o ./config/config.yaml

In config/config.yaml we need to make these changes:

Set server URL:

server_url: https://headscale.mydomain.com

Comment out the sqlite database (add # to the front of every line):

# SQLite config
# db_type: sqlite3
# db_path: /var/lib/headscale/db.sqlite

And uncomment and configure postgres:

# Postgres config
db_type: postgres
db_host: postgres
db_port: 5432
db_name: headscale
db_user: headscale
db_pass: ohngooFaciice2hooGoo1Ahvif3ahl

Make sure all of these are uncommented and you copy the password from .env . It is extremely important that you use a unique password here to prevent attacks from unprivileged host processes to the docker containers.

My recommendation is to reverse proxy headscale using traefik or nginx instead of using the builtin Let’s Encrypt / ACME support. This will allow not only sharing the port & IP address with other services, standard services like Traefik and/or nginx are much more well tested regarding exposure to the internet and hence provide a potential security benefit. Additionally, they make it easier to manage certificates in a service-independent manner and provide an additional layer for debugging etc.

You might also configure custom IP address ranges:

ip_prefixes:
  - fd5d:7b60:4742::/48
  - 100.64.0.0/10

but this is optional.

For more info regarding autostart etc, see How to setup headscale server in 5 minutes using docker-compose