Self-hosted deployment guide — Synology + Docker + Gmail SMTP
A small Node.js web application running in a Docker container on the Synology NAS named Chronicles-NA. It hosts a multi-location checklist that staff complete each Sunday for startup and shutdown procedures. Every Sunday at 3:00 PM the server emails a single digest to media@waterschurch.org summarising what each location checked off and what they missed, then resets the list for the next week.
| Piece | Where | Purpose |
|---|---|---|
| Web app (Node.js + Express) | Container on Chronicles-NA, port 3000 | Serves the UI and stores check states |
| JSON state file | /volume1/docker/recurring-checklist/data/checklists.json | Source of truth for lists, items, and per-location checks |
| Reverse proxy | Boba-Fett (DSM → Control Panel → Login Portal → Advanced → Reverse Proxy) | Terminates HTTPS for the friendly URL and forwards to Chronicles-NA |
| Let’s Encrypt certificate | Boba-Fett (DSM → Control Panel → Security → Certificate) | Issued for check.watersmedia.org; auto-renews every 90 days |
| Scheduler (node-cron) | Inside the container | Fires 0 15 * * 0 in America/New_York to send the digest and reset state |
| Hyper Backup task | Chronicles-NA → Google Drive | Daily 23:30 backup of the docker shared folder |
Internet
│
▼ (https://check.watersmedia.org → public IP)
Router ─ port 443 forwarded to ─► Boba-Fett (10.1.1.4)
│
│ Reverse-proxy rule:
│ check.watersmedia.org
│ → http://10.1.1.1:3000
▼
Chronicles-NA (10.1.1.1)
│
│ Docker container "recurring-checklist"
│ listens on :3000
▼
Node.js / Express
│
├─► /volume1/docker/recurring-checklist/data/ (state file)
└─► SMTP smtp.gmail.com:587 (digest)
| Hostname | Role |
|---|---|
https://check.watersmedia.org | Public URL for the app (TLS via Boba-Fett) |
http://10.1.1.1:3000 | Direct LAN access to the app (bypasses reverse proxy) |
chronicles-na (10.1.1.1) | Synology that runs the container |
boba-fett (10.1.1.4) | Synology that owns the public IP & reverse proxy |
The system is already deployed. The steps below document exactly how it was built so it can be reproduced on another NAS if needed.
The application source lives in this repository under server/. A deploy tarball is produced from that folder (excluding node_modules):
tar --exclude='node_modules' --exclude='.DS_Store' \
-czf checklist-deploy.tgz -C server .
docker shared folder (created automatically when Container Manager is installed).recurring-checklist inside docker.checklist-deploy.tgz into the new folder..tgz in two passes (it leaves an intermediate .tar) — right-click that .tar and extract again.checklist-deploy.tgz, checklist-deploy.tar, and the PaxHeader folder..envThe bundle ships a working .env. The values that matter are listed below. Edit through File Station → right-click → Open with → Text Editor (or any text editor).
| Key | Value used | Notes |
|---|---|---|
PORT | 3000 | Container-internal port. Don’t change unless also editing docker-compose.yml. |
ADMIN_PASSWORD | (set in .env) | Password used to enter “Admin” mode in the web UI. |
REPORT_EMAIL | media@waterschurch.org | Recipient of the Sunday digest. |
REPORT_CRON | 0 15 * * 0 | Sunday at 3:00 PM in REPORT_TZ. |
REPORT_TZ | America/New_York | IANA timezone for the cron. |
RESET_AFTER_DIGEST | true | Server clears all check states after the digest is sent. |
SMTP_HOST | smtp.gmail.com | |
SMTP_PORT | 587 | STARTTLS port. |
SMTP_USER | media@waterschurch.org | Sender — a Google Workspace account. |
SMTP_PASS | Gmail App Password (16 chars) | Generated at myaccount.google.com/apppasswords. NOT the regular Gmail password — requires 2-Step Verification. |
SMTP_FROM | Recurring Checklist <media@waterschurch.org> |
docker shared folder automatically).recurring-checklist. Path: /volume1/docker/recurring-checklist. Source: “Use existing docker-compose.yml”.docker compose up; first build takes ~60 seconds. Final log line should be Exit Code: 0.http://10.1.1.1:3000 serves the app.check.watersmedia.org, External Address Auto. Status should be Normal.HTTPS · check.watersmedia.org · port 443HTTP · 10.1.1.1 · port 3000check.watersmedia.orgmedia@waterschurch.orgcheck.watersmedia.org row to use the new cert → OK.check.watersmedia.org traffic, terminates TLS with the Let’s Encrypt cert, and forwards plain HTTP to Chronicles-NA:3000 over the LAN.
Any browser on any device:
https://check.watersmedia.org
First visit on each device asks for a name. That name is the location identifier (e.g. Woonsocket, North Attleboro). Case and whitespace are normalised — woonsocket, WOONSOCKET, and Woonsocket all collapse to the same canonical name. The name is stored in the browser’s localStorage and can be changed by clicking the name pill in the top-right.
Each location ticks off items independently. Unassigned lists are visible to all locations but checks are per-location. Lists with a specific location are only visible to (and writeable by) that location.
ADMIN_PASSWORD.A node-cron job fires at 0 15 * * 0 in America/New_York (Sunday 3:00 PM local). It:
checklists.json.smtp.gmail.com:587 using the configured app password, to REPORT_EMAIL only. The subject is Checklist Report — <weekday, month, day>.RESET_AFTER_DIGEST=true, the entire check-states map is cleared so the next week starts clean. Items themselves are preserved.To change the schedule, edit REPORT_CRON in .env and rebuild the container (see §8).
Hyper Backup task “Recurring Checklist” on Chronicles-NA:
media@waterschurch.org).docker shared folder (includes everything under /volume1/docker).Restoring an entire run is as simple as: open Hyper Backup → select task → Restore Data → pick a version → restore to /volume1/docker/recurring-checklist/data/checklists.json. The container picks up the new file on next request (no restart needed).
The state file (data/checklists.json) and the configuration file (.env) are preserved between updates because they live in a Docker bind-mount and on disk. The update procedure is:
checklist-deploy.tgz from the latest server/ source./volume1/docker/recurring-checklist/ via File Station..env aside so it’s not overwritten (the bundle ships an example .env, but if the deploy script excludes it, this step is unnecessary — confirm with the developer per release)..tgz / .tar / PaxHeader.http://10.1.1.1:3000 still loads, and any new functionality from the release is visible.Rollback: keep the previous checklist-deploy.tgz archived. To roll back, re-extract the older bundle, rebuild. State file is untouched.
| Symptom | Where to look | Fix |
|---|---|---|
| App URL times out | Container Manager → recurring-checklist → status | If stopped: Action → Start. If crashing: open container Log, look for errors. |
| Digest email never arrived | Container Manager → container Log, search [digest] | Look for Failed: .... Most often Gmail app password revoked or rotated; regenerate at myaccount.google.com/apppasswords and update SMTP_PASS in .env, then rebuild. |
| Cert renewal failed | Boba-Fett → Control Panel → Security → Certificate | Right-click the cert → Renew. Requires port 80 reachable on Boba-Fett. |
| State got corrupted / accidental wipe | Hyper Backup → Recurring Checklist task | Restore Data → pick latest known-good version → restore the data/ folder only. |
| “Wrong password” on admin login | .env on the NAS | Compare ADMIN_PASSWORD. If changed, rebuild container (env vars are read on container start). |
| Reverse proxy returns Boba-Fett DSM instead of the app | Boba-Fett → Control Panel → Login Portal → Advanced → Reverse Proxy | Confirm rule for check.watersmedia.org exists and routes to 10.1.1.1:3000. Cert must be bound (see §3.5). |
# See the running container
sudo docker ps | grep recurring
# Tail logs
sudo docker logs -f recurring-checklist
# Manually restart the container
sudo docker compose -f /volume1/docker/recurring-checklist/docker-compose.yml restart
# Look at the live state file
cat /volume1/docker/recurring-checklist/data/checklists.json | head
.env on the NAS (key ADMIN_PASSWORD). Rotate by editing .env and rebuilding the container.https://myaccount.google.com/apppasswords using the media@waterschurch.org Google Workspace account. Stored in .env as SMTP_PASS. To rotate: revoke the existing app password in Google, generate a new one, paste into .env, rebuild.check.watersmedia.org tied to the Synology Account used during DDNS setup. Renewal is automatic.server/ directory of recurring-checklist-app, maintained by the application developer.