Media & Entertainment Azure

Ryot on Ubuntu 24.04 on Azure User Guide

| Product: Ryot on Ubuntu 24.04 LTS on Azure

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 immutable sha256 digest 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 :80 reverse proxying to the loopback-bound Ryot backend, with WebSocket upgrade headers and an unauthenticated /healthz endpoint 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.service and ryot-firstboot.service as 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

All five Ryot services active with the dedicated PostgreSQL data disk mounted at /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-

The root only credentials file with per-VM values, and a real sign in proving the administrator credential authenticates

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

The first boot log closing with anonymous registration refused, the bootstrap marker created and nginx started, plus both health endpoints returning 200

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.

The Ryot sign in page with username and password only and no sign up link, because public registration is disabled

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.

The Ryot dashboard after signing in with the per-VM administrator, showing an in progress title with its cover art

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.

A live metadata search returning real titles with cover art from the Anilist provider

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.

A media item page with real fetched metadata and the tracking state badges

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.

The personal library listing the tracked title with its cover art and watch count, read back from PostgreSQL

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

Anonymous registration refused, both PostgreSQL accounts rejecting guessable passwords, registration and telemetry both disabled, and no swap present

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 (mode 0600, root only). Any change takes effect with sudo systemctl restart ryot.
  • Logs: sudo docker logs --tail 100 ryot for the application, and sudo journalctl -u ryot -u ryot-firstboot -u nginx for the units.
  • Backups: back up the ryot PostgreSQL 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.yaml to the new digest and restart ryot.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.