Guide
Build a custom image
Custom images are built from a Dockerfile, tested in a machine, and published. A custom image is private to its account: no other account can see or boot it. The images every account can boot are the base images.
1Create the image
Create an image with a name, and keep its id (img_…). API paths accept the id or the name. The name is unique in the account and cannot match a base image.
KEY=$(uuidgen) curl -X POST https:// api.nitrous.sh/ v1/ images \ --fail-with-body \ -H "Authorization: Bearer $NITROUS_KEY" \ -H "Idempotency-Key: $KEY" \ -H "Content-Type: application/ json" \ -d '{ "name": "web-runner" }'
{ "id": "img_01m3kn0phjfxka50m2f7x78fdk", "object": "image", "name": "web-runner", "published_build": null, "latest_build": null, "created_at": "2026-09-28T09:19:58.002Z", "updated_at": "2026-09-28T09:19:58.002Z" }
2Start a build
Create a build from the Dockerfile. The sample keeps the number it returns as BUILD, buildNumber or build_number, for the steps after it; the Go program prints it: run export BUILD=… with the value.
KEY=$(uuidgen) BUILD= $(curl -sS -X POST https:// api.nitrous.sh/ v1/ images/ web-runner/ builds \ --fail-with-body \ -H "Authorization: Bearer $NITROUS_KEY" \ -H "Idempotency-Key: $KEY" \ -H "Content-Type: application/ json" \ -d '{ "dockerfile": "FROM debian:13\nRUN apt-get update & & apt-get install -y --no-install-recommends nodejs npm git\n" }' | jq -er '.number // error(.error)')
{ "object": "build", "number": 1, "image": "img_01m3kn0phjfxka50m2f7x78fdk", "status": "queued", "error": null, "size_mib": null, "created_at": "2026-09-28T09:20:11.408Z", "started_at": null, "finished_at": null, "duration_seconds": null }
3Wait for the build
Read the build every few seconds until its status is ready. If it is failed, its error says why and its log has the output.
curl "https://api.nitrous.sh/ v1/ images/ web-runner/ builds/ $BUILD" \ --fail-with-body \ -H "Authorization: Bearer $NITROUS_KEY"
{ "object": "build", "number": 1, "image": "img_01m3kn0phjfxka50m2f7x78fdk", "status": "ready", "error": null, "size_mib": 812, "created_at": "2026-09-28T09:20:11.408Z", "started_at": "2026-09-28T09:20:12.950Z", "finished_at": "2026-09-28T09:23:40.117Z", "duration_seconds": 207.167, "dockerfile": "FROM debian:13\nRUN apt-get update && apt-get install -y --no-install-recommends nodejs npm git\n" }
4Test the build
Boot the build as name@n in a machine without a command, which stays idle, and run the test in it. The sample keeps the machine's id as MACHINE_ID, machineId or machine_id; the Go program prints it: run export MACHINE_ID=… with the value. A 200 can carry a failed command: the build passes only if exit_code is 0 and timed_out is false. Delete the machine either way.
KEY=$(uuidgen) MACHINE_ID= $(curl -sS -X POST https:// api.nitrous.sh/ v1/ machines \ --fail-with-body \ -H "Authorization: Bearer $NITROUS_KEY" \ -H "Idempotency-Key: $KEY" \ -H "Content-Type: application/ json" \ -d '{ "image": "web-runner@'"$BUILD"'", "ttl_seconds": 600 }' | jq -er '.id // error(.error)')
curl -X POST "https://api.nitrous.sh/ v1/ machines/ $MACHINE_ID/ exec" \ --fail-with-body \ -H "Authorization: Bearer $NITROUS_KEY" \ -H "Content-Type: application/ json" \ -d '{ "command": ["node", "--version"], "timeout_seconds": 60 }'
{ "object": "exec_result", "exec_id": "x-d8144042000c", "exit_code": 0, "exit_signal": null, "timed_out": false, … }
curl -X DELETE "https://api.nitrous.sh/ v1/ machines/ $MACHINE_ID" \ --fail-with-body \ -H "Authorization: Bearer $NITROUS_KEY"
5Publish the build
Publish a build only after it passed. This program does steps 4 and 5 together: it runs the test, requires exit_code 0, deletes the test machine whatever the outcome, and publishes the build only on success. Run it with BUILD set to the number from step 2 (export BUILD in a shell). web-runner then boots the build; publishing another build repoints the name, and running machines are unaffected.
set -euo pipefail : "${BUILD:?set BUILD to the build number}" AUTH="Authorization: Bearer $NITROUS_KEY" JSON="Content-Type: application/ json" MACHINE_ID="" delete_machine() { if [ -n "$MACHINE_ID" ]; then curl -sS --fail-with-body -X DELETE "https:// api.nitrous.sh/ v1/ machines/ $MACHINE_ID" \ -H "$AUTH" -o / dev/ null || echo "could not delete $MACHINE_ID" >& 2 MACHINE_ID="" fi } trap delete_machine EXIT # whatever happens below BOOT_KEY= $(uuidgen) MACHINE_ID= $(curl -sS --fail-with-body -X POST https:// api.nitrous.sh/ v1/ machines \ -H "$AUTH" -H "$JSON" -H "Idempotency-Key: $BOOT_KEY" \ -d "{\"image\": \"web-runner@$BUILD\", \"ttl_seconds\": 600}" | jq -er '.id // error(.error)') RESULT= $(curl -sS --fail-with-body -X POST "https:// api.nitrous.sh/ v1/ machines/ $MACHINE_ID/ exec" \ -H "$AUTH" -H "$JSON" -d '{ "command": ["node", "--version"], "timeout_seconds": 60 }') # a 200 can carry a failed command: the exit code decides if ! jq -e '.exit_code == 0 and .timed_out == false' <<<"$RESULT" >/ dev/ null; then echo "test failed: $RESULT" >& 2 exit 1 fi delete_machine PUBLISH_KEY= $(uuidgen) curl -sS --fail-with-body -X POST https:// api.nitrous.sh/ v1/ images/ web-runner/ publish \ -H "$AUTH" -H "$JSON" -H "Idempotency-Key: $PUBLISH_KEY" \ -d "{\"build\": $BUILD}"
{ "id": "img_01m3kn0phjfxka50m2f7x78fdk", "object": "image", "name": "web-runner", "published_build": 1, "latest_build": { "number": 1, "status": "ready" }, "created_at": "2026-09-28T09:19:58.002Z", "updated_at": "2026-09-28T09:24:05.311Z" }
6Boot the image
Boot the image by name with a command, then stream its output and delete the machine with the id the sample keeps, as in the quickstart.
KEY=$(uuidgen) MACHINE_ID= $(curl -sS -X POST https:// api.nitrous.sh/ v1/ machines \ --fail-with-body \ -H "Authorization: Bearer $NITROUS_KEY" \ -H "Idempotency-Key: $KEY" \ -H "Content-Type: application/ json" \ -d '{ "image": "web-runner", "command": ["node", "--version"], "ttl_seconds": 600 }' | jq -er '.id // error(.error)')
What a build does
A build runs docker build on the Dockerfile in a standard-size machine with a 40 GiB disk (or the largest disk tier the account's limit allows, if smaller) in the account, and converts the result into a machine image. The build machine is billed like any other, per second: $0.15324 an hour with the 40 GiB disk (see Disk). It counts toward the account's machine limit while it runs, and appears on the Usage page as image build. It is excluded from machine lists and is not accessible through exec, logs or a terminal; its output is the build log.
Image configuration
The Dockerfile's ENV, WORKDIR and USER apply to every machine booted from the image: to its command, to exec and to the terminal. A request's env overrides variables of the same name, and its cwd overrides WORKDIR. With USER, the command, exec and the terminal run as that user, with its groups and HOME; install anything that needs root in the Dockerfile. A USER not in the image's /etc/passwd fails the build. Variable names consist of letters, digits and _, and do not start with a digit. CMD and ENTRYPOINT are not run: a machine runs the command it is created with. Dockerfiles and build logs are kept for the image's lifetime, and ENV values are part of the image. Don't put secrets in them.
Bring your own image
For anything that needs local files, build the image yourself, locally or in CI, with docker build --platform linux/amd64, and push it to a registry. Then create a build whose Dockerfile is one line: FROM ghcr.io/you/app:tag. Public registries only.
Commands run as argv, so the image needs no shell. A shell (/bin/sh) is needed only for sh -c commands and the terminal.
Limits
- 10 images per account; 2 builds running at once.
- Dockerfiles of at most 64 KiB. A build is stopped after 30 minutes; the image content must fit in 10 GiB.
- Build logs: the last 1 MiB is retained once the build ends. See Stream build logs.
- No build context:
COPYandADDof local files are rejected. UseRUN curlorgit clone, or bring your own image. - linux/amd64. Public registries only.
- Any Linux image boots, from any base,
FROM scratchincluded: nothing needs to be installed. Commands and exec run as argv, with no shell;sh -cand the terminal need a shell (/bin/sh) in the image. An image whose/sbin/initis systemd boots systemd.
A machine booted from a custom image can use any size, with a disk of at least 10 GiB.