build(project): publish app on loopback and segment Docker networks
Bind the published port to 127.0.0.1 so the app is reachable only through the host reverse proxy, not on the LAN/WAN — a 0.0.0.0 publish would also bypass ufw/firewalld, since Docker's DNAT rules sit ahead of the host firewall. Split the stack onto two networks with deterministic bridge names: `web` (dk-tanabata) for the public-facing side, and `backend` (dk-tanabata-bnd, internal:true) for the private app↔DB tier. The DB sits only on `backend`, which has no gateway, so it has no route off-host. Document TRUSTED_PROXIES and the loopback publish in .env.example. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+11
-1
@@ -14,7 +14,10 @@
|
|||||||
# DATABASE_URL at host.docker.internal (see the Database section below).
|
# DATABASE_URL at host.docker.internal (see the Database section below).
|
||||||
COMPOSE_PROFILES=with-db
|
COMPOSE_PROFILES=with-db
|
||||||
|
|
||||||
# Host port the app is published on. The container always listens on 42776.
|
# Host port the app is published on, bound to 127.0.0.1 (loopback) — a reverse
|
||||||
|
# proxy on the host fronts it (see README → Reverse proxy). The container always
|
||||||
|
# listens on 42776. To expose the app directly without a proxy, drop the
|
||||||
|
# "127.0.0.1:" prefix on the ports line in docker-compose.yml.
|
||||||
APP_PORT=42776
|
APP_PORT=42776
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -51,6 +54,13 @@ JWT_SECRET=change-me-to-a-random-32-byte-secret
|
|||||||
JWT_ACCESS_TTL=15m
|
JWT_ACCESS_TTL=15m
|
||||||
JWT_REFRESH_TTL=720h
|
JWT_REFRESH_TTL=720h
|
||||||
|
|
||||||
|
# Reverse-proxy hops (comma-separated CIDRs/IPs) whose X-Forwarded-For is trusted,
|
||||||
|
# so the auth rate limiter sees real client IPs instead of the proxy's. The default
|
||||||
|
# covers loopback and the Docker bridge ranges a host nginx reaches the container
|
||||||
|
# through; widen/narrow it to match your proxy. Leave at the default for the
|
||||||
|
# standard "host nginx → 127.0.0.1" setup.
|
||||||
|
TRUSTED_PROXIES=127.0.0.1/32,::1/128,172.16.0.0/12
|
||||||
|
|
||||||
# Initial administrator, created on first startup if it does not yet exist.
|
# Initial administrator, created on first startup if it does not yet exist.
|
||||||
# Changing the password later (via the API) is preserved across restarts.
|
# Changing the password later (via the API) is preserved across restarts.
|
||||||
ADMIN_USERNAME=admin
|
ADMIN_USERNAME=admin
|
||||||
|
|||||||
+35
-3
@@ -35,10 +35,23 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
STATIC_DIR: /app/static
|
STATIC_DIR: /app/static
|
||||||
|
|
||||||
# The container always listens on 42776 (Dockerfile default); APP_PORT only
|
# Published on loopback only: a reverse proxy on the host (e.g. nginx) fronts
|
||||||
# changes the host-published port.
|
# the app and proxies to 127.0.0.1:${APP_PORT}. Binding to 127.0.0.1 keeps the
|
||||||
|
# app off the LAN/WAN — a plain "PORT:42776" would publish on 0.0.0.0 and, since
|
||||||
|
# Docker's DNAT rules sit ahead of the host firewall, bypass ufw/firewalld. The
|
||||||
|
# container always listens on 42776 (Dockerfile default); APP_PORT only changes
|
||||||
|
# the host-published port. Drop the 127.0.0.1 prefix if exposing it directly.
|
||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-42776}:42776"
|
- "127.0.0.1:${APP_PORT:-42776}:42776"
|
||||||
|
|
||||||
|
# Two-tier networking. `web` is the app's public-facing bridge (reached via the
|
||||||
|
# published loopback port above; it also provides egress, e.g. to a host
|
||||||
|
# Postgres via host.docker.internal). `backend` is the private tier the app
|
||||||
|
# uses to reach the bundled DB. The DB sits only on `backend`, so nothing on
|
||||||
|
# the host-facing side can reach it.
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
- backend
|
||||||
|
|
||||||
# Wait for the bundled DB when the with-db profile is active. When using a
|
# Wait for the bundled DB when the with-db profile is active. When using a
|
||||||
# host Postgres the db service is disabled, and required:false keeps this
|
# host Postgres the db service is disabled, and required:false keeps this
|
||||||
@@ -76,6 +89,10 @@ services:
|
|||||||
# the app at a Postgres running on the host instead.
|
# the app at a Postgres running on the host instead.
|
||||||
profiles: ["with-db"]
|
profiles: ["with-db"]
|
||||||
|
|
||||||
|
# Private back-end tier only — never on `web`, never published.
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-tanabata}
|
POSTGRES_DB: ${POSTGRES_DB:-tanabata}
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-tanabata}
|
POSTGRES_USER: ${POSTGRES_USER:-tanabata}
|
||||||
@@ -97,6 +114,21 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
|
networks:
|
||||||
|
# Public-facing bridge for this app. The explicit bridge name (instead of
|
||||||
|
# Docker's random br-<hash>) makes it identifiable on the host for tcpdump and
|
||||||
|
# firewall rules.
|
||||||
|
web:
|
||||||
|
driver_opts:
|
||||||
|
com.docker.network.bridge.name: dk-tanabata
|
||||||
|
# Private back-end tier (app ↔ DB). internal:true drops the gateway so the DB
|
||||||
|
# has no route off-host. Note: Linux caps interface names at 15 chars, and
|
||||||
|
# dk-tanabata-bnd is exactly 15 — a longer app name would need a shorter suffix.
|
||||||
|
backend:
|
||||||
|
internal: true
|
||||||
|
driver_opts:
|
||||||
|
com.docker.network.bridge.name: dk-tanabata-bnd
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
app_files:
|
app_files:
|
||||||
app_thumbs:
|
app_thumbs:
|
||||||
|
|||||||
Reference in New Issue
Block a user