Skip to content
GitHub

Running bino in Docker

The official container image ships the bino binary together with everything it needs to render: a Chromium build, fonts, the DuckDB extensions, and the template engine. Nothing is downloaded at runtime, so there is no bino setup step and the image behaves the same on your laptop, on a CI runner, and in an air-gapped cluster.

Use it when you would rather not install bino locally, when a team needs a pinned toolchain, or when you want to serve a LiveReportArtefact as a long-running service.

Two variants are published from the same source:

VariantImageSizeContains
Fullghcr.io/bino-bi/bino-cli:latest~1.1 GBEverything, including Chromium and fonts
Slimghcr.io/bino-bi/bino-cli:latest-slim~410 MBEverything except Chromium

The slim variant runs serve, lint, graph, lsp, and mcp. It cannot run build or preview, which need a browser to render. Use it for bino serve deployments, where a browser is never involved.

PropertyValue
Registryghcr.io/bino-bi/bino-cli
Tagslatest, X.Y, vX.Y.Z — each with a -slim counterpart
Architectureslinux/amd64, linux/arm64
BaseDebian bookworm (glibc)
Entrypointbino
Working directory/work
Useruid 1000, group 0
HOME/opt/bino
Exposed port8080
docker pull ghcr.io/bino-bi/bino-cli:latest
docker run --rm ghcr.io/bino-bi/bino-cli:latest version

The entrypoint is bino, so everything after the image name is a bino subcommand:

docker run --rm ghcr.io/bino-bi/bino-cli:latest --help

Mount the bundle root — the directory containing bino.toml — at /work:

docker run --rm -v "$PWD:/work" ghcr.io/bino-bi/bino-cli:latest lint
docker run --rm -v "$PWD:/work" ghcr.io/bino-bi/bino-cli:latest build --out-dir dist/

/work is a bind mount, not a copy, so the PDFs land in dist/ inside your real project directory.

Forward secrets and substitution variables with -e. Naming a variable without a value passes it through from your shell, so nothing sensitive ends up in your shell history:

docker run --rm \
  -v "$PWD:/work" \
  -e DB_HOST \
  -e POSTGRES_PASSWORD \
  ghcr.io/bino-bi/bino-cli:latest \
  build --out-dir dist/

These reach both the ${VAR} substitution in your manifests and the *FromEnv fields of a ConnectionSecret — see Using database data.

The container runs as uid 1000. What that means for the files bino writes depends on the host:

PlatformBehaviour
macOS, Windows (Docker Desktop)The file-sharing layer maps ownership to your user. Generated files are yours; nothing to do.
LinuxFiles are created as uid 1000. On a single-user machine that is usually already you.

On Linux with a different uid, override the user. The image's caches are group-0 readable, so any uid works as long as you keep group 0:

docker run --rm \
  -u "$(id -u):0" \
  -v "$PWD:/work" \
  ghcr.io/bino-bi/bino-cli:latest build --out-dir dist/

Typing the full docker run line gets old. Add a function to ~/.zshrc or ~/.bashrc:

bino() {
  docker run --rm -it \
    -v "$PWD:/work" \
    -p 8080:8080 \
    ghcr.io/bino-bi/bino-cli:latest "$@"
}

Now bino lint, bino build, and bino version behave as if bino were installed locally.

bino init creates its target directory relative to the working directory, so mount the parent:

docker run --rm -it -v "$PWD:/work" ghcr.io/bino-bi/bino-cli:latest \
  init standard -d monthly-report --title "Monthly Sales" --language en -y

The built-in minimal and standard scaffolds are embedded in the binary and need no network. Remote templates (bino init owner/report-template) do — see Project templates.

bino preview binds 127.0.0.1 by default. Inside a container that is the container's own loopback, so publishing the port with -p gives you a port with nothing behind it. Pass --addr to bind all interfaces instead:

docker run --rm -it -v "$PWD:/work" -p 8080:8080 \
  ghcr.io/bino-bi/bino-cli:latest preview --addr 0.0.0.0:8080

Then open http://localhost:8080/. This works the same on Linux, macOS and Windows — no --network host, which Docker Desktop does not support in a useful way anyway.

VariablePurpose
HOME/opt/bino in the image. Locates the pre-installed extensions and template engine.
BINO_DISABLE_UPDATE_CHECKSet to 1 in the image. Suppresses the 24-hour update check and its network call.
CHROME_PATH/usr/bin/chromium in the full image. Unset in the slim image.
BNR_MAX_QUERY_ROWSMaximum rows returned per query.
BNR_MAX_QUERY_DURATION_MSMaximum query runtime in milliseconds.
NO_COLORDisable ANSI colour — useful when logs go to a collector.

See Runtime limits and environment variables for the full list.

Everything below is baked into the image and needs no network at all:

  • The bino binary.
  • Chromium and a font set for PDF rendering (full variant) — you never run bino setup.
  • The DuckDB extensions excel, httpfs, postgres, mysql, prql, and webdavfs.
  • The bn-template-engine bundle.

You can verify an image end-to-end by cutting off its network entirely:

docker run --rm --network none -v "$PWD:/work" \
  ghcr.io/bino-bi/bino-cli:latest build --out-dir dist/

These features still reach the network:

FeatureWhy
Remote project templates (bino init owner/repo)Fetched from the source repository.
bino registryTalks to the package registry.
PresentationsThe Reveal.js runtime is loaded from cdn.jsdelivr.net at view time.
DataSources reading HTTP, S3, or a databaseThey reach your data, not ours.
bino updateDo not use it here — change the image tag instead.

Each image carries exactly one template engine version, recorded as an image label:

docker image inspect ghcr.io/bino-bi/bino-cli:latest \
  --format '{{ index .Config.Labels "bi.bino.engine-version" }}'
SymptomCauseFix
bino: executable file not foundThe entrypoint is already bino; you repeated the binary name.docker run --rm IMAGE build, not ... IMAGE bino build.
bino reports an empty projectNothing mounted at /work, or the wrong directory was mounted.Run from the bundle root and pass -v "$PWD:/work".
"google-chrome": executable file not foundYou are on the slim variant, which ships no browser.Use the full image for build and preview.
Generated files owned by uid 1000 (Linux)The container user differs from yours.Add -u "$(id -u):0" and leave HOME alone.
Browser cannot reach bino previewpreview defaults to the container's own loopback.Always pass --addr 0.0.0.0:8080 in a container.
Browser cannot reach bino serveserve defaults to 127.0.0.1.Always pass --addr 0.0.0.0:8080 in a container.
template engine ... not foundengine-version in bino.toml is not the version in the image.Remove the pin, or match the bundled version.
Slides render blank or unstyledReveal.js is fetched from a CDN at view time.Allow egress to cdn.jsdelivr.net, or ship the report as PDF.
unresolved variable ${...}The variable was not forwarded into the container.Add -e VAR or --env-file.
Database connection refusedlocalhost inside the container is the container itself.Use host.docker.internal, the Compose service name, or the real hostname.

Next: Recipe: Deploy a live dashboard puts bino serve behind Docker Compose and Kubernetes.