Self-hosting (Docker)
Prefer your own environment? Rollout runs as a Docker container behind Caddy, with the same code as the hosted version. Administration can stay entirely internal; only booking links and the client portal need to be reachable from outside.
Rollout checks Microsoft 365, which lives in Microsoft's cloud. So the server must always be able to reach login.microsoftonline.com and graph.microsoft.com. What is possible: no inbound traffic to the admin interface, all outbound traffic through your proxy, and a license check without internet.
One or two addresses
| One address | Two addresses (recommended) | |
|---|---|---|
| File | deploy/Caddyfile | deploy/Caddyfile.split |
| Public | Booking links, portal, consent and the public gateway | booking.yourmsp.com: only those pages, everything else 404 |
| Admin | Same address, only from ADMIN_ALLOWED_RANGES | rollout.internal.yourmsp.com: internal or VPN only |
- booking.yourmsp.cominternet: booking links and portal only
- CaddyHTTPS, everything else 404
- Rolloutcontainer, read-only, not root
- rollout.internal…office or VPN only
Step by step
- Database
Supabase Cloud (EU) or self-hosted. Migrations and the first administrator:
Terminal$ DATABASE_URL='postgresql://postgres:<password>@<db-host>:5432/postgres' ./deploy/migrate.shKlaar: 17 nieuwe migratie(s).$ psql "$DATABASE_URL" -c "update platform_settings set bootstrap_admin_email = 'you@yourmsp.com';"UPDATE 1 - Configuration and secretsTerminal$ cp deploy/.env.example deploy/.env$ install -m 600 rollout-graph.key deploy/secrets/graph-cert-key.pem$ openssl rand -hex 32 > deploy/secrets/updater-secret$ node scripts/gateway-secret.mjs
Fill in
deploy/.env(ROLLOUT_DOMAIN,APP_URL, Supabase, Graph,ROLLOUT_GATEWAY_SECRET). The Graph certificate is a Docker secret: it never appears in an environment variable, an image or a log. - Two addresses (optional).env
CADDYFILE=Caddyfile.split PUBLIC_DOMAIN=booking.yourmsp.com PUBLIC_URL=https://booking.yourmsp.com ROLLOUT_DOMAIN=rollout.internal.yourmsp.com ADMIN_TLS=internal ADMIN_ALLOWED_RANGES=10.0.0.0/8 192.168.0.0/16In Entra, add
https://booking.yourmsp.com/toestemmingas a redirect URI of Rollout – Readiness. - Build and startTerminal$ docker compose -f deploy/compose.yaml --env-file deploy/.env up -d --build$ docker compose -f deploy/compose.yaml psrollout running (healthy)caddy running$ curl -fsS https://rollout.internal.yourmsp.com/api/ready{"ready":true,"checks":{"database":"ok","graph":"ok","gateway":"not-enforced","license":"valid"}}
Then put the hash of the server gateway in the database.
By default ADMIN_ALLOWED_RANGES is the whole world (0.0.0.0/0). Put your office and VPN IP ranges here, or use two addresses.
Outbound traffic
| To | Why |
|---|---|
login.microsoftonline.com:443 | Tokens for client tenants |
graph.microsoft.com:443 | Readiness, imports, email |
Your Supabase address :443 | Verifying sessions, shared limits |
onboarding-planning.vercel.app:443 (optional) | Signed list of new versions; turn off with ROLLOUT_UPDATE_CHECK=off |
Nothing else: no telemetry, no license server. Behind a proxy: HTTPS_PROXY, NO_PROXY and, with TLS inspection, NODE_EXTRA_CA_CERTS pointing to your own root CA. Never turn off certificate validation.
How the container is hardened
- Read-only file system, no Linux capabilities (
cap_drop: ALL), no privilege escalation, doesn't run as root. - No ports to the outside: only Caddy talks to the app.
- Base image pinned to a fixed version (digest), so the same build gives the same result.
- The optional updater container only installs versions from the list we sign, and rolls back by itself if a new version doesn't start healthy.