mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
215 lines
8.0 KiB
Markdown
215 lines
8.0 KiB
Markdown
# Deployment Guide
|
|
|
|
The site runs as a **Node server inside a container** on your homelab Cosmos
|
|
system, fronted by your reverse proxy (Traefik / Caddy / nginx) with TLS for
|
|
`gregkilmer.com`.
|
|
|
|
---
|
|
|
|
## 1. Configure environment
|
|
|
|
Copy the example env file and fill in what you need:
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
```
|
|
|
|
At minimum, set `SITE_URL` (your canonical public URL). To enable contact-form
|
|
delivery, set **either** SMTP vars **or** `CONTACT_WEBHOOK_URL` (see
|
|
[Contact delivery](#contact-delivery) below). Everything else is optional.
|
|
|
|
| Variable | Purpose | Default |
|
|
| ----------------------------------------------------- | ----------------------------------------- | ------------------------ |
|
|
| `PORT` | Port the Node server binds to | `4321` |
|
|
| `HOST` | Bind address (`0.0.0.0` for container) | `0.0.0.0` |
|
|
| `SITE_URL` | Canonical URL for metadata | `https://gregkilmer.com` |
|
|
| `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS` | SMTP relay | unset |
|
|
| `SMTP_FROM` | From address for outgoing mail | derived from `SITE_URL` |
|
|
| `SMTP_ALLOW_SELFSIGNED` | Accept self-signed cert (internal hop) | `false` |
|
|
| `CONTACT_TO` | Inbox that receives submissions | `contact@gregkilmer.com` |
|
|
| `CONTACT_WEBHOOK_URL` | JSON webhook fallback (Discord/Slack/n8n) | unset |
|
|
| `CONTACT_RATE_LIMIT` / `CONTACT_RATE_WINDOW_MIN` | Per-IP rate limit | `5` / `15` |
|
|
| `TURNSTILE_SITE_KEY` / `TURNSTILE_SECRET_KEY` | Cloudflare Turnstile bot protection | unset (skipped) |
|
|
|
|
---
|
|
|
|
## 2. Build & run with Docker Compose
|
|
|
|
```bash
|
|
docker compose up -d --build
|
|
```
|
|
|
|
This builds the multi-stage image (`Dockerfile`), starts the container, and maps
|
|
host port `${HOST_PORT:-4321}` to container `4321`. Logs:
|
|
|
|
```bash
|
|
docker compose logs -f greg-site
|
|
```
|
|
|
|
Healthcheck hits `/` every 30s. Verify locally:
|
|
|
|
```bash
|
|
curl http://localhost:4321/
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Reverse proxy + TLS
|
|
|
|
Point your Cosmos reverse proxy at the container on port `4321`. The two common
|
|
shapes:
|
|
|
|
### Option A — Traefik (labels)
|
|
|
|
If your Cosmos uses Traefik, add labels to the service in `docker-compose.yml`:
|
|
|
|
```yaml
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.routers.greg-site.rule=Host(`gregkilmer.com`)"
|
|
- "traefik.http.routers.greg-site.entrypoints=websecure"
|
|
- "traefik.http.routers.greg-site.tls.certresolver=your-resolver"
|
|
- "traefik.http.services.greg-site.loadbalancer.server.port=4321"
|
|
```
|
|
|
|
Then drop the `ports:` mapping and join the Traefik network instead.
|
|
|
|
### Option B — Caddy / nginx
|
|
|
|
Forward `gregkilmer.com` (HTTPS) to `127.0.0.1:4321`. With Caddy, automatic TLS is a
|
|
one-liner:
|
|
|
|
```caddyfile
|
|
gregkilmer.com {
|
|
reverse_proxy 127.0.0.1:4321
|
|
}
|
|
```
|
|
|
|
Either way, ensure the proxy forwards `X-Forwarded-For` / `X-Real-IP` so the
|
|
contact API's rate limiter sees real client IPs (not the proxy's).
|
|
|
|
---
|
|
|
|
## 4. Contact delivery
|
|
|
|
The `/api/contact` endpoint delivers submissions through the first configured
|
|
path:
|
|
|
|
1. **SMTP** (if `SMTP_HOST` is set): sends a real email via nodemailer. Works
|
|
with any SMTP relay (Gmail app-password, Mailgun, SES, your homelab mail
|
|
server).
|
|
2. **Webhook** (if `CONTACT_WEBHOOK_URL` is set and SMTP is not): POSTs a JSON
|
|
payload `{ name, email, message, text }`. Great for a Discord/Slack/n8n
|
|
inbound.
|
|
3. **Neither configured**: returns `503` with a friendly "email me directly"
|
|
message so the form never silently swallows input.
|
|
|
|
### SMTP relay DNS requirements (homelab)
|
|
|
|
When pointing the form at a homelab SMTP relay (Postfix, Mailcow, etc.), the
|
|
relay itself must have proper DNS records or mail **will** land in recipients'
|
|
spam folders — this is the relay's responsibility, not the site's. Ensure the
|
|
sending domain (the host part of `SMTP_FROM`, or the relay's HELO name) has:
|
|
|
|
- **SPF** (`TXT` on the domain): lists the relay's IP as an authorized sender.
|
|
- **DKIM** (`TXT` on `selector._domainkey`): signs outgoing messages so the
|
|
relay's identity can be verified.
|
|
- **DMARC** (`TXT` on `_dmarc`): tells receivers how to handle SPF/DKIM
|
|
failures (start at `p=none` to monitor, tighten to `quarantine` later).
|
|
- **rDNS / PTR** on the relay's public IP: must match the relay's HELO name.
|
|
|
|
`SMTP_FROM` should use a domain you control (e.g. `no-reply@gregkilmer.com`),
|
|
not `gmail.com` — SPF on a domain you don't own will reject or sink the mail.
|
|
|
|
Test delivery + spam score with [mail-tester.com](https://www.mail-tester.com/)
|
|
(send a form submission to the unique address it gives you).
|
|
|
|
#### Self-signed certificates on an internal relay
|
|
|
|
If the SMTP relay (e.g. Stalwart) is on the same private Docker network as the
|
|
site container and presents a self-signed TLS certificate, nodemailer's default
|
|
certificate verification will reject the connection with
|
|
`Error: self-signed certificate` (`code: ESOCKET`). For a trusted internal hop
|
|
that never traverses an untrusted network, set:
|
|
|
|
```
|
|
SMTP_ALLOW_SELFSIGNED=true
|
|
```
|
|
|
|
This disables TLS certificate verification for the SMTP connection only. It is
|
|
`false` by default; only enable it when the relay is on a trusted private
|
|
network. For a relay reachable over the public internet, prefer installing a
|
|
real certificate (Let's Encrypt) on the relay instead.
|
|
|
|
### Cloudflare Turnstile (bot protection)
|
|
|
|
The form includes an invisible Cloudflare Turnstile challenge. To enable it:
|
|
|
|
1. Create a widget at
|
|
[Cloudflare Turnstile](https://dash.cloudflare.com/?to=/:account/turnstile).
|
|
Use the **Managed** challenge mode (invisible, no puzzle).
|
|
2. Put the **site key** in `TURNSTILE_SITE_KEY` and the **secret key** in
|
|
`TURNSTILE_SECRET_KEY` (both in `.env`).
|
|
3. The site key is public and ships to the browser; the secret stays
|
|
server-side. The API verifies each token against Cloudflare's `siteverify`
|
|
endpoint before sending mail.
|
|
|
|
If neither key is set, Turnstile is skipped entirely (useful for local dev —
|
|
the form works without a challenge). The honeypot + per-IP rate limit still
|
|
apply regardless.
|
|
|
|
Test it:
|
|
|
|
```bash
|
|
# With Turnstile configured, this is rejected (no/bad token) with 400:
|
|
curl -X POST http://localhost:4321/api/contact \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"name":"Test","email":"test@example.com","message":"Hello there."}'
|
|
|
|
# Without Turnstile (keys unset), this succeeds and delivers mail:
|
|
curl -X POST http://localhost:4321/api/contact \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"name":"Test","email":"test@example.com","message":"Hello there."}'
|
|
```
|
|
|
|
### Anti-scraping
|
|
|
|
The site applies a layered anti-scraping policy:
|
|
|
|
- **`public/robots.txt`** allows legitimate search engines (Google, Bing) but
|
|
blocks known AI training bots (GPTBot, ClaudeBot, CCBot, Bytespider, etc.)
|
|
and disallows `/api/` and `/contact` for everyone. Note: robots.txt is
|
|
voluntary; determined scrapers ignore it.
|
|
- **`noindex` on `/contact`**: the contact page emits
|
|
`<meta name="robots" content="noindex, nofollow">` so it won't be indexed
|
|
even if a bot reaches it.
|
|
- **Email obfuscation**: the Gmail address is never in the HTML source as
|
|
plaintext; it's base64-encoded and decoded to a `mailto:` client-side on
|
|
click. Dumb harvesters that grep for `@` or `mailto:` find nothing.
|
|
|
|
---
|
|
|
|
## 5. Updating the site
|
|
|
|
```bash
|
|
git pull
|
|
docker compose up -d --build
|
|
```
|
|
|
|
The build stage reinstalls deps and rebuilds `dist/`; the runtime stage swaps in
|
|
the new artifact with zero config.
|
|
|
|
---
|
|
|
|
## 6. Without Docker (bare Node)
|
|
|
|
If you prefer to run Node directly on the host:
|
|
|
|
```bash
|
|
npm ci
|
|
npm run build
|
|
HOST=0.0.0.0 PORT=4321 SITE_URL=https://gregkilmer.com node ./dist/server/entry.mjs
|
|
```
|
|
|
|
Run it under a process manager (systemd, pm2) so it restarts on crash/boot.
|