Deployment guide

Fusion RSS

Overview

Fusion is a lightweight, self-hosted RSS reader. It subscribes to RSS and Atom feeds, discovers feeds automatically from any site URL, and organises them into groups. Articles appear 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 SecureAnyCloud 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.

What you get:

  • Fusion 1.2.0 (single Go binary) at /opt/fusion/fusion, verified against the upstream release checksum.
  • An 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 defaults: 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 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: 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. It writes the password 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:

  • fusion.service does not start until first boot has provisioned that password.
  • Fusion itself refuses to start when no password is configured.

Prerequisites

  • An active Azure subscription, an SSH key pair, and a VNet and subnet in the target region.
  • A virtual machine size of 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 that 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 SecureAnyCloud, and select Create.

On the Basics tab, 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 choose Review + create and Create.

Step 2 - Deploy from the Azure CLI

Run these commands from a workstation where you are signed in to the Azure CLI:

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:fusion-rss:default:latest
az vm create \
  --resource-group <your-rg> \
  --name fusion \
  --image sac1647283583153:fusion-rss: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 fusion --port 80 --priority 1010

Step 3 - Connect to your VM

Connect over SSH:

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 report active, nginx listens on port 80, and the API answers. 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/sac/data/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)"

Step 5 - Retrieve your password

Your password was generated on first boot. It is 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-

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.

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 the feed a name, pick a group and save. Import and Export accept OPML, so you can migrate an existing reader in one step.

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.

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 run:

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 you use a Fever client over the public internet. See Enabling HTTPS with Let's Encrypt 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. You can resize it 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

Changing the password

Fusion reads the password from its environment file at start-up. Edit the file 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. Sign in again afterwards, because sessions are held in memory and the restart clears them.

Tuning feed pulling

Fusion reads its configuration from /etc/fusion/fusion.env. These settings are 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 and 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 to run 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 and 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. Run these checks:

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. 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 in the commands 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

Check the service log, the nginx configuration and the effective Fusion settings:

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 that FUSION_PASSWORD is set in /etc/fusion/fusion.env, then run sudo systemctl restart fusion.

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

This image is published and supported by SecureAnyCloud. For assistance, contact sales@secureanycloud.com. Fusion is open source software distributed under the MIT licence. See the upstream project for product documentation.

Deploy on Azure

Launch Fusion 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