Skip to content

Commit 6575b86

Browse files
committed
deploy: compose hardening (restart/healthcheck/limits/secrets)
2 parents aaa6f9d + e7ca6e7 commit 6575b86

2 files changed

Lines changed: 62 additions & 5 deletions

File tree

docker-compose.yml

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
services:
22
# ── RemnaCore ───────────────────────────────────────────────────────────────
33
remnacore:
4+
restart: unless-stopped
5+
mem_limit: ${REMNACORE_MEM_LIMIT:-768m}
46
build:
57
context: .
68
dockerfile: deploy/docker/Dockerfile
@@ -40,10 +42,23 @@ services:
4042
GOMEMLIMIT: "${GOMEMLIMIT:-512MiB}"
4143
REMNAWAVE_URL: "http://remnawave-backend:3000"
4244
REMNAWAVE_API_TOKEN: "${REMNAWAVE_API_TOKEN:-changeme}"
43-
REMNAWAVE_WEBHOOK_SECRET: "${REMNAWAVE_WEBHOOK_SECRET:-changeme}"
45+
# Required (first-deploy.sh generates it). An empty webhook secret makes
46+
# inbound Remnawave webhooks forgeable (empty-key HMAC), so fail fast.
47+
REMNAWAVE_WEBHOOK_SECRET: "${REMNAWAVE_WEBHOOK_SECRET:?set REMNAWAVE_WEBHOOK_SECRET in .env}"
48+
# At-rest encryption for per-shop bot tokens. Without it the shop-bot
49+
# feature fails at runtime; first-deploy.sh generates it into .env.
50+
SECURITY_ENCRYPTION_KEY: "${SECURITY_ENCRYPTION_KEY:-}"
4451
OUTBOX_RELAY_WORKERS: "${OUTBOX_RELAY_WORKERS:-1}"
4552
OUTBOX_PARTITION_LOOKAHEAD: "${OUTBOX_PARTITION_LOOKAHEAD:-2}"
4653
OUTBOX_RETENTION_DAYS: "${OUTBOX_RETENTION_DAYS:-90}"
54+
healthcheck:
55+
# busybox wget (present in the alpine runtime image). /readyz aggregates
56+
# Postgres/Valkey/NATS/outbox checkers, so Caddy only routes once ready.
57+
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:4000/readyz"]
58+
interval: 10s
59+
timeout: 5s
60+
retries: 5
61+
start_period: 40s
4762
depends_on:
4863
platform-db:
4964
condition: service_healthy
@@ -54,6 +69,8 @@ services:
5469

5570
# ── Platform PostgreSQL ─────────────────────────────────────────────────────
5671
platform-db:
72+
restart: unless-stopped
73+
mem_limit: ${PLATFORM_DB_MEM_LIMIT:-1280m}
5774
image: postgres:18
5875
# PG18 async I/O and performance tuning (sized for 1GB container, SSD storage).
5976
# Data checksums are enabled by default in PG18 (initdb --data-checksums).
@@ -102,18 +119,24 @@ services:
102119

103120
# ── Valkey (Redis-compatible) ───────────────────────────────────────────────
104121
valkey:
122+
restart: unless-stopped
123+
mem_limit: ${VALKEY_MEM_LIMIT:-320m}
105124
image: valkey/valkey:9-alpine
106125
command: --maxmemory 256mb --maxmemory-policy allkeys-lru
107126

108127
# ── NATS with JetStream ────────────────────────────────────────────────────
109128
nats:
129+
restart: unless-stopped
130+
mem_limit: ${NATS_MEM_LIMIT:-320m}
110131
image: nats:2.12-alpine
111132
command: --jetstream --store_dir /data --http_port 8222
112133
volumes:
113134
- nats_data:/data
114135

115136
# ── Remnawave Backend ──────────────────────────────────────────────────────
116137
remnawave-backend:
138+
restart: unless-stopped
139+
mem_limit: ${REMNAWAVE_BACKEND_MEM_LIMIT:-1024m}
117140
image: remnawave/backend:2
118141
environment:
119142
APP_PORT: "3000"
@@ -124,8 +147,9 @@ services:
124147
SUB_PUBLIC_DOMAIN: "${SUB_PUBLIC_DOMAIN:-https://sub.example.com}"
125148
PANEL_DOMAIN: "${PANEL_DOMAIN:-https://panel.example.com}"
126149
FRONT_END_DOMAIN: "${FRONT_END_DOMAIN:-https://panel.example.com}"
127-
METRICS_USER: "${METRICS_USER:-admin}"
128-
METRICS_PASS: "${METRICS_PASS:-admin}"
150+
METRICS_USER: "${METRICS_USER:-metrics}"
151+
# Required (first-deploy.sh generates it) — never ship admin/admin.
152+
METRICS_PASS: "${METRICS_PASS:?set METRICS_PASS in .env}"
129153
REDIS_HOST: "remnawave-valkey"
130154
REDIS_PORT: "6379"
131155
IS_DOCS_ENABLED: "${IS_DOCS_ENABLED:-true}"
@@ -142,6 +166,8 @@ services:
142166

143167
# ── Remnawave PostgreSQL ───────────────────────────────────────────────────
144168
remnawave-db:
169+
restart: unless-stopped
170+
mem_limit: ${REMNAWAVE_DB_MEM_LIMIT:-768m}
145171
image: postgres:17
146172
environment:
147173
POSTGRES_DB: "${REMNAWAVE_DB_NAME:-remnawave}"
@@ -157,6 +183,8 @@ services:
157183

158184
# ── Remnawave Valkey ───────────────────────────────────────────────────────
159185
remnawave-valkey:
186+
restart: unless-stopped
187+
mem_limit: ${REMNAWAVE_VALKEY_MEM_LIMIT:-320m}
160188
image: valkey/valkey:9-alpine
161189

162190
# ── Caddy Reverse Proxy ─────────────────────────────────────────────────────
@@ -171,8 +199,10 @@ services:
171199
- ./web/cabinet/dist:/srv/cabinet:ro
172200
- ./web/admin/dist:/srv/admin:ro
173201
depends_on:
174-
- remnacore
175-
- remnawave-backend
202+
remnacore:
203+
condition: service_healthy
204+
remnawave-backend:
205+
condition: service_started
176206
restart: unless-stopped
177207

178208
volumes:

scripts/first-deploy.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ else
3939
REMNAWAVE_WEBHOOK_SECRET="$(rand_secret 48)"
4040
REMNAWAVE_JWT_AUTH_SECRET="$(rand_secret 48)"
4141
REMNAWAVE_JWT_API_TOKENS_SECRET="$(rand_secret 48)"
42+
# AES-256 key for at-rest bot-token encryption: 32 raw bytes, base64-encoded
43+
# (the app base64-decodes it and requires exactly 32 bytes).
44+
SECURITY_ENCRYPTION_KEY="$(openssl rand -base64 32)"
45+
METRICS_PASS="$(rand_secret 24)"
4246

4347
SERVER_IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "localhost")
4448

@@ -72,13 +76,36 @@ REMNAWAVE_WEBHOOK_SECRET=${REMNAWAVE_WEBHOOK_SECRET}
7276
REMNAWAVE_JWT_AUTH_SECRET=${REMNAWAVE_JWT_AUTH_SECRET}
7377
REMNAWAVE_JWT_API_TOKENS_SECRET=${REMNAWAVE_JWT_API_TOKENS_SECRET}
7478
79+
# ── Secrets ─────────────────────────────────────────────────────────────────
80+
SECURITY_ENCRYPTION_KEY=${SECURITY_ENCRYPTION_KEY}
81+
METRICS_USER=metrics
82+
METRICS_PASS=${METRICS_PASS}
83+
7584
SUB_PUBLIC_DOMAIN=http://${SERVER_IP}
7685
PANEL_DOMAIN=http://${SERVER_IP}:8080
7786
FRONT_END_DOMAIN=http://${SERVER_IP}:8080
7887
EOF
7988
ok ".env generated with random secrets"
8089
fi
8190

91+
# ── Step 1.5: Back-fill required secrets into an older .env ─────────────────
92+
# Newer compose requires METRICS_PASS and SECURITY_ENCRYPTION_KEY (and the app
93+
# runtime role password). An .env from an earlier release lacks them, which would
94+
# make every `docker compose` command below fail on a `${VAR:?}` guard. Append
95+
# any missing secret so re-deploys of an existing install keep working.
96+
ensure_env() {
97+
local key="$1" value="$2"
98+
grep -q "^${key}=" .env || printf '%s=%s\n' "$key" "$value" >> .env
99+
}
100+
if [ -f .env ]; then
101+
ensure_env PLATFORM_APP_DB_USER "remnacore_app"
102+
ensure_env PLATFORM_APP_DB_PASSWORD "$(rand_secret 32)"
103+
ensure_env SECURITY_ENCRYPTION_KEY "$(openssl rand -base64 32)"
104+
ensure_env METRICS_USER "metrics"
105+
ensure_env METRICS_PASS "$(rand_secret 24)"
106+
ok "Required secrets present in .env"
107+
fi
108+
82109
# ── Step 2: Generate JWT keys ──────────────────────────────────────────────
83110
if [ -f keys/private.pem ] && [ -f keys/public.pem ]; then
84111
warn "JWT keys already exist, skipping generation"

0 commit comments

Comments
 (0)