11 KiB
molohttp deploy and operations
Production server molodetz. molohttp 0.9.1. Facts below were read from the live system on 2026-10-05. This is a production box: follow the backup specification before every change.
Access
Operator account is retoor. Root login is never needed.
retooris in thesudogroup with(ALL) NOPASSWD: ALL(verified withsudo -n trueandsudo -n -l).- Every privileged command in this guide runs under
sudoand also works non-interactively in scripts. /etc/molohttpismolohttp:molohttp 750: reads and edits both requiresudo.
How it works
molohttp is the single front door on ports 80 and 443. One systemd unit runs one process as user molohttp (24+ days uptime at time of writing).
- Binary:
/usr/local/bin/molohttp(root:root 755). - Unit:
/etc/systemd/system/molohttp.service(root:root 644), enabled and active. - Start:
molohttp run --config /etc/molohttp/config.json --env /etc/molohttp/.env.json, working directory/etc/molohttp. - Reload:
ExecReload=/bin/kill -HUP $MAINPID, sosudo systemctl reload molohttpapplies config without dropping connections. There is no config-validate subcommand; validate JSON by hand (see change procedure). - Privileged ports are bound via
CAP_NET_BIND_SERVICE; the process itself is unprivileged withProtectSystem=strictandProtectHome=true. config.jsonholds 95 sites, allenabled, allreverse_proxytohttp://127.0.0.1:<port>with one exception: a single upstream onhttp://pravda.education:10500. One site carries a basic-auth middleware. Molodetz owns three:site-022molodetz.nl(production, see Cutover),site-094staging.app.molodetz.nl(see Staging),site-095molodetz-test.app.molodetz.nl(see Test).- Hostnames must be unique across sites. On reload molohttp keeps the first site per hostname and silently drops later duplicates, persisting the result. Always assert the hostname is free before appending (observed 2026-10-05: a duplicate
test.app.molodetz.nlentry vanished on reload becausesite-085already claimed it). - TLS: minimum TLS 1.2, HSTS one year with subdomains. Certificates live per hostname as
/etc/molohttp/certs/<host>.pemplus<host>.key. - ACME: production Let's Encrypt, account
retoor@molodetz.nl, renewal check every 12 hours starting 30 days before expiry, state in/var/lib/molohttp/acme. - Logging: JSON to stdout, collected by journald (
SyslogIdentifier=molohttp). Theaccess_log_path/error_log_pathsettings are not materialized as live files. - Admin: the admin UI is deliberately not exposed.
admin.molodetz.nlis not hosted, routed, or resolvable: no site entry, no DNS record. Exposing it would be a security flaw. The only admin path is the local CLI:molohttp admin user-list,molohttp admin hash.molohttp install/uninstallmust run as root and are out of scope for routine work.
Files and owners
Canonical state. Verify with sudo find /etc/molohttp /var/lib/molohttp /var/log/molohttp -maxdepth 2 -exec stat -c '%U:%G %a %n' {} +.
| Path | Owner | Mode | Notes |
|---|---|---|---|
/etc/molohttp |
molohttp:molohttp |
750 | config root, sudo to enter |
/etc/molohttp/config.json |
molohttp:molohttp |
640 | site table, exact path read |
/etc/molohttp/.env.json |
molohttp:molohttp |
640 | global settings, holds password hash |
/etc/molohttp/certs |
molohttp:molohttp |
700 | per-host pairs |
/etc/molohttp/certs/*.key |
molohttp:molohttp |
600 | private keys |
/etc/molohttp/certs/*.pem |
molohttp:molohttp |
644 | certificates |
/etc/molohttp/*.bak-* |
root:root |
640 | operator backups, this spec only |
/var/lib/molohttp |
molohttp:molohttp |
750 | state root |
/var/lib/molohttp/acme |
molohttp:molohttp |
750 | live ACME state |
/var/log/molohttp |
molohttp:molohttp |
750 | empty dir, daemon logs to journal |
/var/www/.well-known |
molohttp:molohttp |
750 | ACME challenges |
/etc/systemd/system/molohttp.service |
root:root |
644 | unit file |
/usr/local/bin/molohttp |
root:root |
755 | binary |
Known anomalies, observed 2026-10-05, left untouched pending a decision:
- Six key pairs under
certs/are ownedcaddy:caddy(thepravda.educationhosts plusmail.molodetz.nl). The.keyfiles at mode 600 are unreadable by themolohttpuser; confirm those hosts serve TLS correctly, then normalize withsudo chown molohttp:molohttpon the pair. /var/lib/molohttp/analyticsplus/var/lib/molohttp/stats(caddy:caddy, stale since Jul 27) are leftovers from a previous server. Nothing reads them. (A stale 2.5 GBcaddy-ownedaccess.login/var/log/molohttpwas already removed on 2026-10-05 after confirming nothing held it open.)- Older backups in
/etc/molohttpuse four different naming schemes and mixed owners. They are harmless history; all new backups follow the one format below.
Backup specification
Always back up before editing. One format, no variants:
<file>.bak-<reason>-<YYYYMMDDHHMMSS>, owned root:root, mode 640, next to the original. Siblings are safe: the daemon reads config.json and .env.json by exact path only, never by glob.
FILE=/etc/molohttp/config.json
REASON=site-add-example
STAMP=$(date +%Y%m%d%H%M%S)
sudo cp -p "$FILE" "$FILE.bak-$REASON-$STAMP"
sudo chown root:root "$FILE.bak-$REASON-$STAMP"
sudo chmod 640 "$FILE.bak-$REASON-$STAMP"
ls -la "$FILE"*
Rules: reason is a required lowercase slug (site-add-<host>, site-remove-<host>, upstream-<host>, tls-<host>, env-<what>). Never delete old backups; at about 52 KB each they are the audit trail. Never invent a second format.
Change procedure
- Back up per the specification above.
- Edit with
sudo(for examplesudo -e /etc/molohttp/config.json). - Restore ownership, since root-written files lock the daemon out:
sudo chown molohttp:molohttp /etc/molohttp/config.json,sudo chmod 640 /etc/molohttp/config.json. - Validate:
sudo -n cat /etc/molohttp/config.json | python3 -m json.tool > /dev/null. - Apply:
sudo systemctl reload molohttp. - Verify:
systemctl is-active molohttp,curl -s -o /dev/null -w "%{http_code}\n" https://<host>/, andsudo journalctl -u molohttp --no-pager -n 20. - Roll back on any failure:
sudo cp -p <backup> /etc/molohttp/config.json,sudo chown molohttp:molohttp /etc/molohttp/config.json,sudo chmod 640 /etc/molohttp/config.json, then reload again and re-verify.
Use restart instead of reload only when the process itself must be replaced. Never stop without start on this box.
Add or remove a site
Append one object to the sites array. Keep id unique (site-094, site-095, ...); name equals the primary hostname.
{
"id": "site-094",
"name": "new.molodetz.nl",
"enabled": true,
"matchers": {"hosts": ["new.molodetz.nl"], "headers": {}, "paths": []},
"handler": {
"type": "reverse_proxy",
"upstreams": [{"url": "http://127.0.0.1:8XXX", "weight": 1}],
"load_balancing": "round_robin",
"websocket": false,
"connection_timeout_ms": 5000,
"read_timeout_ms": 30000
}
}
Pick a free localhost port for the upstream and keep the app itself bound to 127.0.0.1. Certificates are provisioned automatically through ACME within seconds of the first HTTPS traffic; verify with curl and openssl s_client -connect <host>:443 -servername <host>, and check the journal on any TLS error. Per-host files under certs/ may materialize later on molohttp's own schedule (observed with staging: valid certificate served immediately, files not yet present), so their absence right after creation is not a failure. Removal is the reverse: back up, delete the object, validate, reload, verify the hostname no longer routes.
Staging
staging.app.molodetz.nl (site-094) proxies to http://127.0.0.1:19847, the Molodetz docker dev server from this repo (see Docker in README.md). It carries a normal Let's Encrypt certificate. Every saved change under ./molodetz is live on staging within seconds through uvicorn reload; app edits never need a molohttp change. Staging data is the file mount ./data/staging, refreshed from holy production (./data/production) with make staging-refresh (see bin/staging-refresh.sh). All environments live under ./data/<env>; named volumes are never used.
Test
molodetz-test.app.molodetz.nl (site-095) proxies to http://127.0.0.1:19849, the Molodetz docker test server (docker-compose.test.yml, dev server with reload, data in ./data/test, default admin password test-admin-pass-1). test.app.molodetz.nl belongs to another app (site-085) and must not be touched.
Production cutover
molodetz.nl (site-022) was cut over from molodev (127.0.0.1:8084) to the Molodetz prod container (127.0.0.1:19848) on 2026-10-05, backup config.json.bak-site-cutover-prod-20261005145603. Production data is holy in ./data/production; the container runs docker-compose.prod.yml (2 workers, no reload) with secrets from PROD_SECRET_KEY and PROD_ADMIN_PASSWORD, which have no defaults and fail loudly when unset.
Toggle back to the old app (molodev is still running on :8084 for exactly this): back up per the specification with reason site-cutover-rollback, set site-022 upstreams back to [{"url": "http://127.0.0.1:8084", "weight": 1}], restore ownership, validate JSON, reload, verify https://molodetz.nl/ answers the old site. Toggle forward again with the same steps in reverse (:19848).
Static publishing
static.molodetz.nl proxies to http://127.0.0.1:8117, served by rserver running as retoor with working directory /home/retoor/projects/static. Publishing static content needs no molohttp change: write files under that directory and they are live (verified end to end with /pizdetz/, which redirects to /pizdetz/index.html). The Molodetz screenshot gallery is produced this way; see the Screenshots section in README.md.
Ownership ensure
Run after any manual intervention to restore the canonical state. It does not touch the documented anomalies.
sudo chown molohttp:molohttp /etc/molohttp /etc/molohttp/config.json /etc/molohttp/.env.json
sudo chmod 750 /etc/molohttp
sudo chmod 640 /etc/molohttp/config.json /etc/molohttp/.env.json
sudo chown molohttp:molohttp /etc/molohttp/certs /var/lib/molohttp /var/log/molohttp /var/www/.well-known
sudo chmod 700 /etc/molohttp/certs
sudo chmod 750 /var/lib/molohttp /var/log/molohttp /var/www/.well-known
sudo find /etc/molohttp/certs -name '*.key' -user molohttp -exec chmod 600 {} +
sudo find /etc/molohttp/certs -name '*.pem' -user molohttp -exec chmod 644 {} +
sudo find /etc/molohttp -maxdepth 1 -name '*.bak-*' -exec chown root:root {} + -exec chmod 640 {} +
Quick commands
| Task | Command |
|---|---|
| Status | systemctl is-active molohttp, molohttp service-status |
| Logs | sudo journalctl -u molohttp -f, molohttp service-logs |
| Reload config | sudo systemctl reload molohttp |
| Restart | sudo systemctl restart molohttp |
| Site table | sudo -n cat /etc/molohttp/config.json | python3 -m json.tool | less |
| Port owners | sudo -n ss -tlnp |
| Version | molohttp --version |