Standalone Deploys
One folder, one command. Build a self-contained deploy directory, copy it to any server that has Node installed, and run node run.mjs. No node_modules, no npm install, no toolchain on the host.
A normal GioJS deploy runs your app from source: the server compiles and scans routes at startup, which means Node, your dependencies, and npm install all live on the production host. A standalone build moves all of that to build time. It packages the Rust server binary and your entire Node side - React included - into a single directory that runs on a bare server. tsx and esbuild do their work during the build and are never loaded at runtime.
Build
gio build standalone [--out <dir>] [--target <platform>]
--out <dir> output directory (default: ./standalone)
--target <platform> cross-build for another platform (see below)(Plain gio build just prints an explanation - normal deploys have no build step; the server renders on demand. standalone is the packaging mode.)
What you get
standalone/
server(.exe) the Rust HTTP server binary for the target platform
worker.js the entire Node side bundled to one file (React included)
run.mjs launcher: spawns the server wired to worker.js and static/
static/ prebuilt hydration chunks and route stylesheets
public/ your public assets (if any)
gio.toml your server config (if any)
.gio/ manifest (deployment ID input) and generated route typesworker.js is generated from your discovered app modules - every page, layout, route.ts handler, gio.config, and middleware file is bundled, so boot performs no filesystem discovery and no TypeScript transform. Modules are evaluated when gio evaluates them from source: gio.config, middleware and route.ts files at startup, pages and layouts on first use. A module that throws while it is imported (a missing GIO_SESSION_SECRET, a required variable check) makes the URLs that import it answer 500, with the file and the error in the log - the server still starts. The hydration chunks in static/ are built ahead of time too, and so are the route stylesheets: CSS imports and CSS Modules work exactly as with gio, with each CSS Module's class names compiled into worker.js. The build minifies them unless the project's gio.toml says [css] minify = false; changing that key in the deployed gio.toml needs a rebuild.
Deploy
Copy the folder to any server with Node 20+ installed, then:
node run.mjsrun.mjs spawns the server binary with the environment wired up (NODE_ENV=production by default, worker and static paths pointed into the folder), forwards SIGINT/SIGTERM for clean shutdown, and passes any extra arguments through to the server (which takes only --check-config; any other argument exits with 2). If run.mjs itself is killed outright (SIGKILL, the OOM killer), the server still shuts down instead of lingering on the port: it is started with a stdin pipe the launcher holds open, and exits gracefully when that pipe closes (see process supervision).
Environment variables
The build loads the project's production .env files, and GIO_PUBLIC_* values are frozen into the hydration chunks and worker.js at build time - changing one needs a rebuild. Nothing else is baked in and no .env file is copied into the output: server-side variables are read at runtime from the environment, or from .env files you place inside the deploy folder (the server loads them at startup with the usual precedence; real environment variables win).
As a systemd service, the unit needs ExecStart plus a stop policy: systemd's default sends SIGTERM to every process at once, which stops the workers in the middle of requests the server is still draining. KillMode=mixed signals only the launcher (the server then drains and stops its workers itself); the full unit is in Deploying.
[Service]
ExecStart=node /srv/app/run.mjs
Restart=always
Environment=NODE_ENV=production
# SIGTERM to the launcher only: the server drains requests, then stops its workers.
KillMode=mixed
TimeoutStopSec=30Cross-building for another platform
By default the build packages the server binary for the machine you build on. To build on one platform and deploy to another (say, build on Windows or macOS, deploy to a Linux VPS), pass --target:
npm i @gio.js/server-linux-x64 --force # install the target's binary package
gio build standalone --target linux-x64Targets: linux-x64, linux-x64-musl, linux-arm64, win32-x64, darwin-x64, darwin-arm64. The platform package must be installed - if it isn't, the build fails with the exact npm i command to run.
@gio.js/server-linux-arm64 is not published yet. For an arm64 Linux host, build the server from a checkout of the repository (cargo build --release -p giojs-server, on the target platform or with a cross toolchain) and point the build at it - GIO_STANDALONE_SERVER_BIN takes precedence over --target:
GIO_STANDALONE_SERVER_BIN=/path/to/giojs-server gio build standaloneWhen to prefer a normal deploy
A standalone folder is frozen at build time: framework fixes only reach it when you rebuild and re-copy. A normal deploy (npm install on the host, run gio) keeps you on the update path - npm update picks up new GioJS releases - and needs no build step at all. Prefer standalone when the target host should stay minimal (only Node, no npm registry access, no node_modules); prefer a normal deploy when you want the easiest upgrades. See Deploying for the normal path.