Docs
Runtime constraints
The sandbox every container runs in — and the handful of images that need a tweak to live in it.
Every container on Buildfyio runs as a non-root user on a read-only root filesystem with a writable
/tmp, and can only bind ports 1024 and above. Most apps never notice. Stock images that assume root (the official nginx, for instance) do, and the Build Doctor card on the failed deployment links here.The constraints
| Constraint | What it means for your image |
|---|---|
| Non-root user | The process runs as UID 1000, never root. Files the app writes at start must be owned by (or writable for) that user. A Dockerfile that only works because it stays root fails with EACCES / Permission denied. |
| Read-only root filesystem | The image is immutable at runtime — this is what makes a rollback byte-identical. Writes anywhere except /tmp fail with Read-only file system (EROFS). Caches, PID files, rendered config and uploads belong in /tmp or in an attached service (object storage, Redis, Postgres). |
Writable /tmp | /tmp is an in-memory scratch volume: fast, private to the pod, and gone when the pod restarts. Do not keep anything there you cannot regenerate. |
| No privileged ports | Ports below 1024 (80, 443, …) need root and cannot be bound: bind() to 0.0.0.0:80 failed (13: Permission denied) / listen EACCES. Listen on the PORT the platform injects (any value ≥ 1024) and set the same value as the project's Port. The edge terminates TLS and forwards 443 → your port for you. |
| No extra capabilities | No CAP_NET_ADMIN, no privileged mode, no host mounts, no Docker socket. Build-time steps are unrestricted; this applies to the running container only. |
Symptoms the Build Doctor maps to this page
| Log line | Fix |
|---|---|
bind() to 0.0.0.0:80 failed (13: Permission denied) | Use a port ≥ 1024. For nginx, switch to nginxinc/nginx-unprivileged (listens on 8080) and set the project Port to 8080; for your own image, change the listen/--port and the Port setting together. |
Read-only file system on /etc/nginx/conf.d, /var/cache, /run | Generate config at build time (COPY it into the image) and point runtime scratch paths at /tmp. The unprivileged nginx image already does both. |
EACCES: permission denied, mkdir /app/.next/cache | Next.js cannot write its ISR / image cache, so every pod starts cold. In the Dockerfile, before USER: RUN mkdir -p .next/cache && chown -R 1000:1000 .next — or set NEXT_CACHE_DIR=/tmp/next-cache. |
EACCES writing any other file at boot | chown -R 1000:1000 the directory in the Dockerfile before switching user, or write to /tmp (TMPDIR=/tmp covers most libraries). |
A Dockerfile that fits
The platform-generated Dockerfiles already follow this shape. If you ship your own, the three lines that matter are the port, the ownership of anything written at runtime, and the USER switch:
FROM node:20-alpine WORKDIR /app COPY --chown=1000:1000 . . RUN npm ci && npm run build \ && mkdir -p .next/cache && chown -R 1000:1000 .next ENV PORT=3000 TMPDIR=/tmp USER 1000 EXPOSE 3000 CMD ["npm", "run", "start"]
For nginx and other servers that are hard-wired to port 80, prefer the image built for this sandbox:
FROM nginxinc/nginx-unprivileged:alpine # listens on 8080, writes only to /tmp COPY --chown=101:101 dist/ /usr/share/nginx/html/ # Project settings → Port: 8080
Why these constraints
- Non-root + no capabilities — a compromised app cannot escalate past its own container; it is also what lets many tenants share a node safely.
- Read-only root — the image you built is exactly what runs, on every replica, after every restart, and after every rollback.
- Unprivileged ports — a direct consequence of non-root; the edge owns 80/443 and handles TLS, HTTP/2 and redirects so your process never has to.
Image scanning, SBOM and signing
Every build goes through two scanners, and the result is on the deployment page under “Security scan”:
- Committed secrets — after checkout the worker scans the tree (source, config and dotenv files;
node_modulesand build output skipped) for API keys, tokens, private keys and connection strings with credentials. Findings are redacted in the build log. The default is to warn; the operator can make it fail the build withBUILDFYIO_SECRET_SCAN_POLICY=fail. - Image vulnerabilities — every pushed image is scanned with Trivy for HIGH and CRITICAL CVEs (3-minute budget; a scanner problem never fails the build) and an SPDX JSON SBOM is written, downloadable from the deployment page. CRITICAL findings warn by default;
BUILDFYIO_SCAN_FAIL_ON_CRITICAL=1turns them into a failed build. Static sites have no image and get the secret scan only.
Image signing (planned). cosign signing of user images needs a key the platform holds and rotates, and an admission policy that enforces it per namespace; that key-management story is not in place yet. The build pipeline already has the hook for it (
COSIGN_PRIVATE_KEY_PATH on the worker), so this will land as an opt-in without changes to your project. Until then the SBOM plus the image digest recorded on every deployment give you a verifiable record of what ran.