Cum să configurezi Headscale și Traefik ca să faci proxy la orice serviciu

Requirements:

  • a linux server with a public IPv4 address
  • docker and docker compose installed
  • a domain/subdomain with a A DNS record pointing to your IP

I will use example.com as the domain name in this example, please replace it with your domain/subdomain.

Traefik

Ssh to your public server where you want to host your vpn, make a folder called traefik with the following files: mkdir traefik && cd traefik

1. An empty letsencrypt folder: mkdir letsencrypt

2. An empty file touch dynamic.yml (for future expansion)

3. The static config: nano static.yml

 1api:
 2  insecure: true
 3  dashboard: true
 4
 5providers:
 6  docker:
 7    exposedByDefault: false
 8
 9entryPoints:
10  web:
11    address: ":80"
12    http2:
13      maxConcurrentStreams: 250 # fix for network error on the proxy to my immich instance
14    http3:
15      advertisedPort: 443
16    # http:
17    #   redirections:
18    #     entryPoint:
19    #       to: websecure
20    #       scheme: https
21    forwardedHeaders:
22      trustedIPs:
23        - 10.0.0.0/8
24        - 172.16.0.0/12
25        - 192.168.0.0/16
26        - fc00::/7
27    proxyProtocol:
28      trustedIPs:
29        - 10.0.0.0/8
30        - 172.16.0.0/12
31        - 192.168.0.0/16
32        - fc00::/7
33  websecure:
34    address: ":443"
35    http2:
36      maxConcurrentStreams: 250 # fix for network error on the proxy to my immich instance
37    http3:
38      advertisedPort: 443
39    forwardedHeaders:
40      trustedIPs:
41        - 10.0.0.0/8
42        - 172.16.0.0/12
43        - 192.168.0.0/16
44        - fc00::/7
45    proxyProtocol:
46      trustedIPs:
47        - 10.0.0.0/8
48        - 172.16.0.0/12
49        - 192.168.0.0/16
50        - fc00::/7
51
52certificatesResolvers:
53  myresolver:
54    acme:
55      email: "your-email-goes-here@tutorial.example"
56      storage: "/letsencrypt/acme.json"
57      httpChallenge:
58        entryPoint: web

Replace your-email-goes-here@tutorial.example with your email address. Let’s Encrypt send you notifications when your ssl certificate expires.

4. The docker compose config: nano compose.yml

 1services:
 2
 3  traefik:
 4    image: "traefik:v3.2"
 5    container_name: "traefik"
 6    restart: "always"
 7    ports:
 8      - "80:80"
 9      - "443:443"
10    networks:
11      - pub
12    volumes:
13      - "./letsencrypt:/letsencrypt"
14      - "/var/run/docker.sock:/var/run/docker.sock:ro"
15      - "./static.yml:/etc/traefik/traefik.yml:ro"
16      - "./dynamic.yml:/etc/traefik/dynamic.yaml:ro"
17    labels:
18      - "traefik.enable=true"
19      - "traefik.http.routers.dashboard.rule=Host(`example.com`) && (PathPrefix(`/dashboard`) || PathPrefix(`/api/overview`) || PathPrefix(`/api/version`) || PathPrefix(`/api/http`) || PathPrefix(`/api/entrypoints`) || PathPrefix(`/api/tcp`) || PathPrefix(`/api/udp`))"
20      - "traefik.http.routers.dashboard.entrypoints=websecure"
21      - "traefik.http.routers.dashboard.tls.certresolver=myresolver"
22      - "traefik.http.routers.dashboard.service=api@internal"
23      # # if you want to password protect your traefik dashboard uncomment the following and change the user:pass
24      #- "traefik.http.routers.dashboard.middlewares=auth"
25      #- "traefik.http.middlewares.auth.basicauth.users=" # to create user and pass:   bash -c 'echo $(htpasswd -nB admin) | sed -e s/\\$/\\$\\$/g'
26
27networks:
28  pub:
29    external: true

Create the pub network first using the command docker network create pub

Now we can start the traefik instance: docker compose up -d

HeadScale

Let’s make a separate folder for headscale: cd .. && mkdir headscale

1. 2 empty folders: mkdir data && mkdir tailscale-client

2. The headscale config: nano config.yml

  1---
  2# The url clients will connect to.
  3server_url: https://example.com
  4
  5# Address to listen to / bind to on the server
  6listen_addr: 0.0.0.0:8080
  7
  8# Address to listen to /metrics, you may want
  9metrics_listen_addr: 127.0.0.1:9090
 10
 11# Address to listen for gRPC.
 12grpc_listen_addr: 127.0.0.1:50443
 13
 14# Allow the gRPC admin interface to run in INSECURE mode.
 15grpc_allow_insecure: false
 16
 17noise:
 18  # The Noise private key is used to encrypt the traffic
 19  private_key_path: /var/lib/headscale/noise_private.key
 20
 21# List of IP prefixes to allocate tailaddresses from.
 22# Each prefix consists of either an IPv4 or IPv6 address,
 23prefixes:
 24  v6: fd7a:115c:a1e0::/48
 25  v4: 100.64.0.0/10
 26  # Strategy used for allocation of IPs to nodes, available options:
 27  # - sequential (default): assigns the next free IP from the previous given IP.
 28  # - random: assigns the next free IP from a pseudo-random IP generator (crypto/rand).
 29  allocation: sequential
 30
 31# DERP is a relay system that Tailscale uses when a direct connection cannot be established.
 32derp:
 33  server:
 34    enabled: false
 35    # Region ID to use for the embedded DERP server.
 36    region_id: 999
 37    region_code: "headscale"
 38    region_name: "Headscale Embedded DERP"
 39    # Listens over UDP at the configured address for STUN connections - to help with NAT traversal.
 40    stun_listen_addr: "0.0.0.0:3478"
 41    # Private key used to encrypt the traffic between headscale DERP and Tailscale clients.
 42    private_key_path: /var/lib/headscale/derp_server_private.key
 43    # This flag can be used, so the DERP map entry for the embedded DERP server is not written automatically,
 44    automatically_add_embedded_derp_region: true
 45    # For better connection stability (especially when using an Exit-Node and DNS is not working),
 46    ipv4: 1.2.3.4
 47    ipv6: 2001:db8::1
 48  # List of externally available DERP maps encoded in JSON
 49  urls:
 50    - https://controlplane.tailscale.com/derpmap/default
 51  # Locally available DERP map files encoded in YAML
 52  paths: []
 53  # If enabled, a worker will be set up to periodically refresh the given sources and update the derpmap
 54  auto_update_enabled: true
 55  # How often should we check for DERP updates?
 56  update_frequency: 24h
 57# Disables the automatic check for headscale updates on startup
 58disable_check_updates: false
 59# Time before an inactive ephemeral node is deleted?
 60ephemeral_node_inactivity_timeout: 30m
 61database:
 62  type: sqlite
 63  # Enable debug mode. This setting requires the log.level to be set to "debug" or "trace".
 64  debug: false
 65  gorm:
 66    prepare_stmt: true
 67    parameterized_queries: true
 68    skip_err_record_not_found: true
 69    slow_threshold: 1000
 70  sqlite:
 71    path: /var/lib/headscale/db.sqlite
 72    write_ahead_log: true
 73  
 74  ### TLS configuration Let's encrypt / ACME
 75acme_url: https://acme-v02.api.letsencrypt.org/directory
 76acme_email: "your-email-goes-here@tutorial.example"
 77tls_letsencrypt_hostname: ""
 78# Path to store certificates and metadata needed by letsencrypt
 79tls_letsencrypt_cache_dir: /var/lib/headscale/cache
 80
 81# Type of ACME challenge to use, currently supported types: HTTP-01 or TLS-ALPN-01
 82tls_letsencrypt_challenge_type: HTTP-01
 83# When HTTP-01 challenge is chosen, letsencrypt must set up a verification endpoint, and it will be listening on:
 84tls_letsencrypt_listen: ":http" # :http = port 80
 85tls_cert_path: ""
 86tls_key_path: ""
 87
 88log:
 89  format: text # text or json
 90  level: info
 91
 92policy:
 93  mode: file
 94  path: ""
 95
 96dns:
 97  magic_dns: true
 98  # Defines the base domain to create the hostnames for MagicDNS.
 99  base_domain: head.local
100  nameservers:
101    global:
102      - 1.1.1.1
103      - 1.0.0.1
104      - 2606:4700:4700::1111
105      - 2606:4700:4700::1001
106    split: {}
107  search_domains: []
108  # Extra DNS records
109  extra_records: []
110  
111unix_socket: /var/run/headscale/headscale.sock
112unix_socket_permission: "0770"
113logtail:
114  # Enable logtail for this headscales clients.
115  enabled: false
116# Enabling this option makes devices prefer a random port for WireGuard traffic over the default static port 41641.
117randomize_client_port: false

It’s the default config, I removed some comments and replaced server_url, acme_email and the base_domain from dns section. This last config is the Magic DNS that headscale uses to create a DNS entry for every device you connect to your tailscale vpn.

3. The docker compose config: nano compose.yml

 1services:
 2
 3  headscale:
 4    container_name: headscale
 5    image: headscale/headscale
 6    command: serve
 7    restart: always
 8    ports:
 9      - "8080:8080"
10    networks:
11      - pub
12    volumes:
13      - "./data:/var/lib/headscale"
14      - "./config.yml:/etc/headscale/config.yaml"
15    cap_add:
16      - NET_ADMIN
17      - SYS_MODULE
18    sysctls:
19      - net.ipv4.ip_forward=1
20      - net.ipv4.conf.all.src_valid_mark=1
21    labels:
22      - "traefik.enable=true"
23      - "traefik.http.routers.headscale.rule=Host(`example.com`)"
24      - "traefik.http.routers.headscale.entrypoints=websecure"
25      - "traefik.http.routers.headscale.tls.certresolver=myresolver"
26      - "traefik.http.services.headscale.loadbalancer.server.port=8080"
27
28  headplane:
29    container_name: headplane
30    image: ghcr.io/tale/headplane:0.3.2
31    restart: unless-stopped
32    ports:
33      - '3000:3000'
34    networks:
35      - pub
36    depends_on:
37      - headscale
38    volumes:
39      - "./config.yml:/etc/headscale/config.yaml"
40    environment:
41      HEADSCALE_URL: 'http://headscale:8080'
42      API_KEY: ""
43      CONFIG_FILE: "/etc/headscale/config.yaml"
44      COOKIE_SECRET: 'put-some-random-string-here-32j564b64kl6n34lk'
45      DISABLE_API_KEY_LOGIN: 'true'
46      COOKIE_SECURE: 'false'
47      HOST: '0.0.0.0'
48      PORT: '3000'
49    labels:
50      - "traefik.enable=true"
51      - "traefik.http.routers.headplane.rule=Host(`example.com`) && PathPrefix(`/admin`)"
52      - "traefik.http.routers.headplane.entrypoints=websecure"
53      - "traefik.http.routers.headplane.tls.certresolver=myresolver"
54      - "traefik.http.services.headplane.loadbalancer.server.port=3000"
55      #- "traefik.http.routers.headplane.middlewares=auth" # uncomment this only if you generated a password in traefik step 4
56
57  # client:
58  #   container_name: tailscale-client
59  #   image: tailscale/tailscale
60  #   restart: unless-stopped
61  #   environment:
62  #     - "TS_AUTHKEY="
63  #     - "TS_STATE_DIR=/var/lib/tailscale"
64  #     - "TS_USERSPACE=false"
65  #     - "TS_EXTRA_ARGS=--login-server=https://example.com --advertise-exit-node --hostname=server-client --shields-up=false"
66  #   networks:
67  #     - pub
68  #   volumes:
69  #     - "./tailscale-client:/var/lib/tailscale"
70  #     - "/dev/net/tun:/dev/net/tun"
71  #   cap_add:
72  #     - net_admin
73  #     - sys_module
74
75networks:
76  pub:
77    external: true

We can start headscale now: docker compose up -d


Ultima modificare la 2025-01-25

EN: How to configure Headscale and Traefik to proxy any service