Claude Code in a Container: Podman, Docker and Shared Setups
How to run Claude Code in Podman (rootless/rootful) and Docker, share containers and ~/.claude directories across multiple users, and set permissions cleanly with groups, setgid and default ACLs.
by Jean Pierre Kolb ·
Claude Code runs fastest installed where you work: directly on the host. But "directly on the host" isn't always the best idea. The moment you let the agent work unattended, point it at unfamiliar code, or want to offer it to several people on one machine, a container turns from a nice-to-have into a must. This article walks the full range: from the official dev container through hand-built Podman and Docker setups to the scenario I cared about most — one Claude Code installation shared by several user accounts on the same machine, including a shared ~/.claude configuration.
This is deliberately a beginner-friendly tour. If you're already a container pro, skip the fundamentals and jump straight to the permissions and sharing chapters. If you're not, I'll take you by the hand: every concept gets context, every command an explanation.
Why run Claude Code in a container at all?
Claude Code is an agentic tool. It reads files, runs shell commands, calls tools — and the less it has to ask you for permission, the more productive it is, but the larger the blast radius a mistake or a malicious prompt can reach. That's exactly where containerization comes in. It draws a boundary around what the agent can see, write, and reach.
Four concrete reasons it's worth the effort:
- Reproducibility. A container image defines the exact environment — Node version, tools, dependencies — identically for everyone. "Works on my machine" stops being an argument.
- Isolation for autonomous operation. Only a boundary around the whole process makes modes like
--dangerously-skip-permissionsor unattended runs defensible. Without a container, that switch is plainly a free pass over your entire home directory. - Working on untrusted code. A foreign repository can contain prompt-injection traps. Inside a container the damage can barely escape the project directory and whatever the network policy allows.
- One installation for many. Instead of installing, updating, and configuring Claude Code in every user account separately, you provide it once as an image. That's the through-line of the second half of this article.
Anthropic explicitly recommends containers as an extra isolation layer — and ships an important principle with it: isolation reduces the impact of a breach, but it doesn't eliminate the risk. Whatever goes to the Anthropic API goes to the API with or without a container. A container is a wall, not magic.
What this looks like in principle
Before we dive into variants, the shared pattern. Whatever the approach, three things meet inside the container.
- Your project directory is bind-mounted into the container. The container sees your code, and changes Claude makes appear immediately in your real repository on the host. The code doesn't "move in"; it's just passed through.
- Claude Code itself is installed in the image and runs as a process inside the boundary. Every shell command the agent issues runs in the container — not on your host.
- The state under
~/.claude— authentication, settings, history, skills — lives either in a mounted directory or in a container volume, so it survives a rebuild.
Editors like VS Code, Cursor, or JetBrains that support the Dev Containers spec hook into exactly this container: you edit in the UI as usual, but the terminal, language servers, and build tools run inside. A pure terminal workflow (podman run … claude) works just as well — only without the IDE integration.
Remember the two moving parts: the project (comes in from the host, writable) and the config (~/.claude, persisted). Almost every nuance in this article revolves around who may access these two parts, and with what permissions.
The isolation levels at a glance
Containers aren't the only way to fence in Claude Code — and not the right one for every purpose. Anthropic describes a whole ladder of isolation levels, from lightweight to maximally separated. It's worth knowing so you don't crack a nut with a sledgehammer:
| Approach | What's isolated | Docker needed? | Effort |
|---|---|---|---|
Bash sandbox (/sandbox) | Bash commands and their child processes only | No | Minimal |
| Sandbox runtime | The whole Claude Code process (file tools, MCP, hooks) | No | Low |
| Dev container | Full development environment | Yes | Medium |
| Custom container | Full development environment | Yes | Medium–high |
| Virtual machine | Full operating system | No | High |
| Claude Code on the web | Full OS, hosted by Anthropic | No | None (subscription required) |
The first two run without containers, directly on the host OS. The Bash sandbox (built in, enabled with /sandbox) uses OS primitives — bubblewrap on Linux and WSL2, Seatbelt on macOS — and restricts only shell commands. File tools, MCP servers, and hooks still run unrestricted. It's ideal for fewer permission prompts in everyday work on your own machine, but it isn't enough for fully autonomous operation.
The sandbox runtime (@anthropic-ai/sandbox-runtime, a research preview) wraps the same bubblewrap/Seatbelt cage around the entire process. It denies all write and network access by default; in ~/.srt-settings.json you allow your project directory, the config paths ~/.claude and ~/.claude.json, and the domains you need. You launch it with npx @anthropic-ai/sandbox-runtime claude.
Everything from dev container downward puts Claude Code inside a container or VM. Those levels are the subject of this article. The VM offers the strongest separation (its own kernel) and is the choice for genuinely untrusted code; Claude Code on the web is an Anthropic-hosted VM with an egress proxy for anyone who'd rather not set up a local environment at all.
Key point: Permission modes (may this run, do I get asked first?) and isolation (what can a command reach once it runs?) are two different things that work together.
--dangerously-skip-permissionsturns off per-action review — then the isolation boundary is the only thing still limiting the agent. So use that switch only inside a container, VM, or the sandbox runtime.
Fundamentals for beginners
Four terms come up constantly from here on. If you already know them, skip to the next chapter.
Rootless vs. rootful. Classically (rootful) the container daemon runs as root, and processes in the container are real root on the host — a well-known attack surface. Rootless means: containers run under your normal user account, with no root daemon at all. Podman can do both but is built for rootless operation; Docker is traditionally rootful (there's a rootless mode too, but it's a side note here).
User namespaces. The mechanism behind rootless. A user namespace maps user IDs inside the container to different IDs on the host. root (UID 0) in the container is then, say, your host user (UID 1000) — powerful inside, harmless outside. This mapping is why file permissions between host and container sometimes surprise you.
UID/GID. Linux knows users and groups only as numbers: the user ID (UID) and the group ID (GID). Names like alice or shared are just labels pointing to those numbers via /etc/passwd and /etc/group. For file permissions, the number is what counts. This matters in a moment, because a group name the host knows often doesn't exist in the container image at all — the numeric GID, on the other hand, works everywhere.
Bind mount vs. named volume. A bind mount maps a specific host path into the container (-v /home/alice/project:/workspace) — ideal for your code and for shared config. A named volume is engine-managed storage with no fixed host path (-v claude-config:/home/node/.claude) — ideal for keeping state across rebuilds, e.g. in a dev container.
Approach A — The official dev container
Anthropic ships a ready-made way to put Claude Code into a dev container. It's the most convenient entry point if you work with VS Code, Codespaces, or a JetBrains IDE, and at the same time the reference we'll measure the hand-built setups against.
In the simplest case it's a three-liner. You create .devcontainer/devcontainer.json and pull in the Claude Code Dev Container Feature:
{
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}
}
}The version tag :1.0 pins the feature's install script, not the Claude Code version — the feature installs the latest Claude Code, and that auto-updates inside the container by default. "Rebuild Container", run claude in the integrated terminal, sign in via the browser — done.
Three building blocks turn this minimal setup into serious isolation; they live in the reference container of the anthropics/claude-code repo (three files: devcontainer.json, Dockerfile, init-firewall.sh):
1. Persistent authentication. The container's home directory is discarded on every rebuild — you'd have to sign in each time. A named volume on ~/.claude fixes that:
"mounts": [
"source=claude-code-config,target=/home/node/.claude,type=volume"
]/home/node is the home of the default node user in the image. To isolate state per project rather than sharing it across all repos, use source=claude-code-config-${devcontainerId}. If the volume lives somewhere other than ~/.claude, set CLAUDE_CONFIG_DIR to the mount path.
2. Network egress firewall. The reference container ships a script init-firewall.sh that uses iptables to block all outbound traffic except the domains Claude Code and your tools need (default deny). Running a firewall inside a container needs extra permissions, so the reference grants the NET_ADMIN and NET_RAW capabilities via runArgs. Neither is required for Claude Code itself — you can leave them out and rely on your own network controls.
3. Non-root user. The container runs as a non-root user (node), and that's the precondition for the decisive convenience: because command execution is confined to the container and doesn't run as root, you may set --dangerously-skip-permissions for unattended operation. The CLI refuses that flag when launched as root — so remoteUser must be a non-root account.
Organization policy can be anchored centrally with ease: on Linux, Claude Code reads /etc/claude-code/managed-settings.json at the highest precedence (overriding anything from ~/.claude or the project). Via containerEnv you set global variables such as CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC and DISABLE_AUTOUPDATER.
Where the dev container ends
The official dev container is excellent for one developer on one project with one IDE. That's exactly where its limits are, too — and they're the reason for the rest of this article:
- It's tied to an editor. Pure terminal operation, cron jobs, a shared server login without an IDE — it isn't built for that.
- It thinks per user and per project, not "one installation, many people." The
claude-code-configvolume belongs to the container, not to a shared host group. - Anthropic explicitly warns: with
--dangerously-skip-permissions, a malicious project can exfiltrate everything reachable inside the container — including the credentials stored in~/.claude. So don't mount host secrets like~/.sshor cloud credential files into it.
Take that last point seriously: it's the core of the sharing problem we tackle next. Whoever has access to ~/.claude potentially has access to the OAuth token.
Approach B — Rootless Podman
Podman in rootless mode is my preferred path for hand-built setups: no daemon, no root privileges, containers run under your normal account. You need neither an IDE nor the dev-container spec — a single podman run does it.
A minimal Dockerfile (Podman reads the same format):
FROM node:22-bookworm-slim
# Create a non-root user; Claude Code refuses --dangerously-skip-permissions as root
RUN useradd --create-home --shell /bin/bash dev
# Install Claude Code globally, pinned for reproducible builds
RUN npm install -g @anthropic-ai/claude-code@2.0.0
USER dev
WORKDIR /workspace
ENTRYPOINT ["claude"]Build and run:
podman build -t claude-code:local .
podman run --rm -it \
-v "$PWD":/workspace:Z \
-v "$HOME/.claude":/home/dev/.claude:Z \
claude-code:localTwo details that often trip up beginners:
- The
:Zon the mount tells Podman to relabel the directory for SELinux. On SELinux systems (Fedora, RHEL) you'd otherwise get "permission denied" even though the Unix permissions are correct. On systems without SELinux (many Debian/Ubuntu installs, WSL2):Zis a no-op but harmless. - You're mounting your own
~/.claudehere. For the single-user case that's exactly right. For the shared multi-user case we change this fundamentally in a moment.
The real rootless pitfall is group membership. By default Podman maps your host user's supplementary groups onto nobody in the user namespace — so the container loses every extra group membership. The moment you want to write to a group-protected directory (precisely our sharing scenario) you need:
podman run --group-add keep-groups ...keep-groups (technically the annotation run.oci.keep_original_groups=1) preserves the host GIDs for filesystem permission checks. Since your host user is in the shared group, the container process may then write. We come back to this in chapter 11.
Practice · WSL2: Under WSL2, rootless Podman runs without trouble. SELinux isn't active there, so you can drop
:Z. But make sure your project and~/.claudelive in the Linux filesystem (/home/...), not under/mnt/c/...— mounts across the Windows boundary are slow and their permission semantics (no realchmod/ACL) break everything this article says about groups and ACLs.
Approach C — Rootful Docker
With classic, rootful Docker the situation is different — and trickier in one respect. Container GIDs map directly to host GIDs here. That makes sharing a group easier on one hand (no nobody mapping) but harbors the biggest trap of the whole topic on the other: if the container process writes as root, the files it creates are owned root:root on the host — breaking any group model.
The clean variant runs the process as your user and passes the shared group as a numeric GID (the group name usually doesn't exist in the image):
docker run --rm -it \
--user "$(id -u):$(id -g)" \
--group-add 1234 \
-v "$PWD":/workspace \
-v "$HOME/.claude":/home/dev/.claude \
claude-code:localWith Docker Compose the same looks like this:
services:
claude:
image: claude-code:local
user: "1000:1000" # your host UID:GID
group_add:
- "1234" # numeric GID of the 'shared' group
volumes:
- ./:/workspace
- ${HOME}/.claude:/home/dev/.claude
stdin_open: true
tty: trueYou find the 1234 on the host with getent group shared | cut -d: -f3. Write it numerically — rely on the name and it fails, because the slim image has no shared line in /etc/group.
Practice · DDEV: If you work with DDEV, this part is already solved. DDEV maps the container user onto your host user anyway. As long as the mounted directory carries the permissions described in chapter 11, you can run Claude Code there without further
--user/group_addacrobatics.
Core scenario: one installation, many users
Now for the actual goal. Picture a shared development server — or simply a family or team machine — where several people work with their own Linux accounts. You do not want to install, keep updated, and configure Claude Code in each account separately. Instead:
- One image, maintained centrally. You build
claude-code:localonce (or pull it from an internal registry). Updating means: rebuild the image, not touch n accounts. Version pinning in the Dockerfile makes the environment identical for everyone. - Each user starts the same container through their own account. With rootless Podman the container runs under the respective host user — so account separation is preserved even though the software is shared.
- What's shared is the installation and (parts of) the configuration, not the identity. That's the important distinction: a shared code base yes, a shared login only with care (see chapter 10).
The crux isn't the image — that's trivial to share — but the shared directories on the host: a shared skill/command/settings set, perhaps a common project directory. Several accounts that should write to the same files without locking each other out — that's a classic Unix permissions problem, and it isn't solved by "just put everyone in one group." Why, and how to do it right, is the next two chapters.
Sharing ~/.claude — but correctly
The temptation is strong to simply share the entire ~/.claude directory across everyone: sign in once, maintain skills once, everyone benefits. Before you do, you must know what's in that directory — because not everything may be shared.
~/.claude (relocatable via CLAUDE_CONFIG_DIR) holds roughly three kinds of content:
Non-sensitive and safely shareable (configuration & extensions):
settings.json— permissions, hooks, model defaults, environment variablesCLAUDE.md— your global instructions across all projectsskills/,commands/,agents/,rules/,output-styles/,workflows/— reusable extensionsplugins/— installed plugins and marketplacesthemes/,keybindings.json— appearance and shortcuts
Per user and sensitive (identity & history) — do NOT share:
.credentials.json— the OAuth token. On Linux it's a file, protected by file permissions only, not encrypted. Anyone who can read it can impersonate that account.projects/<project>/<session>.jsonl— the session transcripts. Everything Claude read during a session lands here — if a tool reads a.envor a command prints a secret, that value is in the transcript.history.jsonl— your prompt history.claude.json(in the home, next to the directory) — app state and UI preferencesshell-snapshots/,file-history/— transient session state and pre-edit snapshots
From this follow two legitimate paths — and you have to choose one deliberately.
Path 1 (recommended): share config, separate identity
The clean split: you share only the non-sensitive configuration read-only and give each user their own writable ~/.claude for authentication, transcripts, and history.
In practice: the shared set (skills, commands, agents, a base settings.json) lives in a common directory like /srv/claude-shared/ and is mounted read-only into the container. The writable, private part lives per user and is set as the main directory via CLAUDE_CONFIG_DIR:
podman run --rm -it \
-e CLAUDE_CONFIG_DIR=/home/dev/.claude \
-v "$HOME/.claude":/home/dev/.claude:Z \
-v /srv/claude-shared/skills:/home/dev/.claude/skills:ro \
-v /srv/claude-shared/commands:/home/dev/.claude/commands:ro \
-v /srv/claude-shared/agents:/home/dev/.claude/agents:ro \
claude-code:localEveryone signs in once with their own account (.credentials.json stays private), nobody sees the others' transcripts, but all share the same curated skill and command set. When an admin updates a skill in /srv/claude-shared/, everyone has it on next start. The read-only :ro prevents the agent from accidentally altering the shared set.
Path 2: share the whole ~/.claude — with open eyes
There are situations where a shared account is wanted — say a dedicated team/service account with its own subscription, used unattended by several people. Then you share the entire ~/.claude as one writable mount (you build the permissions for that in chapter 11). It works technically — but buy it with a clear view of the risks:
- Shared token = shared identity. All actions run under the same account; any accountability for who did what is gone. The token sits unencrypted and is readable by anyone with access.
- Transcripts for everyone. Every
projects/.../*.jsonlis visible to all group members. If Claude reads a secret in any session, everyone sees it. - Concurrency. Multiple parallel sessions on the same state directory can collide (history, snapshots). For genuine parallel operation, path 1 is more robust anyway.
If you go path 2, do it deliberately and narrowly: a purpose-made account, a tight egress filter (chapter 12), and never mixed into the private home directories of real people. For most setups, path 1 is the right call; path 2 is the tool for the special case "one machine, one bot account, several operators."
Permissions & groups in detail
Now the technical core — and the spot where most "just one group" solutions fail. A shared group is the right foundation, but it isn't enough on its own: newly created files get their creator's primary group by default and, thanks to umask 022, no group write bit. For a shared directory to work permanently, you need three building blocks on the host plus separate handling in the container.
Base: group + setgid + default ACLs
# 1. Create the group and add users
sudo groupadd shared
sudo usermod -aG shared alice
sudo usermod -aG shared bob
# Note: membership takes effect only after re-login (or `newgrp shared`)
# 2. Directory with the setgid bit
sudo mkdir -p /srv/shared
sudo chown root:shared /srv/shared
sudo chmod 2775 /srv/shared # leading 2 = setgid
# 3. Default ACLs (the actual key)
sudo setfacl -R -m g:shared:rwx /srv/shared # existing entries
sudo setfacl -R -d -m g:shared:rwx /srv/shared # default for new entriesWhat happens here, and why each step is needed:
The setgid bit (the leading 2 in 2775) makes new files and subfolders inherit the shared group — instead of their creator's primary group. Without setgid, Alice's new file would belong to group alice, and Bob would be left out.
The default ACL is the actual key. It makes new files group-writable — independent of each user's umask. This is exactly where the simple solutions fail: umask is a per-process / per-login setting. You'd have to force it to 002 for every user and every service — fragile and easy to forget. A default ACL, by contrast, lives in the filesystem and applies universally, no matter what umask someone writes with. That's why setgid + default ACL is more robust than setgid + global umask.
The container bridge
The directory is now correct on the host. Whether a container can write into it depends on which UID/GID the process lands at in the host namespace. Here Podman and Docker part ways, as already hinted:
Rootless Podman maps host supplementary groups onto nobody by default — the container loses the shared membership. Fix:
podman run --group-add keep-groups ...keep-groups preserves the host GIDs for filesystem permission checks. Since your host user is in shared, the container process may then write. Note: newly created files still belong to the mapped UID — but the default ACL catches the group's write bit.
Rootful Docker maps container GIDs directly onto host GIDs. Pass the numeric GID as a supplementary group:
services:
app:
group_add:
- "1234" # numeric GID of 'shared'or docker run --group-add 1234. And once more the warning, because it costs the most here: if the container process writes as root, files land as root:root on the host and break the model. Either run the process as your user (--user) or set umask/GID appropriately in the image.
Two practical traps
Even with perfectly set ACLs there are two ways to wreck the model unnoticed:
- Tools that "carry permissions along."
cp -p,rsync -a, or atarrestore take over original owner and permissions and thereby bypass the inheritance. Populate the shared directory that way and you drag the old, wrong permissions in. Either work without permission preservation deliberately (cpwithout-p,rsyncwithout-aor with--no-perms --no-owner --no-group) or fix it up afterward withsetfacl/chgrp. - Programs that set their files to
0600themselves. Some tools enforce restrictive permissions on their own files, overriding the ACL defaults — unpreventable from outside. This isn't theoretical: Claude Code behaves exactly this way with.credentials.json. It's good that the token stays tight — but it confirms why, per path 1, you don't share credentials via the group anyway, but keep them per user.
Security & threat model
Containers give a deceptive sense of safety if you don't know the limits. The most important points, bundled:
- Credentials are the crown jewels. The OAuth token in
~/.claude/.credentials.jsonsits unencrypted on Linux, protected by file permissions only. With--dangerously-skip-permissions, a malicious repo can exfiltrate everything reachable in the container — including that token. Consequence: token per user, never put it in a mount that foreign code with broad permissions can reach. --dangerously-skip-permissionsonly inside a boundary. Container, VM, or sandbox runtime — never bare on the host. If you want fewer prompts without disabling the safety checks, use auto mode instead: a classifier reviews each action rather than waving it through blindly.- Restrict network egress. The single most effective measure for unattended operation. The reference's
init-firewall.shallows only the necessary domains. Even if the agent is compromised, it can only send data where the policy lets it. - Don't mount host secrets.
~/.ssh, cloud credential files, keychains have no place in the container. If you need access, use short-lived, narrowly scoped tokens instead of the permanent keys. - Untrusted code belongs in a VM — or in Claude Code on the web. A container shares the kernel with the host; for genuine "I don't know this code" operation, the kernel-separated VM is the more honest choice.
And the principle above all: isolation reduces the damage, it doesn't eliminate the risk. Keep reviewing what the agent does.
Decision guide
Which approach for which goal? The short version:
| You want to … | Use … |
|---|---|
| Fewer prompts in everyday work on your own machine | Bash sandbox (/sandbox) |
| Isolate MCP/hooks as well, without Docker | Sandbox runtime |
Work unattended with --dangerously-skip-permissions | Dev container/container with egress firewall |
| A team-wide, standardized environment | Commit the dev container to the repo |
| Work on an untrusted repo | VM or Claude Code on the web |
| One installation for many users | System-wide image + shared group (ch. 9–11) |
| Work on a Windows host | Container/VM or Bash sandbox in WSL2 |
For the multi-user goal this means concretely: rootless Podman as the engine, a centrally maintained, version-pinned image, the setgid + default ACL base on the shared directory, --group-add keep-groups at start, and path 1 for ~/.claude sharing (config shared, identity per user).
Conclusion
Claude Code in a container isn't an end in itself but the precondition for giving the agent more autonomy without losing control. The official dev container is the fastest entry for IDE work; rootless Podman and rootful Docker give you the freedom to build your own multi-user setups.
The real lever in sharing isn't the container engine but the filesystem: setgid for group inheritance, default ACLs for the write bit independent of umask, and depending on the engine keep-groups (Podman) or the numeric group_add (Docker) as the bridge into the container. Throughout, draw a firm line between what's shareable (skills, commands, settings) and what isn't (token, transcripts). Then any number of accounts share a single, centrally maintained installation — clean, accountable, and without anyone holding more rights than they need.
If you want to go deeper into the tools: the cheat sheets for Podman, Docker, and Docker Compose live here in the database, and the Docker/Podman Composer helps you rewrite between run commands and Compose files.