Skip to main content

Running your own ReadyRoam server

A ReadyRoam server lets the app sync your trips across devices, share trips with the people you travel with, and keep an encrypted backup of your personal details. It's a single Docker image with a Postgres database.

Version requirements

Single sign-on, Entra ID support and PASSWORD_LOGIN are available in server 1.3, described in the release notes. The mobile app has its own version number: 1.3. Use the server version when selecting a Docker image.

The dev image is an untested development build and can change between pulls. Back up your database before trying it.

Examples use the fictional Megadodo Publications and reserved .example addresses. Replace them with your own domain, email addresses and provider details.

What you need​

  • A computer that's always on, with Docker and Docker Compose: a home server, a NAS, a Raspberry Pi 4 or 5, or a small cloud server. The image runs on both amd64 and arm64.
  • A domain name pointing at it (for example roam.megadodo.example), and HTTPS in front of the server. The app only connects to servers over HTTPS, apart from bare IP addresses on an iPhone, which is only useful for trying it out at home.

1. Get the files​

Make a folder, for example readyroam, and put two files in it. Also create a config directory for optional SSO configuration:

mkdir -p readyroam/config
cd readyroam

docker-compose.yml

# Runs the ReadyRoam server with its own Postgres, from the published image.
# Copy .env.example to .env and set the passwords first, then:
# docker compose up -d
# To build the image from this repository instead:
# docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
services:
server:
image: ghcr.io/aengeln/readyroam:${READYROAM_VERSION:-latest}
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgresql://readyroam:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@db:5432/readyroam?sslmode=disable
TOKEN_SECRET: ${TOKEN_SECRET:?set TOKEN_SECRET in .env}
PUBLIC_URL: ${PUBLIC_URL:-}
SERVER_NAME: ${SERVER_NAME:-Roam Home}
DIRECTORY: ${DIRECTORY:-false}
DIRECTORY_LIMIT: ${DIRECTORY_LIMIT:-100}
DIRECT_ADD: ${DIRECT_ADD:-false}
REGISTRATION: ${REGISTRATION:-closed}
ACCOUNT_DELETION: ${ACCOUNT_DELETION:-true}
TRUST_PROXY: ${TRUST_PROXY:-false}
SHARING_PAID: ${SHARING_PAID:-false}
# Optional: single sign-on (see .env.example). One provider with the
# SSO_* settings, or several in config/sso.yaml with SSO_CONFIG.
PASSWORD_LOGIN: ${PASSWORD_LOGIN:-true}
SSO_CONFIG: ${SSO_CONFIG:-}
SSO_ID: ${SSO_ID:-}
SSO_NAME: ${SSO_NAME:-}
SSO_ISSUER: ${SSO_ISSUER:-}
SSO_CLIENT_ID: ${SSO_CLIENT_ID:-}
SSO_CLIENT_SECRET: ${SSO_CLIENT_SECRET:-}
SSO_CLIENT_SECRET_FILE: ${SSO_CLIENT_SECRET_FILE:-}
SSO_SCOPES: ${SSO_SCOPES:-}
SSO_SIGNUP: ${SSO_SIGNUP:-}
SSO_TRUST_EMAIL: ${SSO_TRUST_EMAIL:-}
SSO_ALLOWED_DOMAINS: ${SSO_ALLOWED_DOMAINS:-}
SSO_SUBJECT_CLAIM: ${SSO_SUBJECT_CLAIM:-}
SSO_TYPE: ${SSO_TYPE:-}
SSO_TENANT: ${SSO_TENANT:-}
SSO_ALLOW_GUESTS: ${SSO_ALLOW_GUESTS:-}
# Optional: email for password reset links (needs PUBLIC_URL too).
SMTP_HOST: ${SMTP_HOST:-}
SMTP_PORT: ${SMTP_PORT:-}
SMTP_SECURITY: ${SMTP_SECURITY:-}
SMTP_USERNAME: ${SMTP_USERNAME:-}
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
SMTP_FROM: ${SMTP_FROM:-}
volumes:
# Files the server reads: sso.yaml and secrets for it. They must be
# readable by container UID/GID 65534 (the server does not run as root).
- ./config:/config:ro
ports:
# The server speaks plain HTTP: put a reverse proxy that handles HTTPS
# in front of it (see README).
- "127.0.0.1:${PORT:-8080}:8080"

db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_USER: readyroam
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
POSTGRES_DB: readyroam
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U readyroam -d readyroam"]
interval: 5s
timeout: 5s
retries: 10

volumes:
db-data:

.env

# The database password. Pick a long random one; you won't need to type it.
POSTGRES_PASSWORD=

# Signs sign-in tokens. At least 32 characters, for example the output of:
# openssl rand -base64 48
# Changing it signs everyone out.
TOKEN_SECRET=

# The address people reach the server at. Invite links point here.
PUBLIC_URL=https://roam.megadodo.example

# The name the app shows for this server.
SERVER_NAME="Roam Home"

# Who may create an account in the app: closed (you create accounts, see
# step 5) or open (anyone who can reach the server).
REGISTRATION=closed

# Whether people may delete their own account in the app.
ACCOUNT_DELETION=true

# true when a reverse proxy is in front of the server (step 4).
TRUST_PROXY=true

# The image version: latest, a major version such as 1 (new features, nothing
# that breaks), a minor version such as 1.0 (bug fixes only), or an exact
# version such as 1.0.0.
READYROAM_VERSION=latest

2. Fill in the secrets​

Set POSTGRES_PASSWORD and TOKEN_SECRET in .env. Both can be generated with:

openssl rand -base64 48

Set PUBLIC_URL to your server's address.

3. Start it​

docker compose up -d
curl http://localhost:8080/healthz

The answer should be {"status":"ok"}. The database is set up the first time the server starts.

4. Put HTTPS in front​

The server speaks plain HTTP, so put a reverse proxy in front of it that handles HTTPS. Caddy is the simplest: it gets and renews certificates by itself. Install it on the same computer, and use this Caddyfile:

roam.megadodo.example {
reverse_proxy localhost:8080
}

Traefik and nginx work too. Whichever you use, keep TRUST_PROXY=true in .env, so the server sees each visitor's own address (it uses it to slow down password guessing). Without a reverse proxy, set it to false.

5. Create accounts​

New servers don't let people sign up in the app. Create accounts yourself with the admin tool:

docker compose exec server /app/bin/server admin create-user ford@megadodo.example --name "Ford Prefect"

The name is optional; it is what the directory shows (see "Finding each other" below), and people can change it in the app. It prints a random password (or give one after the email address). Then, in the app: Settings → Server and sync → Sign in, with your server's address, the email and the password.

Other admin commands:

docker compose exec server /app/bin/server admin users
docker compose exec server /app/bin/server admin reset-password ford@megadodo.example
docker compose exec server /app/bin/server admin sign-out ford@megadodo.example
docker compose exec server /app/bin/server admin disable ford@megadodo.example
docker compose exec server /app/bin/server admin enable ford@megadodo.example

To let anyone sign up in the app instead, set REGISTRATION=open and run docker compose up -d again.

Finding each other​

By default people share a trip by sending an invite link. On a server for a family or a company, that is an extra step for people who already share a server. Two settings change it:

DIRECTORY=true # everyone with an account can see everyone else's name and email
DIRECT_ADD=true # a trip's owner can add anyone in the directory, with no invite
  • The directory shows up in the app's People tab, one group per server the person has an account on. On a server with up to DIRECTORY_LIMIT accounts (100 unless you change it) it lists everyone; on a bigger one people search by name or email. It is the same for everyone, and nobody can opt out of it, so only turn it on where that is what you want. Disabled accounts are left out.
  • Direct add lets the owner of a trip pick someone from the directory and add them as editor or viewer. They don't have to accept anything: the trip shows up in their app the next time it syncs, marked as changed, and they get an email if the server can send any (see "Password reset by email" for the mail settings). They can leave the trip whenever they like. It needs DIRECTORY=true.
  • Names in the directory come from the person's own profile name, which the app sends when they sign in, or from you: admin set-name ford@megadodo.example "Ford Prefect". Someone without a name is listed by email.

Guests, who join with an invite link and have no account, are never in the directory and can't be added this way.

Sharing trips​

Anyone with an account can share a trip from the app. The people they invite open the link and join. They don't need an account: the invite lets their phone in for that trip only. Invite links point at PUBLIC_URL, so make sure it's set.

Personal backups and recovery keys​

People's personal details (passports, visas, loyalty programs, profile photos and the like) are backed up end-to-end encrypted. The server only stores data it can't read. When someone turns on the backup, the app shows a recovery key, which they need to unlock the backup on a new phone.

As the admin, you can't recover it for them. Resetting someone's password lets them sign in again, but if they lose both their phones and their recovery key, their personal backup is gone. Trips aren't affected: they're not end-to-end encrypted, so they sync to a new phone after signing in.

Password reset by email​

Without email, you reset passwords with the admin tool (step 5). To let people reset their own password from the app, add a mail server to .env:

SMTP_HOST=smtp.megadodo.example
SMTP_PORT=587
SMTP_SECURITY=starttls # starttls (587), tls (465) or none
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM=ReadyRoam <ford@megadodo.example>

PUBLIC_URL must be set too: the email links to a page on your server where the new password is chosen.

Single sign-on​

Requires SSO support

These instructions cover SSO in server 1.3. Run sso check to check your provider configuration before signing in.

People can sign in with an account they already have: Authentik, Microsoft (Office 365) or Google, or any other OpenID Connect provider. Passwords keep working beside it, unless you turn them off.

  1. Register ReadyRoam with the provider. Create an application (Authentik), an app registration (Microsoft Entra ID) or an OAuth client (Google), and give it this redirect address, with your own address and sso as the id:

    https://roam.megadodo.example/auth/sso/sso/callback

    Microsoft and Google need https. Copy the client id and the client secret it gives you.

    In Authentik also check, in the provider:

    • Grant Types: tick "Authorization Code." Recent versions (2026.5 at least) start with none ticked and then refuse every sign-in with invalid_request: The request is otherwise malformed.
    • Signing Key: pick the self-signed certificate. Without a key, Authentik signs with a shared secret (HS256), which ReadyRoam refuses.
    • Redirect URIs: one strict entry with exactly the address above.
    • Scopes: openid, email and profile.
  2. Add the provider to .env. PUBLIC_URL must be set too.

    SSO_NAME=Authentik
    SSO_ISSUER=https://auth.megadodo.example/application/o/readyroam/
    SSO_CLIENT_ID=readyroam
    SSO_CLIENT_SECRET=...
    SSO_SIGNUP=false # true lets a first sign-in create an account

    For Microsoft the issuer is https://login.microsoftonline.com/<tenant id>/v2.0; for Google it is https://accounts.google.com.

  3. Check it. docker compose up -d, then:

    docker compose exec server /app/bin/server sso check

    It reads the provider's settings, prints the redirect address to register and sends the provider a test sign-in request (nobody is signed in), so a provider that refuses it, for example because no grant type is ticked, shows up here and not at the first sign-in.

How people are matched:

  • People who already have an account are matched on their email address the first time they sign in with the provider, so you can turn single sign-on on for a server that already has users. After that they're matched on their id at the provider, so a changed email doesn't matter. Set SSO_TRUST_EMAIL=false for a provider whose email addresses you don't trust.
  • Everyone else gets an account on their first sign-in only if SSO_SIGNUP=true. This is separate from REGISTRATION, which is about passwords. SSO_ALLOWED_DOMAINS=megadodo.example limits new accounts to those email domains.
  • Passwords off. PASSWORD_LOGIN=false makes single sign-on the only way in: no passwords, no sign-ups in the app and no password reset by email. Existing accounts keep working, matched by email as above. It needs a provider, or no one could sign in.
  • Someone who leaves your company can't sign in again, but stays signed in on the devices they already use until you disable them: admin disable (step 5) signs them out at once.

Microsoft Entra ID (Office 365)​

One app registration, and one provider, for each tenant. Everyone in that tenant may sign in, whichever of its verified domains their sign-in name is on, so there is no domain list to keep up.

  1. In the Entra admin center, App registrations → New registration. Give it a name, choose Accounts in this organizational directory only (single tenant), and add a Web redirect URI: the address from step 1 above, with the provider's id in it (https://roam.megadodo.example/auth/sso/megadodo/callback).

  2. On its overview page copy the Application (client) ID and the Directory (tenant) ID. Under Certificates & secrets make a client secret and copy its Value.

  3. Under Token configuration → Add optional claim → ID, add email, upn and acct. They let the server match people to the accounts they already have, and tell guests from members.

  4. Under Enterprise applications → ReadyRoam → Properties, set Assignment required to Yes, and assign who may use it under Users and groups. This is the control that decides who can sign in, and it doesn't depend on any claim.

  5. Add the provider, with type: entra and the tenant id (not its name) instead of an issuer:

    providers:
    - id: megadodo
    name: Megadodo Publications
    type: entra
    tenant: 11111111-2222-3333-4444-555555555555
    client_id: <application id>
    client_secret_file: /config/megadodo.secret
    signup: true

    For one tenant SSO_TYPE=entra and SSO_TENANT=… work in .env. Run sso check: it prints the redirect address and tells you if Entra doesn't know the tenant or the address.

How it works:

  • Who is who. A person is identified by their object id in the tenant (oid), not by their email, so a changed sign-in name or a new domain doesn't matter.
  • People who already have an account are matched on the email claim, or when Entra sends none (it often doesn't) on their sign-in name, the user principal name. A tenant's own admins control both.
  • Guests invited into the tenant are refused unless allow_guests: true. The server recognises them by acct, by an idp that isn't the tenant, or by a #EXT# sign-in name; Assignment required is the safer way to keep them out.
  • Two tenants are two providers with two app registrations; the same person in both is two accounts, until they link them.

Several providers, for example two Microsoft tenants and an Authentik: put them in a file, config/sso.yaml next to docker-compose.yml, and set SSO_CONFIG=/config/sso.yaml. Add ./config:/config:ro under volumes: for the server, and make sure the files are readable by the server process (UID/GID 65534). Restrict secret files to that user or group rather than making them world-readable. The Compose example above already includes this mount.

When using SSO_CONFIG, remove or leave empty all other SSO_* variables in .env, including SSO_SIGNUP=false and SSO_TRUST_EMAIL=true. Non-empty single-provider settings cannot be combined with the YAML file.

providers:
- id: authentik
name: Authentik
issuer: https://auth.megadodo.example/application/o/readyroam/
client_id: readyroam
client_secret_file: /config/authentik.secret
- id: megadodo
name: Megadodo Publications
type: entra
tenant: 11111111-2222-3333-4444-555555555555
client_id: <application id>
client_secret_file: /config/megadodo.secret
signup: true

Backups​

Everything, including tickets and other files kept with trips, is in the Postgres database. Back it up regularly, and always before upgrading:

docker compose exec -T db pg_dump -U readyroam readyroam | gzip > readyroam-$(date +%F).sql.gz

To restore a backup into an empty database:

docker compose down
docker volume rm readyroam_db-data # the volume is named after the folder
docker compose up -d db
gunzip -c readyroam-2026-10-04.sql.gz | docker compose exec -T db psql -U readyroam readyroam
docker compose up -d

Upgrading​

docker compose exec -T db pg_dump -U readyroam readyroam | gzip > before-upgrade.sql.gz
docker compose pull
docker compose up -d

The database is updated when the new version starts. This can't be undone: going back to an older version needs the backup you made first.

To decide yourself when to upgrade, pin a version in .env: READYROAM_VERSION=1 gets new features but nothing that breaks, and READYROAM_VERSION=1.0 gets bug fixes only.

For companies​

A company that runs its own server for its staff will usually want these settings in .env:

SERVER_NAME="Megadodo Travel" # what the app and the start page call the server
PUBLIC_URL=https://roam.megadodo.example # invite and reset links, and the address shown on the start page
REGISTRATION=closed # accounts are created by the company
ACCOUNT_DELETION=false # people can't delete their account themselves
TRUST_PROXY=true # when nginx, Caddy or Traefik is in front (step 4)

# Email, so people can reset their own passwords (needs PUBLIC_URL too)
SMTP_HOST=smtp.megadodo.example
SMTP_FROM="ReadyRoam <ford@megadodo.example>"
SMTP_USERNAME=ford@megadodo.example
SMTP_PASSWORD=...

Then run docker compose up -d again. Some notes:

  • Accounts are created with the admin tool (step 5). Disabling an account (admin disable) signs the person out and keeps their trips.
  • HTTPS is a must: the app only talks to servers over HTTPS. Put a reverse proxy in front (step 4) and set TRUST_PROXY=true, or sign-in limits will treat everyone as one visitor.
  • ACCOUNT_DELETION=false hides account deletion in the app. People can still leave trips, and you can remove accounts yourself.
  • Leave SHARING_PAID alone. It is for a public server that charges for sharing.
  • The server name matters. The app shows it in the header of a trip that lives on this server, and colors the trip's cards differently from private trips, so staff can tell work trips apart. If you rename the server, the app follows within an hour (from the app version that has this).
  • Backups: see the section below. The database holds every trip, so back it up like any other company data.

For a company the directory and direct add (see "Finding each other") are usually what you want: DIRECTORY=true and DIRECT_ADD=true, so a project leader can add the installers to a trip without sending invites. Roles for staff and licensing for larger companies are planned and not part of the server yet.

Limits​

  • Each file kept with a trip (a ticket, a boarding pass) can be up to 20 MB, and a trip's files up to 500 MB together.
  • Encrypted personal files (photos of passports and the like) can be up to 20 MB each.
  • Too many failed sign-ins from one address (10 within 5 minutes) block that address from signing in for 5 minutes. So do more than 5 sign-ups or password reset emails within 5 minutes.

Settings​

SettingDefault
POSTGRES_PASSWORD(required)The database password.
TOKEN_SECRET(required)Signs sign-in tokens; 32 characters or more.
PUBLIC_URLThe address people reach the server at. Needed for invite links and password reset.
SERVER_NAMERoam HomeShown in the app.
REGISTRATIONclosedclosed or open.
ACCOUNT_DELETIONtrueWhether people may delete their own account in the app.
TRUST_PROXYfalseSet to true behind a reverse proxy.
DIRECTORYfalseEveryone with an account can find everyone else.
DIRECTORY_LIMIT100Up to this many accounts the directory is listed whole; above it, people search.
DIRECT_ADDfalseA trip's owner can add anyone in the directory without an invite. Needs DIRECTORY.
SSO_IDssoProvider identifier used in its callback URL.
SSO_CLIENT_SECRET_FILERead the client secret from a mounted file.
SSO_TYPE, SSO_TENANT, SSO_ALLOW_GUESTSoidc, unset, falseUse entra with a tenant UUID for Microsoft Entra ID; guest access is opt-in.
SSO_SCOPES, SSO_SUBJECT_CLAIMopenid email profile, sub (oid for Entra)Advanced provider options.
PASSWORD_LOGINtruefalse makes single sign-on the only way in.
SSO_NAME, SSO_ISSUER, SSO_CLIENT_ID, SSO_CLIENT_SECRETOne single sign-on provider.
SSO_SIGNUP, SSO_TRUST_EMAIL, SSO_ALLOWED_DOMAINSfalse, true, anyWho may sign in with it.
SSO_CONFIGA file with several providers.
PORT8080The port on this computer.
READYROAM_VERSIONlatestThe image version to run.
SMTP_HOST, SMTP_PORT, SMTP_SECURITY, SMTP_USERNAME, SMTP_PASSWORD, SMTP_FROMEmail for password reset.

Troubleshooting​

  • Settings in .env have no effect. Compose must pass them to the server. Use the updated Compose example above, then recreate the server with docker compose up -d server.

  • SSO configuration conflict. Use either SSO_CONFIG or the individual SSO_* settings, never both.

  • SSO command missing. Check that you are running server 1.3 or later, and that the container was recreated after updating its image.

  • SSO config or secret cannot be read. Check the read-only /config mount and file access for UID/GID 65534.

  • Authentik refuses sign-in. Run docker compose exec server /app/bin/server sso check; verify Authorization Code, signing key and the exact callback URL.

  • The app can't reach the server. Check that https://your-address/healthz answers {"status":"ok"} in a browser. The app needs HTTPS.

  • "This server doesn't take sign-ups in the app." Registration is closed; create the account with the admin tool, or set REGISTRATION=open.

  • "Too many attempts." Wait five minutes. If it happens to everyone at once, the server is probably behind a reverse proxy without TRUST_PROXY=true, so it sees everyone as the same visitor.

  • Logs: docker compose logs -f server.