Deploy: serve plain HTTP behind Nginx Proxy Manager
CI / check (push) Successful in 6m11s Details

Caddy listens on port 80 inside the container (the Caddyfile had moved
it to 8080, which the port mapping didn't reach). NPM handles the domain
and HTTPS, so compose publishes only HTTP_PORT (default 8080) and drops
the HTTPS ports, certificate volumes and SITE_ADDRESS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-10 12:03:57 -04:00
parent f415227dbf
commit 41cf145135
7 changed files with 49 additions and 59 deletions

View File

@ -1,8 +1,5 @@
# Settings for deploy/compose.yml. Copy to deploy/.env and fill in. # Settings for deploy/compose.yml. Copy to deploy/.env and fill in.
# What Caddy serves: the domain alone gets HTTPS from Let's Encrypt (ports 80 # The port on this machine that serves the game over plain HTTP. In Nginx Proxy
# and 443 must reach this machine). Use :80 for plain HTTP on a LAN. # Manager, forward the domain (with its SSL certificate) to http://<this machine>:<port>.
SITE_ADDRESS=deadhead.belowland.com HTTP_PORT=8080
# Host ports Caddy listens on.
HTTP_PORT=80
HTTPS_PORT=443

View File

@ -1,19 +1,20 @@
# The game as static files (CR-014): it runs and saves on the player's device. # Static game files
# SITE_ADDRESS is the public domain (HTTPS via Let's Encrypt), or ":80" for # HTTPS is handled by Nginx Proxy Manager, which forwards to this container's port 80
# plain HTTP when testing.
{$SITE_ADDRESS} { :80 {
encode zstd gzip encode zstd gzip
root * /srv root * /srv
# Client-side routes (/fleet, /contracts…) all load the app.
# Client-side routes all load the app
try_files {path} /index.html try_files {path} /index.html
file_server file_server
# Vite's hashed bundles and fonts never change under the same name. # Cache hashed Vite bundles and fonts
@hashed path_regexp hashed ^/assets/[^/]+-[A-Za-z0-9_-]{8}\.(js|css|woff2?)$ @hashed path_regexp hashed ^/assets/[^/]+-[A-Za-z0-9_-]{8}\.(js|css|woff2?)$
header @hashed Cache-Control "public, max-age=31536000, immutable" header @hashed Cache-Control "public, max-age=31536000, immutable"
# The shell and service worker must always be checked for a new version. # Always revalidate app shell and service worker
@shell path / /index.html /sw.js /registerSW.js /manifest.webmanifest @shell path / /index.html /sw.js /registerSW.js /manifest.webmanifest
header @shell Cache-Control "no-cache" header @shell Cache-Control "no-cache"
} }

View File

@ -1,20 +1,23 @@
# Deploying Deadhead # Deploying Deadhead
The game runs and saves on each player's device (CR-014), so deploying it means The game runs and saves on each player's device (CR-014), so deploying it means
serving static files. One machine runs Caddy with Docker Compose: it serves the serving static files. One Caddy container serves the web build over plain HTTP;
web build and gets HTTPS certificates for the domain automatically. There is no Nginx Proxy Manager (NPM) in front of it handles the domain and HTTPS. There is
server, database or backup to run; players keep their own saves (Settings → no server, database or backup to run; players keep their own saves (Settings →
Export save). Export save).
## Before the first start ## Before the first start
1. **DNS:** point an `A` record for `deadhead.belowland.com` at the public IP of 1. **Docker:** Docker Engine with the Compose plugin (`docker compose version`).
the network the machine is on. 2. **Settings:** in this folder, `cp .env.example .env` and set `HTTP_PORT` (the
2. **Router:** forward TCP ports 80 and 443 (and UDP 443 for HTTP/3) to the port on this machine that serves the game; default 8080).
machine. Let's Encrypt checks port 80 before it issues a certificate. 3. **NPM:** add a Proxy Host:
3. **Docker:** Docker Engine with the Compose plugin (`docker compose version`). - Domain: `deadhead.belowland.com`
4. **Settings:** in this folder, `cp .env.example .env` and check - Scheme `http`, forward hostname = this machine's IP, forward port = `HTTP_PORT`
`SITE_ADDRESS`. - SSL: request a Let's Encrypt certificate and turn on Force SSL
DNS and port forwarding (80 and 443 to the NPM machine) are NPM's, as for
any other site it serves.
## Start and update ## Start and update
@ -22,17 +25,17 @@ From this folder, on the machine:
```sh ```sh
git pull git pull
APP_BUILD=$(git rev-parse --short HEAD) docker compose up -d --build APP_BUILD=$(git rev-parse --short HEAD) docker compose --env-file .env up -d --build
``` ```
The About screen in Settings shows the build you deployed. Installed copies pick The About screen in Settings shows the build you deployed. Installed copies pick
up the new version the next time they open (the service worker checks the shell up the new version the next time they open (the service worker checks the shell
on every load). on every load).
## On a LAN only (no domain) ## Without NPM
Set `SITE_ADDRESS=:80` in `.env`. Caddy then serves plain HTTP. Installing the The site also works on its own over plain HTTP at `http://<this machine>:<HTTP_PORT>`.
app to a phone home screen needs HTTPS, so this is for testing. Installing the app to a phone home screen needs HTTPS, so that is for testing.
## Any other static host ## Any other static host
@ -47,5 +50,5 @@ deploy/check.sh
``` ```
This builds and starts the site under a separate project name on port 8088, This builds and starts the site under a separate project name on port 8088,
checks the app shell, client-side routes and caching, then removes it. It never checks the app shell, client-side routes, caching and compression, then removes
touches the real site. it. It never touches the real site.

View File

@ -11,9 +11,7 @@ env_file=".check.env"
port="${CHECK_PORT:-8088}" port="${CHECK_PORT:-8088}"
site="http://localhost:$port" site="http://localhost:$port"
cat > "$env_file" <<ENV cat > "$env_file" <<ENV
SITE_ADDRESS=:80
HTTP_PORT=$port HTTP_PORT=$port
HTTPS_PORT=${CHECK_TLS_PORT:-8443}
ENV ENV
compose() { docker compose -p "$project" --env-file "$env_file" "$@"; } compose() { docker compose -p "$project" --env-file "$env_file" "$@"; }
cleanup() { cleanup() {

View File

@ -1,5 +1,6 @@
# Static hosting for the web build (CR-014): Caddy serves the app; the game runs # Static hosting for the web build (CR-014): Caddy serves the app over plain
# and saves on each player's device, so there is no server or database. # HTTP; Nginx Proxy Manager in front of it handles the domain and HTTPS. The
# game runs and saves on each player's device, so there is no server or database.
# From this folder: docker compose --env-file .env up -d --build # From this folder: docker compose --env-file .env up -d --build
# Settings come from deploy/.env (copy .env.example). # Settings come from deploy/.env (copy .env.example).
name: deadhead name: deadhead
@ -12,16 +13,6 @@ services:
args: args:
APP_BUILD: ${APP_BUILD:-dev} APP_BUILD: ${APP_BUILD:-dev}
restart: unless-stopped restart: unless-stopped
environment:
SITE_ADDRESS: ${SITE_ADDRESS:?set SITE_ADDRESS in deploy/.env}
ports: ports:
- "${HTTP_PORT:-80}:80" # Host port → Caddy's port 80 in the container. Point NPM at this host port.
- "${HTTPS_PORT:-443}:443" - "${HTTP_PORT:-8080}:80"
- "${HTTPS_PORT:-443}:443/udp"
volumes:
- caddy-data:/data
- caddy-config:/config
volumes:
caddy-data:
caddy-config:

View File

@ -1,7 +1,7 @@
# The web app on Caddy (Step 13.2, static since CR-014). Build from the repository root: # The web app on Caddy (Step 13.2, static since CR-014). Build from the repository root:
# docker build -f deploy/web.Dockerfile --build-arg APP_BUILD=$(git rev-parse --short HEAD) . # docker build -f deploy/web.Dockerfile --build-arg APP_BUILD=$(git rev-parse --short HEAD) .
# Caddy serves the built app and gets HTTPS certificates for SITE_ADDRESS # Caddy serves the built app over plain HTTP on port 80; Nginx Proxy Manager
# automatically. # in front of it handles the domain and HTTPS.
FROM node:24-slim AS build FROM node:24-slim AS build
RUN corepack enable RUN corepack enable

View File

@ -95,10 +95,10 @@ Until Step 14.11 a Fastify API with Postgres (Drizzle), Better Auth guest sessio
| File | Role | | File | Role |
|---|---| |---|---|
| `compose.yml` | One service (project `deadhead`): `web`, Caddy on `HTTP_PORT`/`HTTPS_PORT`, certificate volumes | | `compose.yml` | One service (project `deadhead`): `web`, Caddy serving plain HTTP on container port 80, published on `HTTP_PORT` (default 8080) for Nginx Proxy Manager to forward to |
| `web.Dockerfile`, `Caddyfile` | Caddy image with the built app: client routes fall back to `index.html`, hashed assets cached for a year, shell, service worker and manifest never; HTTPS for `SITE_ADDRESS` | | `web.Dockerfile`, `Caddyfile` | Caddy image with the built app on `:80`: client routes fall back to `index.html`, hashed assets cached for a year, shell, service worker and manifest never; HTTPS is NPM's |
| `README.md` | Setup: DNS, port forwarding, `.env`, start/update, LAN-only mode, other static hosts | | `README.md` | Setup behind Nginx Proxy Manager, start/update, other static hosts |
| `.env.example` | `SITE_ADDRESS` and host ports; copy to `deploy/.env` | | `.env.example` | `HTTP_PORT`; copy to `deploy/.env` |
| `check.sh` | Builds and starts the site under a throwaway project, checks shell, routes, caching and compression, removes it | | `check.sh` | Builds and starts the site under a throwaway project, checks shell, routes, caching and compression, removes it |
## Client (`apps/web`) ## Client (`apps/web`)