Ech0 on Ubuntu 24.04 on Azure User Guide
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.servicemanaged by systemd, enabled and active, and gated so it only starts once first boot has provisioned the per-VM secret- 24/7 cloudimg support

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/

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

Back up
/opt/ech0/ech0.env. It holds the per-VMJWT_SECRETthat 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.

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.

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

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

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.

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.