Deployment guide

Chhoto URL

Overview

Chhoto URL is a deliberately minimal, self-hosted URL shortener written in Rust. It turns a long link into a short one, using either 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 SecureAnyCloud 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, and nginx on port 80 is the single reachable entry point.

What you get

  • 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.
  • An 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 and stored as Argon2id hashes, so no shared or default credential ships in the image.
  • chhoto-url.service and nginx.service as systemd units, enabled and active.
  • An Ubuntu 24.04 LTS base, patched at build time, with unattended-upgrades enabled.
  • 24/7 SecureAnyCloud support.

Secure by default: unique secrets on first boot

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

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. It 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:

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

Prerequisites

Before you deploy, make sure you have an active Azure subscription, an SSH key pair, and a VNet and subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a comfortable starting point, because this is a very light workload.

Configure the network security group with these 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 SecureAnyCloud, 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 choose Review + create and Create.

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:chhoto-url:default:latest
az vm create \
  --resource-group <your-rg> \
  --name chhoto-url \
  --image sac1647283583153:chhoto-url:default:latest \
  --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>

On login, 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. 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/sac/data/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)"

Step 5 - Retrieve your password and API key

First boot generated both secrets and stored them in a root-only file, along with your site address. Print the file:

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>/'

Confirm that your password 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.

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.

Select More options to set an expiry and a free-text note for each link.

Step 8 - Create short links from the API

The JSON API offers the same operations. Authenticate with the API key from Step 5, and send it 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;'

Omit shortlink to have a slug generated for you. The expiry_delay (in seconds) and notes fields 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://secureanycloud.com/stacks/chhoto-url/","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 use the address detected on first boot, so they work 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 that 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 and copy the backup over /var/lib/chhoto-url/urls.sqlite. Restore ownership to chhoto:chhoto, then 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, which is the input limit of the reference argon2 tool.

Security updates

The base OS is fully patched at build time, and unattended-upgrades stays enabled, so security updates keep applying 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"

Enabling HTTPS with Let's Encrypt

Point a DNS A record at your VM's public IP and 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 that generated links use it.

Tuning

Chhoto URL reads its configuration from /etc/chhoto-url/chhoto-url.env at start-up. These settings are all optional:

  • CHHOTO_SLUG_STYLE: Pair for adjective-noun slugs (the default here) or UID.
  • CHHOTO_SLUG_LENGTH: the length of generated slugs, with a minimum of 4.
  • CHHOTO_ALLOW_CAPITAL_LETTERS: set to True to permit capitals in slugs.
  • CHHOTO_REDIRECT_METHOD: set to TEMPORARY to send temporary rather than permanent redirects.
  • CHHOTO_FRONTEND_PAGE_SIZE: the number of 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. The start-up guard deliberately treats that as fatal rather than serving an open instance. Confirm that CHHOTO_PASSWORD and CHHOTO_API_KEY are set to Argon2 hashes in /etc/chhoto-url/chhoto-url.env, then run 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 that the network security group allows 80/tcp.

Support

SecureAnyCloud publishes and supports this image. For assistance, contact sales@secureanycloud.com. Chhoto URL is open source software distributed under the MIT license. See the upstream project for product documentation.

Deploy on Azure

Launch Chhoto URL 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