Deployment guide

Ryot

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. It pulls artwork and metadata from public providers, so you search for a title instead of typing it 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 SecureAnyCloud image runs Ryot v10.4.0 from the official container image, pinned to an immutable digest. PostgreSQL 16 is co-hosted on the same VM, with its data directory on a dedicated Azure data disk, and nginx fronts the stack on port 80. Every VM generates its own administrator account, administrator password, database password and admin access token on first boot, and public sign-up is switched off.

What you get:

  • 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 enabled, active systemd units
  • 24/7 SecureAnyCloud support

Prerequisites

You need an active Azure subscription, an SSH key pair, and a VNet with a 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 Standard_B2ms or Standard_B4ms if you plan to import a very large history or share the instance with many people.

Configure the network security group (NSG) to allow inbound 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 that the Ryot container can reach it across the Docker bridge. Its host-based authentication admits only the ryot role from the Docker subnet plus the local socket. Never open 5432 in an NSG rule.

Step 1 - Deploy from the Azure Marketplace

In the Azure portal, choose Create a resource, search for Ryot on Ubuntu 24.04 LTS by SecureAnyCloud, and select Create.

Pick your subscription, resource group and region, and set the size to Standard_B2s. Choose SSH public key authentication with the username azureuser, and allow inbound 22 and 80.

The image already contains the dedicated data disk definition, so Azure creates and mounts the PostgreSQL volume for you.

Step 2 - Deploy from the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:ryot-ubuntu-24-04:default:latest
az vm create \
  --resource-group my-ryot-rg \
  --name my-ryot \
  --image sac1647283583153:ryot-ubuntu-24-04:default: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

Run both commands 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. During that time, ryot-firstboot.service rotates the database password, mints the per-VM secrets, creates your administrator account, and only then starts nginx. Port 80 stays closed until it finishes.

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:

sudo 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. Read the file:

sudo cat /root/ryot-credentials.txt

The file contains four values plus guidance:

ryot.url=http://<your-vm-address>/ ryot.admin.username=sacadmin ryot.admin.password=<24 characters, unique to this VM> ryot.admin.access.token=<64 characters, unique to this VM>

To 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 authorizes 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. It also 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 that Ryot 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 shows that nginx starts last, after 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. The dashboard opens and summarizes 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. The search queries a public metadata provider and returns titles with cover art, release years and ratings.

Select a result to open its details page. It shows 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 that the web interface uses, at /backend/graphql. Sign in with the per-VM administrator to obtain an API token, then read your collections back. The commands read the password straight from the credentials file, so you can copy and paste them as they are:

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

These 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 gap. Registration is disabled before the application starts, and the administrator is created with a per-VM password. You can check each of these points 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. Its data directory is on the dedicated data disk rather than the OS disk, so 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. Confirm it:

grep postgresql /etc/fstab

Adding more people to your instance

Public sign-up is disabled, so nobody can claim an account on your instance before you do. To add someone, sign in as the administrator and open Settings then Users. This creates the account for them.

To run an open instance instead, set USERS_ALLOW_REGISTRATION=true in /etc/ryot/ryot.env and restart Ryot. Do this only if you are happy for anyone who can reach the instance to register.

# 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. Each line below gives the media type, the variable and where to get its value:

Films and television: MOVIES_AND_SHOWS_TMDB_ACCESS_TOKEN. Create a free TMDB account and use its API read access token.

Films and television: MOVIES_AND_SHOWS_TVDB_API_KEY. Use a TheTVDB subscriber key.

Video games: VIDEO_GAMES_TWITCH_CLIENT_ID and VIDEO_GAMES_TWITCH_CLIENT_SECRET. Create a Twitch developer application, which is used for IGDB.

Music: MUSIC_SPOTIFY_CLIENT_ID and MUSIC_SPOTIFY_CLIENT_SECRET. Create a Spotify developer application.

Books: BOOKS_HARDCOVER_API_KEY. Use 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 that 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/sac-ryot. The proxy already forwards X-Forwarded-Proto, so Ryot generates https links once TLS is in front of it.

Maintenance

  • Configuration. The configuration lives in /etc/ryot/ryot.env (mode 0600, root only). Any change takes effect with sudo systemctl restart ryot.
  • Logs. Run 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 on stock Ubuntu 24.04.

Support

SecureAnyCloud publishes this image and provides 24/7 support for it. Raise a request at secureanycloud.com 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.

Deploy on Azure

Launch Ryot on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure