Skip to content
Get started

Containers

How a site gets built without Node on your machine, and what the base image is.

Zellij's engine is a Next.js application, and building a site means running it. Rather than require Node, a matching version and a lockfile on every machine that writes documentation, zel runs the engine in a container.

terminal
zel dev            # the engine, serving your folder
zel build          # the engine, producing a site

That is the whole of what most people need to know. You do not write a Dockerfile, and there is not one in your repository.

The base image

ghcr.io/mylife-inc/zellij-base is public, even though Zellij's source is not. A site can be built and published by anyone, with no access to the engine's code.

It is a Next.js wrapper with the engine at /opt/zellij and nothing installed — around 237 MB. It carries a manifest describing what a site will need, and the install happens when a site is built.

That is deliberate. The image people pull is a quarter of a gigabyte rather than a whole one, and a site build resolves its own lockfile regardless — the cost moved, it did not appear. It also means one image serves a site that wants LaTeX, a site that wants diagrams, and a site that wants neither.

The wrapper inside it is generated by the same scaffold zel new app uses, so it cannot drift from what you would get locally.

Optional features

Mathematics costs about fifty megabytes to install and diagrams about a hundred, and most documentation wants neither. Both are optional: a site that does not ask for them never fetches them, and the engine renders without them rather than failing.

FeatureTurns onCosts
maths$…$ and $$…$$~50 MB at build time, nothing at runtime
diagrams```mermaid fences~100 MB at build time, ~10 MB in the browser
allboth

zel detects which your content uses and installs accordingly. --features overrides it when you want the dependency present before the content that needs it.

Four published variants exist — :latest, :latest-maths, :latest-diagrams and :latest-all — for pipelines that would rather name an image than pass a flag. Because nothing is installed in the base image they are all the same size.

Producing a container of your own site

Different thing, same word. The base image builds sites; you may also want the site itself to be an image.

terminal
zel image build --tag acme-docs:v2
zel image run acme-docs:v2
zel image push acme-docs:v2 --registry ghcr.io/acme

That produces a Node runtime serving your site on port 3000, deployable to Railway, Fly.io, Cloud Run, ECS or anywhere else that runs containers. See zel image for the detail, and zel build for choosing between that and a static site.

Whichever zel writes, the runtime stage copies only the compiled output — so neither the engine nor node_modules reaches what you deploy. The engine is genuinely absent from the image you ship, not merely unused in it.

In a pipeline

A repository with its source and a docs/ folder beside it needs no access to Zellij, and no Docker commands:

terminal
zel ci init github --target image --registry ghcr.io
.github/workflows/docs.yml
      - name: Install zel
        run: |
          curl -fsSL https://raw.githubusercontent.com/mylife-inc/releases/main/zellij/install.sh | sh
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"
      - run: zel check docs
      - run: zel image build docs --tag ghcr.io/acme/docs:${{ github.sha }}
      - run: zel image push ghcr.io/acme/docs:${{ github.sha }}

The only credential is the token GitHub already gives the job. See zel ci.

Requirements

Any OCI runtime. zel looks for Docker, Podman and Colima, in that order.

terminal
zel doctor

reports which one it found and on which socket — the usual cause of "Docker is not running" is a runtime that is running, on a socket the default context does not look at.

The commands that only read the content model — explain, list, context, detect, convert, new — need no runtime at all. They are pure Rust with the schema compiled in.

If you have to own the build

Some organisations require a Dockerfile in the repository, reviewed like any other file. zel image dockerfile prints the one zel would have used, with your arguments resolved.

It becomes a normal file at that point, with the usual consequence: it stops tracking changes to the base image, and updating it is yours.

Getting the wrapper app out

If you want the Next.js app itself rather than a site built from it, zel new app writes one. Extracting it from the image is possible and is not the supported path:

terminal
docker create --name tmp ghcr.io/mylife-inc/zellij-base:latest
docker cp tmp:/app ./my-site
docker rm tmp

See also