# 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.

Source: https://www.jpkc.com/db/en/blog/claude-code-container/

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-permissions` or 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.

1. **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.
2. **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.
3. **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-permissions` turns 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](https://en.wikipedia.org/wiki/Linux_namespaces) 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](https://code.claude.com/docs/en/devcontainer). 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**:

```json
{
	"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](https://github.com/anthropics/claude-code/tree/main/.devcontainer) (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:

```json
"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-config` volume 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 `~/.ssh` or 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](https://www.jpkc.com/db/en/cheatsheets/containers/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):

```dockerfile
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:

```bash
podman build -t claude-code:local .

podman run --rm -it \
	-v "$PWD":/workspace:Z \
	-v "$HOME/.claude":/home/dev/.claude:Z \
	claude-code:local
```

Two details that often trip up beginners:

- The **`:Z`** on 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) `:Z` is a no-op but harmless.
- You're mounting **your own** `~/.claude` here. 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:

```bash
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 `~/.claude` live in the **Linux filesystem** (`/home/...`), not under `/mnt/c/...` — mounts across the Windows boundary are slow and their permission semantics (no real `chmod`/ACL) break everything this article says about groups and ACLs.

## Approach C — Rootful Docker

With classic, rootful [Docker](https://www.jpkc.com/db/en/cheatsheets/containers/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):

```bash
docker run --rm -it \
	--user "$(id -u):$(id -g)" \
	--group-add 1234 \
	-v "$PWD":/workspace \
	-v "$HOME/.claude":/home/dev/.claude \
	claude-code:local
```

With [Docker Compose](https://www.jpkc.com/db/en/cheatsheets/containers/docker-compose/) the same looks like this:

```yaml
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: true
```

You 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_add` acrobatics.

## 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:local` once (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 variables
- `CLAUDE.md` — your global instructions across all projects
- `skills/`, `commands/`, `agents/`, `rules/`, `output-styles/`, `workflows/` — reusable extensions
- `plugins/` — installed plugins and marketplaces
- `themes/`, `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 `.env` or 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 preferences
- `shell-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`:

```bash
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:local
```

Everyone 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/.../*.jsonl` is 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

```bash
# 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 entries
```

What 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](https://en.wikipedia.org/wiki/Access-control_list), 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:

```bash
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:

```yaml
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:

1. **Tools that "carry permissions along."** `cp -p`, `rsync -a`, or a `tar` restore 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 (`cp` without `-p`, `rsync` without `-a` or with `--no-perms --no-owner --no-group`) or fix it up afterward with `setfacl`/`chgrp`.
2. **Programs that set their files to `0600` themselves.** 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.json` sits 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-permissions` only 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.sh` allows 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](https://www.jpkc.com/db/en/cheatsheets/containers/podman/), [Docker](https://www.jpkc.com/db/en/cheatsheets/containers/docker/), and [Docker Compose](https://www.jpkc.com/db/en/cheatsheets/containers/docker-compose/) live here in the database, and the [Docker/Podman Composer](https://www.jpkc.com/db/en/tools/docker/) helps you rewrite between `run` commands and Compose files.

