morphit/ops/ansible/roles/bunkerweb/templates/docker-compose.yml.j2
2026-08-10 19:33:10 -07:00

155 lines
6.3 KiB
Django/Jinja

# Managed by Ansible — BunkerWeb in all-in-one Docker mode per
# https://docs.bunkerweb.io/. Verify against your BunkerWeb
# version at deploy time; this template targets 1.5.x.
#
# Three services:
# bunkerweb — public entry: TLS + WAF, proxies every path to
# `frontend` (REVERSE_PROXY_HOST)
# bunkerweb-scheduler — config management agent
# frontend — plain nginx that serves the SvelteKit static
# build AND reverse-proxies /v1/, /relay/, /rss/
# + SSE to the relay + indexer on the host
# (the frontend/ build context is copied from
# {{ morphit_repo_path }}/ops/bunkerweb/frontend
# by tasks/main.yml)
#
# Topology: client ──TLS──> bunkerweb ──> frontend ──> host relay/indexer
#
# The morphit relay + indexer run on the HOST (not in this compose),
# reachable from the `frontend` container via host.docker.internal
# (Linux: host-gateway). They bind on all interfaces (group_vars
# morphit_*_bind_host) because a loopback-only bind is unreachable
# from the bridge; UFW (default-deny + the bunkerweb-role bridge
# allow) keeps the public out.
services:
{% if not (morphit_tor_only | default(false)) %}
# ── Clearnet TLS edge (BunkerWeb WAF). Omitted for a Tor-only node,
# which serves its onion straight from the frontend below (no cert, no
# public 80/443). ─────────────────────────────────────────────────────
# cp663 #8 — the bw-data named volume is created root-owned, but the
# scheduler + instance run as UID 101 and must write their config DB
# there ("Database is not initialized" otherwise). This one-shot init
# (root) fixes ownership before anything else starts; scheduler
# depends_on it completing.
bw-init:
image: {{ bunkerweb_scheduler_image }}
container_name: bunkerweb-init
user: "0:0"
entrypoint: ["sh", "-c"]
# (a) cp663 #8 — make the config volume writable by UID 101.
# (b) cp663 #7 — make the Let's Encrypt cert readable by GID 101
# (certbot writes it root-only in a 0700 archive dir; the
# scheduler + instance run as 101 and otherwise serve a
# self-signed fallback). Runs on every `compose up`, so it also
# re-asserts perms after a re-create. For RENEWAL and for a
# DEFERRED (first-online) cert, the same fix lives in the certbot
# deploy-hook and morphit-first-online respectively.
command:
- >-
chown -R 101:101 /data && chmod -R u+rwX /data;
chgrp -R 101 /etc/letsencrypt/live /etc/letsencrypt/archive 2>/dev/null || true;
chmod -R g+rX /etc/letsencrypt/live /etc/letsencrypt/archive 2>/dev/null || true
restart: "no"
volumes:
- bw-data:/data
- /etc/letsencrypt:/etc/letsencrypt
networks:
- bunkerweb_net
bunkerweb:
image: {{ bunkerweb_image }}
container_name: bunkerweb
restart: unless-stopped
ports:
- "80:8080"
- "443:8443"
env_file:
- ./bunkerweb.env
volumes:
- bw-data:/data
# Let's Encrypt certs from the host.
- /etc/letsencrypt:/etc/letsencrypt:ro
extra_hosts:
# So BunkerWeb can reach the relay + indexer on the host's
# loopback. Linux: host-gateway resolves to the host's
# docker-internal IP.
- "host.docker.internal:host-gateway"
networks:
- bunkerweb_net
depends_on:
- bunkerweb-scheduler
bunkerweb-scheduler:
image: {{ bunkerweb_scheduler_image }}
container_name: bunkerweb-scheduler
restart: unless-stopped
env_file:
- ./bunkerweb.env
volumes:
- bw-data:/data
# cp663 #13 — the scheduler (not just the instance) reads
# CUSTOM_SSL_CERT to validate + ship the cert; without this mount
# its customcert job logs "not a valid file" and the instance
# falls back to a self-signed cert.
- /etc/letsencrypt:/etc/letsencrypt:ro
# cp663 #10 — BunkerWeb's scheduler manages the instance via the
# Docker API, so it needs the socket. SECURITY: this is a raw
# read-only socket bind; the recommended hardening is a read-only
# docker-socket-proxy (BunkerWeb's `bw-docker` pattern) so a
# scheduler compromise can't drive the host's Docker — deferred
# because it needs real-host validation + a bundled proxy image.
- /var/run/docker.sock:/var/run/docker.sock:ro
# The socket is root:docker mode 660; the scheduler user (101) needs
# the host's docker group to read it. GID resolved by tasks/main.yml.
group_add:
- "{{ bunkerweb_docker_gid }}"
networks:
- bunkerweb_net
depends_on:
bw-init:
condition: service_completed_successfully
{% endif %}
# Serves the built SvelteKit site + reverse-proxies the API paths to
# the relay + indexer on the host. BunkerWeb proxies everything here
# (REVERSE_PROXY_HOST=http://frontend:80). The build context is
# copied to /etc/bunkerweb/frontend by tasks/main.yml.
frontend:
build:
context: ./frontend
container_name: morphit-frontend
restart: unless-stopped
# cp695 — publish the frontend (the nginx that serves the site AND fans out
# /v1 -> indexer, /relay -> relay) on a HOST LOOPBACK port so the Tor + I2P
# hidden services can reach it. They can't go through BunkerWeb (it force-
# redirects http->https, which Tor/I2P can't follow) and the container isn't
# otherwise host-published. Loopback-only: never exposed to the LAN/internet.
ports:
- "127.0.0.1:{{ morphit_onion_frontend_port | default(8090) }}:80"
volumes:
# The SvelteKit production build from the cloned repo, read-only.
- {{ morphit_repo_path }}/apps/web/build:/usr/share/nginx/html:ro
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- bunkerweb_net
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/ || exit 1"]
interval: 30s
timeout: 10s
retries: 3
volumes:
bw-data:
networks:
bunkerweb_net:
name: bunkerweb_net
driver: bridge
ipam:
config:
# Pin the CIDR so the operator can hard-code it in
# MORPHIT_RELAY_TRUSTED_PROXY_IPS without having to
# re-inspect after a rebuild.
- subnet: 172.20.0.0/16