Building on one server and running on another
Build a release on one server and run it on others, behind each one's health gate.
Part of Releases.
A small VPS that runs an application is a poor place to compile it: a Next.js build wants more memory than the application ever will. A release is immutable once it is built, which is what lets it be built somewhere else and carried to the server that serves it as one checksummed file:
# on the server that builds
noust releases bundle shop.example.com -o shop.noust-release.tar.gz
# Packed release 20261017-101500-9f8e7d6 of shop.example.com into shop.noust-release.tar.gz
# SHA-256 4b1c... (also in shop.noust-release.tar.gz.sha256)
# carry it with scp, rsync or a CI artifact, then on the server that runs it
noust releases receive shop.example.com shop.noust-release.tar.gz --sha256 4b1c...bundlebuilds like an update, up to the switch. The source is fetched, the dependencies installed and the project built in the build sandbox, asnoust-build; the system packages the application declares are installed first. It stops there: no migration, nopre_deployorpost_deployhook, no switch and no deployment in the history, and the release it built is removed once packed, so it cannot be activated or rolled back to on the builder.--commit SHAbuilds that commit,--release IDpacks a release that is already built (it is not removed), and--keepkeeps what it built.receiveis the promotion of a release from another application. It activates the release behind the health gate, after the receiving application's ownpre_deployhooks, with that application's own.env, database and persistent paths and after its own system packages are installed. A release that does not answer is put back and removed; going back past a deployment that changed the schema asks for--schema-changed-ok, like a rollback. It is a deployment of the receiver, andnoust releases rollbackgoes back.- The checksum is mandatory and the file is untrusted.
receivereads nothing from a bundle that is not the file the checksum names (a truncated transfer and a swapped file are the same refusal), judges the manifest (the application type must be the receiver's, the release id must be one) before it extracts a byte, and extracts with the checks every archive gets: no absolute path, no.., nothing written through a link, a link only where it stays inside the release, no device file, no setuid bit, and caps on members (2,000,000) and bytes (16 GiB). Give the checksum from a channel the file did not travel in; the.sha256beside the bundle protects against a damaged transfer and no more. - What a bundle carries. The built tree, without the links a release has to its application's
shared/(its.envand persistent paths are the builder's own; the receiver links its own). A tree with a link that points outside the release cannot be packed: a Python virtual environment links to the system's interpreter, so build such an application where it runs. The builder's application directory, if the tree names it in a text file, is rewritten to the receiver's. - The application is registered on both servers, with the same type, the builder with its
source, variables and build settings and the receiver with its domain, site and database.
Prisma's automatic migration belongs to an update: for a bundled application declare the
migration as a
pre_deployhook of the receiver.
A central can drive both ends: it picks the builder, asks it for the bundle and carries it to the
servers that run the application over the fleet's own tunnels, in chunks, with the same checksum and
the same health gate. That is building on one server and running on others
(noust fleet placement ship, /api/fleet/placements/{domain}/ship and Fleet > Applications > On several servers).
The file is still the whole interface when no central is involved, so anything that moves a file
(scp, a CI artifact store) carries it as well.
On a server, the same operations are in the API for whoever carries the file: POST /api/apps/{domain}/releases/bundle queues the build and leaves the bundle in a spool (owner-only, swept
after a day), GET .../releases/bundles/{id}/data reads it by ranges and DELETE lets go of it, and
POST .../releases/uploads (chunks at offsets, the digest checked when it completes) and POST .../releases/receive activate one that arrived. All of them are apps.deploy, and a freeze of deploys
holds the receiving. The largest bundle a server takes is releases.bundle_max_bytes (8 GiB).