PlanarAlly on Ubuntu 24.04 on Azure User Guide
Overview
PlanarAlly is a popular open source, self hosted virtual tabletop (VTT) for tabletop role playing games. A game master and their players share a battle map in the browser, with drag and drop tokens, per token vision and fog of war, an initiative tracker, a dice roller, chat, notes and custom character data, all kept in sync live between everyone at the table.
The cloudimg image builds PlanarAlly 2026.1.2 from the pinned upstream release under /opt/planarally, runs the aiohttp application server as the dedicated planarally system user bound to loopback 127.0.0.1:8000, and fronts it with nginx on port 80. The Vue client is compiled into the image at build time, so there is nothing for you to build. Its datastore is an embedded SQLite save file at /var/lib/planarally/data/planar.sqlite.
Secure by default. No usable credential ships in the image. There is no save file at all until your instance first boots, so no session signing secret, no API token and no account exist beforehand. On first boot a planarally-firstboot.service oneshot creates the save file (which mints a random session secret and API token unique to this VM), generates a strong administrator password, records it in a root only file, and only then starts the application and enables the public nginx site. Until that has happened nothing is listening on port 80 at all.
Self registration is closed by default. A virtual tabletop is invite based: the game master invites players with a campaign invitation link. If you would rather let players create their own accounts, Step 13 shows the single setting that opens it.
What is included:
- PlanarAlly 2026.1.2 (MIT) under
/opt/planarally, Vue client prebuilt - Embedded SQLite save file at
/var/lib/planarally/data/planar.sqlite planarally.servicerunning asplanarally, bound to127.0.0.1:8000- nginx reverse proxy on port 80 with correct websocket proxying, ready for TLS
planarally-firstboot.servicefor per instance secret generation and admin seeding- uv managed Python 3.13 runtime and the Node.js 22 LTS build toolchain
- Ubuntu 24.04 LTS base, fully patched
- 24/7 cloudimg support, 24h response SLA
This image contains the PlanarAlly application only. No game rulesets, maps, tokens or artwork from any published role playing game are included; you bring your own content. The demo artwork in the screenshots below was created by cloudimg for this guide.
Prerequisites
An active Azure subscription, an SSH key pair, and a VNet with a subnet. Recommended VM size: Standard_B2s (PlanarAlly is light; 4 GB RAM comfortably runs a table). Your players need nothing but a modern browser.
Step 1: Deploy from the Azure Portal
Search the Marketplace for PlanarAlly on Ubuntu 24.04, choose your VM size, and attach an NSG that allows TCP 22 (SSH) from your management network and TCP 80 / 443 (the web application, including the websockets that carry live session sync) from the networks your players connect from. Front port 80 with TLS in production, see the HTTPS section below.
Step 2: Deploy from the Azure CLI
RG="planarally-prod"; LOCATION="eastus"; VM_NAME="planarally-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/planarally-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az network vnet create -g "$RG" --name pa-vnet --address-prefix 10.90.0.0/16 --subnet-name pa-subnet --subnet-prefix 10.90.1.0/24
az network nsg create -g "$RG" --name pa-nsg
az network nsg rule create -g "$RG" --nsg-name pa-nsg --name allow-ssh --priority 100 \
--source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp
az network nsg rule create -g "$RG" --nsg-name pa-nsg --name allow-web --priority 110 \
--source-address-prefixes "<your-players-cidr>" --destination-port-ranges 80 443 --access Allow --protocol Tcp
az vm create -g "$RG" --name "$VM_NAME" --image "$GALLERY_IMAGE_ID" \
--size Standard_B2s --storage-sku StandardSSD_LRS \
--admin-username azureuser --ssh-key-values "$SSH_KEY" \
--vnet-name pa-vnet --subnet pa-subnet --nsg pa-nsg --public-ip-sku Standard
Step 3: Connect via SSH
ssh azureuser@<vm-ip>
Step 4: Verify the services are running
PlanarAlly listens on loopback only; nginx fronts it on port 80.
sudo systemctl is-active planarally nginx
ss -tln | grep -E ':80 |127.0.0.1:8000'
curl -s -o /dev/null -w 'GET / through nginx -> HTTP %{http_code}\n' http://127.0.0.1/
Both units report active, the application server is bound to 127.0.0.1:8000 and nothing else, nginx owns port 80, and the site answers 200.

Step 5: Read your per instance administrator credentials
Nothing sensitive ships in the image. The save file that holds the session signing secret and the API token was created on this VM at first boot, and the administrator password was generated here. It is recorded in a root only file.
sudo stat -c '%n perms=%a owner=%U:%G' /root/planarally-credentials.txt
sudo sed -E 's/^(PLANARALLY_ADMIN_PASSWORD|PLANARALLY_API_TOKEN)=.*/\1=**** (unique to this instance)/' /root/planarally-credentials.txt | grep -E '^PLANARALLY_'
grep -E '^allow_signups|^admin_user' /var/lib/planarally/data/config.toml
curl -s -o /dev/null -w 'POST /api/register -> HTTP %{http_code} (self registration closed)\n' -X POST -H 'Content-Type: application/json' -d '{"username":"intruder","password":"intruder"}' http://127.0.0.1/api/register
curl -s -o /dev/null -w 'POST /api/login with a guessed password -> HTTP %{http_code}\n' -X POST -H 'Content-Type: application/json' -d '{"username":"paadmin","password":"planarally"}' http://127.0.0.1/api/login
The credentials file is 600 root:root, the administrator is paadmin and is pinned as the configured admin_user, self registration is refused with 403, and a guessed password is refused with 401. Read the real password with sudo grep PLANARALLY_ADMIN_PASSWORD /root/planarally-credentials.txt.

Step 6: Sign in
Open http://<vm-ip>/ in your browser. Sign in as paadmin with the password from /root/planarally-credentials.txt. Because self registration is closed there is no sign up link, so nobody can create an account on your server without you.

Step 7: Upload your maps and tokens
Open ASSETS and upload your own map images and token artwork. PlanarAlly reads grid dimensions from the file name, so a battle map named great-hall-16x12.png is placed as exactly 16 by 12 grid squares and a token named scout-1x1.png occupies a single square. Folders keep large collections tidy.

Step 8: Create a campaign
Open GAMES, choose NEW GAME +, pick Start from scratch, name your campaign and select CREATE. The dashboard lists the campaigns you run as game master and, separately, the ones you have joined as a player.

Step 9: Run a session
Inside a campaign, open the side menu with the gear at the top left and choose Assets. Select the map layer in the layer selector, drag your battle map from the asset manager onto the board, then switch to the tokens layer and drag your character tokens into place. Everything is saved server side as you go, so reloading the page brings the scene straight back.

Step 10: Invite your players
In DM Settings copy the campaign invitation link and send it to your players. Because self registration is closed, create an account for each player first, then let them redeem the link. To create a player account from the shell:
printf '%s' "<new-password>" | sudo runuser -u planarally -- env PA_CONFIG_PATH=/var/lib/planarally/data/config.toml HOME=/var/lib/planarally /opt/planarally/server/.venv/bin/python /usr/local/sbin/planarally-bootstrap.py "<player-name>"
When a player opens the invitation link they join the campaign and see the same board you do, updated live as you move tokens and reveal the map.

Step 11: Dice, initiative and chat
The tool bar on the right holds the select, pan, spell template, ruler, ping, vision and dice tools. The dice roller accepts expressions and can pull values from a token's custom data, so a character's attack bonus rolls with one click. The initiative tracker and chat sit alongside the board and update live for everyone.

Step 12: Websockets carry the live session
Every live action, token movement, fog reveals, initiative, dice and chat, travels over a socket.io websocket on /socket.io/. The nginx site is configured to forward the Upgrade handshake and to hold those connections open for the length of a session, so live sync works out of the box.
curl -sS -i -N -m 10 -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H "Sec-WebSocket-Key: $(head -c 16 /dev/urandom | base64)" -H 'Sec-WebSocket-Version: 13' 'http://127.0.0.1/socket.io/?EIO=4&transport=websocket' 2>/dev/null | head -4
grep -A4 'location /socket.io/' /etc/nginx/sites-available/cloudimg-planarally
nginx answers HTTP/1.1 101 Switching Protocols, which is the proof the upgrade is proxied rather than being silently downgraded to long polling.

Step 13: Open self registration (optional)
If you would rather let your players create their own accounts, set allow_signups to true and restart:
sudo sed -i 's/^allow_signups = false$/allow_signups = true/' /var/lib/planarally/data/config.toml
sudo systemctl restart planarally
sleep 8
grep '^allow_signups' /var/lib/planarally/data/config.toml
curl -s -o /dev/null -w 'signups open -> POST /api/register HTTP %{http_code}\n' -X POST -H 'Content-Type: application/json' -d '{"username":"demoplayer","password":"a-password-they-choose"}' http://127.0.0.1/api/register
Registration now returns 200 and a sign up link appears on the login page. The block below closes it again, which is the state the image ships in. Only leave registration open on a server you are happy for your players to self serve on.
sudo sed -i 's/^allow_signups = true$/allow_signups = false/' /var/lib/planarally/data/config.toml
sudo systemctl restart planarally
sleep 8
grep '^allow_signups' /var/lib/planarally/data/config.toml
curl -s -o /dev/null -w 'signups closed -> POST /api/register HTTP %{http_code}\n' -X POST -H 'Content-Type: application/json' -d '{"username":"anotherone","password":"anything"}' http://127.0.0.1/api/register
Step 14: The runtime stack
cat /opt/planarally/server/VERSION
/opt/planarally/server/.venv/bin/python --version
node -v; nginx -v
ls /opt/planarally/server/static/vite | wc -l
systemctl is-enabled planarally-firstboot.service
apt-mark showhold
A uv managed Python 3.13 runs the aiohttp server, the Vue client was compiled with Node.js 22 LTS into /opt/planarally/server/static/vite at image build time, nginx fronts the application, the pinned release is 2026.1.2, the first boot service has run and disabled itself for this instance, and there are no held packages so the OS security baseline is intact.

Step 15: Data and persistence
Campaigns, shapes, notes and accounts live in the SQLite save file; uploaded maps and tokens live alongside it.
sudo ls -la /var/lib/planarally/data/planar.sqlite
sudo du -sh /var/lib/planarally/assets
sudo sqlite3 /var/lib/planarally/data/planar.sqlite 'SELECT COUNT(*) AS campaigns FROM room;'
Back the instance up by snapshotting the OS disk, or stop the service and copy /var/lib/planarally. Campaigns can also be exported individually from the dashboard and re-imported on another server.
Step 16: Enable HTTPS
nginx already fronts PlanarAlly on port 80 and is ready for TLS. Point a DNS record at your VM, then obtain a certificate with certbot:
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d <your-domain> --agree-tos -m <your-email> --redirect
certbot installs the certificate, rewrites the nginx server block for TLS and sets up automatic renewal. Afterwards update client_url in /var/lib/planarally/data/config.toml to your https:// address so invitation and password reset links are generated correctly, then restart PlanarAlly.
Managing the service
sudo systemctl status planarally --no-pager
sudo systemctl restart planarally
sudo journalctl -u planarally -n 20 --no-pager
The application logs to the journal. nginx logs are under /var/log/nginx/. Configuration lives at /var/lib/planarally/data/config.toml and is re-read when it changes.
Support
cloudimg provides 24/7 support for this image via email and live chat: deployment, nginx TLS termination, websocket and reverse proxy questions, upgrades, account management and backups. Email support@cloudimg.co.uk.
This is a repackaged open source software product with additional charges for cloudimg support services. PlanarAlly is a trademark of its respective owner. All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them.