Custom Buildpacks

Temps uses Nixpacks to build most apps automatically. For cases where auto-detection isn't enough, you can provide a Dockerfile or a nixpacks.toml for full control.


How Build Detection Works

Temps inspects your repository root in this order:

  1. Dockerfile — if present, used directly; Nixpacks is skipped
  2. nixpacks.toml — if present, overrides Nixpacks auto-detection
  3. Auto-detection — Nixpacks infers the runtime from package.json, requirements.txt, go.mod, Gemfile, etc.

nixpacks.toml

Override specific phases without writing a full Dockerfile:

[phases.setup]
nixPkgs = ["ffmpeg", "imagemagick"]

[phases.install]
cmds = ["npm ci"]

[phases.build]
cmds = ["npm run build"]

[start]
cmd = "node dist/server.js"

Common use cases:

  • Add system packages (nixPkgs) not available from the package manager
  • Change the start command without touching the Dockerfile
  • Pin a specific Node.js or Python version
[phases.setup]
nixPkgs = ["nodejs_20", "python311"]

Dockerfile

A Dockerfile at the repo root gives you complete control. Temps builds it with docker buildx and runs the resulting image.

Multi-stage builds

Multi-stage builds keep the final image small by separating the build environment from the runtime:

# --- Build stage ---
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# --- Runtime stage ---
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

For a Next.js app this typically cuts the image from ~1.5 GB to ~150 MB.

Build arguments

Declare build-time variables in your Dockerfile with ARG, then promote them to runtime environment variables with ENV if your app needs them at runtime:

ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL

Build Caching

Temps caches Docker layer and Nixpacks artifacts between builds. To get the most out of caching:

For Node.js — copy package.json and install before copying source:

COPY package*.json ./
RUN npm ci          # cached if package.json unchanged
COPY . .            # cache busted here when source changes
RUN npm run build

For Python — same pattern with requirements.txt:

COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .

To force a clean build (clear cache), go to Project → Settings → Build → Clear Cache and trigger a new deploy.


Build Resources

Temps does not pin a fixed per-build CPU/memory default. Out of the box each docker build is allowed to grab up to 50% of the host's CPU and RAM (the legacy heuristic). Operators can replace that heuristic with explicit caps via the control-plane Build Limits plugin — see below.


Control-Plane Build Limits

Operators can cap how many builds run at once on the control plane, and how much CPU/memory each build is allowed to consume. This stops several simultaneous docker build runs from saturating the host — without these limits, each build grabs up to 50% of host CPU/RAM, so a few concurrent deploys can pin a busy server.

Configure these in the console under Settings → Infrastructure → Build Limits (route /settings/build-limits). Three knobs are available:

SettingFieldDefaultRangeEffect
Max concurrent buildsmax_concurrent21–32How many docker build operations run at once. Extra builds queue rather than fail.
CPU per build (cores)cpu_limit_cores0.00–64Cores allowed per build (e.g. 2.0 = 2 cores). 0 = use the legacy 50%-of-host default.
Memory per build (MB)memory_limit_mb00–262144Hard cap per build; builds that exceed it are OOM-killed. 0 = use the legacy 50%-of-host default.

A 0 on either resource dimension means that dimension keeps the legacy 50%-of-host heuristic. Resource caps only take effect when both cpu_limit_cores and memory_limit_mb are greater than 0; if either is 0, the override is dropped and both fall back to the heuristic. With the defaults (2 concurrent, 0/0 resources), existing installs see no behavior change until an operator sets real values.

Queuing, not failing. A process-wide semaphore inside the build runtime gates every build to max_concurrent permits. When all slots are in use, additional builds wait for a free slot rather than erroring, and the build log stream shows:

[BUILD QUEUED] Waiting for an available build slot...

When changes take effect

  • Concurrency (max_concurrent) takes effect on the next plugin restart — i.e. the next temps serve start.
  • Per-build CPU/memory caps apply to the very next build, no restart needed.

Worker nodes are not limited

These limits apply to the control plane only. Worker nodes (those joined and run via temps cli agent) skip the build-limits plugin entirely and keep unbounded build behavior — by design, since each worker is dedicated hardware with its own headroom.


Next Steps

Was this page helpful?