--- # The Door — Ansible Playbook # VPS provisioning for the crisis front door # # Usage: # cd deploy && ansible-playbook -i inventory.ini playbook.yml # # This playbook is IDEMPOTENT — safe to run repeatedly. # It handles: swap, nginx, SSL, firewall, site deployment. - name: "The Door — VPS Provisioning" hosts: the_door become: true vars: domain: "alexanderwhitestone.com" domain_www: "www.alexanderwhitestone.com" site_root: "/var/www/the-door" swap_size: "2G" swap_file: "/swapfile" hermes_port: 8644 deploy_dir: "/opt/the-door" tasks: # ================================================================ # PHASE 1: System — swap, updates, packages # ================================================================ - name: "[swap] Check if swapfile exists" stat: path: "{{ swap_file }}" register: swap_stat - name: "[swap] Create swapfile" command: fallocate -l {{ swap_size }} {{ swap_file }} when: not swap_stat.stat.exists - name: "[swap] Set permissions" file: path: "{{ swap_file }}" mode: "0600" when: not swap_stat.stat.exists - name: "[swap] Make swap" command: mkswap {{ swap_file }} when: not swap_stat.stat.exists - name: "[swap] Enable swap" command: swapon {{ swap_file }} when: not swap_stat.stat.exists - name: "[swap] Add to fstab" lineinfile: path: /etc/fstab line: "{{ swap_file }} none swap sw 0 0" state: present when: not swap_stat.stat.exists - name: "[apt] Update cache" apt: update_cache: yes cache_valid_time: 3600 - name: "[apt] Install packages" apt: name: - nginx - certbot - python3-certbot-nginx - ufw - curl state: present # ================================================================ # PHASE 2: Site files — copy static assets # ================================================================ - name: "[site] Create webroot" file: path: "{{ site_root }}" state: directory owner: www-data group: www-data mode: "0755" - name: "[site] Copy index.html" copy: src: "{{ playbook_dir }}/../index.html" dest: "{{ site_root }}/index.html" owner: www-data group: www-data mode: "0644" notify: reload nginx - name: "[site] Copy manifest.json" copy: src: "{{ playbook_dir }}/../manifest.json" dest: "{{ site_root }}/manifest.json" owner: www-data group: www-data mode: "0644" notify: reload nginx - name: "[site] Copy service worker" copy: src: "{{ playbook_dir }}/../sw.js" dest: "{{ site_root }}/sw.js" owner: www-data group: www-data mode: "0644" notify: reload nginx - name: "[site] Copy system prompt" copy: src: "{{ playbook_dir }}/../system-prompt.txt" dest: "{{ site_root }}/system-prompt.txt" owner: www-data group: www-data mode: "0644" - name: "[site] Copy about page" copy: src: "{{ playbook_dir }}/../about.html" dest: "{{ site_root }}/about.html" owner: www-data group: www-data mode: "0644" notify: reload nginx - name: "[site] Copy testimony page" copy: src: "{{ playbook_dir }}/../testimony.html" dest: "{{ site_root }}/testimony.html" owner: www-data group: www-data mode: "0644" notify: reload nginx # ================================================================ # PHASE 3: nginx — config, sites, rate limiting # ================================================================ - name: "[nginx] Ensure sites-available dir" file: path: /etc/nginx/sites-available state: directory - name: "[nginx] Ensure sites-enabled dir" file: path: /etc/nginx/sites-enabled state: directory - name: "[nginx] Deploy site config" copy: src: "{{ playbook_dir }}/nginx.conf" dest: /etc/nginx/sites-available/the-door owner: root group: root mode: "0644" notify: reload nginx - name: "[nginx] Enable site" file: src: /etc/nginx/sites-available/the-door dest: /etc/nginx/sites-enabled/the-door state: link notify: reload nginx - name: "[nginx] Remove default site" file: path: /etc/nginx/sites-enabled/default state: absent notify: reload nginx - name: "[nginx] Add rate limit zone to main config" lineinfile: path: /etc/nginx/nginx.conf insertafter: "http {" line: " limit_req_zone $binary_remote_addr zone=the_door_api:10m rate=10r/m;" notify: reload nginx - name: "[nginx] Test config" command: nginx -t changed_when: false - name: "[nginx] Ensure service is running" service: name: nginx state: started enabled: yes # ================================================================ # PHASE 4: Firewall — UFW # ================================================================ - name: "[ufw] Allow SSH" ufw: rule: allow port: "22" proto: tcp - name: "[ufw] Allow HTTP" ufw: rule: allow port: "80" proto: tcp - name: "[ufw] Allow HTTPS" ufw: rule: allow port: "443" proto: tcp - name: "[ufw] Set default deny incoming" ufw: direction: incoming policy: deny - name: "[ufw] Set default allow outgoing" ufw: direction: outgoing policy: allow - name: "[ufw] Enable firewall" ufw: state: enabled # ================================================================ # PHASE 5: SSL — certbot (manual trigger recommended) # ================================================================ - name: "[ssl] Check if cert exists" stat: path: "/etc/letsencrypt/live/{{ domain }}/fullchain.pem" register: ssl_cert - name: "[ssl] Obtain certificate (if DNS is pointed)" command: > certbot --nginx -d {{ domain }} -d {{ domain_www }} --non-interactive --agree-tos --register-unsafely-without-email when: not ssl_cert.stat.exists register: certbot_result ignore_errors: true - name: "[ssl] Certbot result" debug: msg: "{{ 'SSL cert obtained' if certbot_result.rc == 0 else 'SSL cert needs manual setup — point DNS first, then run: certbot --nginx -d ' + domain + ' -d ' + domain_www }}" when: not ssl_cert.stat.exists # ================================================================ # PHASE 6: Deploy directory + deploy script # ================================================================ - name: "[deploy] Create deploy directory" file: path: "{{ deploy_dir }}" state: directory owner: root group: root mode: "0755" - name: "[deploy] Copy deploy script" copy: src: "{{ playbook_dir }}/deploy.sh" dest: "{{ deploy_dir }}/deploy.sh" owner: root group: root mode: "0755" - name: "[deploy] Copy system-prompt.txt" copy: src: "{{ playbook_dir }}/../system-prompt.txt" dest: "{{ deploy_dir }}/system-prompt.txt" owner: root group: root mode: "0644" # ================================================================ # HANDLERS # ================================================================ handlers: - name: reload nginx service: name: nginx state: reloaded - name: restart nginx service: name: nginx state: restarted