morphit/ops/ansible/roles/bunkerweb/tasks/main.yml
2026-08-10 19:33:10 -07:00

234 lines
8.4 KiB
YAML

# Role: bunkerweb
#
# Deploy Docker Engine + BunkerWeb in front of the morphit
# indexer + relay per OPERATIONS.md §32 + §38.6 (network-layer
# squatter defense).
#
# Caveat: BunkerWeb's env-var names change between major versions.
# Verify against https://docs.bunkerweb.io at deploy time. This
# role targets the 1.5.x line.
---
- name: Install Docker prerequisites
ansible.builtin.apt:
name:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
state: present
- name: Ensure /etc/apt/keyrings exists
# /etc/apt/keyrings is not present by default on Ubuntu 24.04; the morphit
# role's nodejs task also makes it, but ensuring it here removes the ordering
# dependency so bunkerweb is self-contained.
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
owner: root
group: root
mode: '0755'
- name: Add Docker GPG key
# Skipped on a self-contained offline install: docker-ce/cli/containerd/buildx/
# compose all come from the bundled apt closure, so there is no need (and no way)
# to fetch Docker's repo key from the internet. The offline apt override ignores
# sources.list.d anyway, so the repo add below is moot offline too.
ansible.builtin.get_url:
url: https://download.docker.com/linux/ubuntu/gpg
dest: /etc/apt/keyrings/docker.asc
mode: '0644'
force: false
when: not morphit_offline_install
- name: Add Docker apt repo
ansible.builtin.apt_repository:
repo: "deb [arch={{ ansible_architecture | replace('x86_64', 'amd64') | replace('aarch64', 'arm64') }} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu {{ morphit_ubuntu_codename | default(ansible_distribution_release) }} stable"
filename: docker
state: present
when: not morphit_offline_install
- name: Install Docker Engine + compose plugin
ansible.builtin.apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
state: present
update_cache: true
- name: Enable + start Docker
ansible.builtin.systemd:
name: docker
enabled: true
state: started
# cp663 #10 — the scheduler needs the Docker socket (root:docker 660);
# resolve the host's docker-group GID so the compose can group_add it to
# the scheduler container. The GID varies per host, so it must not be
# hard-coded.
- name: Resolve the host docker-group GID (for the scheduler's socket access)
ansible.builtin.getent:
database: group
key: docker
- name: Set bunkerweb_docker_gid
ansible.builtin.set_fact:
bunkerweb_docker_gid: "{{ getent_group['docker'][1] }}"
# ── Offline appliance: load bundled Docker images so compose doesn't pull. ──
# When the self-contained bundle ships saved images under vendor/docker, load
# them now (Docker is up, before compose) so "Bring BunkerWeb up" finds them
# locally and needs no Docker Hub. Gated on the bundle → a no-op online.
- name: Detect bundled Docker images
ansible.builtin.find:
paths: "{{ morphit_repo_path }}/vendor/docker"
patterns: "*.tar.gz"
register: morphit_vendor_docker
failed_when: false
changed_when: false
- name: Load bundled Docker images (offline)
ansible.builtin.shell:
cmd: "set -o pipefail; gzip -dc {{ item.path | quote }} | docker load"
args:
executable: /bin/bash
loop: "{{ morphit_vendor_docker.files | default([]) }}"
loop_control:
label: "{{ item.path | basename }}"
when: (morphit_vendor_docker.files | default([]) | length) > 0
changed_when: true
- name: Create BunkerWeb config directory
ansible.builtin.file:
path: /etc/bunkerweb
state: directory
owner: root
group: root
mode: '0750'
- name: Deploy BunkerWeb docker-compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: /etc/bunkerweb/docker-compose.yml
owner: root
group: root
mode: '0640'
register: bunkerweb_compose
- name: Deploy BunkerWeb environment file
ansible.builtin.template:
src: bunkerweb.env.j2
dest: /etc/bunkerweb/bunkerweb.env
owner: root
group: root
mode: '0640'
register: bunkerweb_env
- name: Deploy frontend build context (serves the SvelteKit build + proxies the API)
ansible.builtin.copy:
src: "{{ morphit_repo_path }}/ops/bunkerweb/frontend/"
dest: /etc/bunkerweb/frontend/
remote_src: true
owner: root
group: root
mode: '0640'
directory_mode: '0750'
register: bunkerweb_frontend
# §32 — the frontend container reaches the host-resident relay +
# indexer from the Docker bridge, but UFW's default-deny (hardening
# role) blocks that traffic. Allow ONLY the bunkerweb_net CIDR to the
# service ports; the public stays blocked. Keep this CIDR in sync
# with the docker-compose subnet (172.20.0.0/16).
- name: Allow the bunkerweb Docker network to reach the relay + indexer
community.general.ufw:
rule: allow
src: 172.20.0.0/16
port: "{{ item }}"
proto: tcp
loop:
- "{{ morphit_relay_bind_port }}"
- "{{ morphit_indexer_bind_port }}"
# §45 — same as above, for the MCP HTTP transport, but only when the MCP
# is enabled (it's the most exposed surface, so don't open the port for
# a service that isn't running). The frontend nginx proxies /mcp here.
- name: Allow the bunkerweb Docker network to reach the MCP
community.general.ufw:
rule: allow
src: 172.20.0.0/16
port: "{{ morphit_mcp_bind_port }}"
proto: tcp
when: morphit_mcp_enabled | bool
- name: Bring BunkerWeb up
community.docker.docker_compose_v2:
project_src: /etc/bunkerweb
state: present
build: always
when: bunkerweb_compose.changed or bunkerweb_env.changed or bunkerweb_frontend.changed
- name: Verify the update surface is served no-cache (stale-worker guard)
# A missing no-cache header on /service-worker.js + /verify.json lets an edge
# cache pin a stale service worker on visitors' devices, so they stop receiving
# deploys until they hard-refresh. The shipped frontend config sets no-cache;
# this confirms it actually reached the wire. Non-fatal (a not-yet-ready site
# shouldn't abort the run) but it reports loudly so the operator can't miss it.
ansible.builtin.shell: >-
set -o pipefail;
curl -fsSI "https://{{ morphit_domain }}/verify.json"
| grep -i '^cache-control:' | grep -qi 'no-cache'
args:
executable: /bin/bash
register: morphit_nocache_check
changed_when: false
failed_when: false
retries: 10
delay: 6
until: morphit_nocache_check.rc == 0
# Tor-only: no clearnet https://<domain> to curl — the same no-cache frontend
# config is served via the onion; skip the clearnet verification.
when: >-
(bunkerweb_compose.changed or bunkerweb_env.changed or bunkerweb_frontend.changed)
and not (morphit_tor_only | default(false))
- name: Report update-surface no-cache result
ansible.builtin.debug:
msg: >-
{{ '✓ /verify.json is served no-cache — the update surface is fresh; visitors will auto-receive deploys.'
if morphit_nocache_check.rc == 0
else '✗ WARNING: could not confirm /verify.json is no-cache. If the site is up,
visitors may be stuck on a stale service worker and stop receiving deploys.
Ensure the server config that serves the build carries the no-cache blocks for
/service-worker.js and /verify.json, then re-check with:
curl -sI https://' ~ morphit_domain ~ '/verify.json | grep -i cache-control
— see docs/OPERATIONS.md §"Caching the update surface".' }}
when: >-
(bunkerweb_compose.changed or bunkerweb_env.changed or bunkerweb_frontend.changed)
and not (morphit_tor_only | default(false))
- name: Print BunkerWeb verification reminder
ansible.builtin.debug:
msg: |
BunkerWeb container started. Verify:
docker ps | grep -E 'bunkerweb|morphit-frontend'
docker logs bunkerweb --tail 50
docker logs morphit-frontend --tail 50
curl -v https://{{ morphit_domain }}/ # SvelteKit app
curl -v https://{{ morphit_domain }}/v1/instance # indexer JSON
If the relay sees every user as the BunkerWeb Docker IP
(rate limits look weird), the trusted-proxy IPs setting
in /etc/morphit/relay.env is wrong. Verify it matches
the Docker network CIDR:
docker network inspect bunkerweb_net --format \
'{% raw %}{{ range .IPAM.Config }}{{ .Subnet }}{{ end }}{% endraw %}'
Whatever that prints MUST appear in
MORPHIT_RELAY_TRUSTED_PROXY_IPS.