Skip to content

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:

Variant Image Size Contains
Full ghcr.io/bino-bi/bino-cli:latest ~1.1 GB Everything, including Chromium and fonts
Slim ghcr.io/bino-bi/bino-cli:latest-slim ~410 MB Everything 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.

Property Value
Registry ghcr.io/bino-bi/bino-cli
Tags latest, X.Y, vX.Y.Z - each with a -slim counterpart
Architectures linux/amd64, linux/arm64
Base Debian bookworm (glibc)
Entrypoint bino
Working directory /work
User uid 1000, group 0
HOME /opt/bino
Exposed port 8080
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:

Platform Behaviour
macOS, Windows (Docker Desktop) The file-sharing layer maps ownership to your user. Generated files are yours; nothing to do.
Linux Files 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.

Variable Purpose
HOME /opt/bino in the image. Locates the pre-installed extensions and template engine.
BINO_DISABLE_UPDATE_CHECK Set 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_ROWS Maximum rows returned per query.
BNR_MAX_QUERY_DURATION_MS Maximum query runtime in milliseconds.
NO_COLOR Disable 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:

Feature Why
Remote project templates (bino init owner/repo) Fetched from the source repository.
bino registry Talks to the package registry.
Presentations The Reveal.js runtime is loaded from cdn.jsdelivr.net at view time.
DataSources reading HTTP, S3, or a database They reach your data, not ours.
bino update Do 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" }}'
Symptom Cause Fix
bino: executable file not found The entrypoint is already bino; you repeated the binary name. docker run --rm IMAGE build, not ... IMAGE bino build.
bino reports an empty project Nothing mounted at /work, or the wrong directory was mounted. Run from the bundle root and pass -v "$PWD:/work".
"google-chrome": executable file not found You 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 preview preview defaults to the container’s own loopback. Always pass --addr 0.0.0.0:8080 in a container.
Browser cannot reach bino serve serve defaults to 127.0.0.1. Always pass --addr 0.0.0.0:8080 in a container.
template engine ... not found engine-version in bino.toml is not the version in the image. Remove the pin, or match the bundled version.
Slides render blank or unstyled Reveal.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 refused localhost 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.