Moving the issuer's state into Postgres removed the last thing that tied either app to one hosting provider. What was left was a deployment tool describing resources that no longer existed — so this replaces it with `wrangler`, which is what actually deploys a Worker, and adds a second way to run each app that involves no provider at all. Each app now has a `wrangler.jsonc` with an environment per stage, and a `Dockerfile` beside it. The handler is the same one in both cases; what differs is only where its settings come from. Two of them gained a second spelling so that nothing has to branch on the runtime: Postgres arrives as a pooled binding or as `DATABASE_URL`, and the route to the issuer is a service binding or `AUTH_INTERNAL_URL`. That last one is new, and it is a split the binding was already making without saying so. `AUTH_ISSUER_URL` has to be the issuer's public name, because it is compared literally against every token's `iss` claim — but the public name is often not routable from inside a deployment. So the name and the route are two settings now rather than one that cannot be both. DNS moves out of code and into `docs/dns.md`, which lists every hostname and what it is for. Six records that change roughly never did not need a tool, and the table outlives whatever is answering the names — which is the point, since some of them will stop being Workers. The sandbox hostnames are hyphenated rather than nested for the same reason: a certificate covering `*.nestri.io` covers one label and not two, so `api-sandbox.nestri.io` can become an ordinary origin later without a certificate having to be ordered for it first. Also drops `EMAIL_DEV_LOG` from committed configuration into `.dev.vars`, which `wrangler deploy` cannot upload. Printing a live sign-in code to a log should not be one forgotten override away from production.
build/ — the guest rootfs
Builds a bootable Artix/OpenRC image for the box's virtio-blk root: Mesa
(virtio-gpu native context) plus the four open guest components —
nescope, neshub,
neswire, nescapture — laid out
the way borealis
lays out its build/: one big multi-stage Dockerfile, --target picks the
flavor, etc/ holds the files that get overlaid onto the image verbatim.
build/
├── Dockerfile everything, in stages: mesa-build, nestri-build,
│ os-base, runtime, runtime_prod, runtime_debug
├── etc/ overlaid onto the image's /etc as-is
├── scripts/
│ └── mkimage.sh docker export → raw ext4, for nesbox's virtio-blk
├── Makefile
└── output/ `make image` writes here (gitignored)
make build # docker build --target runtime_prod → ghcr.io/nestrilabs/nestri/base:latest
make build-debug # docker build --target runtime_debug → ghcr.io/nestrilabs/nestri/base:debug
make image # + pack into output/rootfs.ext4
make image-debug # + pack into output/rootfs-debug.ext4
Design notes
Three things worth knowing about how this is put together:
-
No privileged host chroot. A bare
chrootinto a hand-extracted rootfs needs/proc,/sys,/devbind-mounted in first — they don't exist inside a chroot target until something puts them there.os-basehere isFROM artixlinux/artixlinux:base-openrcdirectly, withpacman -Sas plainRUNsteps — a Docker build step already runs inside a real container with its own/proc,/sys,/dev, so that whole bind-mount mechanism has nothing to do. -
No host-side ownership bug to guard against.
COPY --from=runs as root inside the build with no host user in the loop, so there's no invoking-user uid getting stamped onto/,/usr/bin, or anywhere else a build step touches — a failure mode some overlay approaches need an explicit sanity check for doesn't exist here to check for. -
One
cargo build --release --workspace, not one stage per binary.nescope,neshub,neswireandnescaptureshare one Cargo workspace and oneCargo.lock— a BuildKit cache mount ontarget/gives cargo's own incremental compiler per-crate isolation without needing a separate Docker stage (and a separate full rebuild ofnesprotocol) per binary.
What is deliberately not here: Proton, and Valve's steamclient.so.
nestri/CLAUDE.md is explicit — "Nothing closed may enter this repo. Not
source, not a dependency, not a directory that 'looked convenient'." Both
are closed. runtime_prod from this Dockerfile — tagged
ghcr.io/nestrilabs/nestri/base:latest — is a complete, bootable, Steam-less guest image,
and also the shared foundation other builds start from: nesbox's jail image
(see nesbox/build/) extracts Mesa from it so the guest and host sides of
the virtio-gpu native-context protocol never drift apart. Only Mesa —
virglrenderer is the host half of that protocol and nesbox builds its own,
patched, from nesbox/patches/; nothing in this image carries it.
Whatever layers Proton and the Steam client on top of it is a closed build outside this repo, by design — not something this repo names, links to, or depends on.
The nesinit gap
Nothing in this image starts a payload. The old nestri-guest-hub did that
— per its own commit message, the open neshub "loses --proton,
--steamclient-so, --root and the game uid/gid, and no longer ends by
handing the process to a controller... Deciding when the box is finished
belongs to nesinit." nesinit — the guest init/session-supervisor that
would actually launch nescope -- <payload> and power the box down — is
referenced in commit messages and nesbox/PROGRESS.md but does not exist as
open code in either repo.
So /etc/init.d/nescope here starts nescope in plain-compositor mode
(no command after --): it comes up, provides the Wayland/X11 environment,
and waits for something to connect. That makes the image genuinely bootable
and testable — neshub, neswire, nescope all come up under OpenRC and
you get a real Wayland socket to point a client at — but running an actual
game is still nesinit's job, and nesinit isn't part of this build. Whoever
picks that up next should read apps/neshub/README.md's "What it does not
do" section first.
Network defaults
/etc/init.d/guest-net reads nestri.ip=/nestri.gw= off the kernel
command line, falling back to 172.30.0.2/24 via 172.30.0.1 if neither is
set — nesbox's own default tap addressing. Keep these in step if that
changes on the nesbox side.