This reference outlines the Docker workflow for a Linux dev environment container on Windows. The workflow uses batch scripts to automate container management, emphasizes declarative updates via Dockerfile, versioned images, persistent containers for day-to-day use, and periodic rebuilds for dependency freshness or major changes. The workflow separates container starting from shell attachment to prevent accidental shutdowns when closing shells—all interactions use exec for shells, and the container runs a keep-alive process (e.g., tail -f /dev/null) to stay active independently.
- Images: Immutable, single image per configuration. Rebuild replaces the previous image.
- Containers: Persistent (no
--rm); usestop/startfor sessions. Create new ones only after image updates. Container runs detached with a keep-alive command to avoid shutdown on shell exits. - Persistence: Work files are stored in a subdirectory (configurable via
WORK_FOLDERin.env.container) that is mounted into the container, keeping Docker setup files separate from work. - Security: Secrets (API keys, tokens) are loaded from
secrets.bat(gitignored) or host environment variables, never committed to git. Container configuration (names, paths) is in.env.container(gitignored). - User Management: Fixed UID (2000) ensures consistent file ownership across rebuilds, preventing git "dubious ownership" warnings while preserving security checks for legitimate cross-platform issues.
- Shells: Always attach via
exec -it -u devuser; no "original" shell that can kill the container. - Commands: Run from host terminal (PowerShell/Command Prompt) using batch scripts.
Create .env.container from .env.container.example:
IMAGE_NAME=my-dev-env
CONTAINER_NAME=my-dev-container
WORK_FOLDER=work
# Optional: Port to expose (e.g., 8080:8080 for web servers)
# EXPOSE_PORT=8080:8080
# Optional: Persist Claude Code auth and settings across rebuilds
# CLAUDE_PERSIST_FOLDER=claude_persist
# Optional: Persist pi coding agent state (~/.pi) across rebuilds
# PI_PERSIST_FOLDER=pi_persist
Note: You can set WORK_FOLDER=. to use the current directory as the work folder. This is useful when you want the Docker setup files in a subdirectory.
Set secrets using secrets.bat file (recommended):
- Copy
secrets.bat.exampletosecrets.bat - Fill in your actual secret values in
secrets.bat - Place
secrets.batin either:- The current folder (same directory as
rebuild.bat) - The parent folder (useful when
WORK_FOLDER=.to keep secrets outside the mounted volume)
- The current folder (same directory as
- The
rebuild.batscript will automatically search both locations and load the file when building
Alternatively, set environment variables in your PowerShell session (or system-wide):
$env:ANTHROPIC_API_KEY = "sk-ant-your-key"
$env:GITHUB_TOKEN = "ghp-your-token"
$env:CLAUDE_CODE_OAUTH_TOKEN = "your-token"
$env:GIT_USER_NAME = "Your Name"
$env:GIT_USER_EMAIL = "your.email@example.com"
# Required if using GITHUB_TOKEN:
$env:GITHUB_USERNAME = "your-github-username"
$env:TZ = "America/New_York" # Override auto-detected timezone
# Optional: enables auto-start of `bs serve` inside the container
$env:BS_TOKEN = "your-beads-token"Note: The secrets.bat file is gitignored and will never be committed. It's the recommended way to manage secrets as it's less error-prone than setting environment variables manually each time.
- Build and Create Container:
rebuild.bat
- Removes old image if it exists
- Builds a new image (no version tags)
- Creates (but doesn't start) the container
- Automatically detects Windows timezone and passes it to container
- Passes environment variables (API keys, git config, etc.) to container
- Creates work folder subdirectory if it doesn't exist
- Mounts work folder to
/home/devuser/workin container
- Open Shell:
cbash.bat
- Starts container if not running
- Opens interactive bash shell as
devuser(UID 2000) - Automatically stops container when last shell exits
- Multiple shells can be open simultaneously (container stays running)
Always installed:
- Git: Configured from environment variables
- Python 3: With pip
- Go: golang-go from default apt
- Node.js: v24.x LTS with npm (NodeSource)
- Rust: Stable toolchain via rustup
- Build tools: build-essential, curl, vim, tmux, jq, gosu
- Go binaries (via
go install, on$PATHunder~/go/bin):bs— beads server (github.com/vector76/beads_server/cmd/bs)ray— raymond (github.com/vector76/raymond/cmd/ray)clusage-cli— Claude Code usage dashboard (github.com/vector76/cc_usage_dashboard/cmd/clusage-cli)
Conditionally installed:
- GitHub CLI (
gh): installed if bothGITHUB_USERNAMEandGITHUB_TOKENare set - Claude Code: installed if
CLAUDE_CODE_OAUTH_TOKENorCLAUDE_PERSIST_FOLDERis set - pi (
@earendil-works/pi-coding-agent): installed ifPI_PERSIST_FOLDERis set
- Git user: Configured from
GIT_USER_NAMEandGIT_USER_EMAILenvironment variables - GitHub authentication: Uses
GITHUB_TOKENandGITHUB_USERNAMEfor authenticated git operations - Azure DevOps authentication: Uses
AZURE_DEVOPS_PATfor HTTPS git operations againstdev.azure.com(one PAT covers all projects/repos in the org it was issued from). SetAZURE_DEVOPS_ORG=<org>as well if your repos use the legacy<org>.visualstudio.comURL format. - Timezone: Auto-detected from Windows, can be overridden with
TZenvironment variable - Beads server: If
BS_TOKENis set, the entrypoint launchesbs servein the background from the work folder on every container start, logging to/tmp/bs.log. LeaveBS_TOKENunset to skip.
- Fixed UID: User
devuseralways has UID 2000 for consistent ownership - Windows mount fix: Entrypoint automatically fixes work directory ownership when owned by root (Windows mount artifact)
- Git security: Preserves git "dubious ownership" warnings for legitimate cross-platform issues while preventing false positives
These optional .env.container settings preserve agent login and settings across image rebuilds by bind-mounting just the relevant dotfiles from a host folder — much faster than persisting the entire home directory.
| Variable | Mounts into container | Notes |
|---|---|---|
CLAUDE_PERSIST_FOLDER |
~/.claude/ and ~/.claude.json |
Also triggers Claude Code install |
PI_PERSIST_FOLDER |
~/.pi/ |
Also triggers pi install |
Each setting points at a host folder; rebuild.bat creates the subfolders it needs on first run.
- When to Update: Periodically (e.g., to refresh pip packages) or for major additions (e.g., new tools in Dockerfile).
- Steps:
- Edit
Dockerfileif adding/changing installs. - Rebuild:
- Edit
rebuild.bat
-
Automatically stops and removes old container
-
Removes old image
-
Builds new image
-
Creates new container from updated image
-
Work folder is preserved (not removed)
-
Optional Cleanup (if needed):
docker system prune # Prune dangling items (confirm prompt)
- List containers:
docker ps -a - List images:
docker images - Logs:
docker logs my-dev-container - Check container status:
docker inspect my-dev-container - If runtime changes need saving: Update Dockerfile instead of committing (avoid
docker commit). - Git "dubious ownership" warning: Should not occur with fixed UID. If it does, check ownership with
ls -ld /home/devuser/workandid. - Timezone issues: Check
TZenvironment variable or verify auto-detection inrebuild.bat.