How builds work
Ruust turns your repo into a container image, then runs it. Here is what happens between a git push and a live Egg.
When you deploy a repo, Ruust builds it into a container image and runs that image as your Egg. You do not have to write a Dockerfile: we detect the stack and build it for you. If you do have a Dockerfile, we use it instead.
Detection and build
Ruust builds with Nixpacks. It inspects your repo, works out the language and framework from the files it finds (a package.json, a Gemfile, a go.mod, and so on), then installs dependencies, builds, and sets a start command. The per-framework guides in the sidebar show exactly what is detected and how to override it.
- Install: dependencies are fetched (for example
npm ci,bundle install,pip install). - Build: a build step runs if your stack has one (for example
next build,assets:precompile). - Start: a start command is chosen, which you can always set yourself.
Pinning the runtime version
Pin the language version in your repo and Ruust detects it. For Node, the cleanest way is an engines.node range in your package.json; a .nvmrc or .node-version file works too. Pinning is worth doing: without it the builder falls back to an older default (currently Node 18), which recent dependencies (Prisma, for one) refuse to install on. If your build fails during install with a "please upgrade your Node.js version" message, this is why.
{
"engines": { "node": ">=20.19" }
}You can also set it without touching your repo: pick a Node version on the Egg page (or set a nodeVersion in ruust.yaml). A version pinned in a file (an engines.node, a .nvmrc, a .node-version, or ruust.yaml) always wins, so the picker is read-only and shows where the version comes from when one is present, and the repo stays the source of truth. For a one-off test you can instead set a NIXPACKS_NODE_VERSION environment variable, which overrides detection. The same idea applies to other stacks, for example a .python-version file, or a version in your Gemfile or go.mod.
The port contract
This is the one rule that matters. Ruust sets a PORT environment variable and expects your app to listen on it, bound to 0.0.0.0 so traffic from outside the container reaches it. Binding to localhost or a hard-coded port is the most common reason an Egg builds but never hatches.
const port = process.env.PORT || 3000;
app.listen(port, '0.0.0.0');Setting the start command
If detection picks the wrong command, set your own. On the default Nixpacks build, a Procfile at the repo root with a web process is honoured by Nixpacks and baked into the image as the start command. This has no effect on a Dockerfile build, where the image CMD/ENTRYPOINT wins. You can also set a start command in the dashboard.
web: node server.jsConfigure in code with ruust.yaml
Alongside a Procfile and a Dockerfile, you can commit a ruust.yaml to your repo root to configure the Egg itself, its port, health-check path, replicas, start and release commands, and non-secret env defaults, so the configuration lives with your code and is reviewed in the same pull request. It is optional and best effort: a missing or malformed file never fails the build. See the ruust.yaml reference for the fields and how it settles with your dashboard settings.
Build time and run time
Environment variables you set on the Egg are available both while building and while running. Secrets are encrypted at rest and delivered to the host out of band; they are never printed in build logs. Set them before your first deploy if the build needs them (for example a private package token).
Bring your own Dockerfile
If your repo has a Dockerfile, Ruust builds that instead of using Nixpacks, so you keep full control. Make sure the final image listens on $PORT. See the Custom Dockerfile reference for the details.