- name: Deploy headscale-ui with Docker and configure Caddy reverse proxy hosts: spacey become: yes vars_files: - ../../infra_vars.yml - ../../services_config.yml - ../../infra_secrets.yml - ./headscale_vars.yml vars: headscale_subdomain: "{{ subdomains.headscale }}" caddy_sites_dir: "{{ caddy_sites_dir }}" headscale_domain: "{{ headscale_subdomain }}.{{ root_domain }}" headscale_ui_version: "2025.08.23" headscale_ui_dir: /opt/headscale-ui headscale_ui_http_port: 18080 headscale_ui_https_port: 18443 tasks: - name: Check if Docker is installed command: docker --version register: docker_check changed_when: false failed_when: false - name: Fail if Docker is not installed fail: msg: "Docker is not installed. Please run the docker_playbook.yml first." when: docker_check.rc != 0 - name: Ensure Docker service is running systemd: name: docker state: started enabled: yes - name: Create headscale-ui directory file: path: "{{ headscale_ui_dir }}" state: directory owner: root group: root mode: '0755' - name: Create docker-compose.yml for headscale-ui copy: dest: "{{ headscale_ui_dir }}/docker-compose.yml" content: | version: "3" services: headscale-ui: image: ghcr.io/gurucomputing/headscale-ui:{{ headscale_ui_version }} container_name: headscale-ui restart: unless-stopped ports: - "{{ headscale_ui_http_port }}:8080" - "{{ headscale_ui_https_port }}:8443" owner: root group: root mode: '0644' - name: Deploy headscale-ui container with docker compose command: docker compose up -d args: chdir: "{{ headscale_ui_dir }}" register: docker_compose_result changed_when: "'Creating' in docker_compose_result.stdout or 'Starting' in docker_compose_result.stdout or docker_compose_result.rc != 0" - name: Wait for headscale-ui to be ready uri: url: "http://localhost:{{ headscale_ui_http_port }}" status_code: [200, 404] register: headscale_ui_ready until: headscale_ui_ready.status in [200, 404] retries: 30 delay: 2 ignore_errors: yes - name: Ensure Caddy sites-enabled directory exists file: path: "{{ caddy_sites_dir }}" state: directory owner: root group: root mode: '0755' - name: Ensure Caddyfile includes import directive for sites-enabled lineinfile: path: /etc/caddy/Caddyfile line: 'import sites-enabled/*' insertafter: EOF state: present backup: yes - name: Fail if username is not provided fail: msg: "headscale_ui_username must be set in infra_secrets.yml" when: headscale_ui_username is not defined - name: Fail if neither password nor password hash is provided fail: msg: "Either headscale_ui_password or headscale_ui_password_hash must be set in infra_secrets.yml" when: headscale_ui_password is not defined and headscale_ui_password_hash is not defined - name: Generate bcrypt hash for headscale-ui password become: yes command: caddy hash-password --plaintext "{{ headscale_ui_password }}" register: headscale_ui_password_hash_result changed_when: false no_log: true when: headscale_ui_password is defined and headscale_ui_password_hash is not defined - name: Set headscale-ui password hash from generated value set_fact: headscale_ui_password_hash: "{{ headscale_ui_password_hash_result.stdout.strip() }}" when: headscale_ui_password is defined and headscale_ui_password_hash is not defined - name: Update headscale Caddy config to include headscale-ui /web route with authentication become: yes copy: dest: "{{ caddy_sites_dir }}/headscale.conf" content: | {{ headscale_domain }} { @headscale_ui { path /web* } handle @headscale_ui { basicauth { {{ headscale_ui_username }} {{ headscale_ui_password_hash }} } reverse_proxy http://localhost:{{ headscale_ui_http_port }} } # Headscale API is protected by its own API key authentication # All API operations require a valid Bearer token in the Authorization header reverse_proxy * http://localhost:{{ headscale_port }} } owner: root group: root mode: '0644' - name: Reload Caddy to apply new config command: systemctl reload caddy