Put it behind HTTPS
The site itself speaks plain HTTP on port 3001. For a real site you put a web server in front of it that provides HTTPS and passes requests on. This page is for whoever manages the server, and has a working set-up for Caddy and for nginx.
Why HTTPS is not optional
In production the site marks its login cookies Secure. A browser then keeps them only on an https:// address. If you reach the site over plain http://, an admin or member logs in, the cookie is dropped, and the next page sends them back to the login page. That is the symptom of a missing or wrong HTTPS set-up.
The flag follows the site's own mode, not the request. It ignores X-Forwarded-Proto, so you cannot make plain HTTP work by adding that header.
Two other facts about how the site meets your proxy:
- Links come only from
SITE_URL. In production the site builds every emailed link fromSITE_URLand never from theHostheader or any forwarded header. So setSITE_URLto the exact address people use, withhttps://. - The only forwarded header it reads is the visitor's address,
CF-Connecting-IPor elseX-Forwarded-For, which it passes to your bot-check provider when someone sends the contact form, buys a ticket or gives. SendX-Forwarded-Forfrom your proxy if you use a bot check.
Before you start, point your domain's DNS at the server. For choir.example.org and www.choir.example.org, that is an A record (and AAAA for IPv6) for each, and open ports 80 and 443.
Caddy
Caddy gets and renews certificates by itself. This is the Caddyfile that the Docker set-up ships, with the target changed from the container to the local machine. It was checked with caddy validate.
choir.example.org {
encode zstd gzip
reverse_proxy localhost:3001
}
# Send www to the bare domain
www.choir.example.org {
redir https://choir.example.org{uri} permanent
}
Save it as /etc/caddy/Caddyfile and reload Caddy (sudo systemctl reload caddy). The first request to each name makes Caddy fetch its certificate, so DNS must already point here.
nginx
nginx needs certificates you already have, for example from Let's Encrypt. This server block was written for this guide and checked with nginx -t; it was not run with live traffic. Put it in /etc/nginx/conf.d/choir.conf:
# Send plain http, and www, to the one address
server {
listen 80;
listen [::]:80;
server_name choir.example.org www.choir.example.org;
return 301 https://choir.example.org$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name www.choir.example.org;
ssl_certificate /etc/letsencrypt/live/choir.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/choir.example.org/privkey.pem;
return 301 https://choir.example.org$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name choir.example.org;
ssl_certificate /etc/letsencrypt/live/choir.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/choir.example.org/privkey.pem;
# The biggest upload the site accepts is a gallery file: 100 MB unless you
# changed UPLOAD_MAX_GALLERY_MB. Allow a little more than the largest limit.
client_max_body_size 110m;
location / {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Then test and reload:
sudo nginx -t
sudo systemctl reload nginx
(http2 on; needs nginx 1.25.1 or newer. On an older nginx, put http2 after ssl on the listen lines instead.)
Upload size
nginx refuses a request body bigger than client_max_body_size (1 MB unless you change it) with "413 Request Entity Too Large". The usual sign is a large photo, video or recording that will not upload while small ones do. Set it to a little more than your largest upload limit:
| Setting | Limit it sets | Default |
|---|---|---|
UPLOAD_MAX_IMAGE_MB | One uploaded image (a logo, a poster, a photo in a setting) | 10 |
UPLOAD_MAX_GALLERY_MB | One gallery photo or video | 100 |
UPLOAD_MAX_FILE_MB | One member file | 50 |
UPLOAD_MAX_TRACK_MB | One rehearsal track | 50 |
With the defaults the largest is 100, hence 110m. Caddy sets no limit of its own. See Public file addresses and upload limits.
Do not cache pages at the proxy
The site already sends the right caching headers, so a proxy needs no caching rules, and should have none:
- The page itself, and every page that is only the app's shell, is sent with
Cache-Control: no-cache, so a browser asks again each time. If a proxy kept a copy, visitors would go on seeing the old version after an update. - Files under
/assets/have a fingerprint in their names, so they never change, and are sent aspublic, max-age=31536000, immutable. - Everything else (favicon, sample images) is checked with the server on each use.
Do not cache /api/ at the proxy either.
A health check for a monitor
GET /api/health answers 200 with {"status":"ok","platform":"node","version":"1.6.12"}. Point an uptime monitor at https://choir.example.org/api/health. See Logs and health checks.
Check it works
- Open
https://choir.example.org/admin/loginand log in. You should land on the dashboard and stay logged in. - Open
http://choir.example.org(nos) andhttps://www.choir.example.org. Both should end up athttps://choir.example.org.
If you are bounced back to the login page, the connection is not HTTPS, or SITE_URL is wrong. The security headers the site sends and what to add are in Security.