Pu
Applications Azure

Puter on Ubuntu 24.04 on Azure User Guide

| Product: Puter on Ubuntu 24.04 LTS on Azure

Overview

Puter is an open source, self hosted desktop that runs entirely in a web browser. Open it and you get a real desktop environment - wallpaper, desktop icons, a task bar and draggable windows - on top of a full file manager for storage that lives on your own server. Files and folders are created, renamed, moved between windows, uploaded, downloaded and shared with other accounts on the same instance, and each account gets subdomains for publishing a static site or hosting a private web app. Underneath sits a developer platform: the puter.js SDK lets you build applications against your instance's own file, key value and hosting APIs.

The cloudimg image installs Puter 26.07.1 built from the pinned upstream release with Node.js 24, runs it as a systemd service behind an nginx reverse proxy on port 80, keeps its SQLite database and object storage under /var/lib/puter, and generates a unique administrator account and a unique set of token signing secrets on the first boot of every VM. Backed by 24/7 cloudimg support.

What is included:

  • Puter 26.07.1 (AGPL-3.0), built from the pinned upstream release tag
  • The web desktop, the windowed file manager and per account storage, published on port 80 via nginx
  • A unique administrator account generated on first boot, written to a root only credentials file
  • Unique JWT and URL signature secrets generated per VM, so no two deployments can forge each other's sessions
  • Self registration closed in the shipped configuration, for both permanent and anonymous temporary accounts
  • SQLite plus a local object store under /var/lib/puter, with no external database, cache or container runtime to operate
  • puter-set-domain, a one command helper for moving the appliance onto your own domain
  • puter-s3.service, puter.service and nginx.service as systemd units, enabled and active
  • 24/7 cloudimg support

What is not included - read this before you buy:

  • The self hosted build registers no local applications. Every application entry Puter seeds - Camera, Recorder, Dev Center, Git, PDF, Player, Music Player - points at a puter.com or third party hosted URL, and no text editor is registered at all, so double clicking a text file opens nothing. The desktop, the file manager, storage, sharing, site and app hosting subdomains, and the SDK are what this appliance itself provides.
  • The desktop needs outbound internet from the browser. Puter's compiled interface loads its puter.js SDK from https://js.puter.com/v2/ at runtime. That URL is compiled into the upstream bundle, and this image ships upstream unmodified, so if a browser cannot reach js.puter.com the sign in page does not render. This appliance is not air gappable. (Puter's own code notes that self hosters who need this can route js.puter.com at their own instance via DNS.)
  • The account area shows an "Upgrade" button and a dollar denominated allowance. That is puter.com's hosted billing surface appearing in the self hosted build. Nothing is charged, and there is no payment path on your VM.

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet + subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a good starting point. NSG inbound: allow 22/tcp from your management network and 80/tcp for the web interface (front with TLS for public exposure - see Enabling HTTPS). Do not open 4100/tcp or 4566/tcp; nginx on port 80 is the intended entrance.

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Puter by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Then Review + create -> Create.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name puter \
  --image <marketplace-image-urn> \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Then open the web interface port on the VM's network security group:

az vm open-port --resource-group <your-rg> --name puter --port 80 --priority 1001

Step 3 - Retrieve the per VM administrator login

A unique administrator account is generated on the first boot of every VM and written to a root only credentials file. Nothing is shared between deployments and no default password exists in the image. SSH in and read it:

ssh azureuser@<vm-public-ip>
sudo cat /root/puter-credentials.txt

The file contains the address, the domain the appliance serves on, the administrator login, and a ready made hosts entry:

The per VM Puter credentials file showing the site URL, the serving domain, the VM address, the administrator username and generated password, and the ready made hosts file line, with root only 0600 permissions

Keep this password safe. It is the credential for the desktop. You can change it from Account -> Change Password after your first sign in.

Step 4 - Confirm Puter is healthy

The /healthcheck endpoint is deliberately unauthenticated, and is the one route exempt from Puter's Host header validation, so you can probe it from monitoring without any extra headers. Run these on the VM:

systemctl is-active puter-s3.service puter.service nginx.service
curl -s http://127.0.0.1/healthcheck

Expected output:

active
active
active
{"ok":true}

Every other route validates the Host header against the configured domain, so a version check has to present it:

curl -s -H 'Host: puter.internal' http://127.0.0.1/version

Expected output:

{"version":"2.5.1","major":2,"minor":5,"patch":1,"environment":"prod","location":"cloudimg-node-1","deploy_timestamp":1785054449018}

The reported version is the upstream package.json version string; the release this image is built from is tag 26.07.1.

Confirm what is listening. nginx holds port 80, the application sits on 4100, and the object store on 4566 is fenced to loopback by the kernel:

ss -tln | grep -E ':80 |:4100|:4566'

Expected output:

LISTEN 0      511          0.0.0.0:4566       0.0.0.0:*          
LISTEN 0      511          0.0.0.0:80         0.0.0.0:*          
LISTEN 0      511             [::]:80            [::]:*          
LISTEN 0      511                *:4100             *:*          

Terminal output showing the three Puter services active, the health endpoint returning ok, the version endpoint reporting the running build, and the listening ports for nginx, the application and the object store

Step 5 - Reach the desktop

Puter is reached by name, never by bare IP address. It routes its API on api.<domain>, hosted sites on site.<domain> and host.<domain>, and private apps on app.<domain> and dev.<domain>, and it reads the configured domain as exactly two labels. A three label name such as puter.example.com makes it treat puter as a subdomain, and the desktop, sign in and registration routes all stop matching. The image therefore ships serving on puter.internal, which is inside the TLD ICANN reserved for private use.

You have two ways to reach it.

Option A - try it immediately, with no DNS at all

Copy the puter.hosts.line value from Step 3 and add it to the hosts file on the machine you browse from. On macOS and Linux that file is /etc/hosts; on Windows it is C:\Windows\System32\drivers\etc\hosts. The line looks like this, with your VM's own address:

20.102.81.159 puter.internal api.puter.internal s3.puter.internal site.puter.internal host.puter.internal app.puter.internal dev.puter.internal

Then open http://puter.internal/ in your browser.

Option B - serve it on your own domain

Point both example.com and *.example.com at the VM's public address, then run one command on the VM:

sudo puter-set-domain example.com

The helper rewrites the domain, the four hosting subdomains and the browser facing storage endpoint, keeps the VM's own name resolution in step, restarts the service and then verifies the desktop answers on the new name before returning. It refuses anything that is not a two label domain, because a three label domain silently breaks subdomain routing rather than failing loudly.

Once TLS is terminated in front of the appliance, re-run it as sudo puter-set-domain example.com https.

Step 6 - Sign in

Open the address from Step 5. You are presented with the sign in page. Use the username admin and the password from Step 3.

The Puter sign in page asking for an email address or username and a password

After signing in, go to /desktop for the desktop environment. Files saved to your Desktop folder appear as icons, and the task bar along the bottom holds the launcher, the file manager and the wastebasket.

The Puter desktop showing files stored on the VM as desktop icons, with the task bar holding the launcher, file manager and wastebasket

Step 7 - Work with your files

Open the file manager from the task bar. The sidebar holds Home, Documents, Public, Pictures, Desktop and Videos; the breadcrumb shows where you are; and the window is a real desktop window you can drag, resize and open alongside another one to move files between folders.

The Puter file manager open on a Documents subfolder, showing stored files with the sidebar and breadcrumb navigation

Everything you see is stored on this VM under /var/lib/puter, in the signed in account's own namespace. Folders nest normally, and files can be uploaded by dragging them into a window.

The Puter file manager showing the Documents folder with nested project folders and files stored on the VM

The Usage page in the account area reports what you are actually storing. The dollar figures and the Upgrade link belong to the hosted puter.com service and do not apply to your VM.

The Puter usage page showing real stored bytes against the available storage on the VM

Step 8 - The security posture

Self registration is disabled in the shipped configuration, before the service ever accepts a request - not switched off afterwards. Both permanent signups and Puter's anonymous temporary accounts are refused, and exactly one account can sign in. Prove all three on the VM:

curl -s -o /dev/null -w 'permanent signup: HTTP %{http_code}\n' -H 'Host: puter.internal' \
  -H 'Content-Type: application/json' \
  -d '{"username":"cloudimgprobe","email":"cloudimgprobe@example.com","password":"Probe-Password-123456","p102xyzname":""}' \
  http://127.0.0.1/signup
curl -s -o /dev/null -w 'temporary signup: HTTP %{http_code}\n' -H 'Host: puter.internal' \
  -H 'Content-Type: application/json' -d '{"is_temp":true}' http://127.0.0.1/signup
sudo sqlite3 /var/lib/puter/db/puter-database.sqlite \
  "SELECT username FROM user WHERE password IS NOT NULL AND password <> '';"

Expected output:

permanent signup: HTTP 403
temporary signup: HTTP 403
admin

Terminal output showing both permanent and temporary registration refused with HTTP 403 and a single login capable account named admin

Two things are worth knowing about how that state is reached. Upstream's first start creates the admin account with a temporary password of its own and keeps a plain text copy of it in the database so later boots can tell whether it has been rotated. This image's first boot replaces that password with a per VM one, deletes the plain text copy, and only then starts nginx - so the temporary credential is never live on a reachable port. And the JWT signing secrets are generated per VM, so a session minted on one deployment is worthless on another.

To create more accounts, sign in as admin and invite them from the account area, or set "disable_user_signup": false in /etc/puter/config.json and restart puter.service if you want open registration.

Step 9 - Where your data lives

Puter keeps two things on disk: a SQLite database holding accounts, filesystem metadata and permissions, and an object store holding the file contents themselves. Both live under /var/lib/puter, owned by the unprivileged puter service account.

The object store is the part that has to be reachable by the browser, because Puter hands the browser a presigned URL to upload to. It is published only through nginx on the s3.<domain> name, and the port it listens on is fenced to loopback by the kernel because it does no request signing of its own. The image ships a self test that exercises exactly the path a browser takes:

sudo /usr/local/sbin/puter-s3-selftest
sudo du -sh /var/lib/puter/db /var/lib/puter/fauxqs-s3-data

Expected output (your byte counts will differ):

S3_SELFTEST_OK presigned upload host s3.puter.internal
784K    /var/lib/puter/db
24K /var/lib/puter/fauxqs-s3-data

Terminal output showing the presigned upload self test passing against the browser facing storage endpoint, the on disk size of the database and object store, and the installed Node.js and Puter versions

Taking a backup

The database and the object store must be captured together - metadata without contents, or contents without metadata, restores to nothing. Stop the application first so the two are consistent:

sudo systemctl stop puter.service
sudo mkdir -p /var/backups/puter
sudo sqlite3 /var/lib/puter/db/puter-database.sqlite ".backup '/var/backups/puter/puter-database.sqlite'"
sudo tar -czf /var/backups/puter/puter-objects.tar.gz -C /var/lib/puter fauxqs-s3-data fauxqs-data
sudo systemctl start puter.service
sudo ls -la /var/backups/puter

Copy the resulting files to storage outside the VM. To restore, stop puter.service and puter-s3.service, put the database back at /var/lib/puter/db/puter-database.sqlite owned by puter:puter, remove any stale -wal and -shm files beside it, extract the object archive back into /var/lib/puter, then start both services again.

Step 10 - Enabling HTTPS

The image serves plain HTTP on port 80 so it works the moment it boots. For anything reachable from the internet, put TLS in front of it.

Because Puter routes on subdomains, the certificate has to cover the wildcard as well as the bare domain. Point your DNS at the VM's public IP, open 443/tcp on the network security group, then issue a wildcard certificate with a DNS-01 challenge:

sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot certonly --manual --preferred-challenges dns \
  -d example.com -d "*.example.com" \
  --agree-tos -m you@example.com

Install the certificate into the nginx server blocks at /etc/nginx/sites-available/cloudimg-puter and /etc/nginx/sites-available/cloudimg-puter-s3, reload nginx, then tell Puter it is being served over TLS so the origins, redirects and storage URLs it builds all use https:

sudo puter-set-domain example.com https

Step 11 - Maintenance

Check service health and read the application log:

systemctl is-active puter.service
sudo journalctl -u puter.service -n 20 --no-pager -o cat

The image ships fully patched with unattended security updates enabled, so the operating system keeps itself current. To apply updates immediately:

sudo apt-get update && sudo apt-get upgrade -y

The three units that make up the appliance are puter-s3.service (the object store), puter.service (the application) and nginx.service (the reverse proxy, and the only public listener). They are all enabled, and the application will not start until the first boot bootstrap has written this VM's own secrets - a guard that stops it ever coming up unconfigured after a reboot.

systemctl is-enabled puter-s3.service puter.service nginx.service

Expected output:

enabled
enabled
enabled

Support

Every cloudimg image comes with 24/7 support. Raise a ticket at cloudimg.co.uk and include the output of systemctl is-active puter-s3.service puter.service nginx.service and the last 50 lines of sudo journalctl -u puter.service.

Puter is a trademark of Puter Technologies Inc. This image is an unmodified distribution of the Puter open source software and is not affiliated with or endorsed by Puter Technologies Inc.