Ryot on Ubuntu 24.04 on Azure User Guide
Overview
Ryot ("Roll your own tracker") is an open source, self hosted tracker and aggregator for the things you consume. It records films, television, books, video games, audiobooks, podcasts, manga and anime alongside workouts, exercises and body measurements, pulling artwork and metadata from public providers so a title only has to be searched for rather than typed out. Everything lands in one library with collections, watchlists, progress, ratings and reviews, plus importers that bring history across from other trackers and a GraphQL API for automation.
This cloudimg image runs Ryot v10.4.0 from the official container image, pinned to an immutable digest, with PostgreSQL 16 co-hosted on the same VM and its data directory on a dedicated Azure data disk, fronted by nginx on port 80. Every VM generates its own administrator account, administrator password, database password and admin access token on first boot, and ships with public sign up switched off. Backed by 24/7 cloudimg support.
What is included:
- Ryot v10.4.0 (container
ghcr.io/ignisda/ryot, GPL-3.0), pinned to an immutablesha256digest so the shipped version never drifts - PostgreSQL 16, with its data directory on a dedicated 20 GiB Azure data disk mounted at
/var/lib/postgresql - nginx on
:80reverse proxying to the loopback-bound Ryot backend, with WebSocket upgrade headers and an unauthenticated/healthzendpoint for load balancer and uptime checks - A per-VM administrator account, administrator password and Ryot
SERVER_ADMIN_ACCESS_TOKEN, all generated on first boot and recorded in a root-only file - Public self registration disabled by default, so no stranger can claim an account on your instance
- Upstream usage telemetry disabled by default (
DISABLE_TELEMETRY=true) docker.service,postgresql.service,nginx.service,ryot.serviceandryot-firstboot.serviceas systemd units, enabled and active- 24/7 cloudimg support
Prerequisites
An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is the recommended size and is comfortable for a personal or small team instance; move up to a Standard_B2ms or Standard_B4ms if you plan to import a very large history or share the instance with many people.
Network security group inbound rules: allow 22/tcp from your management network and 80/tcp from wherever you will use Ryot. Ryot serves plain HTTP on port 80 — for anything reachable from the internet, terminate TLS in front of it (an Azure Application Gateway or Front Door, or your own certificate on the VM's nginx) and use a real hostname.
PostgreSQL listens on 5432 so the Ryot container can reach it across the Docker bridge, but its host based authentication only admits the ryot role from the Docker subnet plus the local socket, and no NSG rule should ever open 5432.
Step 1 - Deploy from the Azure Marketplace
In the Azure portal choose Create a resource, search for Ryot on Ubuntu 24.04 LTS by cloudimg, and select Create. Pick your subscription, resource group and region, set the size to Standard_B2s, choose SSH public key authentication with username azureuser, and allow inbound 22 and 80. The image already contains the dedicated data disk definition, so the PostgreSQL volume is created and mounted for you.
Step 2 - Deploy from the Azure CLI
az vm create \
--resource-group my-ryot-rg \
--name my-ryot \
--image /subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.Compute/galleries/<gallery>/images/ryot-ubuntu-24-04/versions/latest \
--size Standard_B2s \
--storage-sku StandardSSD_LRS \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard \
--os-disk-delete-option Delete --nic-delete-option Delete
# then open the port Ryot serves on
az vm open-port --resource-group my-ryot-rg --name my-ryot --port 80 --priority 900
Both commands run on your own workstation, not on the VM.
Step 3 - Connect to your VM
ssh azureuser@<vm-ip>
First boot takes roughly 60 to 90 seconds: ryot-firstboot.service rotates the database password, mints the per-VM secrets, creates your administrator account, and only then starts nginx. Until it finishes, port 80 is deliberately closed.
Step 4 - Confirm the services are running
systemctl is-active docker postgresql nginx ryot ryot-firstboot
All five report active:
active
active
active
active
active
The Ryot container and the dedicated PostgreSQL data disk:
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
df -h /var/lib/postgresql | tail -2
NAMES IMAGE STATUS
ryot 69abca475e2b Up 3 minutes
Filesystem Size Used Avail Use% Mounted on
/dev/sdc 20G 67M 19G 1% /var/lib/postgresql

Step 5 - Retrieve your per-VM administrator credentials
Every VM generates its own credentials on first boot and writes them to a root-only file. Nothing is shared between deployments and there is no default password to change.
sudo cat /root/ryot-credentials.txt
The file contains four values plus guidance:
ryot.url=http://<your-vm-address>/
ryot.admin.username=cloudimgadmin
ryot.admin.password=<24 characters, unique to this VM>
ryot.admin.access.token=<64 characters, unique to this VM>
Read a single value without printing the whole file:
sudo grep '^ryot.admin.username=' /root/ryot-credentials.txt | cut -d= -f2-

ryot.admin.access.token is Ryot's SERVER_ADMIN_ACCESS_TOKEN. It authorises server administrator GraphQL operations, including creating users while public sign up is disabled. Treat it like a root password.
Step 6 - Confirm the health endpoints
nginx serves a static, unauthenticated /healthz for load balancer and uptime probes, and proxies Ryot's own /health:
curl -s -o /dev/null -w 'nginx /healthz -> HTTP %{http_code}\n' http://127.0.0.1/healthz
curl -s -o /dev/null -w 'ryot /health -> HTTP %{http_code}\n' http://127.0.0.1/health
nginx /healthz -> HTTP 200
ryot /health -> HTTP 200
Confirm the version Ryot itself reports:
curl -s -m 15 http://127.0.0.1/backend/graphql -H 'Content-Type: application/json' \
-d '{"query":"query { coreDetails { version } }"}'
{"data":{"coreDetails":{"version":"v10.4.0"}}}
The first boot log ends with nginx being started last, once the instance is locked down:
journalctl -u ryot-firstboot.service --no-pager | tail -6

Step 7 - Sign in to the web interface
Browse to http://<your-vm-address>/. Ryot redirects to /auth. There is no sign up link, because public registration is disabled on this image — the only way in is the per-VM administrator.

Sign in with ryot.admin.username and ryot.admin.password from Step 5. You land on the dashboard, which summarises what is upcoming, in progress and recently finished.

Step 8 - Track your first title
Choose a media type in the left hand navigation, for example Media then Anime, and switch to the Search tab. Searching queries a public metadata provider and returns real titles with cover art, release years and ratings.

Select a result to open its details page: artwork, release year, episode or chapter counts, community rating, genres, a synopsis and the full cast, with tabs for Actions, History, Reviews and Suggestions.

To record progress, open the Actions tab, choose Update progress, then Mark ... as watched, and Submit the date form. The title now appears in your own library with a watch count, and on the dashboard under In Progress.

Step 9 - Verify authentication from the command line
Ryot exposes the same GraphQL API the web interface uses, at /backend/graphql. Sign in with the per-VM administrator to obtain an API token, then read your collections back. This reads the password straight from the credentials file, so it is safe to copy and paste as is:
CREDS=/root/ryot-credentials.txt
U=$(sudo grep '^ryot.admin.username=' "$CREDS" | cut -d= -f2-)
P=$(sudo grep '^ryot.admin.password=' "$CREDS" | cut -d= -f2-)
GQL=http://127.0.0.1/backend/graphql
Q="mutation { loginUser(input: { password: { username: \"$U\", password: \"$P\" } }) { __typename ... on ApiKeyResponse { apiKey } ... on LoginError { error } } }"
TOKEN=$(curl -s -m 20 "$GQL" -H 'Content-Type: application/json' \
-d "$(jq -nc --arg q "$Q" '{query:$q}')" | jq -r '.data.loginUser.apiKey')
echo "API token issued: ${#TOKEN} characters"
curl -s -m 20 "$GQL" -H 'Content-Type: application/json' -H "Authorization: Bearer $TOKEN" \
-d '{"query":"query { userCollectionsList { response { name } } }"}' \
| jq -r '[.data.userCollectionsList.response[].name] | join(", ")'
API token issued: 56 characters
In Progress, Monitoring, Completed, Reminders, Watchlist, Custom, Owned
Those seven collections are the ones Ryot creates for a new account. An unauthenticated request to a protected query is refused:
curl -s -m 20 http://127.0.0.1/backend/graphql -H 'Content-Type: application/json' \
-d '{"query":"query { userCollectionsList { cacheId } }"}' | jq -r '.errors[0].message'
NO_USER_ID
Step 10 - Confirm the security posture
Upstream Ryot allows public sign up by default and makes the first account to register the administrator. This image closes that: registration is disabled before the application ever starts, and the administrator is created with a per-VM password. You can prove all of it on your own VM.
Anonymous self registration is refused:
GQL=http://127.0.0.1/backend/graphql
Q='mutation { registerUser(input: { data: { password: { username: "anon", password: "Anon123!" } } }) { __typename ... on RegisterError { error } } }'
curl -s -m 20 "$GQL" -H 'Content-Type: application/json' \
-d "$(jq -nc --arg q "$Q" '{query:$q}')" | jq -r '.data.registerUser.error'
DISABLED
Neither PostgreSQL account accepts a guessable password, and no swap is present:
for p in ryot postgres password; do
PGPASSWORD=$p psql -w -h 127.0.0.1 -U ryot -d ryot -tAc 'SELECT 1' >/dev/null 2>&1 </dev/null \
&& echo "role ryot + '$p' -> ACCEPTED" || echo "role ryot + '$p' -> refused"
done
grep '^USERS_ALLOW_REGISTRATION=' /etc/ryot/ryot.env
grep '^DISABLE_TELEMETRY=' /etc/ryot/ryot.env
role ryot + 'ryot' -> refused
role ryot + 'postgres' -> refused
role ryot + 'password' -> refused
USERS_ALLOW_REGISTRATION=false
DISABLE_TELEMETRY=true

Step 11 - The PostgreSQL datastore on its dedicated disk
Every user, tracked title, collection, review, workout and import lives in PostgreSQL, whose data directory is on the dedicated data disk rather than the OS disk. That means you can resize or snapshot your Ryot data independently of the operating system.
sudo -u postgres psql -tAc 'SHOW data_directory'
sudo -u postgres psql -tAc 'SHOW server_version' | tr -d ' '
findmnt -no SOURCE,TARGET,FSTYPE /var/lib/postgresql
/var/lib/postgresql/16/main
16.14(Ubuntu16.14-0ubuntu0.24.04.1)
/dev/sdc /var/lib/postgresql ext4
The mount is keyed by filesystem UUID in /etc/fstab with nofail, so it is restored on every boot:
grep postgresql /etc/fstab
Adding more people to your instance
Public sign up is disabled, which is why nobody can claim an account on your instance before you do. To add someone, sign in as the administrator and use Settings then Users, which creates the account for them.
If you would rather run an open instance, set USERS_ALLOW_REGISTRATION=true in /etc/ryot/ryot.env and restart Ryot. Only do this on an instance you are happy for anyone who can reach it to register on.
# This is the setting as shipped:
grep '^USERS_ALLOW_REGISTRATION=' /etc/ryot/ryot.env
# To open public sign up, flip it and restart Ryot:
# sudo sed -i 's/^USERS_ALLOW_REGISTRATION=false$/USERS_ALLOW_REGISTRATION=true/' /etc/ryot/ryot.env
# sudo systemctl restart ryot
Connecting metadata providers
Ryot ships with no third party API keys, so the providers that do not need one work immediately: Anilist and MangaUpdates for anime and manga, Audible for audiobooks, iTunes for podcasts, OpenLibrary for books, and the built in exercise database for fitness.
Films, television, video games, music and some book sources need a free credential of your own. Add the relevant variables to /etc/ryot/ryot.env and restart Ryot:
| Media | Variable | Where to get it |
|---|---|---|
| Films and television | MOVIES_AND_SHOWS_TMDB_ACCESS_TOKEN |
A free TMDB account, API read access token |
| Films and television | MOVIES_AND_SHOWS_TVDB_API_KEY |
A TheTVDB subscriber key |
| Video games | VIDEO_GAMES_TWITCH_CLIENT_ID and VIDEO_GAMES_TWITCH_CLIENT_SECRET |
A Twitch developer application, used for IGDB |
| Music | MUSIC_SPOTIFY_CLIENT_ID and MUSIC_SPOTIFY_CLIENT_SECRET |
A Spotify developer application |
| Books | BOOKS_HARDCOVER_API_KEY |
A Hardcover account |
echo 'MOVIES_AND_SHOWS_TMDB_ACCESS_TOKEN=<your-token>' | sudo tee -a /etc/ryot/ryot.env
sudo systemctl restart ryot
Serving Ryot on your own domain
FRONTEND_URL is used when Ryot generates links, so point it at the address people actually use. Set it, then restart:
sudo sed -i 's|^FRONTEND_URL=.*|FRONTEND_URL=https://ryot.<your-domain>/|' /etc/ryot/ryot.env
sudo systemctl restart ryot
Terminate TLS in front of the VM with an Azure Application Gateway or Front Door, or add your own certificate to the VM's nginx at /etc/nginx/sites-available/cloudimg-ryot. The proxy already forwards X-Forwarded-Proto, so Ryot generates https links once TLS is in front of it.
Maintenance
- Configuration lives in
/etc/ryot/ryot.env(mode0600, root only). Any change takes effect withsudo systemctl restart ryot. - Logs:
sudo docker logs --tail 100 ryotfor the application, andsudo journalctl -u ryot -u ryot-firstboot -u nginxfor the units. - Backups: back up the
ryotPostgreSQL database, and take Azure snapshots of the data disk mounted at/var/lib/postgresql. - Upgrades: the container image is pinned to a digest so your version never changes underneath you. To move to a newer Ryot, update the
image:line in/etc/ryot/compose.yamlto the new digest and restartryot.service; Ryot applies its own database migrations on start. Read the upstream release notes first. - OS updates: unattended security upgrades are enabled, exactly as stock Ubuntu 24.04.
Support
This image is published and supported by cloudimg with 24/7 support. Raise a request at www.cloudimg.co.uk and include the offer name, the region, and the output of systemctl is-active docker postgresql nginx ryot ryot-firstboot.
Ryot itself is open source under GPL-3.0. Upstream project documentation is at docs.ryot.io.