Part of RollerHub. This is the short operator path; the Roller repository's
docker_deployment.mdis the exhaustive reference (custom domains, analytics, newsletter, restore procedures, troubleshooting). For admin accounts specifically, the step-by-step is RollerAdminAccountRunbook.
Availability. The admin tool described below ships in Roller 0.1.11 and later. On 0.1.10 and earlier there is no supported way to create the first admin; upgrade first.
Everything is in docker-compose.prod.yml, started from one directory (for example /opt/roller):
| Service | Role |
|---|---|
postgres | PostgreSQL 18, the only database |
provision | One-shot: creates the Umami and listmonk databases and applies the schema migrations before anything else starts |
app | Roller itself |
caddy | Reverse proxy and TLS, the only ingress |
umami, listmonk | Analytics and newsletter |
backup | Daily pg_dump plus a tarball of the media, search-index and upload volumes |
admin | Tool service for admin accounts. Never started by up or deploy.sh; you invoke it with docker compose run |
The host needs Docker with the Compose plugin (v2.17 or newer) and three files from the GitHub Release you are deploying: docker-compose.prod.yml, env.example and deploy.sh. There is no git checkout on the host.
mkdir -p /opt/roller && cd /opt/roller
curl -LO https://github.com/jakefearsd/roller/releases/download/v<version>/docker-compose.prod.yml
curl -LO https://github.com/jakefearsd/roller/releases/download/v<version>/env.example
curl -LO https://github.com/jakefearsd/roller/releases/download/v<version>/deploy.sh
chmod +x deploy.sh
mv env.example .env
Edit .env. The settings that matter first:
IMAGE_VERSION - the release tag; pin it to the version you mean to run.POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD - and keep ROLLER_DATABASE_JDBC_USERNAME, ROLLER_DATABASE_JDBC_PASSWORD and the database name in ROLLER_DATABASE_JDBC_CONNECTIONURL in step with them by hand. Generate the password with openssl rand -base64 33 | tr '+/' '-_'; it must be URL-safe because it is spliced into Umami's database URL.SITE_DOMAIN - see the next section.ROLLER_SITE_ABSOLUTEURL - the site's own address, such as https://blog.example.com.Then run ./deploy.sh. It pulls the images, brings the stack up, waits for app to report healthy, and then runs admin status: if there is no enabled admin it prints the exact create command to run next. A missing admin never fails the deploy.
SITE_DOMAIN to a real domain (blog.example.com) with DNS pointing at the host and ports 80 and 443 reachable. Caddy obtains and renews a Let's Encrypt certificate itself.SITE_DOMAIN=:80. Caddy serves plain HTTP only and never tries to obtain a certificate. Set ROLLER_SITE_ABSOLUTEURL to the address you really use, for example http://<host-name>, with :<port> if you moved SITE_HTTP_PORT.On this network, public traffic reaches every host through cloudflared on host cloudflare (HomeLabFleetAndIngress), and Roller runs on docker2. For that layout:
Set .env:
SITE_DOMAIN=:80
TRUSTED_PROXIES=192.168.5.50
SITE_HTTP_PORT=8080
SITE_HTTPS_PORT=8443
TRUSTED_PROXIES names the connector: the one peer whose X-Forwarded-For and X-Forwarded-Proto Caddy keeps. Name that machine only, never a private range. Separate several addresses with spaces; a malformed value stops Caddy, and the site with it.SITE_DOMAIN to the public hostname. Caddy would then redirect to HTTPS, and through the tunnel that redirect loops.Run ./deploy.sh. Any .env change recreates app as well as caddy, so expect a short restart.
Add the route. In the Cloudflare dashboard, add the public hostname to the tunnel, pointing at http://docker2.lan:8080. jakemon's CloudflaredConfigChanged alert fires; that is expected.
Set the site's address. In Admin -> Global Config, set Absolute URL to site (if required) to the public https:// address.
Availability. SITE_HTTP_PORT, SITE_HTTPS_PORT and TRUSTED_PROXIES ship in Roller 0.1.12 and later. With an older release, Caddy publishes 80 and 443 and trusts no proxy.
Why TRUSTED_PROXIES. Caddy discards X-Forwarded-For and X-Forwarded-Proto from any peer it does not trust, and writes the peer's address and http in their place. Without the setting, behind the tunnel:
http://.With it, Caddy keeps both headers and appends 192.168.5.50. The app reads X-Forwarded-For right to left and takes the first address that is not private, which is the reader's. Host and CF-Connecting-IP pass through either way, so Umami, which reads CF-Connecting-IP first, counts readers correctly with or without the setting.
Roller serves from the root of the host (https://<domain>/<handle>/); do not assume a context path.
Self-registration is removed, so a fresh install has no way in until the host owner creates an admin. That is deliberate: whoever can run docker compose on the host already controls .env and the database credentials, and nothing reachable over HTTP can create an admin.
From the deploy directory:
C="docker compose -f docker-compose.prod.yml"
$C run --rm admin status
$C run --rm admin create --username NAME --email ADDRESS
create prompts for the password twice (at least 8 characters). Use an address you read: reset links are sent there. The username may contain ASCII letters and digits only (1-255 characters), and the email address may be at most 255 characters. The account gets the admin and editor roles and can log in immediately. Full detail, including the non-interactive form, is in RollerAdminAccountRunbook.
Log in at /roller-ui/login.rol, then open Admin -> Global Config and set:
https://blog.example.com (a value saved here wins over ROLLER_SITE_ABSOLUTEURL). Password-reset links are built from it, which is why, from 0.1.14, the API and MCP cannot change it: only this page can. Agents connect as described in RollerMcp.On 0.1.12 and earlier, Global Config cannot be saved until at least one weblog exists: the save fails with "Property site.frontpage.weblog.handle is null". Upgrade to 0.1.13, or create a weblog first. Saving on those versions also makes the first weblog the front page.
/ shows/ serves that weblog./ is a public site directory. It shows the site description from Global Config, then each enabled weblog marked active ("included in community listings"), by name. With no weblogs it says nothing has been published yet. A signed-in admin also sees a link to choose a front page./ without a front page sent every reader to the sign-in form.Password-reset and notification mail go through an SMTP relay. Brevo works with these .env settings (placeholders shown; use your own SMTP login and key):
ROLLER_MAIL_CONFIGURATIONTYPE=properties
ROLLER_MAIL_HOSTNAME=smtp-relay.brevo.com
ROLLER_MAIL_PORT=587
ROLLER_MAIL_USERNAME=<your Brevo SMTP login>
ROLLER_MAIL_PASSWORD=<your Brevo SMTP key>
Leave ROLLER_MAIL_SECURITY at its default, starttls. The From address is the Site Administrator's email address, so it must be a sender or domain you have verified in Brevo. Reset mail needs both these settings and a non-blank Site Administrator's email address in Global Config; until both are set, the forgot-password page says the server has no outgoing mail configured (or no site email address set). A change to .env needs docker compose -f docker-compose.prod.yml up -d --force-recreate app; a plain restart does not re-read .env.
Forgot password first. Use "Forgot your password?" on the login page (enter the username or email address). The emailed link lasts 1 hour and works once. This needs mail configured as above.
Last resort: reset-password. When mail itself is broken:
$C run --rm admin reset-password --username NAME
$C restart app
The restart is required: the running app caches accounts it has already loaded, so a password changed underneath it does not take effect until app restarts. The tool prints this command when it finishes.
Download the new release's docker-compose.prod.yml and deploy.sh (both, not just one), bump IMAGE_VERSION in .env, and run ./deploy.sh. Never overwrite .env; diff the new env.example against it when the release notes mention new keys. The script is idempotent and safe to re-run. Take a backup first before crossing a schema change.
curl -LO https://github.com/jakefearsd/roller/releases/download/v<new-version>/docker-compose.prod.yml
curl -LO https://github.com/jakefearsd/roller/releases/download/v<new-version>/deploy.sh
chmod +x deploy.sh
./deploy.sh
The backup service runs daily at BACKUP_HOUR (UTC), writing a PostgreSQL dump and a tarball of the media, search-index and upload volumes to the roller-backups volume, and deleting files older than BACKUP_RETENTION_DAYS. The dumps contain password hashes: copy them off the host and restrict access. Run one by hand with:
docker compose -f docker-compose.prod.yml exec backup /app/backup/backup.sh
Restore commands are in docker_deployment.md (and in the header of deploy/backup/backup.sh). Test a restore against a scratch copy before you need it.