From 5a05bb86e106d90c5c5e5af0080afdb6719fa13b Mon Sep 17 00:00:00 2001 From: Masahiko AMANO Date: Mon, 15 Jun 2026 14:51:57 +0300 Subject: [PATCH] build(project): publish app on loopback and segment Docker networks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .env.example | 12 +++++++++++- docker-compose.yml | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 9c9c19d..7d65d5e 100644 --- a/.env.example +++ b/.env.example @@ -14,7 +14,10 @@ # DATABASE_URL at host.docker.internal (see the Database section below). 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 # --------------------------------------------------------------------------- @@ -51,6 +54,13 @@ JWT_SECRET=change-me-to-a-random-32-byte-secret JWT_ACCESS_TTL=15m 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. # Changing the password later (via the API) is preserved across restarts. ADMIN_USERNAME=admin diff --git a/docker-compose.yml b/docker-compose.yml index 1dba992..8bc9ff8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,10 +35,23 @@ services: environment: STATIC_DIR: /app/static - # The container always listens on 42776 (Dockerfile default); APP_PORT only - # changes the host-published port. + # Published on loopback only: a reverse proxy on the host (e.g. nginx) fronts + # 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: - - "${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 # 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. profiles: ["with-db"] + # Private back-end tier only — never on `web`, never published. + networks: + - backend + environment: POSTGRES_DB: ${POSTGRES_DB:-tanabata} POSTGRES_USER: ${POSTGRES_USER:-tanabata} @@ -97,6 +114,21 @@ services: timeout: 5s retries: 10 +networks: + # Public-facing bridge for this app. The explicit bridge name (instead of + # Docker's random br-) 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: app_files: app_thumbs: