Applications Azure

Ech0 on Ubuntu 24.04 on Azure User Guide

| Product: Ech0 on Ubuntu 24.04 LTS on Azure

Overview

Ech0 is a free, open source, self hosted, lightweight publishing platform for the flow of personal thoughts. It is a fast, minimal place to post short notes, links, images and audio to a public timeline that you run yourself, rather than trusting the stream of your ideas to a third party feed that can change its rules. Composing a post takes seconds with Markdown, and the timeline offers a public feed, RSS output, search and a posting activity heatmap. Signing in can be protected with OpenID Connect single sign on or WebAuthn passkeys, and a documented REST API with personal access tokens covers the same operations for automation.

The cloudimg image delivers Ech0 fully installed and configured on Ubuntu 24.04 as a single, statically linked Go binary that serves both the web interface and its REST API, backed by a self contained SQLite database, so a working timeline is running within minutes of launch with no separate application server, web server or database to operate. Backed by 24/7 cloudimg support.

Ech0 is licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). All product and company names are trademarks or registered trademarks of their respective holders. This image repackages the upstream open source release, unmodified, with cloudimg's provisioning and support.

What is included:

  • Ech0 5.4.6 (AGPL-3.0-or-later), installed at /opt/ech0
  • A single statically linked Go binary that serves the web UI and the REST API on port 6277, with the compiled frontend embedded in the binary
  • Self contained SQLite storage under /opt/ech0/data, so there is no separate database server to run or secure
  • A unique administrator password and a unique authentication token signing secret, both generated on first boot and written to a root-only file, so no default or shared login and no shared signing key ship in the image
  • Public self registration closed by default, so nobody who reaches your instance can claim it before you do
  • ech0.service managed by systemd, enabled and active, and gated so it only starts once first boot has provisioned the per-VM secret
  • 24/7 cloudimg support

Ech0 sign-in page served on first boot

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a good starting point and is ample for personal or small team use. NSG inbound: allow 22/tcp from your management network and 6277/tcp from your users. Add 443/tcp if you put Ech0 behind HTTPS (Step 11).

Step 1 — Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Ech0 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 add a custom rule for 6277. Then Review + create and Create.

Step 2 — Deploy from the Azure CLI

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

Open the Ech0 port to reach the web UI:

az vm open-port --resource-group <your-rg> --name ech0 --port 6277 --priority 900

Step 3 — Connect to your VM

ssh azureuser@<vm-public-ip>

Step 4 — Confirm the service is running

Ech0 runs as a single systemd service on port 6277. The unit is enabled, so it comes back automatically after a reboot; it is deliberately gated so it only starts once first boot has generated the per-VM signing secret.

systemctl is-active ech0.service ech0-firstboot.service

Check the version that shipped in this image and that it is listening on 6277:

/opt/ech0/ech0 version 2>/dev/null | tail -1
ss -tlnH | awk '{print $4}' | grep 6277

Confirm the application answers on the loopback interface. /healthz is the built in health endpoint:

curl -s -o /dev/null -w 'health %{http_code}\n' http://127.0.0.1:6277/healthz
curl -s -o /dev/null -w 'app    %{http_code}\n' http://127.0.0.1:6277/

Ech0 service, version and listening port on a freshly launched VM

Step 5 — Retrieve your per-VM administrator credentials

On the first boot of each VM, ech0-firstboot.service generates a unique administrator password and a unique JWT signing secret, creates the administrator account, and writes the login to a root-only file. Nothing is shared between deployments and nothing is baked into the image.

sudo cat /root/ech0-credentials.txt

The credentials file and the environment file that holds the per-VM signing secret are both readable only by root:

sudo stat -c '%a %U:%G %n' /root/ech0-credentials.txt /opt/ech0/ech0.env

Per-VM Ech0 credentials and file permissions

Back up /opt/ech0/ech0.env. It holds the per-VM JWT_SECRET that signs your login tokens. Also back up /opt/ech0/data, which contains the SQLite database and any uploaded media.

Step 6 — Browse your timeline and sign in

Browse to http://<vm-public-ip>:6277/. The public timeline is what visitors see; a fresh instance starts empty and fills as you post. Select Login and sign in with the username and password from the credentials file. That account is the administrator (owner) of the installation.

The Ech0 public timeline with published posts

There is deliberately no open Register flow: public self registration is closed on this image so that nobody who reaches your instance can create an account before you do. Step 10 explains how to reopen it or add users directly.

Step 7 — Publish your first post

Signed in, choose Publish. Write your note in Markdown, optionally attach an image or audio clip or add a tag, and publish it to your timeline.

The authenticated Ech0 compose and publish editor

You can confirm the whole publish path end to end from the shell, using the per-VM administrator credentials the image already wrote to disk. This logs in, reads back the number of posts on the public timeline, and requires no browser:

B=http://127.0.0.1:6277
U=$(sudo grep '^ech0.admin.user=' /root/ech0-credentials.txt | cut -d= -f2-)
P=$(sudo grep '^ech0.admin.pass=' /root/ech0-credentials.txt | cut -d= -f2-)
TOKEN=$(curl -s -H 'Content-Type: application/json' -X POST \
  --data "{\"username\":\"$U\",\"password\":\"$P\"}" $B/api/login \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["data"]["access_token"])')
echo -n "login token acquired: "; [ -n "$TOKEN" ] && echo yes || echo no
curl -s -X POST -H 'Content-Type: application/json' --data '{"page":1,"pageSize":10}' \
  $B/api/echo/page | python3 -c 'import json,sys; d=json.load(sys.stdin).get("data") or {}; print("posts on the public timeline:", d.get("total"))'

Step 8 — Health and the REST API

Every operation in the UI is also available over a documented REST API. The public endpoints need no token:

curl -s -o /dev/null -w 'GET /healthz -> HTTP %{http_code}\n' http://127.0.0.1:6277/healthz
curl -s http://127.0.0.1:6277/api/init/status

Ech0 health, init status and published post count

Create a personal access token from the admin panel under Settings, then pass it as a bearer token from your own machine:

curl -s -H "Authorization: Bearer <your-personal-access-token>" \
  http://<vm-public-ip>:6277/api/echo/page -X POST \
  -H 'Content-Type: application/json' --data '{"page":1,"pageSize":20}'

Step 9 — Security model

The image is hardened so that a freshly launched instance is safe before you have touched it. Upstream Ech0 has two defaults that are unsafe on a public VM, and both are closed here:

  • No baked signing secret. Upstream's quick-start signs authentication tokens with a well known default (JWT_SECRET="Hello Echos"), which would let anyone forge an administrator token. This image bakes no secret; first boot generates a unique 64-hex secret per instance.
  • No first-registered-becomes-admin window. Upstream makes the first account to register the administrator. This image creates the administrator on first boot, so the owner slot is already taken, and closes public registration.
  • Only SSH and Ech0 listen. SQLite is a local file, so there is no database port to expose.

You can prove all of that on your own instance. The following logs in with the per-VM administrator credentials, then presents a token whose header and payload are copied from a valid login token but re-signed with the upstream default "Hello Echos" secret, and shows it is rejected — proof the running secret is not the default:

B=http://127.0.0.1:6277
sudo grep -q 'JWT_SECRET=Hello Echos' /opt/ech0/ech0.env && echo "UPSTREAM DEFAULT (unsafe)" || echo "signing secret is unique to this instance (not the upstream default)"
U=$(sudo grep '^ech0.admin.user=' /root/ech0-credentials.txt | cut -d= -f2-)
P=$(sudo grep '^ech0.admin.pass=' /root/ech0-credentials.txt | cut -d= -f2-)
T=$(curl -s -H 'Content-Type: application/json' -X POST --data "{\"username\":\"$U\",\"password\":\"$P\"}" $B/api/login | python3 -c 'import json,sys; print(json.load(sys.stdin)["data"]["access_token"])')
HP=$(printf %s "$T" | cut -d. -f1,2)
SIG=$(printf %s "$HP" | openssl dgst -binary -sha256 -hmac 'Hello Echos' | openssl base64 -A | tr '+/' '-_' | tr -d '=')
curl -s -o /dev/null -w 'token forged with the upstream default secret -> HTTP %{http_code} (rejected)\n' -H "Authorization: Bearer $HP.$SIG" $B/api/user

And that public self registration creates no usable account:

B=http://127.0.0.1:6277
curl -s -o /dev/null -w 'register attempt -> HTTP %{http_code}; ' -H 'Content-Type: application/json' -X POST --data '{"username":"stranger","password":"StrangerPass-123","email":"s@x.local"}' $B/api/register
curl -s -o /dev/null -w 'then login as that user -> HTTP %{http_code} (refused, no account created)\n' -H 'Content-Type: application/json' -X POST --data '{"username":"stranger","password":"StrangerPass-123"}' $B/api/login
ss -tlnH | awk '{print $4}' | sort -u

Ech0 security posture: unique per-VM signing secret, a forged default token rejected, and closed registration

Step 10 — User management and reopening registration

To add people, sign in as the administrator and open the admin panel under Settings. Under Users you can create accounts directly, which is the recommended approach. The Allow registration switch reopens public self registration if you would rather people sign themselves up. Note that only administrators can publish to the timeline.

Ech0 admin settings, including the registration control

Step 11 — Put Ech0 behind HTTPS with a custom domain

Ech0 serves plain HTTP on 6277. For a public site, front it with a reverse proxy that terminates TLS. With Caddy, a two line Caddyfile gets you an automatic certificate:

your-domain.example.com {
  reverse_proxy 127.0.0.1:6277
}

Then tell Ech0 its public address so links, RSS and share URLs are correct, and restart it:

sudo sed -i 's#^ECH0_SETTING_SERVER_URL=.*#ECH0_SETTING_SERVER_URL=https://your-domain.example.com#' /opt/ech0/ech0.env
sudo systemctl restart ech0.service

Step 12 — Back up and restore

Everything stateful lives in two places: the environment file with the signing secret, and the data directory with the SQLite database and uploads.

sudo ls -la /opt/ech0/ech0.env /opt/ech0/data

To back up, copy /opt/ech0/ech0.env and the whole /opt/ech0/data directory somewhere safe. To restore onto a new VM, stop ech0.service, put both back, and start it again.

Step 13 — Maintenance and updates

The image ships fully patched and keeps receiving Ubuntu security updates automatically. To move to a newer Ech0 release, download the new ech0-linux-amd64 binary from the upstream releases, replace /opt/ech0/ech0, and restart the service — your data and per-VM secret are untouched.

Support

cloudimg provides 24/7 technical support for this Ech0 product by email (support@cloudimg.co.uk) and live chat. We help with deployment, retrieving first-boot credentials, publishing, REST API and personal access token setup, single sign on and passkeys, opening or closing registration, HTTPS and custom domains, backups, upgrades and troubleshooting. Critical issues receive a one hour average response time.