Core ConceptsBuild Pipeline

Build Pipeline

Lizard builds a container image from your source on its build nodes, then runs the image in an isolated pod. You do not need Docker locally. The rules below determine how Lizard builds the image. For a comparison of hosts that build images and hosts that require an image, see container as a service.

Build decision order

The platform picks exactly one build strategy, in this order:

  1. Repo Dockerfile at dockerfilePath: if dockerfilePath is set on the service, that Dockerfile is used unchanged, even when build or start commands are also set. A set startCommand still replaces the Dockerfile’s ENTRYPOINT and CMD at run time. See Deploy with a Dockerfile.
  2. Synthesized Dockerfile: otherwise, if buildCommand and/or startCommand are set on the service (or passed via lizard up --build-command / --start-command), Lizard generates a Dockerfile from those commands. lizardpack is not invoked.
  3. Dockerfile in the source: otherwise a Dockerfile at the root of the build directory is used (see the GitHub exception below).
  4. lizardpack auto-detect: otherwise the platform runs lizardpack, its buildpack/Dockerfile generator.

lizardpack auto-detect

lizardpack inspects your repo and builds a multi-stage image. Supported stacks, matched in this order:

Hugo → Go → Node → Python → Rust → Ruby → PHP → Java → static

See Framework guides for production scripts, adapters, output paths, and ports: Hugo, Go, Node (Node.js, Next.js, Nuxt, SvelteKit, Astro, React, Vue, Docusaurus, VitePress), Python (Django, FastAPI, Streamlit) and Ruby (Rails). Detection uses project files and dependencies; a repository that matches several providers follows this order. To pin a Node.js or Python version, see runtime versions.

On this path:

  • Uploaded source (lizard up) always uses a root Dockerfile as written.
  • GitHub source uses a root Dockerfile as written unless it looks incomplete: it has a COPY of build, dist, out, .next or public written without a trailing slash (COPY dist /usr/share/nginx/html) and no RUN line that starts with npm, yarn, pnpm, bun, pip, pip3, bundle, cargo, go build, mvn or gradle. lizardpack then generates a replacement.
  • With no Dockerfile, lizardpack generates one for you.
  • The start command is auto-detected: a Procfile web: line (Python/Ruby) or package.json scripts.start (Node) is picked up automatically. For Django, Flask and FastAPI start commands, see Python app hosting.
  • The port is inferred from EXPOSE, framework defaults, or an explicit PORT env.

Gotcha: On GitHub source, a Dockerfile that copies pre-built output (COPY dist /dest) and has no package-manager RUN step is treated as incomplete and regenerated by lizardpack. Add a real build step, or set dockerfilePath to force verbatim use. See Dockerfile changes don’t apply.

What triggers a rebuild

ActionRebuilds?
git push to the tracked branch✅ via GitHub webhook
lizard redeploy / lizard up✅ explicit
Changing VITE_* or NEXT_PUBLIC_* vars✅ rebuilds a running service; see build-time variables for when the values reach the build
service set of build fields (repoUrl, branch, sourceType, buildCommand, dockerfilePath, rootDirectory)✅ auto-rebuilds a running service; otherwise run lizard redeploy
service set of runtime-only fields (startCommand, preDeployCommand, containerPort, watchPatterns)❌ run lizard redeploy to apply
Any other env var / secret change❌ applied on a quick restart (no rebuild)

Don’t double-build. After a service set that changes a build field on a running service, Lizard starts a rebuild. Running lizard redeploy afterward cancels that build and starts it again (why). A crashed or never-deployed service does not rebuild on its own; run lizard redeploy.

Build-time variables

Lizard writes VITE_* and NEXT_PUBLIC_* variables into the Dockerfile as ENV lines in two cases:

  • GitHub services without dockerfilePath: the lizardpack Dockerfile or the repo’s root Dockerfile gets them.
  • Services with buildCommand or startCommand whose synthesized Dockerfile is a Node build.

Services deployed with lizard up and no build or start command get no Lizard variables at build time. No service gets build args other than COMMIT_SHA, so an ARG in your Dockerfile stays empty. For public values with lizard up, commit them in .env.production (check that .gitignore does not exclude it), write them into your Dockerfile, or, for a Node server app, set both buildCommand and startCommand.

Watching a build

Build logs stream during lizard up. For any service:

lizard logs --build            # the most recent build's logs
lizard events                  # deploy history + replica status

If a build fails, read lizard logs --build, fix the cause (in your repo, or by adjusting buildCommand / startCommand via lizard service set), then lizard redeploy.

Runtime notes

  • No Docker HEALTHCHECK. The runtime doesn’t run Docker’s healthcheck loop, so HEALTHCHECK directives are ignored. Lizard runs a TCP probe against your port instead (skipped in worker mode).
  • Don’t write a Dockerfile unsolicited. lizardpack auto-detects most stacks: try a deploy first, and only add a Dockerfile (or set dockerfilePath) if the auto-build doesn’t fit.

Build configuration reference

FieldDescription
buildCommandCommand to build your app → selects the synthesized Dockerfile path unless dockerfilePath is set
startCommandCommand to start your app at runtime; replaces the image’s ENTRYPOINT and CMD
preDeployCommandRuns at the start of every container (each replica, each restart), before startCommand; only runs when startCommand is set. Make it safe to run many times and at the same time
dockerfilePathPath to a repo Dockerfile to use verbatim; wins over buildCommand / startCommand for the image build
rootDirectorySubdirectory to build from (monorepos)
watchPatternsOnly redeploy when matching paths change
containerPortTCP port your app listens on (default 3000; 0 = worker)

Set any of these with lizard service set <svc> --set <field>=<value>. Clear a field with an empty value, such as --set buildCommand=. See lizard service.

Updated