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.
# What Caddy serves: the domain alone gets HTTPS from Let's Encrypt (ports 80
# and 443 must reach this machine). Use :80 for plain HTTP on a LAN.
SITE_ADDRESS=deadhead.belowland.com
# Host ports Caddy listens on.
HTTP_PORT=80
HTTPS_PORT=443
# The port on this machine that serves the game over plain HTTP. In Nginx Proxy
# Manager, forward the domain (with its SSL certificate) to http://<this machine>:<port>.
HTTP_PORT=8080

View File

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

View File

@ -1,20 +1,23 @@
# Deploying Deadhead
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
web build and gets HTTPS certificates for the domain automatically. There is no
server, database or backup to run; players keep their own saves (Settings →
serving static files. One Caddy container serves the web build over plain HTTP;
Nginx Proxy Manager (NPM) in front of it handles the domain and HTTPS. There is
no server, database or backup to run; players keep their own saves (Settings →
Export save).
## Before the first start
1. **DNS:** point an `A` record for `deadhead.belowland.com` at the public IP of
the network the machine is on.
2. **Router:** forward TCP ports 80 and 443 (and UDP 443 for HTTP/3) to the
machine. Let's Encrypt checks port 80 before it issues a certificate.
3. **Docker:** Docker Engine with the Compose plugin (`docker compose version`).
4. **Settings:** in this folder, `cp .env.example .env` and check
`SITE_ADDRESS`.
1. **Docker:** Docker Engine with the Compose plugin (`docker compose version`).
2. **Settings:** in this folder, `cp .env.example .env` and set `HTTP_PORT` (the
port on this machine that serves the game; default 8080).
3. **NPM:** add a Proxy Host:
- Domain: `deadhead.belowland.com`
- Scheme `http`, forward hostname = this machine's IP, forward port = `HTTP_PORT`
- 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
@ -22,17 +25,17 @@ From this folder, on the machine:
```sh
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
up the new version the next time they open (the service worker checks the shell
on every load).
## On a LAN only (no domain)
## Without NPM
Set `SITE_ADDRESS=:80` in `.env`. Caddy then serves plain HTTP. Installing the
app to a phone home screen needs HTTPS, so this is for testing.
The site also works on its own over plain HTTP at `http://<this machine>:<HTTP_PORT>`.
Installing the app to a phone home screen needs HTTPS, so that is for testing.
## 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,
checks the app shell, client-side routes and caching, then removes it. It never
touches the real site.
checks the app shell, client-side routes, caching and compression, then removes
it. It never touches the real site.

View File

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

View File

@ -1,5 +1,6 @@
# Static hosting for the web build (CR-014): Caddy serves the app; the game runs
# and saves on each player's device, so there is no server or database.
# Static hosting for the web build (CR-014): Caddy serves the app over plain
# 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
# Settings come from deploy/.env (copy .env.example).
name: deadhead
@ -12,16 +13,6 @@ services:
args:
APP_BUILD: ${APP_BUILD:-dev}
restart: unless-stopped
environment:
SITE_ADDRESS: ${SITE_ADDRESS:?set SITE_ADDRESS in deploy/.env}
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
- "${HTTPS_PORT:-443}:443/udp"
volumes:
- caddy-data:/data
- caddy-config:/config
volumes:
caddy-data:
caddy-config:
# Host port → Caddy's port 80 in the container. Point NPM at this host port.
- "${HTTP_PORT:-8080}:80"

View File

@ -1,7 +1,7 @@
# 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) .
# Caddy serves the built app and gets HTTPS certificates for SITE_ADDRESS
# automatically.
# Caddy serves the built app over plain HTTP on port 80; Nginx Proxy Manager
# in front of it handles the domain and HTTPS.
FROM node:24-slim AS build
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 |
|---|---|
| `compose.yml` | One service (project `deadhead`): `web`, Caddy on `HTTP_PORT`/`HTTPS_PORT`, certificate volumes |
| `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` |
| `README.md` | Setup: DNS, port forwarding, `.env`, start/update, LAN-only mode, other static hosts |
| `.env.example` | `SITE_ADDRESS` and host ports; copy to `deploy/.env` |
| `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 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 behind Nginx Proxy Manager, start/update, other static hosts |
| `.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 |
## Client (`apps/web`)