Haskell Dev Container
The pre-built image ivelten/haskell-devcontainer (Docker Hub) provides a complete, zero-setup Haskell environment: GHC 9.10.3, Cabal 3.12.1.0, HLS, Hoogle, fast-tags, cabal-gild, ormolu, direnv, and Claude Code CLI — all pre-installed.
Detection — check before asking
When starting any Haskell project task, check whether the devcontainer is already configured:
grep -q "ivelten/haskell-devcontainer" .devcontainer/docker-compose.yml 2>/dev/null \
&& echo "devcontainer in use" || echo "not configured"
If .devcontainer/docker-compose.yml references the image → the environment is already set up. Acknowledge it and proceed. The toolchain versions match the defaults in haskell-project-setup; no local GHCup, mise.toml or .tool-versions is required for development (though a pin is still recommended for CI reproducibility).
When the devcontainer is not configured
The environment choice itself belongs to the haskell router skill, which weighs the container against the native mise + GHCup path (haskell-toolchain-mise). Load this skill once the container has been chosen.
What the container buys: zero setup, a disposable environment, parity with a Linux CI, and Docker services (Postgres) alongside the app. What it costs: on macOS every container is a Linux VM, so CPU and bind-mount I/O are slower than native.
Once chosen, create the files below. The user then opens VS Code and selects "Reopen in Container"; the image is pulled from Docker Hub automatically.
Files to create
.devcontainer/devcontainer.json
{
"name": "Haskell Development Environment",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/haskell",
"customizations": {
"vscode": {
"extensions": [
"haskell.haskell",
"justusadam.language-haskell",
"eriksik2.vscode-ghci",
"mkhl.direnv",
"usernamehw.errorlens",
"editorconfig.editorconfig",
"yzhang.markdown-all-in-one",
"davidanson.vscode-markdownlint",
"Anthropic.claude-code",
"phoityne.phoityne-vscode"
],
"settings": {
"haskell.manageHLS": "GHCup",
"haskell.ghcupExecutablePath": "/home/vscode/.ghcup/bin/ghcup",
"haskell.formattingProvider": "ormolu",
"editor.formatOnSave": true,
"terminal.integrated.defaultProfile.linux": "zsh"
}
}
},
"remoteUser": "vscode",
"postCreateCommand": "bash .devcontainer/post-create.sh",
"postStartCommand": "bash .devcontainer/post-start.sh",
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {
"installZsh": true,
"configureZshAsDefaultShell": true,
"installOhMyZsh": false
}
}
}
.devcontainer/docker-compose.yml
services:
app:
image: ivelten/haskell-devcontainer
command: /bin/sh -c "while sleep 1000; do :; done"
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
pids_limit: 2048
volumes:
- ..:/workspaces/haskell:cached
- ${HOME}/.claude:/home/vscode/.claude:cached
- ${HOME}/.claude.json:/home/vscode/.claude.json:cached
The cap_drop/security_opt/pids_limit trio keeps the container from acquiring privileges it never needs. Mounting ~/.claude and ~/.claude.json from the host makes the global skills, settings and the ormolu PostToolUse hook live inside the container.
.devcontainer/post-create.sh — runs once, when the container is created:
#!/bin/bash
# Install Claude Code if it is not already available
if command -v claude >/dev/null 2>&1; then
echo "🤖 Claude Code already installed, skipping."
else
echo "🤖 Installing Claude Code..."
curl -fsSL https://claude.ai/install.sh | bash
echo "🤖 Claude Code installation completed."
fi
.devcontainer/post-start.sh — runs on every container start:
#!/bin/bash
# Check if .envrc file exists and automatically allow it with direnv
if [ -f "/workspaces/haskell/.envrc" ]; then
echo "📁 Found .envrc file, running direnv allow..."
direnv allow /workspaces/haskell
echo "📂 direnv allow completed."
else
echo "📁 No .envrc file found, skipping direnv allow."
fi
.editorconfig
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.{hs,lhs}]
indent_style = space
indent_size = 2
[*.sh]
indent_style = space
indent_size = 2
[*.{yml,yaml,json}]
indent_style = space
indent_size = 2
direnv inside the container
The postStartCommand runs post-start.sh on every container start, which calls direnv allow automatically if .envrc is present. The image has direnv hooked into both bash and zsh, so entering any directory with an .envrc activates the environment immediately.
Add a .envrc at the project root for per-project env vars (database URLs, API keys, feature flags):
# .envrc — committed to the repo
export DATABASE_URL="postgres://localhost/myapp_dev"
.direnv/ (the cache directory) is gitignored.
Toolchain pinning with the devcontainer
The image pins GHC 9.10.3 and Cabal 3.12.1.0. For CI consistency, still add:
# .tool-versions
ghc 9.10.3
cabal 3.12.1.0
and with-compiler: ghc-9.10.3 in cabal.project. CI runs on a fresh GHCup install, not the image, so the pin keeps builds reproducible there — and it is the same pin the native path declares in mise.toml.
If the image is ever rebuilt on a newer GHC, the new version must appear in the HLS bindist list, or the container ships a broken language server. HLS targets specific GHC patch versions — see the compatibility rule in haskell-toolchain-mise. GHC 9.10.3 and HLS 2.14 are consistent today.
What the image does not ship
Pre-installed: GHC 9.10.3, Cabal 3.12.1.0, Stack, GHCup, HLS, Hoogle (index pre-generated), ormolu, fast-tags, cabal-gild, direnv, zsh.
Not pre-installed, despite being assumed by other skills: hlint and pre-commit (haskell-quality-gates), doctest (haskell-documentation), haskell-dap/ghci-dap/haskell-debug-adapter (haskell-debugging), ghc-prof-flamegraph/hp2pretty (haskell-benchmarking). Install them with cabal install inside the container when the corresponding workflow starts.
Related
haskell-project-setup— project layout,.cabaltemplate, language extensions.haskell-toolchain-mise— the native alternative to this container, and the HLS↔GHC compatibility rule.haskell-debugging— DAP setup (phoityne-vscodeextension, already in the devcontainer's VS Code extensions list, handles this).