Phases: Git for container filesystems, or, what I built after leaving CNCF buildpacks.io
Table Of Contents
Many people think Dockerfiles work like Git, with linked hashes:
Instead, container images use a more flexible variant of Merkle tree, without cryptographic links between the layers. It's more like this:
This linked hash assumption is totally understandable: Dockerfile builds use layers for caching the results of each RUN or COPY command, and rebuild downstream layers when the command text changes, so they behave like a chain.
Buildpacks.io
When I co-founded Cloud Native Buildpacks in 2018, I wanted to build something that takes advantage of the flexibility provided by the container image spec, so that large enterprises could patch thousands of different workload images with registry metadata requests, instead of running thousands of layer-generating builds in thousands of containers:
When images are built this way, rollouts are efficient too. Each Kube node downloads the updated base image layers once and reuses the cached app layers. There's much less to download before restarting pods, which makes patching CVEs at scale much faster.
The project has come a long way. Buildpacks just graduated CNCF (congrats team!!), and is used by some pretty large companies. But to be honest, I've never used it for my own workloads. For one, builds are opaque. When I talked to users, the ideal end user for the Buildpacks project was often someone writing code who wasn’t responsible for building or deploying it (like data scientists or contractors).
Buildpacks are composable build modules that work kind of like this:
Each buildpack puts its dependencies in separate directories, which become image layers that can be replaced or preserved on rebuilds. Applications are built and launched with special env vars setups (PATH, LD_LIBRARY_PATH, etc.) so that they can find dependencies.
But this means adding a bunch of complex, self-coordinating, often third-party dependencies to your build process. And like a Dockerfile build, a buildpack build is a single transformation function:
buildpacks + app source → built image
Unlike a conventional Dockerfile build, it's possible to swap the base image out with registry metadata updates, which allows patching libraries that provide compatibility guarantees with the built app (very cool!):
At the end of the day though, a buildpack build is still a build function with a special-case base image replacement feature. And that build function is opinionated and opaque -- not a sharp tool with a transparent model you can extend (like Dockerfiles, or Git).
A different model
I feel like we'd be better served with a set of invariants, instead of a better build function:
- When I start a container, I know dependencies are up-to-date within suitable version constraints.
- My container doesn't keep running indefinitely with known unpatched vulnerabilities.
- When extending my container, I can easily choose what is persisted and what is rebuilt.
And no one has a great story for how containers and container builds replace “pet” VMs that you work with every day. I feel like 2026 can do better than Ubuntu on EC2 with unattended-upgrades enabled. I want reproducible dev environments without one-time setup scripts and drifting configuration.
And with the backstory out of the way, I am very happy to announce Phases for Beams!


I'm shipping Phases as a new feature of Teleport's Beams agentic runtime, providing image builds and persistence. It's not available as an OSS project quite yet, but stay tuned! (And please try Beams!)
Phases provide images for beams, which are Kata Containers that run in Cloud Hypervisor microVMs. A phase holds saved filesystem state and build instructions, while a beam runs it.
Instead of treating layers like Git commits, Phases deliver a Git-like UX for container filesystems, combining continuous updates with persistent snapshots that are forkable and rebaseable. And instead of using layers to cache (often stale) build command results, Phases use layers to represent reusable build stages, which can be rebuilt automatically or created interactively (like VM snapshots).
In their default configuration, Phases let you:
- Define build stages in a Kube-style YAML file, with automated periodic rebuilds.
- Write to
/homeinteractively, and take snapshots - Easily copy local files into the container and persist them.
- Receive automatic updates to base layers via rebasing (like Buildpacks)
The novel part is that these steps can be applied in any order, creating a pipeline that composes mutable, immutable, and regenerated state, with rules that prevent unsafe mutations and data corruption. Each step is called a "phase", and each phase periodically outputs a new image.
There are three types of phases:
- Snapshot phases, which persist your changes in
/homein durable (but editable and forkable) layers. You can edit these until descendant snapshot phases depend on them -- then you have to fork. - Rebuild phases, which periodically rerun commands to make changes outside of
/home(like a Dockerfile on a timer). You can change the commands between rebuilds. - Copy phases, which supply fixed sets of files outside of
/home(base image overlays, like Dockerfile'sCOPY --link). You can always edit these.
It’s much easier to demonstrate this than explain. Here are two examples.
First, you can use phases like ultra-lightweight VMs that you can snapshot and fork. Let's build an app in a phase:
local:~$ tsh phase add workspace
Phase "workspace" created.
Linux workspace-98a05ec0 6.18.56 #1 SMP Fri Oct 16 18:41:32 UTC 2026 x86_64
Beam ID: workspace-98a05ec0
Beam UUID: 98a05ec0-16bb-46bd-966c-cdacd9484efb
workspace-98a05ec0:~$ git clone [email protected]:example/parakeets
workspace-98a05ec0:~$ cd parakeets
workspace-98a05ec0:~/parakeets$ codex 'Build Facebook for moody parakeets. Install Go in my home dir, and use only the Go standard library to write the app, with main.go at the repo root. Listen on 127.0.0.1:8080 and serve /home/beams/parakeets/logo.svg at /logo.svg. Commit the app when you are done.'
Now fork from inside that beam and give an agent a task. You can leave it working when you disconnect by starting it as a service:
workspace-98a05ec0:~/parakeets$ tsh phase add beak-buttons --fork
Phase "beak-buttons" created.
Linux beak-buttons-6f2c8a91 6.18.56 #1 SMP Fri Oct 16 18:41:32 UTC 2026 x86_64
Beam ID: beak-buttons-6f2c8a91
Beam UUID: 6f2c8a91-3d75-4b26-91e0-a7c6f2d84319
beak-buttons-6f2c8a91:~$ beamctl start sh -c 'codex exec -C ~/parakeets "Add a beak-friendly interface with big buttons. Test it, commit and push the changes to a branch, then run tsh phase snapshot." </dev/null'
beak-buttons-6f2c8a91:~$ ^D
workspace-98a05ec0:~/parakeets$
Now you're back in the original beam. Start another fork and have a second agent work on something else:
workspace-98a05ec0:~/parakeets$ tsh phase add flock-chat --fork
Phase "flock-chat" created.
Linux flock-chat-b4e70d32 6.18.56 #1 SMP Fri Oct 16 18:41:32 UTC 2026 x86_64
Beam ID: flock-chat-b4e70d32
Beam UUID: b4e70d32-8f19-46c5-a2d8-0e91f7c36b54
flock-chat-b4e70d32:~$ beamctl start sh -c 'codex exec -C ~/parakeets "Add a flock chat feature. Test it, commit and push the changes to a branch, then run tsh phase snapshot." </dev/null'
flock-chat-b4e70d32:~$ ^D
workspace-98a05ec0:~/parakeets$ tsh phase snapshot --final
local:~$
Both agents have their own filesystem, including their own copy of the repo. Each commits its feature and saves its phase, so you can pick up later in another ephemeral container with task-specific state.
Now let's define custom base images with rebuild phases. Instead of installing Go on-the-fly in /home, we can create a base image with Go pre-installed. We'll start over with a copy phase, a rebuild phase, and then a development snapshot, using the same app as before. Start with phase.yaml:
version: v1
metadata:
name: dev-files
files:
- from: ./dev-files
to: /opt/dev
---
version: v1
metadata:
name: environment
from: dev-files
commands:
- shell: |
apt-get update
apt-get install -y curl
- comment: "Install the latest Go 1.27.x"
shell: |
v=$(curl -fsSL 'https://go.dev/dl/?mode=json' | jq -r '.[].version | select(startswith("go1.27."))')
curl -fsSL "https://go.dev/dl/$v.linux-amd64.tar.gz" | tar -xz -C /usr/local
ln -sf /usr/local/go/bin/go /usr/local/bin/go
services:
- name: setup-app
type: pre-start
context: user
shell: |
if [ ! -d /home/beams/parakeets ]; then
git clone [email protected]:example/parakeets /home/beams/parakeets
fi
cp /opt/dev/logo.svg /home/beams/parakeets/logo.svg
cp /opt/dev/.profile /home/beams/.profile
- name: dev-server
type: background
context: snapshot
shell: |
export GOCACHE=/tmp/go-build GOPATH=/tmp/go
go run /home/beams/parakeets/main.go
The copy stage uploads the files from your local dev-files directory:
local:~$ ls -a dev-files
. .. .profile logo.svg
Apply it and start an interactive build on top:
local:~$ tsh phase apply -f phase.yaml
local:~$ tsh phase add dev --from=environment
Phase "dev" created.
Linux dev-c7a91e48 6.18.56 #1 SMP Fri Oct 16 18:41:32 UTC 2026 x86_64
Beam ID: dev-c7a91e48
Beam UUID: c7a91e48-2b63-4f05-8d16-9a4e72c0b835
dev-c7a91e48:~$
The pre-start service clones the repo on first use, then copies the logo and shell profile into /home. The git repo state persists in the dev phase. The dev server starts in interactive builds, as selected by context: snapshot.
Once it's listening:
dev-c7a91e48:~$ beamctl logs dev-server
2026/11/06 14:32:08 serving static file /home/beams/parakeets/logo.svg
2026/11/06 14:32:08 listening on 127.0.0.1:8080
dev-c7a91e48:~$ curl http://127.0.0.1:8080/
...
<h1>Parakeet Book</h1>
...
dev-c7a91e48:~$ curl -I http://127.0.0.1:8080/logo.svg
HTTP/1.1 200 OK
Content-Type: image/svg+xml
...
# Have your agent work on the app, or whatever.
dev-c7a91e48:~$ tsh phase snapshot --final # and save when done
local:~$
We can build a pipeline that compiles the app, just by adding another rebuild phase on top. Put this in app.yaml:
version: v1
metadata:
name: app
from: dev
commands:
- shell: |
export GOCACHE=/tmp/go-build GOPATH=/tmp/go
go build -o /usr/local/bin/app /home/beams/parakeets/main.go
services:
- name: app
type: background
context: runtime
shell: exec /usr/local/bin/app
Apply it:
local:~$ tsh phase apply -f app.yaml
The novel part is that dev is still editable, right in the middle:
local:~$ tsh phase edit dev
Linux dev-e53b0f76 6.18.56 #1 SMP Fri Oct 16 18:41:32 UTC 2026 x86_64
Beam ID: dev-e53b0f76
Beam UUID: e53b0f76-94a2-487d-bc61-3f8a05d927e4
dev-e53b0f76:~$
That starts another interactive build, with the services running again. Ask Codex to change the page:
dev-e53b0f76:~$ cd parakeets
dev-e53b0f76:~/parakeets$ codex 'Change the page title to Phases, and add a short caption below the logo. Keep serving /logo.svg.'
dev-e53b0f76:~/parakeets$ tsh phase snapshot --final
local:~$
Saving dev rebuilds app. Your code and edits stay in /home, while Go can be updated underneath them. The setup service refreshes the logo and profile from the copied files on each start.
Start a stateless beam from the compiled app, then publish it:
local:~$ tsh beams add --phase=app --no-console
local:~$ tsh beams publish new-beam
The app service starts the compiled binary in a stateless beam (context: runtime). Publishing exposes its HTTP service on port 8080 through Beams app publishing and prints the app URL, automatically protected by Teleport's auth.
Phases are rebuilt continuously, with upstream changes triggering downstream changes. Only rebuild phases need to execute build commands during a refresh. Snapshot and copy phases are quickly stitched on top of new layers by updating image registry metadata.
By default, snapshots own /home, and rebuilds own everything else. You can also give a rebuild phase specific paths inside /home, like node_modules, using metadata.rebuild_home_paths. Later snapshots must leave those paths alone.
Phases provide a single model for container persistence and security patching that covers agentic development workflows, dev environments, building apps, and whatever else you do in containers.
Compatibility
Updating dependencies underneath a saved workspace is only useful if you can control which updates you get. For Beams, we start with a stable Debian base image, so OS security fixes arrive within an ABI-stable release. Rebuild phases rerun every 24 hours to pick up updates allowed by the commands you specify in phase.yaml, while snapshots preserve your work on top.
Tests can run as part of rebuilds as well, including long integration tests that require access to other services connected to Teleport. If the build fails, the previous published image stays available. You choose which updates to accept and how to test them in phase.yaml, and Phases handle rebuilding the environment around your saved work.
How it works
The Phases model can be implemented on any Linux-based platform that supports raw container primitives like OverlayFS, but here's how we've implemented it for Beams.
Builds run in a Kubernetes pod, isolated in a Cloud Hypervisor microVM using Kata Containers. The pod has an unprivileged (but VM-like) user container and a privileged builder container. Before user code starts, a trusted launcher passes open file descriptors for the user container's mount namespace, root, and cgroup to the builder over a Unix socket (SCM_RIGHTS). The builder enters that mount namespace, mounts an OverlayFS filesystem, and uses pivot_root to switch roots and detach the old root. The setup socket is then hidden from the user environment.
For snapshots, the builder freezes the workload's cgroup to quiesce filesystem writes, captures the writable layer, then thaws it before uploading. Image assembly, rebasing, and registry operations use the go-containerregistry (ggcr) library to manipulate layers and manifests directly on the registry.
Please try it out now at beams.run!
Also, check out beam-init, a new open source service manager for containers, written in Rust, that my team built along with Tweede golf to run services for beams. This backs the service types and service logging used by Phases and Beams.
Table Of Contents
Teleport Newsletter
Stay up-to-date with the newest Teleport releases by subscribing to our monthly updates.
Teleport Newsletter
Stay up-to-date with the newest Teleport releases by subscribing to our monthly updates.