A Docker-based dev environment that pulls in the same toolchain CI uses (OCaml/opam, Rust, Go, system libs). This is the recommended path when:
- You don't want to install Nix.
- Your host OS isn't covered by the native Linux/macOS guides.
- You want a one-command dev shell with everything pre-installed.
For other paths see the top-level README-dev.md.
Note: this container matches CI's toolchain, not its runtime. The dev container boots as root, runs a small UID-remap entrypoint, then drops to
opam. Use it for builds and iteration — for reproducing CI-specific runtime behavior, use the local CI runner inbuildkite/local/instead.
- Docker (with the
docker composev2 subcommand). - ~10 GB of free disk for the toolchain image + the opam switch volume.
- Optional: VS Code with the Dev Containers extension for the IDE path below.
cd dev
make start # pulls the toolchain image, boots the container
# in a second terminal:
make ssh # drops you into the container as `opam`, in /mina
# inside the container:
eval "$(opam config env)"
make build # build the daemonThat's it. Files you edit on the host are live in the container at /mina,
and files you create inside the container land back on the host with your
own UID/GID — no sudo chown step.
The container also bind-mounts the host Docker socket at
/var/run/docker.sock, so Docker commands inside the container use the host
Docker daemon rather than a nested Docker daemon. This is
Docker-outside-of-Docker, not DinD. It avoids privileged nested containers and
lets container workloads manage the same Docker Swarm/stack resources you
would see from the host with docker stack ls. The VS Code devcontainer init
script auto-detects DOCKER_SOCKET, DOCKER_HOST=unix://...,
/var/run/docker.sock, and $XDG_RUNTIME_DIR/docker.sock. For terminal-only
use, if your Docker socket lives somewhere else, set DOCKER_SOCKET before
starting the dev environment:
export DOCKER_SOCKET="$XDG_RUNTIME_DIR/docker.sock"
cd dev
make startmake prints the available commands. make stop stops the Compose service.
make rebuild wipes the named volumes and rebuilds from scratch — use it after
a major toolchain bump or when the opam switch gets into a weird state.
The VS Code Dev Container scaffold is opt-in — it's not at the repo root by default, to avoid pushing a "Reopen in Container?" prompt on anyone who'd rather use a different editor or their own setup. To enable it:
cd dev
make devcontainer # copies template → <repo>/.devcontainer/mina/ (gitignored)The scaffold lands under .devcontainer/mina/, not .devcontainer/
itself, so it coexists cleanly with any existing devcontainer config you
may already have in this repo (e.g. your own .devcontainer/devcontainer.json
or .devcontainer/<other-config>/). VS Code will list both as labeled
options when you "Reopen in Container".
With the Dev Containers extension installed:
- Open the repo in VS Code.
- Command Palette → Dev Containers: Reopen in Container → pick Mina toolchain (non-Nix) if prompted.
- Wait for first-time setup (image pull, opam-lsp-server install — ~5 min the first time, fast after).
- The integrated terminal lands you in
/minaasopam, with the OCaml Platform extension pre-installed.
What this gives you on top of the terminal flow:
- OCaml LSP-driven jump-to-definition, type-on-hover, go-to-references.
- An integrated terminal that's already inside the container.
- All extensions installed inside the container, so your host VS Code stays clean and reproducible across machines.
Shutting the VS Code window down stops the compose project; reopening reuses the persisted volumes (fast boot).
The generated .devcontainer/mina/ is yours — customize it freely
(extra extensions, settings, etc.). To refresh from the template, delete
that subdirectory and re-run make devcontainer. Your other configs at
.devcontainer/devcontainer.json or .devcontainer/<other>/ are
untouched.
Any editor with a "run / attach to docker container" workflow works:
- JetBrains (IntelliJ, CLion, etc.) — Start the container with
make, then in the IDE: File → Remote Development → Docker → Attach to running container → pickmina. - Neovim / other CLI editors —
make sshand edit from inside the container. Installocaml-lsp-serveronce viaopam install ocaml-lsp-serverif you want LSP features.
For all editor paths: the container's opam user has your host UID/GID
after the entrypoint remap, so editor file writes land with sane host
ownership.
The default image is pulled from
docker.io/minaprotocol/mina-toolchain,
the public mirror of the Mina toolchain images. CI itself pulls from an
internal Artifact Registry (see minaToolchain in
buildkite/src/Constants/ContainerImages.dhall)
which isn't publicly accessible, so the dev pin tracks the latest tag
mirrored to docker.io rather than CI's exact commit pin — close enough
for local development, and usually within a few commits of CI.
If the pinned tag ever returns manifest unknown (tags do age out),
browse the link above and pick another tag, then either edit
docker-compose.yml or set MINA_TOOLCHAIN_IMAGE for a one-off run:
export MINA_TOOLCHAIN_IMAGE=docker.io/minaprotocol/mina-toolchain:<tag>
makeYou can also use this knob to switch to bookworm / noble / focal
variants without editing the file.
The toolchain image's opam user is UID 65533, but the source tree is
bind-mounted from the host into /mina, so any writes from inside the
container would land on the host with that UID — which is none of the
things your shell calls "yours". To avoid that, the container uses a small
entrypoint (dev-uid-fixup.sh) that boots as root,
rewrites opam's entry in /etc/passwd//etc/group to match the host's
UID/GID, chowns the named volumes accordingly, then drops privileges via
setpriv. The host UID/GID are passed in either by dev/Makefile
(HOST_UID=$(id -u) HOST_GID=$(id -g)) for the terminal flow, or by
dev/devcontainer-template/init.sh
writing dev/.env for the VS Code flow (copied to .devcontainer/mina/
by make devcontainer).
The script lives in this directory and is reached through the repo
bind-mount at /mina/dev/dev-uid-fixup.sh — no toolchain image rebuild
is required. CI continues to use the stock image (USER opam, no
entrypoint override) and the entrypoint is a transparent passthrough
when started as non-root.
Don't bypass
make/ VS Code. If you rundocker compose updirectly without the env vars set, the entrypoint will refuse to start rather than silently pick UID 1000 (or worse, run as root and write root-owned files into your source tree). Usemakefor terminal use and Reopen in Container for VS Code.
First-boot cost. The first time you start fresh volumes, the entrypoint has to
chown -R /home/opam/.opam(the opam switch inside the named volume), which takes a few seconds on most images and up to ~1 minute on heavier ones. Subsequent boots short-circuit — the script checks the top-level target ownership and only recurses when out of sync.
- Always run compose against
dev/docker-compose.yml— either viamakeindev/, viadocker compose -f dev/docker-compose.yml ...from elsewhere, or via the VS Code Dev Container (which uses the copied.devcontainer/mina/devcontainer.json'sdockerComposeFilereference). The bind-mount (../:/mina) and entrypoint path (/mina/dev/dev-uid-fixup.sh) assume the compose file lives at<repo>/dev/docker-compose.yml. - Don't pass
-p(custom project name) — it would split your named volumes into a new namespace and force you to redo the first-boot chown.