Migrating from Docker Desktop to Nix DevShells
Migrating from Docker Desktop to Nix DevShells
docker run golang:1.24 go test is a VM, a daemon, and a bind mount to run a compiler. The boring default is: Nix devShell for go / gopls / linters; containers only for Postgres, Redis, and other long-running state.
Mental model
Docker Desktop for go test |
Nix nix develop --command go test |
|
|---|---|---|
| What runs | Linux VM + dockerd + container | Host go from /nix/store |
| Startup | Seconds to tens of seconds | Milliseconds after first substitute |
| Editor | Dev Containers / remote Linux | Local gopls on PATH |
| Files | Virtio/osxfs bind mounts | Native disk |
Keep Docker/Podman for databases. Do not keep it for compilers. Same pkgs.go as buildGoModule on 26.05; GOTOOLCHAIN=local so Go does not download another SDK.
Worked examples
Case 1: Replace a compile container
Was:
docker run --rm -v "$PWD":/src -w /src golang:1.24 go test ./...Save as flake.nix:
# flake.nix
{
description = "Native desk Go environment";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }:
let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in
{
devShells.x86_64-linux.default = pkgs.mkShell {
packages = [ pkgs.go pkgs.gopls pkgs.golangci-lint pkgs.delve ];
env.GOTOOLCHAIN = "local";
env.CGO_ENABLED = "0";
};
};
}nix develop --command go test ./...Output (shape):
ok desk/service 0.015s
Same tests, no VM.
Case 2: Time it once
time nix develop --command go version
time docker run --rm golang:1.24 go versionAfter Nix has substituted go, the first command should be far cheaper. Docker always pays daemon + create + start. Write the numbers in a comment in the flake so the next argument dies.
Case 3: Hybrid — Nix CLI, Podman Postgres
Save as start_db.sh:
# start_db.sh
#!/usr/bin/env bash
set -euo pipefail
podman rm -f desk-db >/dev/null 2>&1 || true
podman run -d --name desk-db \
-p 5432:5432 \
-e POSTGRES_PASSWORD=desk-dev \
postgres:16-alpineSave as the shell packages line:
# flake.nix fragment
{
packages = [ pkgs.go pkgs.postgresql pkgs.podman ];
}nix develop --command bash start_db.sh
nix develop --command psql postgres://postgres:desk-dev@127.0.0.1:5432/postgres -c 'SELECT 1'The server is a container. The client (psql, go) is Nix. Password is a dev secret; production uses sops.
Case 4: Editor stays on the host
nix develop --command which goplsVS Code / Zed / Neovim should see that path via direnv (use flake). Disable Dev Containers for this repo. If gopls is missing, add pkgs.gopls to the shell, not a golang image tag.
Case 5: When to keep Compose
Keep compose.yml for:
- Postgres + Redis + localstack as a set
- Matching production’s Linux userspace for a weird native addon
Do not keep golang / node / rust services in Compose if Nix already provides them.
# compose.yml — keep
services:
db:
image: postgres:16-alpine
ports: ["5432:5432"]
# delete:
# go:
# image: golang:1.24CI: nix develop --command go test (or buildGoModule doCheck), not docker compose run go.
The trap
The trap is moving the IDE into the container because “that is how we did Node.” You wanted PATH isolation. Nix already did that. Remote-container then fights file watchers and extensions.
The other trap is pinning golang:latest in CI and Nix go on the laptop. Same pin: 26.05 pkgs.go in both, or buildGoModule in CI and no golang image.
The boring rule
- Compilers and LSPs: Nix devShell.
- Stateful daemons: Podman/Docker.
- Time
go versionboth ways once; keep the faster one for the compiler. GOTOOLCHAIN=local. SamegoasbuildGoModule.- Dev Compose passwords stay out of production flakes.
Try this
- Case 2 timings; write the numbers in a comment in the flake.
which goplsafterdirenv allow.- Start Postgres with Case 3;
go testagainstlocalhost:5432without agolangcontainer. - Delete the
golang:service from Compose if it only ran tests.