Applications Azure

Chhoto URL on Ubuntu 24.04 on Azure User Guide

| Product: Chhoto URL on Ubuntu 24.04 LTS on Azure

Overview

Chhoto URL is a deliberately minimal, self-hosted URL shortener written in Rust. It turns a long link into a short one, either with an auto-generated readable slug or a custom word you choose. Links can carry a note, count their hits and expire automatically. A small responsive web interface covers everyday use, and a JSON API with API-key authentication covers scripting and automation. There is no tracking and no third party in the path, so every link and every click stays in your own account.

The cloudimg image delivers Chhoto URL 7.5.0 fully installed and reverse-proxied with nginx. It is a single self-contained Rust binary with an embedded SQLite datastore, running under systemd. The application listens on the loopback interface only; nginx on port 80 is the single reachable entry point. Backed by 24/7 cloudimg support.

What is included:

  • Chhoto URL 7.5.0 at /opt/chhoto-url/chhoto-url, compiled from the upstream source tagged 7.5.0 and pinned to that exact commit
  • nginx reverse proxy on :80 in front of the Chhoto URL server on 127.0.0.1:4567
  • An embedded SQLite datastore at /var/lib/chhoto-url/urls.sqlite in WAL journal mode
  • Secure by default: a unique password and a unique API key are generated on first boot into a root-only file, stored as Argon2id hashes, so no shared or default credential ships in the image
  • chhoto-url.service + nginx.service as systemd units, enabled and active
  • A fully patched Ubuntu 24.04 LTS base with unattended-upgrades enabled
  • 24/7 cloudimg support

Secure by default - unique secrets on first boot

Chhoto URL signs in with a password only - there is no username field.

This matters more than usual here. Upstream Chhoto URL has no default password at all: if no password is configured, the instance is completely public and anyone can create and delete links. This image never ships in that state. On the very first boot, chhoto-url-firstboot.service generates a unique 32-character password and a unique 64-character API key for your virtual machine, writes their Argon2id hashes into the runtime environment file, and records the plaintext in /root/chhoto-url-credentials.txt (mode 0600, readable only by root).

Three independent guards make it impossible for this image to serve an open instance:

  1. chhoto-url.service will not start until first boot has provisioned those secrets,
  2. a start-up guard refuses to run the service if either secret is missing or is not an Argon2 hash, and
  3. only hashes are stored in the runtime configuration, so the plaintext exists in exactly one root-only file.

The Chhoto URL sign-in dialog, password only with no username field

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 comfortable starting point; this is a very light workload.

Network security group inbound rules: allow 22/tcp from your management network and 80/tcp from wherever you use the shortener. There is no need to publish port 4567. The application binds to 127.0.0.1 only, so nginx on port 80 is the sole entry point by design.

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Chhoto URL 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 and Create.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name chhoto-url \
  --image <marketplace-image-urn> \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --vnet-name <your-vnet> --subnet <your-subnet> \
  --public-ip-sku Standard

az vm open-port --resource-group <your-rg> --name chhoto-url --port 80 --priority 1010

Step 3 - Connect to your VM

ssh azureuser@<vm-public-ip>

The message of the day shows your Chhoto URL address and where to find your secrets.

Step 4 - Confirm the services are running

Both units should report active. Note the listeners: the application is bound to 127.0.0.1:4567 and only nginx listens publicly on :80. An unauthenticated write correctly returns 401.

for s in chhoto-url.service nginx.service; do
    printf '%-22s %s\n' "$s" "$(systemctl is-active "$s")"
done

printf 'first boot completed: %s\n' \
    "$([ -f /var/lib/cloudimg/chhoto-url-firstboot.done ] && echo yes || echo no)"
echo

echo "listeners:"
ss -tln | grep -E ':80 |:4567 ' | awk '{print "  ", $1, $4}'
echo

printf 'GET /api/version        -> HTTP %s  %s\n' \
    "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/api/version)" \
    "$(curl -s http://127.0.0.1/api/version)"
printf 'GET /                   -> HTTP %s\n' \
    "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/)"
printf 'POST /api/new (no auth) -> HTTP %s\n' \
    "$(curl -s -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/json' \
       --data-binary '{"longlink":"https://example.com"}' http://127.0.0.1/api/new)"

Chhoto URL and nginx active, the backend bound to loopback and the API answering

Step 5 - Retrieve your password and API key

Both secrets were generated on first boot and stored in a root-only file, along with your site address.

sudo stat -c '%n  %a  %U:%G' /root/chhoto-url-credentials.txt
sudo grep -E '^CHHOTO_URL=' /root/chhoto-url-credentials.txt
echo
echo "Your password (keep it safe):"
sudo grep '^chhoto.password=' /root/chhoto-url-credentials.txt | cut -d= -f2-
echo
echo "Your API key (keep it safe):"
sudo grep '^chhoto.apikey=' /root/chhoto-url-credentials.txt | cut -d= -f2-

The runtime environment file holds only Argon2id hashes, never the plaintext:

sudo stat -c '%n  %a  %U:%G' /etc/chhoto-url/chhoto-url.env
sudo grep -E '^CHHOTO_(PASSWORD|API_KEY)=' /etc/chhoto-url/chhoto-url.env \
    | sed -E 's/(\$argon2id\$v=[0-9]+\$m=[0-9,tp=]+\$).*/\1<salt>$<hash>/'

The per-VM credentials file and the Argon2id hashes in the runtime environment

Confirm that your password actually authenticates and that a well-known password is rejected:

PASS="$(sudo grep '^chhoto.password=' /root/chhoto-url-credentials.txt | cut -d= -f2-)"

printf 'sign in with your per-VM password -> HTTP %s\n' \
    "$(curl -s -o /dev/null -w '%{http_code}' -X POST --data-binary "$PASS" \
       http://127.0.0.1/api/login)"

printf 'sign in with "password"           -> HTTP %s (401 = rejected)\n' \
    "$(curl -s -o /dev/null -w '%{http_code}' -X POST --data-binary 'password' \
       http://127.0.0.1/api/login)"

Step 6 - Sign in to Chhoto URL

Browse to http://<vm-public-ip>/. Enter the password from Step 5 and select Log in. The shortener form appears above your Active links table.

The Chhoto URL shortener form and the active links table

Step 7 - Create your first short link

Paste a long URL into Long URL. Leave Short URL empty to get an automatically generated adjective-noun slug, or type your own word. Select Shorten! - the new short link is copied to your clipboard and appears in the Active links table with its hit counter.

A long URL shortened, with the resulting short link in the active links table

More options exposes an expiry and a free-text note for each link.

Per-link options: custom slug, expiry and notes

Step 8 - Create short links from the API

The same operations are available over the JSON API using the API key from Step 5. The key goes in the X-API-Key header.

KEY="$(sudo grep '^chhoto.apikey=' /root/chhoto-url-credentials.txt | cut -d= -f2-)"

echo "Creating a short link:"
curl -s -X POST -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
    --data-binary '{"shortlink":"azure-cli","longlink":"https://learn.microsoft.com/en-us/cli/azure/"}' \
    http://127.0.0.1/api/new | jq .
echo

printf 'GET /azure-cli -> HTTP %s  Location: %s\n' \
    "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/azure-cli)" \
    "$(curl -s -o /dev/null -w '%{redirect_url}' http://127.0.0.1/azure-cli)"
echo

echo "All links:"
curl -s -H "X-API-Key: $KEY" http://127.0.0.1/api/all \
    | jq -r '.[] | "  \(.shortlink)  ->  \(.longlink)   hits=\(.hits)"' 2>/dev/null \
    || sudo sqlite3 /var/lib/chhoto-url/urls.sqlite \
         'SELECT "  " || short_url || "  ->  " || long_url || "   hits=" || hits FROM urls;'

Creating a short link over the API and following the redirect

Omit shortlink to have a slug generated for you. expiry_delay (seconds) and notes are optional:

KEY="$(sudo grep '^chhoto.apikey=' /root/chhoto-url-credentials.txt | cut -d= -f2-)"
curl -s -X POST -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
    --data-binary '{"longlink":"https://www.cloudimg.co.uk/guides/","expiry_delay":86400,"notes":"expires in a day"}' \
    http://127.0.0.1/api/new | jq .

Deleting a link is a DELETE against its slug:

KEY="$(sudo grep '^chhoto.apikey=' /root/chhoto-url-credentials.txt | cut -d= -f2-)"
printf 'DELETE /api/del/azure-cli -> HTTP %s\n' \
    "$(curl -s -o /dev/null -w '%{http_code}' -X DELETE -H "X-API-Key: $KEY" \
       http://127.0.0.1/api/del/azure-cli)"

Your public address

Short links are returned using the address detected on first boot, so they are usable immediately. That value lives in CHHOTO_SITE_URL:

sudo grep '^CHHOTO_SITE_URL=' /etc/chhoto-url/chhoto-url.env

If you put a DNS name in front of the VM, set that name instead so generated links use it:

sudo sed -i 's#^CHHOTO_SITE_URL=.*#CHHOTO_SITE_URL=https://links.your-domain.example#' \
    /etc/chhoto-url/chhoto-url.env
sudo systemctl restart chhoto-url

The datastore

The whole application state is one SQLite file in WAL journal mode.

sudo ls -l /var/lib/chhoto-url/urls.sqlite
printf 'journal mode: %s\n' \
    "$(sudo sqlite3 /var/lib/chhoto-url/urls.sqlite 'PRAGMA journal_mode;')"
printf 'links stored: %s\n' \
    "$(sudo sqlite3 /var/lib/chhoto-url/urls.sqlite 'SELECT COUNT(*) FROM urls;')"

Backing up and restoring

Take a consistent copy with SQLite's online backup, which is safe while the service is running:

sudo install -d -m 0750 /var/backups/chhoto-url
sudo sqlite3 /var/lib/chhoto-url/urls.sqlite \
    ".backup '/var/backups/chhoto-url/urls-backup.sqlite'"
sudo sqlite3 /var/backups/chhoto-url/urls-backup.sqlite 'PRAGMA integrity_check;'
sudo ls -l /var/backups/chhoto-url/

To restore, stop the service, copy the backup over /var/lib/chhoto-url/urls.sqlite, restore ownership to chhoto:chhoto, and start the service again.

Changing your password or API key

The runtime environment file stores Argon2id hashes, so hash the new value before writing it:

printf '%s' 'your-new-secret' | argon2 "$(openssl rand -hex 16)" -id -t 3 -m 15 -p 1 -e

Put the resulting $argon2id$... string into CHHOTO_PASSWORD (or CHHOTO_API_KEY) in /etc/chhoto-url/chhoto-url.env, then restart:

sudo systemctl restart chhoto-url

Update /root/chhoto-url-credentials.txt too so the record stays accurate. Secrets must be 127 characters or shorter - that is the reference argon2 tool's input limit.

Security updates

The base OS is fully patched at build time and unattended-upgrades stays enabled, so security updates continue to apply on your VM.

echo "held packages (should be none):"
H="$(apt-mark showhold)"; [ -z "$H" ] && echo "  (none)" || echo "$H"
echo

printf 'packages still upgradable: %s\n' \
    "$(apt-get -s -o APT::Get::Always-Include-Phased-Updates=true dist-upgrade | grep -c '^Inst ' || true)"
echo

printf 'unattended-upgrades: %s\n' "$(systemctl is-enabled unattended-upgrades.service)"
grep -h Unattended /etc/apt/apt.conf.d/20auto-upgrades
echo

echo "swap (must be empty on an Azure image):"
S="$(swapon --show)"; [ -z "$S" ] && echo "  (empty)" || echo "$S"

The OS security baseline: fully patched with unattended-upgrades enabled

Enabling HTTPS with Let's Encrypt

Point a DNS A record at your VM's public IP, open 443/tcp in the network security group, then issue a certificate. nginx is already the front end, so certbot can configure it in place:

sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d <your-domain> --agree-tos -m <your-email> --redirect

Certbot installs a renewal timer automatically. Afterwards set CHHOTO_SITE_URL to the https:// address so generated links use it.

Tuning

Configuration is read from /etc/chhoto-url/chhoto-url.env at start-up. Useful settings, all optional:

  • CHHOTO_SLUG_STYLE - Pair for adjective-noun slugs (default here) or UID
  • CHHOTO_SLUG_LENGTH - length of generated slugs, minimum 4
  • CHHOTO_ALLOW_CAPITAL_LETTERS - set to True to permit capitals in slugs
  • CHHOTO_REDIRECT_METHOD - TEMPORARY to send temporary rather than permanent redirects
  • CHHOTO_FRONTEND_PAGE_SIZE - rows per page in the links table

Apply changes with sudo systemctl restart chhoto-url.

Troubleshooting

echo "--- recent chhoto-url log ---"
journalctl -u chhoto-url.service --no-pager -n 15
echo
echo "--- nginx configuration test ---"
nginx -t 2>&1
echo
echo "--- effective configuration (secrets not shown) ---"
sudo grep -vE '^CHHOTO_PASSWORD=|^CHHOTO_API_KEY=|^\s*#|^\s*$' /etc/chhoto-url/chhoto-url.env

If the service is not running, the usual cause is that the per-VM secrets are missing, which the start-up guard deliberately treats as fatal rather than serving an open instance. Confirm CHHOTO_PASSWORD and CHHOTO_API_KEY are set to Argon2 hashes in /etc/chhoto-url/chhoto-url.env, then sudo systemctl restart chhoto-url. If the web interface is unreachable from outside the VM but curl http://127.0.0.1/ succeeds locally, check the network security group allows 80/tcp.

Support

This image is published and supported by cloudimg. For assistance, contact support@cloudimg.co.uk. Chhoto URL is open source software distributed under the MIT licence; see the upstream project for product documentation.