Run the small box on Render
The small box (the installer’s Postgres + server + Caddy) runs on Render as one
web service with a disk, plus a Render Postgres database. Tested end to end on
2026-09-28 with v0.7.4, on a Starter service with a 1 GB disk and the smallest
paid Postgres: first deploy (the Pre-Deploy Command loaded the schema and
applied every migration), both custom domains with HTTPS, first admin and admin
sign-in, a participant account, publish, update, rollback, saved data, the
Caddy routes, /internal/* hidden, a redeploy (migrations: nothing to apply)
and a restart with the site and data intact, and the visitor’s real address in
visitor analytics (a spoofed X-Forwarded-For is ignored). Then everything was
deleted. The service and database were created through Render’s API with the
exact settings in render.yaml, and Render’s Blueprint validator accepts that
file as it is.
The files are in deploy/platforms/render/:
render.yaml (a Blueprint), 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 Render |
|---|---|---|
| HTTPS | Caddy, on-demand certificates | Render’s edge: a custom domain for each name |
Sites served from disk, /internal/* hidden, access log |
Caddy container | Caddy in the same service as the server, on Render’s port (10000) |
| Server | app container |
same service, as a normal user, on 127.0.0.1:8090 |
| Database | db container |
Render Postgres, linked as DB_DSN (private network only) |
| Sites on disk | Docker volume | 1 GB Render disk at /data |
| Schema and migrations | the installer runs simple-host migrate |
the Pre-Deploy Command loads schema.sql into an empty database, then runs simple-host migrate, before every deploy |
Minimum size and cost
Prices from render.com/pricing on 2026-09-28. The server and Caddy used about 75 MB on Fly, so 512 MB is plenty. The test box used 16 MB of its 1 GB disk.
| Item | Size | Per month |
|---|---|---|
| Web service | Starter (0.5c-512mb), the smallest plan that takes a disk |
$7 |
| Disk | 1 GB, $0.25/GB | $0.25 |
| Postgres | 0.1c-256mb |
$6 |
| Postgres storage | 1 GB, $0.30/GB | $0.30 |
| Custom domains and certificates | free | |
| Workspace | Hobby | $0 |
| Total | about $13.55 |
Why not free: a free web service has no disk, so published sites live in the container and are gone after a deploy, a restart, or the sleep that follows 15 minutes without traffic (seen in a free-plan test: after a redeploy the site was a 404 while its database rows were still there). Free services also have no Pre-Deploy Command, and a free Postgres expires after 30 days.
Commands
You need a Render account with a card, a GitHub (or GitLab) repository with
these files (a fork of Simple Host, or your own repository with
deploy/platforms/render/ copied in), a domain, and the release you want
(this guide: 0.7.12, set in the Dockerfile as SIMPLE_HOST_VERSION). Below,
the address is example.com, with participant sites on
sites.example.com.
-
Edit
deploy/platforms/render/render.yaml: set the three hostnames (SITE_DOMAIN,CONTENT_HOST,PUBLIC_BASE_URL) and the two names underdomains. Changeregion(both places) if Oregon is far from you. Commit and push. -
In the Render dashboard: New > Blueprint, pick the repository, and set Blueprint Path to
deploy/platforms/render/render.yaml. Render creates the database, then builds the image and deploys the service. The Pre-Deploy Command loads the schema and applies every migration (empty database: loading schema.sql,applied 28 migration(s)) before the service starts. -
Point both names at the service. Its address is on the service’s page (
simple-host-xxxx.onrender.com).example.com CNAME simple-host-xxxx.onrender.com sites.example.com CNAME simple-host-xxxx.onrender.comRender verifies each name and issues its certificate by itself; in the test both were live within a minute. (An apex name,
example.com, takes an A record instead; the service’s Settings page shows it.) -
Sign in: the admin key is the service’s
ADMIN_API_KEY(Environment tab, generated by the Blueprint). Openhttps://example.com/adminand paste it. 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_VERSIONin theDockerfileand push. The Pre-Deploy Command runs the new release’s migrations first; if they fail, the deploy stops and the old version keeps running. A redeploy with nothing new logsdatabase is up to date; nothing to apply. With a disk there is no zero-downtime deploy: the old instance stops before the new one starts. In the test, sites answered 502 for about 30 seconds. - Restart: Manual Deploy > Restart service on the service page. Sites, saved data and accounts all survive; about 30 seconds of 502 in the test.
- Settings: anything in configuration.md goes in the
service’s Environment tab. Email sign-in codes need
RESEND_API_KEYandMAIL_FROM; without them participants use the keys the admin issues, which is how a small box runs by default. - Backups: Render snapshots the disk daily and keeps at least 7 days (the disk’s page restores one). Paid Postgres has point-in-time recovery (3 days on Hobby). “Download my data” and the admin export work as on any small box.
- Delete everything: delete the Blueprint’s service and database in the dashboard (deleting the service takes its disk and custom domains with it), then remove the two DNS records.
Gotchas
- A card is required. Without one, Render refuses the disk, the Pre-Deploy Command and paid Postgres (“Payment information is required to complete this request”). Do not try to run it on the free plan: sites vanish.
- The admin page’s disk figure is the Render disk (973 MB usable on a 1 GB disk). Grow the disk on its page if it fills; it cannot shrink.
- Health checks use your domain. Once the service has custom domains,
Render sends its checks with one of them as the Host, which can be
sites.<domain>. There the content routes answer 404, so the first deploy after adding the domains never went live and failed after 15 minutes. The Caddyfile sends/healthzto the server on every host. - Visitor addresses. Render’s proxy reaches Caddy over loopback (
::1) and sits behind Cloudflare. The firstX-Forwarded-Forentry is whatever the visitor sent (a test withX-Forwarded-For: 6.6.6.6came through as the visitor), so the Caddyfile readsCF-Connecting-IP, which Cloudflare sets and refuses (403) from a visitor, and sends the server that address alone inX-Forwarded-For. Without this, rate limits would share a few buckets (Render’s internal proxies) and analytics could be spoofed. - Caddy’s file capability. Render will not start a program that carries
file capabilities (
caddy: Operation not permitted), so the Dockerfile removes Caddy’s (it only lets Caddy bind ports below 1024). - Root and the disk. A new disk belongs to root, and in Render’s
containers root cannot signal another user’s processes.
start.shhands/datato the server’s user and then runs everything as that user. - The release image cannot run alone. 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. Pointing Render 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. - CMD, not ENTRYPOINT. The image starts with
CMD, so the Pre-Deploy Command (/usr/local/bin/start.sh release) runs on its own rather than being handed to an entrypoint.
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.