Building from a Dockerfile¶
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 at docs/examples/build-demo contains a minimal Python app
plus a Dockerfile that exercises the directives most builds actually
use:
docs/examples/build-demo/
├── .dockerignore
├── Dockerfile
├── hello.py
└── src/
├── __init__.py
└── banner.py
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
ARG GREETING=hello
ENV APP_GREETING=${GREETING}
WORKDIR /app
COPY hello.py /app/hello.py
COPY src /app/src
ADD https://github.com/nebius/contree-cli/archive/refs/heads/master.zip /tmp/contree-cli.zip
RUN python -c "import sys; print('python', sys.version)"
RUN python -m zipfile -e /tmp/contree-cli.zip /opt/
RUN pip install --no-cache-dir /opt/contree-cli-master
RUN contree --help | head -20
RUN python /app/hello.py
Six directives, in order:
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:
# Demo .dockerignore: keep build context lean
**/*.log
**/__pycache__
.env*
The matcher uses the same rule set as 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:
contree build docs/examples/build-demo --tag contree-cli-build-demo:latest
You should see one log line per directive plus a stdout dump after
each RUN:
[INFO] FROM python:3.12-alpine -> tag:python:3.12-alpine
[INFO] COPY hello.py -> /app/hello.py
[INFO] COPY src -> /app/src
[INFO] ADD https://.../master.zip -> /tmp/contree-cli.zip
[INFO] RUN spawned op=019e... RUN python -c "import sys; print('python', sys.version)"
[INFO] stdout:
python 3.12.13 ...
[INFO] RUN spawned op=019e... RUN python /app/hello.py
[INFO] stdout:
+---------------+
| hello |
| contree build |
+---------------+
[INFO] tagged <uuid> as contree-cli-build-demo:latest
IMAGE TAG SESSION
<uuid> contree-cli-build-demo:latest build:<sha16>
The final tagged image is now usable everywhere a tag is accepted:
eval $(contree use tag:contree-cli-build-demo:latest)
contree run python /app/hello.py
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:
sha256(parent_layer_hash || state(workdir/env/user/args)
|| directive || pending_files)
That means a layer is reused if and only if:
the previous layer was identical,
the directive text is byte-for-byte the same,
the resolved environment (
WORKDIR,ENV,USER,ARG) matches, andfor
COPY/ADD, the content of the staged files matches (the SHA-256 of every uploaded file, not their timestamps).
Edit 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 list --filter build:
contree session use build:<sha16>
contree session show
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:
contree build docs/examples/build-demo --no-cache \
--tag contree-cli-build-demo:latest
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.
The demo declares ARG GREETING=hello and uses it through
ENV APP_GREETING=${GREETING}. Override it at the CLI:
contree build docs/examples/build-demo --build-arg GREETING=ciao \
--tag contree-cli-build-demo:ciao
The final 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:
ADD https://github.com/nebius/contree-cli/archive/refs/heads/master.zip /tmp/contree-cli.zip
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 |
|---|---|
|
Auto-imports missing tags; |
|
Shell-form and JSON exec-form. Spawns one instance per |
|
Honours |
|
Local paths behave like |
|
Accumulated and applied to subsequent steps. |
Directives that are parsed and skipped with a warning (the build continues, the image is still produced):
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 |
|
You want reproducible, cacheable setup driven from version control |
|
You are still experimenting and do not know the final steps |
|
You need |
Neither – those are runtime concerns for OCI runtimes, not for ConTree. |
You want multi-stage builds today |
Not yet – stage |
Cheat sheet¶
# Simplest build; finds ./Dockerfile in the context, tags the result.
contree build . --tag myapp:dev
# Out-of-tree Dockerfile.
contree build ./service \
--dockerfile ./service/Dockerfile.prod \
--tag svc:prod
# Override build-time variables.
contree build . \
--build-arg VERSION=2.5 \
--build-arg DEBUG=1
# Force a full rebuild.
contree build . --no-cache --tag myapp:dev
# Raise the per-RUN timeout to 30 minutes.
contree build . --timeout 1800 --tag myapp:dev
# Inspect the build's layer history.
contree session list --filter build:
contree session use build:<sha16>
contree session show
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.