$ aoox

DocsApplications

How builds work

Three ways to turn a repo into an image: your own Dockerfile, automatic detection by Nixpacks, or a static site served by nginx.

Options
Dockerfile · Nixpacks · Railpack · Static site, per application
Runs on
Docker daemon (classic builder); Railpack via its own BuildKit
Result
Image <registry>/<project>/<app>:<id>
This page is about applications built from a Git repo. Applications whose source is a ready-made image skip the build stage entirely — see Two application sources.

Which one should I pick?

Is the output just HTML/CSS/JS files (Vite, CRA, Astro, Hugo, docs site)?yes → Static siteno → keep going
Does the repo already have a Dockerfile?yes → Dockerfileno → keep going
Need fast rebuilds / layer caching?yes → Dockerfile or Railpackno → keep going
Want zero config and caching between deploys (host only, not remote servers)?yes → Railpackno → keep going
Want zero config and can accept a slow build every time?yes → Nixpacksno → Dockerfile
DockerfileNixpacksRailpackStatic site
SetupWrite your own DockerfileZero configZero configBuild command + output folder
RebuildsDocker layer cacheLayer cache only (no cache mounts, --no-cache)BuildKit cache between deploysLayer cache only
First buildDepends on the base imageSlow: base ± 350 MB + nix-env284 seconds (helper + BuildKit, once)Fast: node alpine + nginx alpine
Later buildsFast if layers are unchangedFast if layers are unchanged; slow when dependency files change±8 seconds — dependencies from cacheFast if layers are unchanged
Image controlFullLimited to nixpacks optionsLimited to railpack optionsNone — nginx :80
Remote serverYesYesNo — host onlyYes
Best forProduction, lean imagesPrototypes, repos without a DockerfileRepeated deploys without a DockerfileSPAs, landing pages, docs sites

Dockerfile

Choose Build method → Dockerfile and set the Dockerfile path in the repo (default Dockerfile at the root). The build runs directly on the Docker daemon from the Git URL (POST /build?remote=…) — the daemon does the cloning, the API doesn't need git.

Minimal example (Node.js)
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "server.js"]
  • Classic builder, not BuildKit: syntax like RUN --mount, COPY --link, and heredocs isn't supported.
  • Layer caching applies between builds as long as the base image and early steps don't change — put COPY package*.json + npm ci before COPY . ..
  • Include wget or curl in the image if you use a health check (alpine already ships busybox's wget).

Nixpacks

Choose Build method → Nixpacks for repos without a Dockerfile. There's no nixpacks binary on the host or in the API image — everything runs in a container:

  1. 01helperthe aoox-nixpacks image is built once (debian + git + the nixpacks CLI)
  2. 02clonea one-shot container runs git clone --depth 1 (credentials never leave the helper)
  3. 03plannixpacks build → writes .nixpacks/Dockerfile
  4. 04buildthe source is tarred → POST /build with the generated Dockerfile
Trade-offs to know about
  • First build is slow: the nixpacks base image (± 350 MB) is pulled and the helper image is built once; aoox pins the nixpacks version.
  • No dependency cache mounts — --no-cache is required because nixpacks' cache mounts need BuildKit. The Docker layer cache still applies: a redeploy with an unchanged source reuses every layer (seconds), while a change to dependency files rebuilds from that layer and downloads dependencies again.
  • The source tree is held in the API's memory during the build (without .git) — very large repos are better off with a Dockerfile.

Steering Nixpacks

Nixpacks reads NIXPACKS_* variables. Set them via Build args:

Build args
NIXPACKS_NODE_VERSION=22
NIXPACKS_BUILD_CMD=npm run build
NIXPACKS_START_CMD=node server.js

Alternatively, put a nixpacks.toml in the repo — it's read automatically during plan.

Railpack

Choose Build method → Railpack for repos without a Dockerfile that get deployed repeatedly and where you want dependency caching to stick around. Unlike Nixpacks, Railpack executes its build plan through a long-lived moby/buildkit container with its own volume — dependency layers survive between deploys.

  1. 01helperthe aoox-railpack image is built once (docker CLI + git + the railpack binary)
  2. 02buildkitan aoox-buildkit container (moby/buildkit) starts once, with its own cache volume
  3. 03clonethe helper does git clone --depth 1, then railpack build --cache-key <project>/<app>
  4. 04cachedependency layers are kept in the BuildKit volume — the next deploy reuses them
Host only
Railpack runs BuildKit alongside the registry on aoox's own host — there's no per-remote-server BuildKit yet. Applications with a remote server must use Dockerfile or Nixpacks.
Memory on small servers
Railpack runs BuildKit, and the first (cold) build is the most memory-hungry. On a 2 GB server it can be killed for lack of memory (exit code 137). Use a server with 4 GB, or make sure swap is enabled and retry: later builds reuse the BuildKit cache and need far less. You can also build with a Dockerfile instead.
  • Images produced by Railpack listen on $PORT (Railway's convention). aoox automatically fills PORT with the configured container port, unless the application already sets its own PORT in env.
  • Build args are passed through as --env to railpack — same as Nixpacks, they also affect its provider detection.
  • Cache is scoped per project/application, so other applications never share layers by accident. The BuildKit volume is pruned automatically by the nightly maintenance job — Docker's own prune doesn't reach it.

Static site (nginx)

Choose Build method → Static site (nginx) for repos whose final output is just static files. aoox builds a two-stage Dockerfile: an (optional) build stage on node:22-alpine, then nginx:1.27-alpine serving the output folder on port 80 — the create form fills in Container port 80 for you when you pick Static site (you can change it, but nginx still listens on 80).

FieldDetails
Build commandE.g. npm ci && npm run build. Leave empty if the files already exist in the repo (no Node stage).
Output folderRelative to the Root directory (repo root when empty): dist (default), build, out, or . for the whole repo.
SPA modeOn (default): unknown paths are served index.html (client-side routing). Off: a normal 404 for multi-page sites.
Generated Dockerfile (illustration)
FROM node:22-alpine AS build
WORKDIR /src
COPY . .
RUN npm ci && npm run build

FROM nginx:1.27-alpine
COPY --from=build /src/dist/ /usr/share/nginx/html/
COPY .aoox/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
  • The container port must be 80. A health check path of / is enough (nginx alpine has wget).
  • The build runs through the same helper as Nixpacks (clone in a container, tar context) — private repos are safe, but there are no dependency cache mounts (unchanged layers are still reused).
  • Runtime env is useless for static files, and Build args aren't yet passed to the static build stage. Build-time values (e.g. VITE_API_URL) currently need to live in the repo (.env.production) or in the build command: VITE_API_URL=https://api.example.com npm run build.
  • The Node version is pinned to 22 by aoox; not yet changeable from the UI.

Root directory (monorepo)

A monorepo keeps several applications in one repository (for example apps/web and services/api). Set Root directory in the Git part of the application form to the folder you want built; empty means the repository root. It applies to every build type, pull request previews included.

Build typeWhat is relative to the Root directory
DockerfileThe build context is that folder (POST /build?remote=URL#branch:folder), and the Dockerfile path is relative to the same folder.
NixpacksStack detection and the build run in that folder; .nixpacks/Dockerfile is written there.
Railpackrailpack build runs in that folder.
Static siteThe build command runs in that folder and the Output folder is relative to it too.
Example: one repo, three applications
apps/web        Root directory: apps/web       (Nixpacks)
services/api    Root directory: services/api   (Dockerfile at services/api/Dockerfile)
apps/site       Root directory: apps/site      (Static site, output: .)
Limits
  • Files outside the folder are not available during the build. A Dockerfile in apps/web cannot COPY ../../packages/shared. Bundle shared packages first, or use a Dockerfile at the repository root with an empty Root directory.
  • Format: a relative folder, only letters, digits, ., _, - separated by /; no .. and no leading or trailing /.
  • A folder that does not exist on that branch fails the deployment with Root directory … was not found; the old container keeps running.
  • Symbolic links in the repository that point outside the clone are rejected.

Want deploys only when that folder changed? See Webhook for monorepos.

Build args

The Build args field (Settings tab) uses KEY=VALUE per line and is passed as --build-arg. For Nixpacks and Railpack, the values also become env during plan/build. Not yet applied to static sites.

Dockerfile that uses it
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN npm run build
Build args do not support ${{…}} references, and they end up baked into the pushed image. Don't put secrets here — use environment variables, which are applied at runtime.

The resulting image

<registry.url>/<project-slug>/<app-slug>:<12-character deployment id>
# example: localhost:5000/toko/shop:a1b2c3d4e5f6
  • Old tags stay in the registry until deleted from the Registry page — that's what makes rollback possible. Delete + garbage collect to reclaim disk.
  • On a remote server the image isn't pushed; it stays on that server's daemon with ref aoox/<project>/<app>:<tag>.

Common pitfalls

SymptomCause & fix
Build fails: unknown flag / --mountBuildKit syntax in the Dockerfile. Replace it with a plain step.
Nixpacks: build hangsShouldn't happen — aoox forces --no-cache. Check the helper log on the Deploy tab.
Nixpacks picks the wrong Node/Python versionSet NIXPACKS_NODE_VERSION / NIXPACKS_PYTHON_VERSION in Build args, or engines in package.json.
Env is empty during the buildEnv genuinely isn't available at build time. Use Build args (non-secret) or read env at runtime.
Railpack: "run on the aoox host only"This application has a remote server. Switch to Dockerfile/Nixpacks, or remove its server placement.
Railpack: application doesn't accept connectionsCheck the application reads process.env.PORT instead of a hardcoded port. aoox fills it in automatically from the container port.

Next steps

Found a mistake? Edit this page on GitHub ↗