Roller on Docker

Part of RollerHub. This is the short operator path; the Roller repository's docker_deployment.md is 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.

What runs

Everything is in docker-compose.prod.yml, started from one directory (for example /opt/roller):

ServiceRole
postgresPostgreSQL 18, the only database
provisionOne-shot: creates the Umami and listmonk databases and applies the schema migrations before anything else starts
appRoller itself
caddyReverse proxy and TLS, the only ingress
umami, listmonkAnalytics and newsletter
backupDaily pg_dump plus a tarball of the media, search-index and upload volumes
adminTool service for admin accounts. Never started by up or deploy.sh; you invoke it with docker compose run

Install

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:

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.

LAN vs public mode

Behind a tunnel (cloudflared)

On this network, public traffic reaches every host through cloudflared on host cloudflare (HomeLabFleetAndIngress), and Roller runs on docker2. For that layout:

  1. Set .env:

    SITE_DOMAIN=:80
    TRUSTED_PROXIES=192.168.5.50
    SITE_HTTP_PORT=8080
    SITE_HTTPS_PORT=8443
    
    • Caddy serves plain HTTP, and the tunnel holds the public certificate.
    • 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.
    • The two port variables move only the host side of Caddy's ports.
    • Don't set SITE_DOMAIN to the public hostname. Caddy would then redirect to HTTPS, and through the tunnel that redirect loops.
  2. Run ./deploy.sh. Any .env change recreates app as well as caddy, so expect a short restart.

  3. 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.

  4. 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:

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.

The first admin

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.

Site URL and admin email

Log in at /roller-ui/login.rol, then open Admin -> Global Config and set:

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.

What / shows

Mail via Brevo

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.

Recovery

  1. 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.

  2. 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.

Upgrades

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

Backups

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.