A Docker image based on the Microsoft .NET devcontainer image with the GitHub CLI and Claude CLI added for isolated development environments.
The image is built automatically on every push to main and published to the GitHub Container Registry.
| Tool | Purpose |
|---|---|
| .NET SDK (latest) | .NET runtime + SDK (base image) |
| git | Version control |
GitHub CLI (gh) |
GitHub operations from the terminal |
| Bun | Runtime required by the Claude Discord plugin |
| Claude CLI | AI-powered coding assistant |
The easiest way to run the container on Unraid is with the included docker-compose.yml.
Place it anywhere accessible, e.g. /mnt/user/appdata/claude-dev/docker-compose.yml.
Create a .env file next to docker-compose.yml:
# Name this instance (defaults to "claude-dev").
# Use a unique name for each instance when running multiple side-by-side.
INSTANCE_NAME=claude-dev
# Optional: map file ownership to your Unraid user.
PUID=99
PGID=100
# Optional: pass tokens into the container environment.
# Useful for automation or tools that read tokens directly.
GH_TOKEN=
DISCORD_BOT_TOKEN=Interactive auth is still the recommended default for Claude CLI and GitHub CLI: sign in once inside the container and let the mounted home directories persist the CLI state across image updates. Environment variables are also supported when a tool inside the container expects them.
The INSTANCE_NAME variable controls the container name and the host volume
paths under /mnt/user/appdata/. Each unique name gets its own workspace and
home directories, so you can run several instances at the same time without
conflicts.
The compose file mounts these persistent paths:
| Host path | Container path | Purpose |
|---|---|---|
/mnt/user/appdata/<INSTANCE_NAME>/workspace |
/workspace |
Git repos, project files, local scripts |
/mnt/user/appdata/<INSTANCE_NAME>/home |
/home/devuser |
Non-root auth, dotfiles, user-space tooling |
/mnt/user/appdata/<INSTANCE_NAME>/root |
/root |
Root auth and config when using root shells |
docker compose pull # fetch the latest image from ghcr.io
docker compose up -d # start in the backgrounddocker exec -it --user 99:100 -e HOME=/home/devuser -w /workspace claude-dev bash
docker exec -it --user 99:100 -e HOME=/home/devuser -w /workspace my-project bashReplace 99:100 with your configured PUID:PGID. If you need a root shell,
drop the --user flag.
When opening a root shell from the Unraid console, claude will automatically
drop to the mapped PUID:PGID user so it can run without hitting Claude's
root/sudo safety restriction.
Run these commands inside the container shell:
claude auth
gh auth loginThose credentials are stored in the mounted home directories, so they survive container recreation and image updates.
If you set GH_TOKEN or DISCORD_BOT_TOKEN in .env, recreate or restart the
container after changing the file so the new values are loaded into the
container environment.
Container logs now show startup information and tool versions. View them from the Unraid Docker tab (Logs icon) or from the command line:
docker logs claude-dev # default instance
docker logs -f claude-dev # follow / stream logsTo run more than one instance, create a separate directory (or .env file) for
each instance with a unique INSTANCE_NAME:
# Instance 1 (default name)
INSTANCE_NAME=claude-dev docker compose up -d
# Instance 2
INSTANCE_NAME=claude-project-b docker compose up -dEach instance will have isolated workspace and home volumes under
/mnt/user/appdata/<INSTANCE_NAME>/.
The container respects the standard Unraid PUID / PGID environment variables so that files written inside the container are owned by the correct host user. The defaults in docker-compose.yml are:
| Variable | Default | Meaning |
|---|---|---|
PUID |
99 |
Unraid nobody user |
PGID |
100 |
Unraid users group |
Set both to 0 to run as root.
For local testing before pushing to GitHub, use the local-only compose file and helper script in this repo.
docker-compose.local.ymlbuilds from the current checkout instead of pulling from GHCR.local-dev.shwraps the common local test commands..docker-test/stores the local workspace and persisted home directories.
./local-dev.sh up
./local-dev.sh logs
./local-dev.sh shellThat flow will:
- build the image from the current
Dockerfile - start a local container named
claude-dev-local - persist test data in
.docker-test/workspace,.docker-test/home, and.docker-test/root
Useful local commands:
./local-dev.sh build
./local-dev.sh recreate
./local-dev.sh root-shell
./local-dev.sh down
./local-dev.sh cleanIf you want to run the compose commands directly instead of using the script:
mkdir -p .docker-test/workspace .docker-test/home .docker-test/root
docker compose -f docker-compose.local.yml up -d --build
docker exec -it --user "$(id -u):$(id -g)" -e HOME=/home/devuser -w /workspace claude-dev-local bashAnything stored in /workspace, /home/devuser, or /root survives image
updates because those paths are bind-mounted from the host.
This covers:
- cloned repositories and project files
- Claude CLI auth, settings, and local state
- GitHub CLI auth and config
- dotfiles and user-space tooling installed under the mounted home directory
What does not persist automatically is software installed into the container's
system image at runtime, such as apt install inside a running container. If
you want extra tools to survive recreation, install them into the mounted home
directory or keep them in /workspace.
Every push to main triggers the Build and Publish workflow, which:
- Builds the image and pushes it to
ghcr.io/dvkwong/claudedevelopmentdockerimage:latest - Automatically sets the package visibility to public so no authentication is needed when pulling from Unraid
No — the workflow makes the package public automatically after each build, so you can pull the image on Unraid without any credentials:
docker pull ghcr.io/dvkwong/claudedevelopmentdockerimage:latestThe first-time API call may fail if the package does not exist yet (it is created on the first push). After the first successful build, either:
- Re-run the workflow once from the Actions tab, or
- Manually set the visibility: GitHub → Packages → claudedevelopmentdockerimage → Package settings → Change visibility → Public
If you prefer to keep the image private, remove the Make package public step from the workflow and authenticate on Unraid with a GitHub Personal Access Token:
# On your Unraid server (via terminal or User Scripts plugin)
docker login ghcr.io -u YOUR_GITHUB_USERNAME -p YOUR_PATThe PAT only needs the read:packages scope. Once logged in, docker compose pull will work as normal.