contree build turns a familiar Dockerfile into a ConTree image. Each
directive becomes one image layer, every layer is a real session
checkpoint, and re-running the same Dockerfile reuses prior layers
through a content-addressed cache. This tutorial walks through the
build-demo example shipped with the repo so you can see the moving
parts end-to-end.
The example project
The tree atdocs/examples/build-demo contains a minimal Python app
plus a Dockerfile that exercises the directives most builds actually
use:
hello.py reads a greeting from an environment variable and prints a
boxed banner; src/banner.py provides the box renderer. Nothing
exotic – it just gives the Dockerfile something to COPY and RUN.
The Dockerfile itself:
FROM python:3.12-alpine– resolves the base image. Iftag:python:3.12-alpineis not already in the project,contree buildauto-imports it from the registry.ARG GREETING=helloandENV APP_GREETING=${GREETING}– declare a build-time variable and pin its value into a runtime environment variable that the app will read.WORKDIR /app– sets the working directory for everything below.- Two
COPYdirectives stage local files (hello.pyand thesrc/directory) into the build’s pending uploads. ADD https://...master.zip /tmp/contree-cli.zip– streams a remote archive straight from GitHub into the contree file store, without creating a local temp file.- Five
RUNdirectives prove the toolchain works, unpack the zip, install the CLI from source, and run the demo app.
Build context and .dockerignore
The first positional argument to contree build is the build
context – the directory that anchors every COPY and ADD source
path. Anything outside that directory is invisible to the build. In
this example the context is docs/examples/build-demo, and the
Dockerfile sits at the top of it.
A .dockerignore next to the Dockerfile keeps junk out of the upload:
run --file: * is a
single-segment wildcard, ** crosses directories, ? matches one
character, and a leading ! re-includes a previously ignored path.
The last matching rule wins. On top of your .dockerignore, the CLI
always filters .git, __pycache__, *.pyc, .venv,
node_modules, dist, and build, so you do not need to repeat the
usual suspects.
Your first build
From the repository root, run:RUN:
Layer cache: the second build is free
Run the same command a second time. Every step prints cache hit and the build finishes in seconds without spawning a single instance. The cache key for each layer is a chain hash:- the previous layer was identical,
- the directive text is byte-for-byte the same,
- the resolved environment (
WORKDIR,ENV,USER,ARG) matches, and - for
COPY/ADD, the content of the staged files matches (the SHA-256 of every uploaded file, not their timestamps).
hello.py, run the build again, and only the last RUN step
plus everything depending on it rebuilds. The earlier RUN python -c 'import sys; print(sys.version)' layer is reused because it has no
dependency on hello.py.
Each cached layer is materialised as a branch named
layer:<chain-hash> inside a session keyed by the absolute path of
the context directory: build:<sha16(abspath(context))>. So the
cache is per-context-path – moving the directory or building from
a sibling worktree starts a fresh cache.
To inspect the layer history:
contree session show prints the DAG with one row per layer and the
chain hash visible in the branch column. Switching to a layer: branch
puts you on that layer’s image, so you can contree run against any
intermediate snapshot to debug a step in isolation.
To force a rebuild ignoring all cached layers:
Build args and variable substitution
Variables ($VAR and ${VAR}) expand in FROM, RUN, COPY/ADD
arguments, WORKDIR, ENV values, and USER. The lookup order is:
--build-arg KEY=VALUEfor anyARGalready declared.ENVdirectives processed so far.ARGdefaults from the Dockerfile.- Empty string for unknown names.
ARG GREETING=hello and uses it through
ENV APP_GREETING=${GREETING}. Override it at the CLI:
RUN python /app/hello.py step now prints ciao in the
boxed banner because the chain hash of the layer that ran
ENV APP_GREETING=... changed, invalidating every layer below it.
ADD URL streams without a temp file
The ADD line in the demo points at a GitHub archive:
contree build opens the HTTP connection and pipes the response body
directly into POST /v1/files – the bytes never touch your local
disk. The CLI also remembers the URL’s ETag, Last-Modified, and
Content-MD5 validators in the per-context cache. On the next build
it issues a conditional HEAD first; if the validators match, the
upload is skipped entirely and the log line reads
URL cache hit (HEAD validators match).
Two things this does not do:
- It does not extract tarballs/zips. Use a
RUN python -m zipfile -e(ortar xf) directive when you need extraction, exactly like the demo does. - It does not follow private auth – the request is anonymous. Mirror
the asset to a public URL, or
COPYit from your build context.
Supported and skipped directives
The MVP interpreter implements the directives most Dockerfiles actually rely on:| Implemented | Notes |
|---|---|
FROM ref[:tag] [AS name] | Auto-imports missing tags; AS name is parsed but multi-stage is not yet executed. |
RUN ... | Shell-form and JSON exec-form. Spawns one instance per RUN. |
COPY [--chown=] [--chmod=] SRC... DEST | Honours .dockerignore, dedups by SHA-256. |
ADD ... | Local paths behave like COPY; URLs stream through POST /v1/files. |
WORKDIR, ENV, ARG, USER | Accumulated and applied to subsequent steps. |
CMD, ENTRYPOINT, LABEL, EXPOSE, VOLUME, STOPSIGNAL,
MAINTAINER, HEALTHCHECK, ONBUILD, SHELL,
COPY --from=stage.
ConTree images are filesystem snapshots, not OCI runtime configs, so
CMD/ENTRYPOINT have nowhere to live – you express the entrypoint
explicitly at contree run time instead.
When to reach for build vs run
The same image you can produce with contree build can be produced
by hand with a sequence of contree run calls. Pick the right tool:
| Situation | Prefer |
|---|---|
You already have a working Dockerfile | contree build – just reuse it. |
| You want reproducible, cacheable setup driven from version control | contree build. |
| You are still experimenting and do not know the final steps | contree run interactively; tag a checkpoint when you are happy. |
You need CMD/ENTRYPOINT/HEALTHCHECK semantics | Neither – those are runtime concerns for OCI runtimes, not for ConTree. |
| You want multi-stage builds today | Not yet – stage AS parses but is skipped. Track Phase 2. |
Cheat sheet
You now have a tagged image that came from a
Dockerfile, a cached
layer history you can branch off, and a feel for which directives
behave and which are parsed-but-skipped. Next, see
Scripting & Automation for scripting builds into pipelines, or
build - Build an image from a Dockerfile for the full reference.