Build Configuration
The build block in pilum.yaml says how to compile a service on the host before (or instead of) the recipe’s packaging step.
Basic Structure
build: language: go version: "1.23" cmd: "go build -o ./dist/app ." env_vars: CGO_ENABLED: "0"Fields
| Field | Type | Default | Description |
|---|---|---|---|
language | string | - | Build language. Picks pilum init defaults, the memory estimate for worker planning, and the toolchain for automatic cache warming. Does not generate a build command |
version | string | - | Language version. Informational; written by pilum init |
cmd | string | - | Shell command that builds the service. Empty means no host build |
env_vars | map | - | Environment variables for this service’s steps |
flags | map of lists | - | Flags appended to the end of cmd (see Build Flags) |
version_var | string | - | Homebrew only. Go variable that receives the release tag via -X |
resources.memory | int (MB) | language default | Peak build memory, used to cap parallel builds |
resources.cpu | int (cores) | 2 | Cores one build uses, used to size parallel builds |
warm | string or false | automatic | Command that fills the shared build cache once per run, or false to opt out |
warm_dir | string | project root | Directory to run warm in, relative to where you run Pilum |
output_dir | string | dist | Cloudflare Pages only. Directory to deploy, relative to the service directory |
platform | string | linux/amd64 | Container recipes. Image platform passed to docker build --platform (see Image Platform) |
docker_cache | bool | false | Container recipes. Keep the image’s layer cache in its registry (see Docker Layer Cache). Can be set project-wide in .pilum.yml |
output and platforms appear in older examples and in the JSON schema, but Pilum ignores them. The Homebrew recipe always builds darwin/linux × amd64/arm64.
How cmd Runs
cmd runs through /bin/sh -c, so pipes, && and environment variable expansion work.
| Recipe | Step that runs cmd | Working directory |
|---|---|---|
gcp-cloud-run, gcp-cloud-run-job, gcp-artifact-registry-image, gcp-mig-container, azure-container-apps | build binary | Service directory |
npm | build package | Service directory |
cloudflare-pages | build site | Project root (so pnpm --filter works) |
aws-lambda | — (runs sam build) | — |
homebrew | — (runs a fixed Go cross-compile) | — |
If cmd is empty, the build step is skipped for that service. For container recipes this means a Dockerfile-only build: the image is built entirely by docker build with your _templates/<template> Dockerfile. That’s the right choice when the Dockerfile compiles the code itself. Keep cmd when the Dockerfile copies a binary built on the host (for example from dist/).
Languages
Pilum knows 12 languages. Each has a template used by pilum init and a default memory estimate:
language | Aliases | pilum init default cmd | Default memory |
|---|---|---|---|
go | golang | go build -o ./dist (plus ldflags) | 1024 MB |
node | nodejs, javascript, js, typescript, ts | npm run build | 1536 MB |
bun | bun run build | 1536 MB | |
python | pip install -r requirements.txt | 512 MB | |
rust | cargo build --release (plus target flag) | 2048 MB | |
java | ./mvnw -B package -DskipTests | 3072 MB | |
kotlin | ./gradlew build -x test | 3072 MB | |
dotnet | csharp, c#, fsharp, f#, .net | dotnet publish -c Release -o ./dist | 2048 MB |
c | cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build --parallel | 1024 MB | |
cpp | c++, cxx | same as c | 2048 MB |
php | composer install --no-dev --optimize-autoloader --no-interaction | 1024 MB | |
ruby | rb | bundle install | 1024 MB |
An unknown language is allowed and gets 1024 MB. The language never changes what cmd does.
Build Flags
flags is a map from flag name to a list of values. Pilum renders each entry as -name='value1 value2' and appends it to the end of cmd, sorted by flag name:
build: language: go cmd: "go build -o ./dist/app" flags: ldflags: - "-s" - "-w"runs:
go build -o ./dist/app -ldflags='-s -w'For language: rust, flags get a double dash, so target: ["x86_64-unknown-linux-musl"] becomes --target='x86_64-unknown-linux-musl'.
Flags are only appended for container recipes. The npm and Cloudflare Pages recipes run cmd as written.
Version Injection
Pilum does not substitute ${TAG} or any other placeholder in cmd or flags. A -X main.version=${TAG} flag passes the literal text (or an empty string) to the linker.
-
Homebrew: set
version_varand the recipe adds-X <version_var>=<tag>to its cross-compile:build:language: goversion_var: github.com/my-org/my-cli/cmd.version -
Other recipes: export the version in your CI environment and reference it from
cmd. Steps inherit the shell environment, and/bin/sh -cexpands it:build:language: gocmd: 'go build -ldflags "-s -w -X main.version=$VERSION" -o ./dist/app .'Terminal window VERSION=v1.2.3 pilum deploy --tag=v1.2.3
Environment Variables
build: env_vars: CGO_ENABLED: "0" GOOS: linux GOARCH: amd64- Values must be strings; quote numbers and booleans.
- The variables are added to Pilum’s own environment for every step of that service, not only the build command. A recipe step’s own variables override them.
$NAMEand${NAME}in values are expanded from Pilum’s environment, soGIT_SHA: "${GITHUB_SHA}"works. There is no escape, so a value cannot contain a literal$.- Pilum sets
GOMAXPROCSper task from its worker plan unless you set it here or in your shell. - Build variables also distinguish warm groups: services with different
GOOS/GOARCHwarm separately.
Runtime variables for the deployed service go in the top-level env_vars, not here. See Service Configuration.
Resources
Pilum runs builds in parallel and sizes the pool from the machine:
- Workers for CPU-bound steps = CPU cores ÷ the largest
resources.cpuacross services (default 2). - That number is capped so the largest build memory estimate times the worker count fits in 75% of RAM. The estimate is
resources.memory, or the language default from the table above. - Steps that call Docker are sized from the Docker daemon’s CPUs and memory (the Docker Desktop VM on macOS).
- Push, deploy and execute steps use up to 16 workers.
--max-workers Noverrides all of this.
Set resources when a build needs much more or less than its language default:
build: language: rust cmd: "cargo build --release" resources: memory: 4096 # MB cpu: 4 # coresBuild Cache Warming
Before the builds start, Pilum can fill the language’s shared cache (Go build cache, pnpm store, Cargo registry, …) once, so parallel builds don’t all download and compile the same dependencies.
build: language: node warm: pnpm install --frozen-lockfile # explicit command warm_dir: apps # optional, relative to where you run Pilum cmd: pnpm build- Unset: automatic. Pilum detects the toolchain (Go, Node, Python uv/pip, Cargo, Gradle, Maven, .NET, Composer) and warms when two or more services with a host
cmdshare it. - A string: run this command once. Services with the same
warm,warm_dirand buildenv_varsshare one run. false: never warm this service.--no-warmdisables warming for the whole run.
Warming runs only when a build step will run, and a failed warm falls back to cold builds. See Build Cache Warming for the commands Pilum generates per toolchain.
Image Platform
Every docker build that Pilum runs passes --platform, from build.platform or linux/amd64 when it is unset. This applies to the recipes with a build docker image step: gcp-cloud-run, gcp-cloud-run-job, gcp-artifact-registry-image, gcp-mig-container and azure-container-apps.
Before 0.9 no platform was passed, so an image built on an Apple Silicon Mac was arm64 and Cloud Run couldn’t run it. Set platform only when the target runs ARM, such as a MIG on Tau T2A machines:
build: platform: linux/arm64platform only affects the image. A Go binary built on the host by cmd still needs GOOS/GOARCH in build.env_vars to match.
Docker Layer Cache
docker_cache: true stores the image’s layer cache in its registry, next to the image, as <image>:buildcache. A fresh CI runner then pulls unchanged layers instead of rebuilding them. It works with any registry Pilum pushes to (Artifact Registry, ECR, ACR, Docker Hub).
build: docker_cache: trueWith it on, the build step becomes:
docker buildx build \ --cache-from type=registry,ref=<image>:buildcache \ --cache-to type=registry,ref=<image>:buildcache,mode=max,ignore-error=true \ --load --platform linux/amd64 -t <image>:<tag> ...--load keeps the image in the local Docker store, so the push step works as before.
- Builder: exporting a cache needs a buildx builder that supports it, such as the
docker-containerdriver (docker buildx create --use --driver docker-container, ordocker/setup-buildx-actionin GitHub Actions). Docker’s defaultdockerdriver can’t, unless the containerd image store is enabled. - Fallback: before building, Pilum checks the active builder. If it can’t export a cache, Pilum turns
docker_cacheoff for that run, prints a warning, and builds with plaindocker build. The check is skipped on dry runs. - Failures don’t fail the build: a missing cache on
--cache-fromonly warns, andignore-error=truedoes the same when the cache upload fails (wrong registry, no write access). - Project default: set
build.docker_cache: truein.pilum.ymlto turn it on for every service. A service’s owntrueorfalsewins.
The cache pays off for Dockerfiles that compile inside Docker. Templates that copy a binary built on the host onto a small base image have few layers to reuse.
Language Examples
Go
build: language: go version: "1.23" cmd: "go build -o ./dist/app" env_vars: CGO_ENABLED: "0" GOOS: linux GOARCH: amd64 flags: ldflags: - "-s" - "-w"Node.js
build: language: node version: "20" cmd: "npm run build" env_vars: NODE_ENV: productionFor a pnpm workspace, build a package and its workspace dependencies in order with the ... suffix:
build: language: node cmd: "pnpm --filter @scope/package... build"Which install command runs before the build is set by the recipe, not by build:
- npm recipe:
npm.package_manager—npm(default),pnpm,yarnorbun. See npm. - Cloudflare Pages recipe:
cloudflare.package_manager—npm(default) orpnpm. See Cloudflare Pages.
Python
build: language: python version: "3.12" cmd: "pip install -r requirements.txt" env_vars: PYTHONDONTWRITEBYTECODE: "1" PYTHONUNBUFFERED: "1"Rust
build: language: rust version: "1.75" cmd: "cargo build --release" flags: target: - "x86_64-unknown-linux-musl" # rendered as --target='...' env_vars: CARGO_NET_GIT_FETCH_WITH_CLI: "true"Java
build: language: java version: "21" cmd: "./mvnw -B package -DskipTests"Dockerfile-only
type: gcp-cloud-runtemplate: node # _templates/node does the whole buildbuild: language: node # still used for memory planningCustom commands
build: language: go cmd: "make build"build: language: go cmd: "./scripts/build.sh"CLI Help
pilum build --help prints build examples by language.
Next Steps
- Service Configuration — Full
pilum.yamlreference - Build Cache Warming — Warm commands per toolchain
- How Recipes Work — Build steps, tags and execution modes
- npm Provider — npm/pnpm/yarn/bun publishing
- Cloudflare Pages — Static site deployment