docker-compose.yaml

services:
  headplane:
    # I recommend you pin the version to a specific release
    image: ghcr.io/tale/headplane:0.6.0
    container_name: headplane
    restart: unless-stopped
    ports:
      - '3000:3000'
    volumes:
      - './headplane-data/config.yaml:/etc/headplane/config.yaml'
      - './headscale-config/config.yaml:/etc/headscale/config.yaml'
      - './headscale-config/dns_records.json:/etc/headscale/dns_records.json'
      - './headplane-data:/var/lib/headplane'
      - "./letsencrypt:/letsencrypt"
      - '/var/run/docker.sock:/var/run/docker.sock:ro'
    labels:
      - "traefik.enable=true"
      - "traefik.http.services.headscale-admin.loadbalancer.server.port=3000"
      - "traefik.http.routers.headscale-admin.rule=Host(`vpn.lhk.o-r.kr`) && PathPrefix(`/admin`)"
      - "traefik.http.routers.headscale-admin.entrypoints=websecure"
      - "traefik.http.routers.headscale-admin.tls=true"
 
  headscale:
    image: headscale/headscale:0.26.1
    container_name: headscale
    restart: unless-stopped
    command: serve
    ports:
      - '8080:8080'
    volumes:
      - './headscale-data:/var/lib/headscale'
      - './headscale-config:/etc/headscale'
    environment:
      TZ: 'Asia/Seoul'
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.headscale.rule=Host(`vpn.lhk.o-r.kr`)"
      - "traefik.http.routers.headscale.tls.certresolver=myresolver"
      - "traefik.http.routers.headscale.entrypoints=websecure"
      - "traefik.http.routers.headscale.tls=true"
      - "traefik.http.services.headscale.loadbalancer.server.port=8080"
      - "me.tale.headplane.target=headscale"
 
  traefik:
    image: "traefik:latest"
    container_name: "traefik"
    restart: unless-stopped
    command:
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entryPoints.web.address=:80"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--entryPoints.websecure.address=:443"
      - "--certificatesresolvers.myresolver.acme.httpchallenge=true"
      - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
      - "--certificatesresolvers.myresolver.acme.email=lhk1415043@gmail.com"
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "./letsencrypt:/letsencrypt"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
 

headscale config.yaml

server_url: https://vpn.lhk.o-r.kr
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: false
noise:
  private_key_path: /var/lib/headscale/noise_private.key
prefixes:
  v4: 100.64.0.0/10
  v6: fd7a:115c:a1e0::/48
  allocation: sequential
derp:
  server:
    enabled: true
    region_id: 999
    region_code: "headscale"
    region_name: "Headscale Embedded DERP"
    stun_listen_addr: "0.0.0.0:3478"
    private_key_path: /var/lib/headscale/derp_server_private.key
    automatically_add_embedded_derp_region: true
    ipv4: 1.2.3.4
    ipv6: 2001:db8::1
  urls:
    - https://controlplane.tailscale.com/derpmap/default
  paths: []
  auto_update_enabled: true
  update_frequency: 24h
disable_check_updates: false
ephemeral_node_inactivity_timeout: 30m
database:
  type: sqlite
  debug: false
  gorm:
    prepare_stmt: true
    parameterized_queries: true
    skip_err_record_not_found: true
    slow_threshold: 1000
  sqlite:
    path: /var/lib/headscale/db.sqlite
    write_ahead_log: true
    wal_autocheckpoint: 1000
acme_url: https://acme-v02.api.letsencrypt.org/directory
acme_email: ""
tls_letsencrypt_hostname: ""
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
tls_letsencrypt_challenge_type: HTTP-01
tls_letsencrypt_listen: ":http"
tls_cert_path: ""
tls_key_path: ""
log:
  format: text
  level: info
policy:
  mode: database
  path: ""
dns:
  magic_dns: true
  base_domain: lhknet.com
  nameservers:
    global:
      - 10.1.1.120
      - 8.8.8.8
      - 1.1.1.1
      - 1.0.0.1
      - 2606:4700:4700::1111
      - 2606:4700:4700::1001
    split: {}
  search_domains: []
  #extra_records: []
  extra_records_path: "/etc/headscale/dns_records.json"
    #extra_records:
    #- name: adguard.lhk.o-r.kr
    #type: A
    #value: 10.1.1.120
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
logtail:
  enabled: false
randomize_client_port: false
# OIDC (OpenID Connect)
oidc:
  enabled: true
  issuer: "https://auth.lhk.o-r.kr/application/o/headscale/"
  client_id: "아이디"
  client_secret: "키"
  scopes: [ "openid", "profile", "email" ]
 

headscale dns_records.json.yaml

[
    {
        "name": "*.lhk.o-r.kr",
        "type": "A",
        "value": "10.1.1.120"
    }
]

headsplane config.yaml

# Configuration for the Headplane server and web application
server:
  host: "0.0.0.0"
  port: 3000
 
  # The secret used to encode and decode web sessions
  # Ensure that this is exactly 32 characters long
  cookie_secret: "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"
 
  # Should the cookies only work over HTTPS?
  # Set to false if running via HTTP without a proxy
  # (I recommend this is true in production)
  cookie_secure: true
 
# Headscale specific settings to allow Headplane to talk
# to Headscale and access deep integration features
headscale:
  # The URL to your Headscale instance
  # (All API requests are routed through this URL)
  # (THIS IS NOT the gRPC endpoint, but the HTTP endpoint)
  #
  # IMPORTANT: If you are using TLS this MUST be set to `https://`
  url: "https://vpn.lhk.o-r.kr"
 
  # If you use the TLS configuration in Headscale, and you are not using
  # Let's Encrypt for your certificate, pass in the path to the certificate.
  # (This has no effect `url` does not start with `https://`)
  # tls_cert_path: "/letsencrypt/tls.crt"
 
  # Optional, public URL if they differ
  # This affects certain parts of the web UI
  # public_url: "https://headscale.example.com"
 
  # Path to the Headscale configuration file
  # This is optional, but HIGHLY recommended for the best experience
  # If this is read only, Headplane will show your configuration settings
  # in the Web UI, but they cannot be changed.
  config_path: "/etc/headscale/config.yaml"
 
  # Headplane internally validates the Headscale configuration
  # to ensure that it changes the configuration in a safe way.
  # If you want to disable this validation, set this to false.
  config_strict: true
 
  # If you are using `dns.extra_records_path` in your Headscale
  # configuration, you need to set this to the path for Headplane
  # to be able to read the DNS records.
  #
  # Pass it in if using Docker and ensure that the file is both
  # readable and writable to the Headplane process.
  # When using this, Headplane will no longer need to automatically
  # restart Headscale for DNS record changes.
  dns_records_path: "/etc/headscale/dns_records.json"
 
# Integration configurations for Headplane to interact with Headscale
integration:
  agent:
    # The Headplane agent allows retrieving information about nodes
    # This allows the UI to display version, OS, and connectivity data
    # You will see the Headplane agent in your Tailnet as a node when
    # it connects.
    enabled: false
    # To connect to your Tailnet, you need to generate a pre-auth key
    # This can be done via the web UI or through the `headscale` CLI.
    pre_authkey: "<your-preauth-key>"
    # Optionally change the name of the agent in the Tailnet.
    # host_name: "headplane-agent"
 
    # Configure different caching settings. By default, the agent will store
    # caches in the path below for a maximum of 1 minute. If you want data
    # to update faster, reduce the TTL, but this will increase the frequency
    # of requests to Headscale.
    # cache_ttl: 60
    # cache_path: /var/lib/headplane/agent_cache.json
 
    # Do not change this unless you are running a custom deployment.
    # The work_dir represents where the agent will store its data to be able
    # to automatically reauthenticate with your Tailnet. It needs to be
    # writable by the user running the Headplane process.
    # work_dir: "/var/lib/headplane/agent"
 
  # Only one of these should be enabled at a time or you will get errors
  # This does not include the agent integration (above), which can be enabled
  # at the same time as any of these and is recommended for the best experience.
  docker:
    enabled: true
 
    # By default we check for the presence of a container label (see the docs)
    # to determine the container to signal when changes are made to DNS settings.
    container_label: "me.tale.headplane.target=headscale"
 
    # HOWEVER, you can fallback to a container name if you desire, but this is
    # not recommended as its brittle and doesn't work with orchestrators that
    # automatically assign container names.
    #
    # If `container_name` is set, it will override any label checks.
    # container_name: "headscale"
 
    # The path to the Docker socket (do not change this if you are unsure)
    # Docker socket paths must start with unix:// or tcp:// and at the moment
    # https connections are not supported.
    socket: "unix:///var/run/docker.sock"
 
  # Please refer to docs/integration/Kubernetes.md for more information
  # on how to configure the Kubernetes integration. There are requirements in
  # order to allow Headscale to be controlled by Headplane in a cluster.
  kubernetes:
    enabled: false
    # Validates the manifest for the Pod to ensure all of the criteria
    # are set correctly. Turn this off if you are having issues with
    # shareProcessNamespace not being validated correctly.
    validate_manifest: true
    # This should be the name of the Pod running Headscale and Headplane.
    # If this isn't static you should be using the Kubernetes Downward API
    # to set this value (refer to docs/Integrated-Mode.md for more info).
    pod_name: "headscale"
 
  # Proc is the "Native" integration that only works when Headscale and
  # Headplane are running outside of a container. There is no configuration,
  # but you need to ensure that the Headplane process can terminate the
  # Headscale process.
  #
  # (If they are both running under systemd as sudo, this will work).
  proc:
    enabled: false
 
# OIDC Configuration for simpler authentication
# (This is optional, but recommended for the best experience)
oidc:
  issuer: "https://auth.lhk.o-r.kr/application/o/headplane/"
  client_id: "au1qnPS0SmFhn9FEsjmk6JbwvhoY03XeZ2MPyg7R"
 
  # The client secret for the OIDC client
  # Either this or `client_secret_path` must be set for OIDC to work
  client_secret: "vbYIU7I6j8U2vxL2ozOe18U5ZZNylTypUD2RfhvAXM1O7WOjd2WVFaLxt9Tuh9wMTUAGvZlw6mVdmlCnFjRINrTF8aUISHIxAjYflh6OhviymxyaU3ZwV9HGxO0jtpra"
  # You can alternatively set `client_secret_path` to read the secret from disk.
  # The path specified can resolve environment variables, making integration
  # with systemd's `LoadCredential` straightforward:
  # client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret"
 
  disable_api_key_login: false
  token_endpoint_auth_method: "client_secret_post"
 
  # If you are using OIDC, you need to generate an API key
  # that can be used to authenticate other sessions when signing in.
  #
  # This can be done with `headscale apikeys create --expiration 999d`
  headscale_api_key: "키"
 
  # Optional, but highly recommended otherwise Headplane
  # will attempt to automatically guess this from the issuer
  #
  # This should point to your publicly accessibly URL
  # for your Headplane instance with /admin/oidc/callback
  redirect_uri: "https://vpn.lhk.o-r.kr/admin/oidc/callback"
 
  # Stores the users and their permissions for Headplane
  # This is a path to a JSON file, default is specified below.
  user_storage_file: "/var/lib/headplane/users.json"
 

서버 치트시트

# 스크립트 접두사 
sudo docker exec -it headscale 
 
# 로그인 키 생성
sudo docker exec -it headscale headscale apikeys create --expiration 999d

클라이언트 치트시트

# 로그인
tailscale up --login-server https://vpn.lhk.o-r.kr
 
# 아이피 대역 셋팅
tailscale set --advertise-routes=10.1.1.0/24 --accept-routes
 
# 서브넷 허용
tailscale set --accept-routes