EdgibleCards

Cards / ech0

ech0

Ech0, a lightweight publishing platform for short posts, public with the owner's own login

App cardnot tested yetSource on GitHub

The apps, ports, auth modes, and places in the ech0 card

Apps

AppWhatWho can reach itPlace
ech0Ech0, the public timeline and the owner's editoropen to anyoneech0

Machine

A place is one serving device. Minimum recommended for each:

PlaceMemoryDiskArchitectureGPU
ech0256 MB1.5 GBamd64, arm64none

Why

A short-post blog or notes feed on a hosted service lives under that service's terms. Ech0 is a lightweight publishing platform for personal posts, with a public timeline, that runs on a machine you own. It has no newsletter or paid members: it suits a personal blog or micro-blog, not a replacement for Substack's email list.

What

One app, ech0, on the place ech0, on none. A blog is public, so readers need no Edgible sign-in, and the owner's own login in Ech0 protects publishing. prepare.sh makes that owner from ECH0_ADMIN_USER and ECH0_ADMIN_PASSWORD and closes sign-up before the site is published, because the first account to register on a fresh Ech0 becomes its owner. The image is sn0wl1n/ech0:v5.8.0, from Docker Hub, and the Compose file adds a healthcheck on /api/hello.

Posts, uploads, users and settings live in the volume ech0-data, at /app/data. Back it up. Minimum recommended for the place ech0: 256 MB of memory and 1.5 GB of disk, on an arm64 or amd64 machine, with no GPU.

The Compose file is docker-compose.yml, the settings card.env, the card card.yml.

How

Five steps. Edit card.env before you start.

1. Fetch

mkdir -p ech0
curl -fsSL https://github.com/edgible-org/cards/archive/refs/heads/main.tar.gz \
  | tar -xz --strip-components=2 -C ech0 cards-main/ech0

2. Edit card.env

Set DEVICE to a name edgible device list prints, and ORG_LABEL to the part after the app name in a hostname edgible app list prints. The owner's email is only an address on the account, so any will do. The password and the token secret are generated in the next step.

nano ech0/card.env

3. Check

curl -fsSLo check-env.py https://raw.githubusercontent.com/edgible-org/card-kit/main/check-env.py
python3 check-env.py ech0

It fills each empty secret and moves a taken port, as lines to paste. Run it again until it says no conflicts.

4. Start

docker compose --env-file ech0/card.env -f ech0/docker-compose.yml up -d --wait
docker compose --env-file ech0/card.env -f ech0/docker-compose.yml ps

--wait returns when each service is running, and healthy when it has a healthcheck.

sh ech0/prepare.sh

prepare.sh is:

#!/bin/sh
# Prepare for the ech0 card: make the owner and close sign-up before the site is public.
# Run it after Start and before Publish, from the directory that holds ech0/:
#   sh ech0/prepare.sh
set -eu
cd "$(dirname "$0")"
set -a; . ./card.env; set +a

B="http://127.0.0.1:$ECH0_PORT/api"
H='Content-Type: application/json'

n=0
until curl -fsS "$B/init/status" >/dev/null 2>&1; do
  n=$((n + 1)); [ "$n" -lt 60 ] || { echo "ech0 did not answer"; exit 1; }
  sleep 1
done

# The first account to register becomes the owner, so make it here, not on a public page.
curl -fsS -X POST "$B/init/owner" -H "$H" \
  -d "{\"username\":\"$ECH0_ADMIN_USER\",\"email\":\"$ECH0_ADMIN_EMAIL\",\"password\":\"$ECH0_ADMIN_PASSWORD\",\"locale\":\"en-US\"}" >/dev/null

token=$(curl -fsS -X POST "$B/login" -H "$H" \
  -d "{\"username\":\"$ECH0_ADMIN_USER\",\"password\":\"$ECH0_ADMIN_PASSWORD\"}" \
  | jq -er '.data.access_token')

# Close sign-up: the owner is the only account until the owner adds more.
settings=$(curl -fsS "$B/settings" | jq -c '.data | .allow_register = false')
curl -fsS -X PUT "$B/settings" -H "$H" -H "Authorization: Bearer $token" -d "$settings" >/dev/null
curl -fsS "$B/settings" | jq -e '.data.allow_register == false' >/dev/null
echo "owner made from ECH0_ADMIN_USER and ECH0_ADMIN_PASSWORD, sign-up closed"

The owner exists and sign-up is closed, so no setup page is left for a stranger to find.

5. Publish

set -euo pipefail
set -a
. ech0/card.env
set +a
device_id=$(edgible device list --json | jq -er --arg name "$DEVICE" '
  map(select(.name == $name))
  | if length == 1 then .[0].id
    else error("need exactly one device named " + $name + " (" + (map(.status + " " + .id) | join(", ")) + ")")
    end
')
edgible app create existing \
  --non-interactive \
  --name ech0 \
  --port "$ECH0_PORT" \
  --protocol https \
  --auth-modes none \
  --device-id "$device_id"
edgible app list

Without Edgible: only Publish is Edgible's. The app listens on 127.0.0.1 at the port in card.env, so any reverse proxy or tunnel on the machine, such as Caddy, nginx, Cloudflare Tunnel or Tailscale, can publish it instead, and sign-in is then that tool's to provide. Every other step stays the same.

Verify

ech0 uses none.

curl -sS -o /dev/null -w '%{http_code}\n' "https://<ech0 hostname>"

The public timeline, with no Edgible sign-in. Sign in as ECH0_ADMIN_USER with ECH0_ADMIN_PASSWORD, and change the password in Ech0's settings.

The rest of the setup is in the Ech0 docs.

Tear down

Four steps, in this order. Step 1 runs wherever edgible is logged in. Steps 2 to 4 run on the machine that runs the containers. Steps 2 and 3 read card.env, so keep it until step 4.

1. Unpublish

edgible app delete ech0 --yes

2. Stop

docker compose --env-file ech0/card.env -f ech0/docker-compose.yml down

3. Delete the data

Skip this step to keep the data. The loop copies each volume to a .tgz file in this directory first.

for volume in $(docker volume ls -q --filter label=com.docker.compose.project=ech0); do
  docker run --rm -v "$volume:/data:ro" -v "$PWD:/backup" alpine tar -czf "/backup/$volume.tgz" -C /data .
done
docker compose --env-file ech0/card.env -f ech0/docker-compose.yml down --volumes

4. Remove the card

rm -rf ech0