PlanarAlly
Overview
PlanarAlly is an 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. Everything stays in sync live between everyone at the table.
This SecureAnyCloud image builds PlanarAlly 2026.1.2 from the pinned upstream release under /opt/planarally. It 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. The 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 until your instance first boots, so no session signing secret, API token or account exists 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. It then generates a strong administrator password and records it in a root-only file. Only after that does it start the application and enable the public nginx site. Until then, nothing is listening on port 80.
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 registration.
What you get:
- PlanarAlly 2026.1.2 (MIT) under
/opt/planarally, with the Vue client prebuilt - An embedded SQLite save file at
/var/lib/planarally/data/planar.sqlite planarally.servicerunning asplanarally, bound to127.0.0.1:8000- An nginx reverse proxy on port 80 with correct websocket proxying, ready for TLS
planarally-firstboot.servicefor per-instance secret generation and admin seeding- A uv-managed Python 3.13 runtime and the Node.js 22 LTS build toolchain
- An Ubuntu 24.04 LTS base, patched at build time
- 24/7 SecureAnyCloud 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, so you bring your own content. The demo artwork in the screenshots below was created by SecureAnyCloud for this guide.
Prerequisites
You need an active Azure subscription, an SSH key pair, and a VNet with a subnet.
The recommended VM size is Standard_B2s. PlanarAlly is light, and 4 GB RAM comfortably runs a table. Your players need only a modern browser.
Deploy from the Azure Portal
Search the Marketplace for PlanarAlly on Ubuntu 24.04 and choose your VM size.
Attach a network security group (NSG) that allows TCP 22 (SSH) from your management network. Also allow TCP 80 / 443 (the web application, including the websockets that carry live session sync) from the networks your players connect from. In production, front port 80 with TLS (see Step 16).
Deploy from the Azure CLI
RG="planarally-prod"; LOCATION="eastus"; VM_NAME="planarally-01" IMAGE_URN="sac1647283583153:planarally-ubuntu-24-04:default:latest" 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
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:planarally-ubuntu-24-04:default:latest
az vm create -g "$RG" --name "$VM_NAME" --image "$IMAGE_URN" \
--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 StandardConnect via SSH
Connect to the VM over SSH, using its IP address.
ssh azureuser@<vm-ip>Verify the services are running
PlanarAlly listens on loopback only, and nginx fronts it on port 80. Run the following checks:
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.
Read your per-instance administrator credentials
Nothing sensitive ships in the image. First boot created the save file that holds the session signing secret and the API token on this VM, and generated the administrator password here. The password is recorded in a root-only file. Run the following checks:
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/loginThe credentials file is 600 root:root. The administrator is paadmin, 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.
Sign in
Open http://<vm-ip>/ in your browser. Sign in as paadmin with the password from /root/planarally-credentials.txt.
Self-registration is closed, so there is no sign-up link, and nobody can create an account on your server without you.
Upload your maps and tokens
Open ASSETS and upload your own map images and token artwork. Use folders to keep large collections tidy.
PlanarAlly reads grid dimensions from the file name. 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.
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.
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 and 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.
Invite your players
In DM Settings, copy the campaign invitation link and send it to your players. Self-registration is closed, so create an account for each player first, then let them redeem the link. To create a player account from the shell, run:
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. The board updates live as you move tokens and reveal the map.
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.
Websockets carry the live session
Every live action travels over a socket.io websocket on /socket.io/. This includes token movement, fog reveals, initiative, dice and chat. The nginx site forwards the Upgrade handshake and holds those connections open for the length of a session, so live sync works out of the box. Run these checks:
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/sac-planarallynginx answers HTTP/1.1 101 Switching Protocols. This shows that the upgrade is proxied, not silently downgraded to long polling.
Open self-registration (optional)
To 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/registerRegistration now returns 200, and a sign-up link appears on the login page. The following commands close registration again, which is the state the image ships in. Leave registration open only on a server where you are happy for players to sign themselves up.
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/registerThe runtime stack
Check the installed versions and the first-boot state:
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 showholdThe output confirms the following:
- 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/viteat image build time. - nginx fronts the application, and the pinned release is 2026.1.2.
- The first-boot service has run and disabled itself for this instance.
- There are no held packages, so the OS security baseline is intact.
Data and persistence
Campaigns, shapes, notes and accounts live in the SQLite save file. Uploaded maps and tokens live alongside it. Inspect them with:
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;'To back up the instance, snapshot the OS disk, or stop the service and copy /var/lib/planarally. You can also export campaigns individually from the dashboard and re-import them on another server.
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> --redirectcertbot 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 that invitation and password-reset links are generated correctly. Then restart PlanarAlly.
Managing the service
Check, restart and read the logs of the service with:
sudo systemctl status planarally --no-pager sudo systemctl restart planarally
sudo journalctl -u planarally -n 20 --no-pagerThe application logs to the journal, and nginx logs are under /var/log/nginx/. Configuration lives at /var/lib/planarally/data/config.toml and is re-read when it changes.
Support
SecureAnyCloud provides 24/7 support for this image via email and live chat. Support covers deployment, nginx TLS termination, websocket and reverse-proxy questions, upgrades, account management and backups. Email sales@secureanycloud.com.
This is a repackaged open source software product, with additional charges for SecureAnyCloud 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.
Deploy on Azure
Launch PlanarAlly 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