Skip to content
Get started

zel build

Two things a Zellij site can be — a folder of files, or a container that serves itself.

terminal
zel build                        # a static site, into ./dist
zel build --target image         # a container image
zel build ./docs --out ./public  # a different content folder, a different output

There are two products, not four

A Zellij site can be one of two things, and the choice is about where the HTML is produced:

| --target static (default) | A folder of HTML, CSS and JS | Netlify, Cloudflare Pages, GitHub Pages, S3, any CDN | | --target image | A container image running the site on port 3000 | Railway, Fly.io, Cloud Run, ECS, any registry |

Everything else is a detail of those two.

static — the HTML already exists

Every route is rendered to a file at build time.

dist/
├── index.html
├── guide/
│   ├── index.html
│   └── quickstart/index.html
├── _next/static/…            # CSS and JS
└── _zellij/assets/…          # your images

A visitor requesting /guide/quickstart gets guide/quickstart/index.html straight from a CDN edge. No server runs. No process is warm or cold. Hosting is free nearly everywhere, and it cannot fall over under load, because there is nothing to fall over.

The one thing it gives up is next/image optimisation — images are served at the size and format you authored them in, because there is no server to resize them on request. For documentation that is almost always the right trade. For a marketing site built on large photography it is the wrong one.

image — a server produces the HTML

A Docker image containing the Next.js application and a Node runtime, running next start and listening on port 3000.

terminal
zel build --target image --tag acme-docs:v2
docker run -p 3000:3000 acme-docs:v2

Pages are still statically generated — the server is mostly handing over pre-rendered HTML. What the server adds is next/image on demand, response headers you control, and the ability to put the site behind whatever your platform does with a container.

Which one you want

Choose static

You are shipping documentation or a marketing site, you want it to cost nothing, and your images are already the right size. This is nearly everyone.

Choose image

You need image optimisation, your platform deploys containers, you have to sit behind a corporate proxy or an existing ingress, or the site must live where your other services live.

If you cannot decide, build static. Moving to image later is a flag, and the content does not change.

What happened to node and image:static

Earlier drafts of this documentation listed four targets. Two of them were not products:

  • node — a traced Node server, uncontainerised. That is an artefact for a platform to consume, not something you choose. Vercel builds it for you; on any other host you would put it in a container, which is image.
  • image:static — nginx serving the static folder from a container. That is the static site in a box: a packaging preference, not a different site. If you want it, build static and copy dist/ into whatever base image your organisation requires.

Offering four choices where there are two made the decision look harder than it is.

Options

[dir]The content folderResolved
--target <static|image>What to producestatic
--out <path>Output folder — static only./dist
--tag <name:version>Image tag — image only<site-name>:latest
--base-path <path>Serve under a prefix, e.g. /docsnone
--site-url <url>Absolute origin, for canonicals and the sitemapsiteUrl in site.yaml
--features <list>maths, diagrams, allfrom the content
--platform <list>Image architecturesthe host's
--no-checkSkip validation. Not recommendedoff

What you get

  Engine   ghcr.io/mylife-inc/zellij-base:latest
  Content  ./content
  Target   static → dist

  …

✓ dist  (19 entries)

  Serve it from any static host.
dist/
├── index.html          one file per route
├── guide.html
├── guide/
│   └── setup.html
├── 404.html
├── sitemap.xml
├── robots.txt
├── _next/static/…      CSS and JS, content-hashed
└── _zellij/assets/…    your images

What it actually does

  1. Resolves the content folder

    And fails immediately if it cannot find one.

  2. Validates it

    The same pass as zel check, inside the same container. A build that is going to fail on a broken link fails here, in a second, rather than after a four-minute compile.

    This is not a convenience. A site with a dead internal link is a site that should not have been built, and catching it at the end — or not at all — is how one gets published.

  3. Starts the engine container

    Pulling ghcr.io/mylife-inc/zellij-base if it is not already local, mounting your content folder read-only.

  4. Installs what this site needs

    The base image ships with nothing installed. Maths and diagram support are fetched only if your content uses them.

  5. Builds

    next build, with static export when the target is static.

  6. Writes the artefact

    The folder into --out, or the image into your local Docker daemon.

Steps 3 to 5 need a container runtime. zel doctor reports whether you have one.

See also

  • zel deploy — build and put it online in one step
  • zel image — building, running and pushing images in detail
  • zel serve — check a static build before it goes anywhere