3.6 KiB
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
patiun.com.
1. Configure environment
Copy the example env file and fill in what you need:
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 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://patiun.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@patiun.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
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:
docker compose logs -f greg-site
Healthcheck hits / every 30s. Verify locally:
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:
labels:
- "traefik.enable=true"
- "traefik.http.routers.greg-site.rule=Host(`patiun.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 patiun.com (HTTPS) to 127.0.0.1:4321. With Caddy, automatic TLS is a
one-liner:
patiun.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:
- SMTP (if
SMTP_HOSTis set): sends a real email via nodemailer. Works with any SMTP relay (Gmail app-password, Mailgun, SES, your homelab mail server). - Webhook (if
CONTACT_WEBHOOK_URLis set and SMTP is not): POSTs a JSON payload{ name, email, message, text }. Great for a Discord/Slack/n8n inbound. - Neither configured: returns
503with a friendly "email me directly" message so the form never silently swallows input.
Test it:
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
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:
npm ci
npm run build
HOST=0.0.0.0 PORT=4321 SITE_URL=https://patiun.com node ./dist/server/entry.mjs
Run it under a process manager (systemd, pm2) so it restarts on crash/boot.