Files
website/docker-compose.yml
wtclaude 4151f7d44e
All checks were successful
PR Checks / bot-install (pull_request) Successful in 18s
PR Checks / client-build (pull_request) Successful in 30s
PR Checks / server-tests (pull_request) Successful in 9m29s
fix(ntfy): publish ntfy host port so the external reverse proxy can reach it
The ntfy service was configured with no published host port, on the
assumption that the public reverse proxy shares the compose network and
can dial ntfy:80 directly. It does not — Pangolin runs outside the
compose network and reaches every service through a published host port
(exactly why `app` publishes 3000). With no published port there was
nothing for the notification subdomain to forward to, so push delivery
could never work in production.

Publish container :80 on a host port (NTFY_HOST_PORT, default 2586,
binds 0.0.0.0 like `app`) and correct the now-inaccurate comments in
docker-compose.yml and ntfy/server.yml. Document NTFY_HOST_PORT in
.env.example. No code change — deploy config only.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 03:57:32 -05:00

128 lines
5.8 KiB
YAML

services:
db:
image: mariadb:11
restart: unless-stopped
environment:
MARIADB_DATABASE: ${DB_NAME}
MARIADB_USER: ${DB_USER}
MARIADB_PASSWORD: ${DB_PASSWORD}
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- dbdata:/var/lib/mysql
- ./server/db/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
# No host port published by default — only the app needs the DB, over the
# private compose network. Uncomment to inspect from the host:
# ports:
# - "3306:3306"
app:
# Prebuilt image from the Gitea registry (published by
# .gitea/workflows/build-images.yml on every merge to main). This file is
# production-shaped — image only, NO build: — so a production host can only
# ever pull, never accidentally build. IMAGE_TAG defaults to `latest`; pin a
# specific build for a reproducible deploy / rollback, e.g.
# IMAGE_TAG=sha-042a151 (see .env / .env.example). To build locally instead,
# overlay docker-compose.dev.yml (see README).
image: gitea.whitlocktech.com/runicgateway/website-app:${IMAGE_TAG:-latest}
restart: unless-stopped
env_file: .env
environment:
DB_HOST: db
UPLOAD_DIR: /app/uploads
LOG_DIR: /app/logs
depends_on:
db:
condition: service_healthy
volumes:
- uploads:/app/uploads
# Bind-mount logs to the host so app.log is directly readable at ./logs/
- ./logs:/app/logs
# Instance branding assets (logo/hero/favicon), served at /brand when
# BRAND_LOGO/HERO/FAVICON point there. Optional — defaults are baked into
# the image, so this mount only matters for custom brand images. Create
# ./brand/ on the host and drop assets in; read-only in the container.
- ./brand:/app/brand:ro
# Only the PUBLIC API port (3000) is published. The internal server<->bot
# port (INTERNAL_PORT, default 3001) is deliberately NOT listed here, so it
# stays reachable only over the private compose network — Pangolin/the public
# reverse proxy can never forward to it. See issue #33.
# Binds 0.0.0.0 (no 127.0.0.1 prefix) so Pangolin can reach the container.
ports:
- "3000:3000"
ntfy:
# Self-hosted UnifiedPush relay for the app's opt-in push notifications
# (docs/android/PLAN.md §11). Pinned upstream image — fits this file's
# pull-only, never-build model. All config is declarative (./ntfy/server.yml
# + the NTFY_BASE_URL override below), so bringing the stack up provisions a
# working relay with NO interactive steps (no `ntfy user add`, no accounts).
# The backend treats ntfy as an untrusted relay and publishes only
# content-free tickles, so anonymous read-write to unguessable topics is safe.
image: binwiederhier/ntfy:v2.11.0
restart: unless-stopped
command: ["serve"]
environment:
# Public URL devices reach it at (behind the reverse proxy). MUST match the
# origin of the endpoints the app registers — the backend's SSRF allow-set
# (NTFY_BASE_URL / NTFY_ALLOWED_ORIGINS on the app) is derived from it.
NTFY_BASE_URL: ${NTFY_BASE_URL:-https://ntfy.localhost}
volumes:
- ntfydata:/var/lib/ntfy
- ./ntfy/server.yml:/etc/ntfy/server.yml:ro
# Published so the PUBLIC reverse proxy (Pangolin) can forward the
# notification subdomain here. Pangolin lives OUTSIDE the compose network and
# reaches every service through a published host port — never by joining the
# internal network — exactly like `app` above (3000). So ntfy must publish a
# port too: the reverse proxy maps notify.<host> -> host:NTFY_HOST_PORT ->
# ntfy:80. Unlike INTERNAL_PORT / the bot, ntfy is DEVICE-facing, so it is
# SUPPOSED to be reachable through the proxy. Binds 0.0.0.0 (no 127.0.0.1
# prefix) so Pangolin can reach the container. Both the app (SSE subscribe) and
# the backend (POSTing content-free tickles to each device's registered
# endpoint) reach ntfy on this same public origin — NTFY_ALLOWED_ORIGINS pins
# it — so all ntfy traffic flows through the proxy; there is no separate
# internal publish port.
ports:
- "${NTFY_HOST_PORT:-2586}:80"
bot:
# Same as app: prebuilt bot image, pulled in production. Build locally via
# docker-compose.dev.yml.
image: gitea.whitlocktech.com/runicgateway/website-bot:${IMAGE_TAG:-latest}
restart: unless-stopped
env_file: .env
environment:
DB_HOST: db
# Pin the bot's own listen port. Both services share env_file: .env, so
# without this the site's PORT=3000 leaks in and the bot binds 3000 instead
# of 4100 — then the server's BOT_INTERNAL_URL (http://bot:4100) can't reach
# it ("failed to fetch" in the admin panel). Must match that URL's port.
PORT: 4100
# Likewise override the log filename so the bot doesn't inherit the site's
# LOG_FILE and write into app.log — keep the bot's log distinct.
LOG_FILE: bot.log
# Internal config fetch goes to the app's UNPUBLISHED internal port (3001),
# not the public 3000. Keep the port in sync with the app's INTERNAL_PORT.
SITE_INTERNAL_URL: http://app:3001/internal/bot-config
SITE_PUBLIC_URL: http://app:3000/api/v1/public
LOG_DIR: /app/bot/logs
depends_on:
db:
condition: service_healthy
app:
condition: service_started
volumes:
- ./bot/logs:/app/bot/logs
# No published port — the bot's internal API (/internal/*) is reached only
# by `app` over the private compose network, and must NEVER be exposed
# through Pangolin/the public reverse proxy.
volumes:
dbdata:
uploads:
ntfydata: