diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index a1b8223..765a627 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -12,9 +12,14 @@ services: restart: always ports: !reset [] # Remove the dev 1883:1883 mapping — internal only + # !override, not a plain list: compose MERGES `ports` by appending, so a plain + # list leaves the base file's "8888:8000" in place alongside this one. The + # container then tries to bind 8888 twice — 0.0.0.0 and 127.0.0.1 — and the + # second bind fails with "address already in use". It also silently defeated + # the whole point of this override, publishing the port on every interface. c2-core: restart: always - ports: + ports: !override - "127.0.0.1:8888:8000" # Caddy proxies, not exposed publicly discord-bot: @@ -22,5 +27,5 @@ services: frontend: restart: always - ports: + ports: !override - "127.0.0.1:3000:3000" # Caddy proxies, not exposed publicly diff --git a/infra/ansible/roles/deploy/handlers/main.yml b/infra/ansible/roles/deploy/handlers/main.yml new file mode 100644 index 0000000..54b2a7d --- /dev/null +++ b/infra/ansible/roles/deploy/handlers/main.yml @@ -0,0 +1,13 @@ +--- +# The "Deploy Caddyfile" task notifies this. Without this file the play aborts +# with "The requested handler 'Reload Caddy' was not found" — notify does not +# tolerate a missing handler. +# +# reloaded, not restarted: caddy reload swaps config with zero downtime and +# keeps existing TLS certs/connections; a restart drops every in-flight request. + +- name: Reload Caddy + ansible.builtin.systemd_service: + name: caddy + state: reloaded + enabled: true diff --git a/infra/ansible/roles/deploy/tasks/main.yml b/infra/ansible/roles/deploy/tasks/main.yml index a10c2af..6e26773 100644 --- a/infra/ansible/roles/deploy/tasks/main.yml +++ b/infra/ansible/roles/deploy/tasks/main.yml @@ -2,12 +2,19 @@ # First-time setup: clone repo, write secrets, pull pre-built images and start stack. # Images are built and pushed by Gitea CI — this role never builds on the VM. -- name: Clone repo (skipped if already present) +# update: true (was false) — with update disabled, every re-run of this playbook +# redeployed the code that happened to be on the VM at first clone, so any fix +# pushed to main was invisible here and the only way to ship one was CI or a +# manual pull. force: true discards local edits made on the VM; the templated +# .env files and Caddyfile live outside git tracking, so nothing generated by +# this role is at risk. +- name: Clone or update repo git: repo: "{{ repo_url }}" dest: "{{ app_dir }}" version: main - update: false + update: true + force: true become: false - name: Set ownership of app directory diff --git a/infra/ansible/site.yml b/infra/ansible/site.yml index 2f89a40..004913d 100644 --- a/infra/ansible/site.yml +++ b/infra/ansible/site.yml @@ -36,16 +36,33 @@ path: /swapfile mode: "0600" + # mkswap refuses to touch a file that is already active as swap, so a + # re-run would fail here without this guard. The swap file survives + # reboots via the fstab entry below, so on any second run it IS active. + - name: Check whether the swap file is already active + command: swapon --show=NAME --noheadings + register: _active_swaps + changed_when: false + failed_when: false + - name: Format swap file command: mkswap /swapfile + when: "'/swapfile' not in _active_swaps.stdout" register: _mkswap changed_when: _mkswap.rc == 0 + # Guarded by the same check as mkswap above. The stderr test alone was not + # enough: an already-active swap file reports "Device or resource busy", + # not "already", so the original failed_when never matched it. - name: Enable swap command: swapon /swapfile + when: "'/swapfile' not in _active_swaps.stdout" register: _swapon - failed_when: _swapon.rc != 0 and 'already' not in _swapon.stderr - changed_when: _swapon.rc == 0 + failed_when: > + _swapon.rc is defined and _swapon.rc != 0 + and 'already' not in _swapon.stderr + and 'busy' not in _swapon.stderr + changed_when: _swapon.rc is defined and _swapon.rc == 0 - name: Persist swap in fstab lineinfile: diff --git a/infra/ansible/vault.yml.example b/infra/ansible/vault.yml.example index d630361..1ee84e0 100644 --- a/infra/ansible/vault.yml.example +++ b/infra/ansible/vault.yml.example @@ -22,7 +22,14 @@ vault_firestore_database: "c2-server" # ── Gitea Container Registry ────────────────────────────────────────────────── vault_registry_host: "git.vpn.cusano.net" vault_registry_user: "logan" -vault_registry_token: "" # Gitea access token with package:write scope +vault_registry_token: "" # Gitea access token, READ-ONLY package scope. + # The VM only pulls (roles/deploy/tasks/main.yml:62-72); + # nothing here pushes. Pushing is CI's job and uses a + # separate write-scoped token (BUILD_TOKEN in Gitea + # repo secrets). Keep them separate: this token sits on + # an internet-facing VM, and a write-scoped one there + # would let an attacker publish a poisoned image that + # every future deploy and edge node would install. vault_registry: "git.vpn.cusano.net/logan" # full image prefix # ── Discord Bot ───────────────────────────────────────────────────────────────