Part of PressroomHub. This is the short operator path; the Pressroom repository's
docker_deployment.mdis the exhaustive reference (custom domains, analytics, newsletter, restore procedures, troubleshooting). For admin accounts specifically, the step-by-step is PressroomAdminAccountRunbook.
Naming. Pressroom was named Roller until release 0.5.0 (2026-10-29, SA-14); releases before 0.5.0 used the old names (ROLLER_*, ghcr.io/jakefearsd/roller, /roller-ui/, rlr_ tokens). A 0.4.x host does not upgrade to 0.5.0: it cuts over once, see "Cutting over a 0.4.x host" below.
Availability. The admin tool described below ships in Pressroom 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/pressroom):
| 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 | Pressroom 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/pressroom && cd /opt/pressroom
curl -LO https://github.com/jakefearsd/pressroom/releases/download/v<version>/docker-compose.prod.yml
curl -LO https://github.com/jakefearsd/pressroom/releases/download/v<version>/env.example
curl -LO https://github.com/jakefearsd/pressroom/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 PRESSROOM_DATABASE_JDBC_USERNAME, PRESSROOM_DATABASE_JDBC_PASSWORD and the database name in PRESSROOM_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.PRESSROOM_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 PRESSROOM_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 Pressroom runs on docker1 (it ran on docker2 until 2026-10-18). 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://docker1.lan:<SITE_HTTP_PORT> (8084 on docker1). 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 Pressroom 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.
Pressroom 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 PressroomAdminAccountRunbook.
Log in at /pressroom-ui/login.rol, then open Admin -> Global Config and set:
https://blog.example.com (a value saved here wins over PRESSROOM_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 PressroomMcp.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):
PRESSROOM_MAIL_CONFIGURATIONTYPE=properties
PRESSROOM_MAIL_HOSTNAME=smtp-relay.brevo.com
PRESSROOM_MAIL_PORT=587
PRESSROOM_MAIL_USERNAME=<your Brevo SMTP login>
PRESSROOM_MAIL_PASSWORD=<your Brevo SMTP key>
Leave PRESSROOM_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.
From 0.5.0 on, upgrade within the same release family as follows. 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/pressroom/releases/download/v<new-version>/docker-compose.prod.yml
curl -LO https://github.com/jakefearsd/pressroom/releases/download/v<new-version>/deploy.sh
chmod +x deploy.sh
./deploy.sh
0.5.0 renames the database and role (pressroom, was rollerdb/roller), the tables (pressroom_*), the images, the volumes, the compose project and the deploy directory (~/pressroom, was ~/roller). A 0.4.x host therefore does not upgrade; upgrade.sh refuses and points to the repository's docs/dev/pressroom-cutover.md. The cutover script copies the old stack into the new project and renames the copy, so nothing in the old directory or volumes is modified; the rollback (rollback-pressroom.sh) puts the old 0.4.x stack back, losing anything written after the cutover. Keep the old directory, volumes and images for 30 days. Environment variables must be renamed ROLLER_* to PRESSROOM_* (the script does it); a leftover ROLLER_* variable or roller.* system property makes Pressroom refuse to start, by design. The .env keys otherwise stay.
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 pressroom-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.