-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.prod.yml
More file actions
163 lines (158 loc) · 7.56 KB
/
Copy pathdocker-compose.prod.yml
File metadata and controls
163 lines (158 loc) · 7.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# Production deployment — single-origin serve, no Vite dev server.
#
# The backend serves both the API and the pre-built frontend (RT_STATIC_DIR).
# Add the Caddy service below for TLS; comment out the bare port if using the
# reverse proxy so the app is only reachable through HTTPS.
#
# Quick-start:
# RT_SECRET=$(openssl rand -hex 32) \
# RT_ADMIN_PASSWORD=$(openssl rand -base64 12) \
# docker compose -f docker-compose.prod.yml up -d
# Fixed project name so the app's own `docker compose up` and the updater
# sidecar operate on the same project/containers regardless of directory name.
name: reqmesh
services:
reqmesh:
# Pull the published image by version tag (the updater bumps REQMESH_VERSION).
# `build` is kept so `docker compose build` still works for local/CI images.
image: ghcr.io/callumnunesvaz/reqmesh:${REQMESH_VERSION:-latest}
build:
context: .
dockerfile: Dockerfile.prod
ports:
- "${RT_BIND:-127.0.0.1}:8000:8000"
# Any RT_* setting an operator writes into a `.env` file beside this compose
# file is passed through to the container. Settings *not* named there fall
# back to the app's own defaults and stay editable from the admin UI. The
# `environment:` list below therefore pins only what the deployment must —
# never a runtime setting (SMTP, offline mode, base URL, self-update, …) an
# operator might want to change from the UI. Previously those were exported
# with `${RT_X:-default}` shell defaults, which compose substitutes and
# exports even when unset, so `is_env_locked` saw every one as pinned.
env_file:
- path: .env
required: false
environment:
- RT_STATIC_DIR=/app/frontend/dist
- RT_UPDATE_CONTROL_DIR=/control
- RT_DATA_ROOT=/data/projects
# Accounts and the signing secret belong on the durable volume, beside the
# project data. Left at its default this resolved to $HOME/.reqmesh =
# /app/.reqmesh, which the tmpfs below made root-owned and unwritable —
# login returned 500, and any account that had been written would have
# been discarded on the next restart.
- RT_STATE_DIR=/data/.reqmesh
# TECTONIC_CACHE_DIR is deliberately NOT set. The image ships a pre-warmed
# cache at /opt/tectonic-cache (see Dockerfile.prod), so LaTeX rendering
# needs no network. Pointing it at /data (the empty bind volume) is what
# made fresh installs and upgrades flaky: the baked cache was ignored and
# the first export had to download TeX packages inside the compile timeout —
# slow, network-dependent, and impossible air-gapped, silently dropping to
# the degraded HTML renderer. Mirrors the deploy template.
- RT_HOST=0.0.0.0
- RT_PORT=8000
- "RT_SECRET=${RT_SECRET:?RT_SECRET is required}"
- "RT_ADMIN_PASSWORD=${RT_ADMIN_PASSWORD:?RT_ADMIN_PASSWORD is required}"
- RT_GIT_AUTOCOMMIT=true
- RT_GIT_REMOTE_URL=${RT_GIT_REMOTE_URL:-}
- RT_GIT_PUSH_ON_COMMIT=${RT_GIT_PUSH_ON_COMMIT:-false}
- RT_GIT_PUSH_INTERVAL_MINUTES=${RT_GIT_PUSH_INTERVAL_MINUTES:-0}
- RT_GIT_COMMIT_SCHEDULE=${RT_GIT_COMMIT_SCHEDULE:-every_change}
- RT_GIT_COMMIT_INTERVAL_HOURS=${RT_GIT_COMMIT_INTERVAL_HOURS:-0}
- RT_GIT_COMMIT_CHANGES_THRESHOLD=${RT_GIT_COMMIT_CHANGES_THRESHOLD:-0}
- RT_SEED_DEMO=${RT_SEED_DEMO:-true}
# Single-origin deployment: the SPA is served by the app itself, so no
# cross-origin request should be allowed. Left blank this falls back to
# the *development* default (localhost:5173), which has no business
# being an allowed credentialed origin in production.
- RT_CORS_ORIGINS=${RT_CORS_ORIGINS:-[]}
# Host-header defence. Deliberately NOT defaulted to localhost here: nginx
# forwards the original `Host` (proxy_set_header Host $$host), so the app
# sees the public hostname and a localhost-only allowlist would 400 every
# request behind a reverse proxy. The installer writes the real hostname
# into .env from the domain it already collects; set it yourself
# (comma-separated) for a hand-rolled deployment. Empty means no check.
- RT_ALLOWED_HOSTS=${RT_ALLOWED_HOSTS:-}
- RT_PROFILE=${RT_PROFILE:-team}
- RT_COOKIE_SECURE=${RT_COOKIE_SECURE:-true}
# Loopback alone is wrong *in a container*: the app is published on the
# host (`ports:` above), so traffic from a host-level nginx is DNAT'd and
# arrives from the Docker bridge gateway (172.17.0.1), not 127.0.0.1.
# Trusting only loopback would drop every X-Forwarded-For and key each
# rate-limit bucket on the gateway — i.e. all users sharing one bucket.
# Still much narrower than the old RFC1918-wide default: inside the
# bridge network there are no untrusted LAN hosts.
- RT_PROXY_TRUSTED_CIDR=${RT_PROXY_TRUSTED_CIDR:-127.0.0.0/8,172.16.0.0/12}
volumes:
- reqmesh-data:/data
# Shared with the updater sidecar. The app writes update requests here; it
# never mounts the Docker socket itself.
- reqmesh-control:/control
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true
tmpfs:
# /app/.reqmesh used to be listed here so the read-only rootfs had a
# writable spot for auth state. It was the wrong fix twice over: docker
# mounts it root-owned, so uid 999 could not write it anyway, and tmpfs
# would have thrown the accounts away on restart. State now lives in the
# shared data root via RT_STATE_DIR.
- /tmp
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
# ═══ Update sidecar — performs the supervised container swap ═════════════════
# Holds the Docker socket (which the app container does not) and recreates the
# reqmesh service when an admin triggers an update from the UI. Enable it with
# the `self-update` profile:
#
# docker compose -f docker-compose.prod.yml --profile self-update up -d
#
# Without the profile the app runs normally but reports self-update as
# unavailable (admins see manual update instructions instead).
updater:
image: docker:27-cli
profiles: ["self-update"]
working_dir: /deploy
command: ["sh", "/deploy/scripts/updater/watch.sh"]
environment:
- CONTROL_DIR=/control
- DEPLOY_DIR=/deploy
- COMPOSE_FILE=docker-compose.prod.yml
- IMAGE_REPO=ghcr.io/callumnunesvaz/reqmesh
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- reqmesh-control:/control
# The deployment dir (compose file, .env, scripts). Read-only: the sidecar
# writes only to the control volume.
- ./:/deploy:ro
restart: unless-stopped
# ═══ Optional — reverse proxy with automatic TLS via Caddy ═══════════════════
# Uncomment this block, comment the `ports:` section on the reqmesh service,
# and access via https://<hostname> instead of http://<ip>:8000.
#
# Requires a Caddyfile (see repo root). For a local-only domain use
# `tls internal` to get a self-signed certificate; for a public domain
# Caddy will automatically provision a Let's Encrypt certificate.
#
# caddy:
# image: caddy:2-alpine
# ports:
# - "80:80"
# - "443:443"
# volumes:
# - ./Caddyfile:/etc/caddy/Caddyfile:ro
# - caddy-data:/data
# depends_on:
# - reqmesh
# restart: unless-stopped
volumes:
reqmesh-data:
reqmesh-control:
# caddy-data: