Skip to content
Pages in Guides

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.

Guide Updated View as Markdown

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:

Shell
# 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...
  • bundle builds like an update, up to the switch. The source is fetched, the dependencies installed and the project built in the build sandbox, as noust-build; the system packages the application declares are installed first. It stops there: no migration, no pre_deploy or post_deploy hook, 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 SHA builds that commit, --release ID packs a release that is already built (it is not removed), and --keep keeps what it built.
  • receive is the promotion of a release from another application. It activates the release behind the health gate, after the receiving application's own pre_deploy hooks, 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, and noust releases rollback goes back.
  • The checksum is mandatory and the file is untrusted. receive reads 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 .sha256 beside 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 .env and 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_deploy hook 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).

Something on this page is wrong or unclear? Report it on GitHub.

Help, contributing and the project’s policies are under Project.

Search the documentation

Type to search every page.

↑ ↓ to move, Enter to open, Esc to close