npx skills add https://github.com/getcolors/skills --skill neon-single-nodeSKILL.md
Single-node self-hosted Neon
Symptom index
Load the rest of this skill when any of these appear; each has a full entry
with verbatim symptoms in references/failure-catalogue.md:
- compute_ctl prints only
Error: Permission denied (os error 13)right after its build tag and the container restarts forever could not start the compute node: File exists (os error 17)pluscould not access the server configuration file ... postgresql.confFATAL: lock file "/tmp/.s.PGSQL.55433.lock" already existsafter a container stop/start or a host reboot — compute crash-loops until recreatedFailed to create tenants root dir at '/data/.neon/tenants' … Permission denied (os error 13)from the pageserver{"msg":"\"Cannot run timeline checkpoint because pageserver was compiled without testing APIs\""}- rclone against R2:
AccessDenied: Access Denied status code: 403on writes a bucket-scoped token should allow, orNotImplemented: Not Implemented status code: 501fromrcat remote_consistent_lsnis0/0right after a pageserver restart or a fresh attach, and someone concludes uploads are broken- Ansible:
Error loading tasks: failed at splitting arguments, either an unbalanced jinja2 block or quoteson a shell task that looks fine - an
ssh -ftunnel probe that "succeeds" and then getsConnection refused— the runner blocked until the tunnel expired - Ubuntu 24.04:
No package matching 'awscli' is available
Neon separates Postgres into a stateless compute (compute_ctl + Postgres),
a pageserver (the storage engine, backed by S3), safekeepers (WAL quorum),
and a storage broker. Upstream's docker-compose/ gets that running — as a
development fixture: MinIO, three colocated safekeepers, trust auth, floating
latest tags, a committed JWKS keypair, fsync=off, and a compute.sh
whose --dev flag is lost to a missing line continuation. The gap this skill
covers is the distance between that and a deployment whose durability claims
were proven: one storage image + one compute image on one host, Cloudflare R2
as the remote storage, credentialed auth, and recovery that was rehearsed,
not assumed. That distance was measured on a live build — fifteen converges
against the real platform (twelve iterating green, one through the pinned
launcher, one each for the red and blue ports), a three-round adversarial
plan review, a two-round post-build inspection, and recovery/rotation
rehearsals on the live host.
Everything here was verified against a running deployment unless it says otherwise. Where this skill contradicts the docs, the pinned source or a live probe is the authority, and the entry says which.
The reference implementation, and why this skill ships no assets
The working files live in the
getcolors/neon Package Skill —
compose, pageserver/compute configuration, the tenant/timeline bootstrap,
the smoke gates, rotation — under
green/src/resources/io/github/getcolors/neon/tools/, covered by that
repo's tests, golden fixtures, and three-colour parity, and consumed by the
neon-vultr deployment. This
skill carries no copies of them, per the Context Skill Standard's
no-second-copy rule. Read the templates there; read why they are shaped
that way here. Outside the Colors ecosystem the topology and traps below
transfer wholesale — only the OpenTofu/Ansible packaging is local.
Topology that survived
One host, four containers: storage_broker (:50051), pageserver
(:9898 HTTP, :6400 page service), one safekeeper (:7676, :5454), and a
compute-node-v17 running compute_ctl. Everything binds loopback; the
supported client path is an SSH tunnel. Three safekeepers on one host is
redundancy theater — commit acknowledgement needs that disk either way.
- The R2 prefix plus the tenant/timeline ids ARE the database. Fix the 32-hex tenant and timeline ids in desired state; the bootstrap then reconciles (read-before-write, 409 tolerated, postcondition reads) instead of minting identities, and recovery becomes describable.
- Pageserver runs as uid 1000 (
neon) and must own its data directory. Its config dir is its data dir (pageserver.toml+identity.tomlin/data/.neon/); a bind mount there makes wipingtenants/for recovery trivial. - Compute is a disposable projection. No volume on pgdata (compute_ctl
must create it itself), a tmpfs on
/tmp(the socket-lock trap), and a recreate-only doctrine:docker compose up -d --force-recreate compute, never stop/start — recreate is also how spec changes apply, because compute_ctl reconciles roles at startup only. - The compute spec is the auth system.
cluster.roles[].encrypted_passwordaccepts a SCRAM-SHA-256 verifier string directly (SCRAM-SHA-256$4096:…, pbkdf2/hmac — verified live, password auth works over the published port);cluster.databases[] {name, owner}creates the database. compute_ctl renders pg_hba itself: container-loopback trust (how-C postgresql://cloud_admin@localhost:55433/postgresconnects), everything elsehost all all all md5— and host-side connections arrive from the Docker bridge gateway, so they DO hit the password rule. Generate verifiers once and store them: the salt is random, so regenerating per converge makes the spec non-deterministic and recreates compute every run. - Rotation = new verifier in the spec + recreate, proven live: the old password stops working, the new works. Stage everything before the live checks, commit as adjacent renames, journal the new plaintext at a fixed path across the one window renames cannot close, and keep the stored verifier in sync or the next converge silently un-rotates.
- R2 works as Neon remote storage — pageserver
remote_storage= { endpoint=…, bucket_name=…, bucket_region='auto', prefix_in_bucket=… }, safekeeper--remote-storage={…}, credentials via AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY env. Verified: layer objects,initdb.tar.zst, offloaded 16MB WAL segments, and a full re-attach that read the data back. - Guard shared buckets with two-phase ownership markers (
.colors-initbefore any data,.colors-readyonly after gates pass) plus a generation counter object persisted before each attach — pageserver generations must rise across re-attaches of the same tenant, and a counter that trails the attach can hand out a stale generation after a crash. Emptiness counts as absence and every marker write is verified by read-back: a 0-byte marker satisfies a bare existence check forever.
Durability, measured
The honest single-node RPO surprised the build twice; get it right:
- The safekeeper offloads closed segments (force with
pg_switch_wal()— the/v1/.../checkpointAPI is a testing-build feature absent from release images). The pageserver uploads layers on its own checkpoint cadence;remote_consistent_lsnwas observed advancing while running and reads 0/0 after any restart or fresh attach — never gate on it post-restart. - Losing storage-tier local state on a surviving host loses nothing: a wiped pageserver re-attaches from R2 at the next generation and the live safekeeper replays WAL to the latest LSN (rehearsed: 38MB wiped, everything back). A wiped safekeeper volume also rejoins: the walproposer bootstraps it from the compute basebackup (rehearsed).
- A fresh safekeeper cannot serve its offloaded WAL back. Full host loss therefore recovers what the pageserver had uploaded — the RPO is the activity since its last checkpoint upload, minutes not seconds. Say this in your docs instead of implying the WAL backup closes the gap.
- Moving buckets is a migration, not an edit: quiesce compute, drain uploads
(watch
remote_consistent_lsncatchlast_record_lsnon the running pageserver), stop storage,rclone syncthe prefix, verify, re-point.
Release archaeology
Upstream stopped cutting versioned releases in July 2025 and moved to
untagged CI pushes; Docker Hub is stale and ghcr.io is canonical. The last
deliberate pairing is storage release-9129 + compute release-compute-9073
(the trains version independently — see references/pins.md). Read
docker-compose/ at the pinned tag, not main: a year of drift separates
them, including the compute config layout.
References
references/pins.md— the verified version set and its generation rules.references/failure-catalogue.md— symptom-indexed verbatim errors.references/acceptance.md— the gates and rehearsals that make the durability claims checkable, including the negative-space auth gates.