Skip to content
Acropolis is experimental: interfaces, output and defaults can change between releases.What that means
Acropolisby OlimpiaExperimental
Esc
↑↓navigate↵open⌘Jpreview
On this page

How it works

No builder image, downloads that don't hang, a base that is never decompressed, small runtimes and portable caches.

A build has three phases: detect the app and write a plan, run the plan’s steps, and assemble the image. acropolis plan prints the first phase on its own.

Detection and the plan

Acropolis reads the repo (lockfiles, package.json, go.mod, Cargo.toml, pyproject.toml, framework configs, acropolis.json or railpack.json) and picks a provider with Railpack’s precedence. The plan lists toolchains, install and build steps, the runtime base and the files each layer takes. Its hash is in info.json as planHash, and tests/plans keeps a snapshot of the plan of each of the 131 Railpack examples, so any detection change shows up as a diff before anything is built.

No builder image

Node, Go and Rust are downloaded from their official sources with a checksum and run directly. Dependencies (npm, pnpm, yarn, bun, Go modules, crates) are installed by Acropolis itself from the lockfiles and verified while they download: Go modules against go.sum, npm packages against their integrity hashes. Go builds then run with GOPROXY=off and no network.

Languages without relocatable binaries (Ruby, Python with extensions, PHP, Java, .NET, Elixir) and apt packages run inside the rootfs of the official image instead: its layers are unpacked once, each step gets its own overlayfs upper dir, and that upper dir becomes an OCI layer.

Downloads that don’t hang

Blobs of 12 MB or more are downloaded in 8 MB segments, six in parallel over independent HTTP/1.1 connections. If the segment that blocks in-order output falls behind (more than 2.5 s or 2.5 times the median), a duplicate is started and the first to finish wins; streams with no bytes for 6 s are cut and resumed with Range. Copying node:22-bookworm-slim (80 MB) from Docker Hub went from 12.2 s to 2.9 s.

The base is never decompressed

Base image layers are copied registry to registry by digest, starting as soon as the platform manifest arrives. Only the new layers are compressed: each one is a concatenation of independent ~1 MB zstd frames (or gzip members) compressed in parallel, valid and deterministic. Production node_modules is written straight from the store tarballs, without unpacking to disk.

Small runtimes

  • Node servers run on distroless with a minimal busybox shell when nothing from Debian is needed, with exactly the node binary they were built with; otherwise on node:<version>-bookworm-slim.
  • Next 15+ apps without output configured are built as standalone, so the image carries only what the server traces.
  • Bun apps that start with bun <file> run on distroless/cc with a bun layer and no node.
  • Go runs on distroless/static and Rust on distroless/cc.
  • SPAs and static sites are served by Caddy.

Portable caches

Go and Cargo build caches, .next/cache, the reused node_modules and the rest live in a per-app cache in the home. On an ephemeral builder it travels as a file: acropolis cache export|import writes and reads it as a tar.zst.

Isolation

Each build step runs in its own PID namespace (it can’t see Acropolis or its variables), with the filesystem read-only except its working directory, its app’s cache, /tmp and /var/tmp, a minimal /dev, the capabilities of an unprivileged container, a seccomp filter modeled on Docker’s default profile, and its own network namespace when it doesn’t need network. What Acropolis processes as root (config, lockfiles, patches, layers, caches) is validated so it cannot read or write outside its tree. See Running in production for what that does and doesn’t protect.

Every measurement behind these choices, and what was tried and discarded, is in docs/decisions.md.

Last updated on

Was this page helpful?