--- # ── Docker ──────────────────────────────────────────────────────────────────── - name: Install Docker prerequisites ansible.builtin.apt: name: - ca-certificates - curl - gnupg state: present update_cache: true - name: Add Docker GPG key ansible.builtin.shell: cmd: | install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg \ | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg creates: /etc/apt/keyrings/docker.gpg - name: Add Docker apt repository ansible.builtin.apt_repository: repo: >- deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} stable state: present filename: docker - name: Install Docker CE ansible.builtin.apt: name: - docker-ce - docker-ce-cli - containerd.io state: present update_cache: true - name: Ensure Docker service is enabled and running ansible.builtin.systemd: name: docker enabled: true state: started # ── systemd-resolved: free up port 53 ──────────────────────────────────────── - name: Check if systemd-resolved is active ansible.builtin.systemd: name: systemd-resolved register: resolved_status failed_when: false - name: Disable systemd-resolved stub listener on port 53 ansible.builtin.lineinfile: path: /etc/systemd/resolved.conf regexp: '^#?DNSStubListener=' line: 'DNSStubListener=no' state: present when: resolved_status.status.ActiveState | default('') == 'active' notify: Restart systemd-resolved - name: Flush handlers to restart resolved before checking resolv.conf ansible.builtin.meta: flush_handlers - name: Check that non-stub resolv.conf exists and has nameservers ansible.builtin.shell: cmd: grep -q '^nameserver' /run/systemd/resolve/resolv.conf register: resolv_check failed_when: false changed_when: false when: resolved_status.status.ActiveState | default('') == 'active' - name: Point /etc/resolv.conf to non-stub systemd-resolved config ansible.builtin.file: src: /run/systemd/resolve/resolv.conf dest: /etc/resolv.conf state: link force: true when: - resolved_status.status.ActiveState | default('') == 'active' - resolv_check.rc | default(1) == 0 - name: Warn if non-stub resolv.conf is missing or empty ansible.builtin.debug: msg: >- WARNING: /run/systemd/resolve/resolv.conf not found or has no nameservers. Skipping resolv.conf symlink — DNS resolution is unchanged. Verify /etc/resolv.conf manually before running Technitium. when: >- resolved_status.status.ActiveState | default('') != 'active' or resolv_check.rc | default(1) != 0 # ── Disable libvirt default network (frees 192.168.122.1:53) ───────────────── - name: Check if libvirt daemon is running ansible.builtin.systemd: name: libvirtd register: libvirtd_status failed_when: false - name: Destroy libvirt default network if active ansible.builtin.command: virsh net-destroy default register: net_destroy failed_when: - net_destroy.rc != 0 - "'not active' not in net_destroy.stderr" - "'not found' not in net_destroy.stderr" - "'Connection refused' not in net_destroy.stderr" changed_when: net_destroy.rc == 0 when: libvirtd_status.status.ActiveState | default('') == 'active' - name: Disable libvirt default network autostart ansible.builtin.command: virsh net-autostart default --disable register: net_autostart failed_when: - net_autostart.rc != 0 - "'not found' not in net_autostart.stderr" - "'Connection refused' not in net_autostart.stderr" changed_when: net_autostart.rc == 0 when: libvirtd_status.status.ActiveState | default('') == 'active' # ── Pre-pull Docker image (requires DNS — do this before touching resolved) ─── - name: Pre-pull Technitium Docker image ansible.builtin.command: cmd: docker pull {{ technitium_image }} register: docker_pull changed_when: "'Pull complete' in docker_pull.stdout or 'Downloaded' in docker_pull.stdout" failed_when: docker_pull.rc != 0 # ── Technitium container ────────────────────────────────────────────────────── - name: Ensure Technitium data directory exists ansible.builtin.file: path: "{{ technitium_data_dir }}" state: directory mode: "0755" - name: Install Technitium admin password environment file ansible.builtin.copy: dest: /etc/technitium-dns.env mode: "0600" content: | DNS_SERVER_ADMIN_PASSWORD={{ technitium_admin_password }} notify: Restart Technitium DNS - name: Install Technitium DNS systemd service ansible.builtin.copy: dest: /etc/systemd/system/technitium-dns.service mode: "0644" content: | [Unit] Description=Technitium DNS Server (secondary) After=docker.service network-online.target Requires=docker.service Wants=network-online.target [Service] Restart=always RestartSec=5 TimeoutStopSec=30 EnvironmentFile=/etc/technitium-dns.env ExecStartPre=-/usr/bin/docker stop technitium-dns ExecStartPre=-/usr/bin/docker rm technitium-dns ExecStart=/usr/bin/docker run --rm --name technitium-dns \ -p 53:53/udp \ -p 53:53/tcp \ -p {{ technitium_web_port }}:5380/tcp \ -v {{ technitium_data_dir }}:/etc/dns \ --env-file /etc/technitium-dns.env \ {{ technitium_image }} ExecStop=/usr/bin/docker stop technitium-dns [Install] WantedBy=multi-user.target notify: Restart Technitium DNS - name: Flush handlers to (re)start Technitium before configuring ansible.builtin.meta: flush_handlers - name: Ensure Technitium DNS service is enabled and running ansible.builtin.systemd: name: technitium-dns daemon_reload: true enabled: true state: started - name: Wait for Technitium web UI to be available ansible.builtin.uri: url: "http://localhost:{{ technitium_web_port }}/" status_code: 200 register: technitium_ui_check retries: 20 delay: 5 until: technitium_ui_check.status == 200 # ── Technitium API configuration ────────────────────────────────────────────── - name: Authenticate with Technitium API ansible.builtin.uri: url: "http://localhost:{{ technitium_web_port }}/api/user/login" method: GET body_format: form-urlencoded body: user: "{{ technitium_admin_user }}" pass: "{{ technitium_admin_password }}" register: technitium_auth failed_when: technitium_auth.json.status != 'ok' - name: Set Technitium API token fact ansible.builtin.set_fact: technitium_token: "{{ technitium_auth.json.token }}" - name: Create .{{ technitium_domain }} secondary zone (pulls from primary) ansible.builtin.uri: url: "http://localhost:{{ technitium_web_port }}/api/zones/create" method: POST body_format: form-urlencoded body: token: "{{ technitium_token }}" zone: "{{ technitium_domain }}" type: Secondary primaryNameServerAddresses: "{{ technitium_primary_ip }}" register: zone_create failed_when: - zone_create.json.status == 'error' - "'Zone already exists' not in (zone_create.json.errorMessage | default(''))" changed_when: (zone_create.json.status | default('')) == 'ok' - name: Trigger initial zone transfer from primary ansible.builtin.uri: url: "http://localhost:{{ technitium_web_port }}/api/zones/resync" method: POST body_format: form-urlencoded body: token: "{{ technitium_token }}" zone: "{{ technitium_domain }}" ignore_errors: true changed_when: false