Run the small box on Fly.io
The small box (the installer’s Postgres + server + Caddy) runs on Fly.io as two
apps: one for Simple Host, one for its Postgres. Tested end to end on
2026-09-28 with v0.7.4: deploy, first admin, admin sign-in, publish, update,
rollback, saved data, download my data, machine restart, redeploy with
migrations, and /internal/* hidden. Then everything was deleted.
The files are in deploy/platforms/fly/:
fly.toml, a Dockerfile that copies the released server into the Caddy
image, a Caddyfile (the small box’s, without its TLS parts), and start.sh.
Nothing is compiled; no change to the server is needed.
What it looks like
| Piece | On a VPS (installer) | On Fly |
|---|---|---|
| HTTPS | Caddy, on-demand certificates | Fly’s edge: fly certs add for both names |
Sites served from disk, /internal/* hidden, access log |
Caddy container | Caddy in the same machine as the server (port 8080) |
| Server | app container |
same machine, as a normal user, on 127.0.0.1:8090 |
| Database | db container |
a separate Fly Postgres app, attached as DB_DSN |
| Sites on disk | Docker volume | 1 GB Fly volume at /data |
| Schema and migrations | the installer runs simple-host migrate |
release_command loads schema.sql into an empty database, then runs simple-host migrate, before every deploy |
Minimum size and cost
Everything at the smallest size worked: the Simple Host machine used about 75 MB of its 256 MB (server ~25 MB, Caddy ~50 MB).
| Item | Size | Per month |
|---|---|---|
| Simple Host machine | shared-cpu-1x, 256 MB | about $2 |
| Postgres machine | shared-cpu-1x, 256 MB | about $2 |
| Two volumes | 1 GB each, $0.15/GB | $0.30 |
| Two certificates | $0.10 each | $0.20 |
| Shared IPv4 and IPv6 | free | |
| Total | about $4.50, plus egress ($0.02/GB in North America and Europe) |
Why this Postgres: Fly’s Managed Postgres starts at $38/month (Basic plan),
eight times the rest of the box put together. The unmanaged fly postgres
cluster is a normal Fly app on a volume, the same thing the VPS small box runs
in a container. Fly does not support it (no managed failover or help), but it
does take daily volume snapshots, and a small box did not have more than that
on a VPS either. Any other Postgres works too: set DB_DSN to its URL.
A Fly account needs a card. On a trial account without one, Fly stops
every machine after 5 minutes (“Trial machine stopping”). The box then goes
dark, and the next deploy fails with database not reachable: EOF because
Postgres is stopped. Add a card before you start.
Commands
You need flyctl (fly auth login), a domain, and the release you want
(this guide: 0.7.12, set in fly.toml as SIMPLE_HOST_VERSION). Below, the app
is my-sh, the region sjc, and the address example.com, with
participant sites on sites.example.com.
-
Edit
deploy/platforms/fly/fly.toml: setapp,primary_region, and the three hostnames (SITE_DOMAIN,CONTENT_HOST,PUBLIC_BASE_URL). -
Create Postgres, the app, and the volume, then attach the database.
attachstores the connection string as the secretDB_DSN, which the server reads. It also prints the string, so do not paste that output anywhere.fly postgres create --name my-sh-db --org personal --region sjc --vm-size shared-cpu-1x --volume-size 1 --initial-cluster-size 1 fly apps create my-sh --org personal fly postgres attach my-sh-db -a my-sh --variable-name DB_DSN fly volumes create simple_host_data --size 1 -r sjc -a my-sh -y -
Make the admin key and store it. Save the file somewhere safe: it is the first admin’s sign-in.
umask 077; echo "sh_admin_$(head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n')" > admin.key; fly secrets set ADMIN_API_KEY="$(cat admin.key)" -a my-sh --stage -
Deploy from the directory with the files. The first deploy loads the schema and applies every migration (
applied 28 migration(s)) before the machine starts.cd deploy/platforms/fly && fly deploy --ha=false--ha=falsekeeps it at one machine: the volume holds the sites, so the app cannot run two. -
Point both names at the app, then ask Fly for their certificates. A CNAME to
my-sh.fly.devfor each name is enough (or the A and AAAA records thatfly certs addprints). The certificates were issued within a minute.example.com CNAME my-sh.fly.dev sites.example.com CNAME my-sh.fly.devfly certs add example.com -a my-sh && fly certs add sites.example.com -a my-sh -
Sign in: open
https://example.com/adminand paste the admin key. From there, issue participant accounts (“Issue participant accounts”) and hand out their keys, as on any small box.
Everyday operations
- Upgrade: set the new
SIMPLE_HOST_VERSIONinfly.tomlandfly deploy --ha=false. The release command runs the new release’s migrations first; if they fail, the deploy stops and the old version keeps running. - Restart:
fly machine restart <id> -a my-sh(fly machines list -a my-shshows the id). Sites, saved data and accounts all survive. - Settings: anything in configuration.md goes in
[env]infly.toml(limits, times) orfly secrets set(keys). Email sign-in codes needRESEND_API_KEYandMAIL_FROMas secrets; without them participants use the keys the admin issues, which is how a small box runs by default. - Admin key lost:
fly ssh console -a my-sh -C "printenv ADMIN_API_KEY". - Backups: Fly snapshots each volume daily and keeps 5 days
(
fly volumes snapshots list <vol-id>); snapshots over 10 GB cost $0.08/GB. “Download my data” and the admin export work as on any small box. - Delete everything:
fly apps destroy my-sh -y && fly apps destroy my-sh-db -y, then remove the two DNS records. Destroying an app takes its machines, volume, IP addresses and certificates with it.
Gotchas
- Trial accounts stop machines after 5 minutes. See above.
- The release image cannot run alone on Fly. It is the server only. Sites
are served from disk by Caddy, and Caddy also hides
/internal/*and writes the access log that visitor analytics reads. So the Dockerfile puts both in one machine; pointing Fly straight at the server leaves every site a 404 and exposes/internal/*. - A new database is empty.
simple-host migrateonly adds to an existing schema.start.sh releaseloadsdb/schema.sqlfrom the same release tag first, only when theuserstable is missing. - New Fly volumes belong to root.
start.shhands/data/sitesto the server’s user on first boot. - Visitor addresses. Every request reaches Caddy from Fly’s proxy. The
Caddyfile trusts the private range and reads
Fly-Client-IP, then sends the server that address alone inX-Forwarded-For. Without this, rate limits would put every visitor in one bucket, and analytics would count the proxy. - Health checks and the log. Fly checks
/healthzabout twice a second. Logged, that is roughly 250 MB a day on a 1 GB volume, so the Caddyfile does not log/healthz. sites.<domain>/healthzis a 404. Only the main address answers it, as on a VPS.
Your home page (2026-10-05): home selection, public showcase feed, bio, pins and manual order also work on small-box path installs. The public person page opens the selected site’s usual URL; the owner dashboard keeps its own origin. No whole-space domain is included. See Your home page.