Applications Azure

Fusion on Ubuntu 24.04 on Azure User Guide

| Product: Fusion on Ubuntu 24.04 LTS on Azure

Overview

Fusion is a lightweight, self-hosted RSS reader. It subscribes to RSS and Atom feeds, discovers feeds automatically from any site URL, organises them into groups, and presents articles in a fast, responsive web interface with unread tracking, bookmarks, full-text search and Google Reader style keyboard shortcuts. A Fever-compatible API lets third-party clients such as Reeder, Unread and FeedMe sync against your own server. Fusion ships no AI features by design.

The cloudimg image delivers Fusion 1.2.0 fully installed and reverse-proxied with nginx. Fusion is a single self-contained Go binary with an embedded SQLite datastore, running under systemd, with nginx in front on port 80. The database lives on a dedicated Azure data disk mounted at /var/lib/fusion, separate from the OS disk and re-provisioned with every VM. Backed by 24/7 cloudimg support.

What is included:

  • Fusion 1.2.0 (single Go binary) at /opt/fusion/fusion, verified against the upstream release checksum
  • nginx reverse proxy on :80 in front of the Fusion server on :8080
  • A dedicated Azure data disk at /var/lib/fusion holding the SQLite database (fusion.db), separate from the OS disk and independently resizable
  • Secure by default: a unique 32-character administrator password is generated on first boot into a root-only file, so no shared or default credential ships in the image
  • fusion.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 - a unique password on first boot

Fusion signs in with a password only - there is no username field. This image does not bake in a password. On the very first boot, fusion-firstboot.service generates a unique 32-character password for your virtual machine, writes it into Fusion's runtime environment file and records it in /root/fusion-credentials.txt (mode 0600, readable only by root).

Two independent guards make it impossible for this image to serve an instance with a default credential:

  1. fusion.service will not start until first boot has provisioned that password, and
  2. Fusion itself refuses to start when no password is configured.

Fusion sign-in screen, 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; scale up for very large feed counts or many concurrent readers.

Network security group inbound rules: allow 22/tcp from your management network and 80/tcp from wherever you browse Fusion. Do not publish port 8080. Fusion's own listener binds to all interfaces, and nginx on port 80 is the intended single public entry point.

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Fusion 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). Review the dedicated data disk on the Disks tab, then Review + create and Create.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name fusion \
  --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 fusion --port 80 --priority 1010

Step 3 - Connect to your VM

ssh azureuser@<vm-public-ip>

The message of the day shows your Fusion URL and where to find your password.

Step 4 - Confirm the services are running

Both units should report active, nginx should be listening on port 80, and the API should answer. An unauthenticated request to the API correctly returns 401.

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

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

printf 'GET /api/oidc/enabled    -> HTTP %s\n' \
    "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/api/oidc/enabled)"
printf 'GET /                    -> HTTP %s\n' \
    "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/)"
printf 'GET /api/feeds (no auth) -> HTTP %s\n' \
    "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/api/feeds)"

Fusion and nginx active, listeners bound and the API answering

Step 5 - Retrieve your password

Your password was generated on first boot and stored in a root-only file, along with your Fusion URL and Fever API details.

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

The per-VM credentials file, generated on first boot with the password redacted

Confirm that the password actually authenticates, and that the example password from the upstream project's documentation is rejected:

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

CODE="$(curl -s -o /dev/null -w '%{http_code}' -c "$JAR" \
    -H 'Content-Type: application/json' \
    --data-binary "$(jq -nc --arg p "$PASS" '{password:$p}')" \
    http://127.0.0.1/api/sessions)"
printf 'sign in with your per-VM password -> HTTP %s\n' "$CODE"
[ "$CODE" = "200" ] || { echo "sign in did not succeed"; rm -f "$JAR"; exit 1; }

printf 'authenticated GET /api/feeds      -> HTTP %s\n' \
    "$(curl -s -o /dev/null -w '%{http_code}' -b "$JAR" http://127.0.0.1/api/feeds)"

echo '{"password":"fusion"}' > /tmp/fusion-example-pass.json
printf 'sign in with the upstream example password -> HTTP %s (401 = rejected)\n' \
    "$(curl -s -o /dev/null -w '%{http_code}' -H 'Content-Type: application/json' \
       --data-binary @/tmp/fusion-example-pass.json http://127.0.0.1/api/sessions)"
rm -f /tmp/fusion-example-pass.json "$JAR"

Step 6 - Sign in to Fusion

Browse to http://<vm-public-ip>/. Enter the password from Step 5 and select Sign in. Fusion opens on your unread articles.

The left sidebar holds your saved views (Unread, Starred, All) and your feeds grouped into folders, each with an unread count. The centre pane lists articles; the All / Unread / Starred tabs filter it.

The Fusion article list with feeds, groups and unread counts

Step 7 - Add your first feed

In the web interface choose Manage Feeds, then Add. Paste either a feed URL or an ordinary site URL - Fusion discovers the feed automatically - give it a name, pick a group and save. Import and Export accept OPML, so you can migrate an existing reader in one step.

Managing feeds and groups, with per-feed article counts

The same operations are available over the API. This example signs in, checks that a feed URL resolves, and lists your current feeds:

PASS="$(sudo grep '^fusion.password=' /root/fusion-credentials.txt | cut -d= -f2-)"
JAR="$(mktemp)"
curl -s -o /dev/null -c "$JAR" -H 'Content-Type: application/json' \
    --data-binary "$(jq -nc --arg p "$PASS" '{password:$p}')" \
    http://127.0.0.1/api/sessions

echo "Feed discovery for https://blog.rust-lang.org/ :"
curl -s -b "$JAR" -H 'Content-Type: application/json' \
    --data-binary '{"url":"https://blog.rust-lang.org/"}' \
    http://127.0.0.1/api/feeds/validate | jq -r '.data[]?.link' | head -5

echo
echo "Your feeds:"
curl -s -b "$JAR" http://127.0.0.1/api/feeds \
    | jq -r '.data[] | "  \(.name)  ->  \(.link)"' | head -20
rm -f "$JAR"

Reading articles

Select any article to open the reading pane. Fusion renders the full article content supplied by the feed, with the source, publication date and a link to the original page. Mark as unread, Star and Original sit along the top; Previous and Next move through the list.

An article rendered in the Fusion reading pane

Fusion pulls each feed on a schedule (every 30 minutes by default). To refresh everything immediately, use Refresh All on the Manage Feeds screen, or:

PASS="$(sudo grep '^fusion.password=' /root/fusion-credentials.txt | cut -d= -f2-)"
JAR="$(mktemp)"
curl -s -o /dev/null -c "$JAR" -H 'Content-Type: application/json' \
    --data-binary "$(jq -nc --arg p "$PASS" '{password:$p}')" \
    http://127.0.0.1/api/sessions

curl -s -o /dev/null -b "$JAR" -X POST http://127.0.0.1/api/feeds/refresh
sleep 5
printf 'articles currently stored: %s\n' \
    "$(curl -s -b "$JAR" http://127.0.0.1/api/items | jq -r '.total')"
rm -f "$JAR"

Keyboard shortcuts

Fusion uses Google Reader style shortcuts. Press ? in the web interface for the full list, and Cmd/Ctrl + K to open search.

Connecting a Fever API client

Fusion exposes a Fever-compatible endpoint, so mobile and desktop readers can sync against your server. In your client choose Fever as the account type and enter:

  • Endpoint: http://<vm-public-ip>/fever
  • Username: fusion
  • Password: the password from Step 5

Put the VM behind HTTPS before using a Fever client over the public internet - see Enabling HTTPS below.

The data disk

The SQLite database lives on a dedicated Azure data disk mounted at /var/lib/fusion, keyed by UUID in /etc/fstab with nofail. It is captured into the image and re-provisioned on every VM, and can be resized independently of the OS disk.

findmnt -no SOURCE,TARGET,FSTYPE,SIZE /var/lib/fusion
df -h /var/lib/fusion | tail -1
echo
sudo ls -l /var/lib/fusion/fusion.db
printf 'feeds:    %s\n' "$(sudo sqlite3 /var/lib/fusion/fusion.db 'SELECT COUNT(*) FROM feeds;')"
printf 'articles: %s\n' "$(sudo sqlite3 /var/lib/fusion/fusion.db 'SELECT COUNT(*) FROM items;')"
echo
grep /var/lib/fusion /etc/fstab

The dedicated data disk and the SQLite datastore

Changing the password

The password is read from Fusion's environment file at start-up. Edit it and restart the service:

sudo sed -i 's/^FUSION_PASSWORD=.*/FUSION_PASSWORD=<new-password>/' /etc/fusion/fusion.env
sudo systemctl restart fusion

Update /root/fusion-credentials.txt too so the record stays accurate. Signing in again is required afterwards, because sessions are held in memory and are cleared by the restart.

Tuning feed pulling

Fusion reads its configuration from /etc/fusion/fusion.env. Useful settings, all optional:

  • FUSION_PULL_INTERVAL - seconds between pulls (default 1800)
  • FUSION_PULL_TIMEOUT - per-request timeout in seconds (default 30)
  • FUSION_PULL_CONCURRENCY - maximum concurrent pulls (default 10)
  • FUSION_LOGIN_RATE_LIMIT, FUSION_LOGIN_WINDOW, FUSION_LOGIN_BLOCK - login throttling

Apply changes with sudo systemctl restart fusion.

Backing up and restoring

The whole application state is one SQLite file. Take a consistent copy with SQLite's online backup, which is safe while Fusion is running:

sudo install -d -m 0750 /var/backups/fusion
sudo sqlite3 /var/lib/fusion/fusion.db ".backup '/var/backups/fusion/fusion-backup.db'"
sudo sqlite3 /var/backups/fusion/fusion-backup.db 'PRAGMA integrity_check;'
sudo ls -l /var/backups/fusion/

To restore, stop Fusion, copy the backup over /var/lib/fusion/fusion.db, restore ownership to fusion:fusion, and start Fusion again.

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. Once TLS terminates at nginx, Fusion marks its session cookie Secure, because nginx forwards X-Forwarded-Proto.

Upgrading Fusion

Fusion is a single binary, so an upgrade is a download, a checksum check and a restart. Replace the version below with the release you want from the project's releases page:

sudo systemctl stop fusion
sudo curl -fsSL -o /opt/fusion/fusion.new \
  https://github.com/0x2E/fusion/releases/download/v<version>/fusion-linux-amd64
sudo sha256sum /opt/fusion/fusion.new   # compare with upstream checksums.txt
sudo install -m 0755 /opt/fusion/fusion.new /opt/fusion/fusion
sudo rm -f /opt/fusion/fusion.new
sudo systemctl start fusion

Back up the database first - see Backing up and restoring.

Troubleshooting

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

If Fusion is not running, the usual cause is a missing password. Confirm FUSION_PASSWORD is set in /etc/fusion/fusion.env, then sudo systemctl restart fusion. 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. Fusion is open source software distributed under the MIT licence; see the upstream project for product documentation.