+
+
+
+
diff --git a/infra/README.md b/infra/README.md
index 5a40cca..3046561 100644
--- a/infra/README.md
+++ b/infra/README.md
@@ -257,36 +257,50 @@ The Forgejo Actions workflow (`.forgejo/workflows/deploy.yml`) triggers on push
## Countdown Static Site (Caddy)
-`deploy-countdown.yml` provisions a **fresh standalone VPS** that serves only the
-static `countdown.html` (repo root). It installs Caddy, uploads the page, and lets
-Caddy obtain + auto-renew the TLS certificate for `hl-mutter-teresa-chemnitz.de`
-and `www.hl-mutter-teresa-chemnitz.de`.
+The static `countdown.html` (repo root) is served by the **existing test VPS**
+Caddy alongside the app reverse-proxies — no separate box. The domains live in
+`countdown_domains` in `inventory/test.yml`:
+
+```yaml
+countdown_domains:
+ - hl-mutter-teresa-chemnitz.de
+ - www.hl-mutter-teresa-chemnitz.de
+```
+
+When this var is set, the `caddy` role uploads `countdown.html` to
+`/var/www/countdown/index.html` and renders a Caddyfile block that serves it for
+all listed names (`chemnitz_live` defaults to `false` = countdown). Caddy obtains
+and auto-renews TLS automatically — point each domain's DNS A/AAAA record at the
+VPS first so the ACME challenge resolves. `setup.yml` provisions it as part of the
+normal server setup.
+
+### Go live: countdown → staging app
+
+`switch-to-staging.yml` re-renders the Caddyfile so the chemnitz domains
+`reverse_proxy` to the staging app (port 3001) instead of serving the static
+page, then validates and reloads Caddy:
```bash
cd infra/ansible
-# 1. Point the inventory at the VPS and confirm the domains
-# edit inventory/countdown/hosts.yml -> ansible_host, countdown_domain, countdown_aliases
+# Switch the domains to the live staging app:
+ansible-playbook playbooks/switch-to-staging.yml -i inventory/test.yml --ask-vault-pass
-# 2. Make sure DNS A/AAAA records for both names point at the VPS first
-# (Caddy's ACME challenge needs them resolving), then run with the root
-# password (--ask-pass needs `sshpass` installed locally):
-ansible-playbook playbooks/deploy-countdown.yml -i inventory/countdown/hosts.yml --ask-pass
+# Revert to the countdown page:
+ansible-playbook playbooks/switch-to-staging.yml -i inventory/test.yml --ask-vault-pass -e chemnitz_live=false
```
-> Authenticating with the root password? Install `sshpass` first
-> (`brew install hudochenkov/sshpass/sshpass` on macOS, `apt install sshpass` on
-> Debian/Ubuntu). Once you've added an SSH key to the box you can drop
-> `--ask-pass` and set `ansible_ssh_private_key_file` in the inventory instead.
+The playbook is idempotent, so you can run it ahead of time to dry-run the
+cutover and flip it back. It reuses the TLS cert Caddy already holds from
+countdown mode (same domains), so there's no re-issue at switch time.
-**What it does:**
-
-1. Installs Caddy + ufw (allows 22/80/443)
-2. Uploads `countdown.html` to `/var/www/countdown/index.html`
-3. Deploys a Caddyfile serving both domains; Caddy handles HTTPS automatically
-
-**Re-running** is the normal way to publish content changes — it re-uploads the
-HTML and reloads Caddy. The certificate is managed entirely by Caddy.
+> After go-live, set `chemnitz_live: true` in `inventory/test.yml` so a later full
+> `setup.yml` run keeps the domains pointed at the app instead of reverting to the
+> countdown page.
+>
+> Note: the staging image is built with `NEXT_PUBLIC_SERVER_URL` set to its
+> `skick.app` domain, so canonical URLs/sitemap still point there. If
+> `hl-mutter-teresa-chemnitz.de` should be canonical, rebuild staging with that URL.
---
diff --git a/infra/ansible/inventory/test.yml b/infra/ansible/inventory/test.yml
index 42877c8..929217e 100644
--- a/infra/ansible/inventory/test.yml
+++ b/infra/ansible/inventory/test.yml
@@ -33,6 +33,16 @@ all:
- domain: git.skick.app
proxy_port: 3003
+ # Chemnitz domains: served by Caddy as the static countdown page until go-live.
+ # The setup/caddy role renders them in countdown mode (chemnitz_live defaults
+ # to false). Run playbooks/switch-to-staging.yml to flip them to the staging
+ # app (port 3001) and reload Caddy. Point DNS at this VPS first so Caddy can
+ # obtain TLS for each name.
+ countdown_domains:
+ - hl-mutter-teresa-chemnitz.de
+ - www.hl-mutter-teresa-chemnitz.de
+ countdown_live_port: 3001 # staging app port the domains proxy to once live
+
# Forgejo
forgejo_domain: git.skick.app
forgejo_container_name: forgejo
diff --git a/infra/ansible/playbooks/switch-to-staging.yml b/infra/ansible/playbooks/switch-to-staging.yml
new file mode 100644
index 0000000..4ccb77d
--- /dev/null
+++ b/infra/ansible/playbooks/switch-to-staging.yml
@@ -0,0 +1,47 @@
+---
+# Flip the chemnitz domains (countdown_domains) from the static countdown page
+# to the live staging app, then validate + reload Caddy.
+#
+# ansible-playbook playbooks/switch-to-staging.yml -i inventory/test.yml --ask-vault-pass
+#
+# Re-runnable and idempotent — run it to go live, or to dry-run the cutover.
+# To revert to the countdown page:
+# ansible-playbook playbooks/switch-to-staging.yml -i inventory/test.yml --ask-vault-pass -e chemnitz_live=false
+- name: Switch chemnitz domains between countdown and the staging app
+ hosts: all
+ become: true
+
+ vars:
+ chemnitz_live: true
+
+ tasks:
+ - name: Fail early if the chemnitz domains are not configured
+ ansible.builtin.assert:
+ that:
+ - countdown_domains is defined
+ - countdown_domains | length > 0
+ fail_msg: "countdown_domains must be set in the inventory before switching."
+
+ - name: Render Caddyfile ({{ 'staging app' if chemnitz_live | bool else 'countdown page' }})
+ ansible.builtin.template:
+ src: "{{ playbook_dir }}/../roles/caddy/templates/Caddyfile.j2"
+ dest: /etc/caddy/Caddyfile
+ mode: "0644"
+ register: caddyfile_result
+
+ - name: Validate Caddyfile
+ ansible.builtin.command: caddy validate --adapter caddyfile --config /etc/caddy/Caddyfile
+ changed_when: false
+
+ - name: Reload Caddy
+ ansible.builtin.systemd:
+ name: caddy
+ state: reloaded
+ when: caddyfile_result is changed
+
+ - name: Report active mode
+ ansible.builtin.debug:
+ msg: >-
+ chemnitz domains now serving
+ {{ 'the staging app (port ' ~ (countdown_live_port | default(3001)) ~ ')'
+ if chemnitz_live | bool else 'the countdown page' }}.
diff --git a/infra/ansible/roles/caddy/tasks/main.yml b/infra/ansible/roles/caddy/tasks/main.yml
index 0c6972c..08223c7 100644
--- a/infra/ansible/roles/caddy/tasks/main.yml
+++ b/infra/ansible/roles/caddy/tasks/main.yml
@@ -24,6 +24,24 @@
state: present
update_cache: true
+- name: Create countdown web root
+ ansible.builtin.file:
+ path: /var/www/countdown
+ state: directory
+ owner: caddy
+ group: caddy
+ mode: "0755"
+ when: countdown_domains is defined and countdown_domains | length > 0
+
+- name: Upload countdown page
+ ansible.builtin.copy:
+ src: "{{ playbook_dir }}/../../../countdown.html"
+ dest: /var/www/countdown/index.html
+ owner: caddy
+ group: caddy
+ mode: "0644"
+ when: countdown_domains is defined and countdown_domains | length > 0
+
- name: Deploy Caddyfile
ansible.builtin.template:
src: Caddyfile.j2
diff --git a/infra/ansible/roles/caddy/templates/Caddyfile.j2 b/infra/ansible/roles/caddy/templates/Caddyfile.j2
index 94cf12f..a7f2379 100644
--- a/infra/ansible/roles/caddy/templates/Caddyfile.j2
+++ b/infra/ansible/roles/caddy/templates/Caddyfile.j2
@@ -4,3 +4,14 @@
}
{% endfor %}
+{% if countdown_domains is defined and countdown_domains | length > 0 %}
+{{ countdown_domains | join(', ') }} {
+{% if chemnitz_live | default(false) | bool %}
+ reverse_proxy localhost:{{ countdown_live_port | default(3001) }}
+{% else %}
+ root * /var/www/countdown
+ file_server
+{% endif %}
+}
+
+{% endif %}
diff --git a/launch.md b/launch.md
new file mode 100644
index 0000000..e10fe12
--- /dev/null
+++ b/launch.md
@@ -0,0 +1,13 @@
+# Launch countdown
+
+## Switch to live
+
+```bash
+cd infra/ansible && ansible-playbook playbooks/switch-to-staging.yml -i inventory/test.yml --vault-password-file /home/benno/.config/chemnitz-vault-pass -e chemnitz_live=true
+```
+
+## Switch to countdown
+
+```bash
+cd infra/ansible && ansible-playbook playbooks/switch-to-staging.yml -i inventory/test.yml --vault-password-file /home/benno/.config/chemnitz-vault-pass -e chemnitz_live=false
+```
\ No newline at end of file