# 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` | | `CONTACT_TO` | Inbox that receives submissions | `greg@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` | --- ## 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. Test it: ```bash curl -X POST http://localhost:4321/api/contact \ -H "Content-Type: application/json" \ -d '{"name":"Test","email":"test@example.com","message":"Hello there."}' ``` --- ## 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.