Build system
Every deployment comes from one build, and every build runs the same way: a fresh pod, a clone of your repository at one commit, an install, your build command, and an upload of whatever directory you nominated.
Nothing survives between builds. There is no cache to poison and no state to clear, which also means each build pays the full install cost.
What a build does
Section titled “What a build does”- Clone the repository at the branch and commit that triggered the deploy
- Decrypt and inject the environment variables scoped to this build
- Install dependencies, if the framework has an install step
- Run the build command
- Upload the output directory to CDN storage
- Publish the deployment and notify whoever asked to be told
A failure at any step marks the deployment failed and leaves the previous production deployment serving traffic. A broken build never takes your site down.
The sandbox
Section titled “The sandbox”Each build gets its own Kubernetes pod, running as an unprivileged user with every Linux capability dropped and no ability to regain one.
| CPU | 0.5 cores guaranteed, 1.5 available |
| Memory | 2.5 GiB guaranteed, 4 GiB limit |
| Disk | 10 GiB, discarded when the build ends |
| Wall clock | 15 minutes on Free, 30 minutes on paid plans |
A build that exceeds the memory limit is killed by the kernel, which usually surfaces as an install or bundler step dying without an error message. Every build container gets NODE_OPTIONS=--max-old-space-size=3072, which keeps a Node build’s heap inside the limit and is ignored by the others.
A build that reaches its time limit is stopped and the deployment is marked failed. The failure names the step that was running when it stopped. A successful build that used 70% or more of its limit gets a warning on its deployment page, with the slowest step, so you hear about it one build before the limit does.
The usual cause of a slow build is image optimisation in the framework. AVIF is by far the slowest format to encode, often several seconds per image and width. Prefer WebP, generate fewer widths, or commit images that are already optimised.
What a build can reach
Section titled “What a build can reach”Builds run in a shared namespace, so the boundary that matters is the pod and its network policy rather than the namespace:
- Nothing can connect in. Ingress is denied completely.
- Outbound HTTP and HTTPS only, to public addresses. Private ranges (
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16) and the cloud metadata endpoint (169.254.169.254) are blocked, so a build cannot reach our database, our API, another build, or the credentials attached to the node it happens to be running on. - Port 53 is open to any destination. Resolution has to work for package registries to work, and the rule that allows it does not restrict where queries may go. The restrictions above apply to HTTP and HTTPS.
In practice: your build can install from npm, PyPI, crates.io or a private registry over HTTPS, and it cannot see anything of ours or anyone else’s.
Package managers
Section titled “Package managers”Node projects install with npm, pnpm, yarn or bun. When you import a repository, the manager is worked out in this order and stored on the project:
- The project’s
package_managersetting, when you have set one - The
packageManagerfield inpackage.json, the Corepack convention, for example"packageManager": "pnpm@9.15.0" - The lockfiles committed to the repository
- npm
From then on the build uses the project’s stored setting, so changing your lockfile later does not change the manager on its own. Set it explicitly on the project if you switch.
| Package manager | Lockfiles |
|---|---|
| npm | package-lock.json, npm-shrinkwrap.json |
| pnpm | pnpm-lock.yaml |
| yarn | yarn.lock |
| bun | bun.lock, bun.lockb |
When several lockfiles are present and package.json says nothing, precedence is pnpm, then yarn, then bun, then npm, and the build log records that the repository was ambiguous.
A committed lockfile changes the install. With one, the reproducible variant runs (npm ci, or --frozen-lockfile / --immutable), so a lockfile that disagrees with package.json fails the build instead of quietly resolving different versions. Without one, a plain install runs and you get whatever the registry offers that day.
Dependency install scripts are disabled
Section titled “Dependency install scripts are disabled”Install runs with --ignore-scripts for npm, pnpm and bun, so a dependency cannot execute code at install time. For bun this overrides trustedDependencies and any bunfig.toml in the repository, because the flag wins over config.
Yarn classic with a committed yarn.lock gets it too. Two cases do not: Yarn Berry, which uses --mode=skip-builds to the same end, and a Yarn project with no lockfile at all, which installs with a plain yarn install and does run dependency scripts. Jekyll’s bundle install has no equivalent restriction.
Your own build script always runs. This restricts dependencies, not you.
If your build genuinely needs a dependency’s postinstall step, a native module that compiles on install being the usual case, commit the built artefact or switch to a prebuilt package. The alternative is executing arbitrary code from your dependency tree on every build.
How the build command is run
Section titled “How the build command is run”The build runs in two steps, in the project’s root directory (the repository root unless you set a root directory):
- The install command for your package manager. If it fails, the build fails and your command never runs.
- Your build command, run with
sh.
Hugo skips the install step. So does Static HTML without a build command of its own. With one, Static HTML installs dependencies first, and only when the repository has a package.json.
Your build command is ordinary shell: &&, pipes, redirection, quotes and inline variables all work, and your environment variables are set in it. It has to fit on one line. What a build command may contain has examples and the length limit.
What your build command can see
Section titled “What your build command can see”The command runs inside the sandbox above, with your repository, your environment variables and the tools in the build image. It does not hold any of our credentials:
- No Git token. The clone happens in a separate container before the build starts, and the token for a private repository stays there.
- No storage credentials. The upload to CDN storage happens in a separate container after the build, so the build step never has the keys it would need to write to storage.
- No Kubernetes credentials. The build pod gets no service account token, so the build cannot talk to the cluster.
Your own environment variables are visible to everything the build runs, dependencies included. Scope secrets to the environments that need them.
What gets published
Section titled “What gets published”The contents of your output directory are uploaded to CDN storage under a path that starts with your team’s identifier:
/{team_id}/{project_slug}/{deployment_id}/That prefix is the tenant boundary for stored files. Uploads run ten at a time, with Content-Type set from the file extension.
What is never published
Section titled “What is never published”Some entries are withheld from every deploy, at any depth in the output directory:
| Withheld | Why |
|---|---|
.git, .svn, .hg |
Repository metadata. Publishing .git exposes your full history, including anything you committed and later removed |
.env, .env.* |
These hold credentials far more often than not |
.npmrc, .netrc, .ssh, .aws |
Registry, network and cloud credentials, which a build step can leave behind |
node_modules |
A dependency tree is not site content, and publishing one is slow and enormous |
This matters most when your output directory is the repository root, which is the default for Static HTML: the build workspace is your repository, so without these rules the clone itself would be served.
Every other dotfile is published normally. .well-known works, so ACME challenges, security.txt and app association files behave as expected. If your site loads a file from inside node_modules by path, copy it into your own assets directory at build time.
Build minutes
Section titled “Build minutes”Build time is metered per billing period, and the allowance depends on your plan. Current limits are on the pricing page.
A build counts its running time, rounded down to whole minutes with a minimum of one, when it:
- succeeds,
- times out, or
- fails in your own build: a failing build command, a missing output directory, or a repository that cannot be cloned.
A build never counts when it fails on our side or when you cancel it.
curl -X POST "https://api.deploybase.eu/api/v1/deployments/$DEPLOYMENT_ID/cancel" \ -H "Authorization: Bearer $DEPLOYBASE_API_KEY"Cancellation works while a deployment is pending, queued or building, and the deployment is never promoted afterwards. Cancelling one that has not started yet costs nothing. Cancelling one that is already building stops its build within a few seconds, and a cancelled build is not metered.
When a build fails
Section titled “When a build fails”The build log is the whole story, and the failure is almost always in the last twenty lines:
curl "https://api.deploybase.eu/api/v1/deployments/$DEPLOYMENT_ID/logs" \ -H "Authorization: Bearer $DEPLOYBASE_API_KEY"Five failures account for most of what we see:
The install fails on a lockfile mismatch. Someone edited package.json without running the install locally. Run it, commit the lockfile.
The build passes locally and fails here on a missing file. Linux is case-sensitive and macOS is not, so import './Header.svelte' finds header.svelte on your laptop and nothing in the build.
The build is killed with no error. Memory. Reduce concurrency in your build tool, or split the work.
The site deploys empty. The output directory does not match what the build actually wrote. Check the log for the directory your tool reports, and set the project to match.
The build stops with exit code 127. The build command names a program that is not installed in the build image, and the log says which one: sh: bun: not found. Check the spelling, or choose the framework whose image provides the program. The build defaults list the image each framework builds in.
Live a8b7eb1d built