Skip to content
Docs

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"}'
201Created
{"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)')
202Accepted
{"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"
200OK
{"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}'
200OK· Abbreviated response · full object
{"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" ]; thencurl -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" >&2MACHINE_ID=""fi}trap delete_machine EXIT # whatever happens belowBOOT_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 decidesif ! jq -e '.exit_code == 0 and .timed_out == false' <<<"$RESULT" >/dev/null; thenecho "test failed: $RESULT" >&2exit 1fidelete_machinePUBLISH_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}"
200OK
{"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: COPY and ADD of local files are rejected. Use RUN curl or git clone, or bring your own image.
  • linux/amd64. Public registries only.
  • Any Linux image boots, from any base, FROM scratch included: nothing needs to be installed. Commands and exec run as argv, with no shell; sh -c and the terminal need a shell (/bin/sh) in the image. An image whose /sbin/init is systemd boots systemd.

A machine booted from a custom image can use any size, with a disk of at least 10 GiB.