# Container Image and Entrypoint

> The Dockerfile that builds an Ubuntu 24.04 image with Claude Code, uv, Node 22, ripgrep, and a UID-1000 claude user; the minimal entrypoint.sh that sources /workdir/.env before exec.

- Repository: hans/claude-container
- GitHub: https://github.com/hans/claude-container
- Human wiki: https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958
- Complete Markdown: https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958/llms-full.txt

## Source Files

- `Dockerfile`
- `entrypoint.sh`
- `.dockerignore`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [Dockerfile](Dockerfile)
- [entrypoint.sh](entrypoint.sh)
- [.dockerignore](.dockerignore)
- [README.md](README.md)
- [.superset/launch.sh](.superset/launch.sh)
- [SETUP.md](SETUP.md)
</details>

# Container Image and Entrypoint

The `claude-sandbox` repository provides a minimal, reproducible Docker image for running Anthropic Claude Code inside isolated containers. Superset launches these containers per workspace so that the agent's file operations occur against a bind-mounted worktree while all Claude Code state remains visible to Superset's diff viewer and git tooling on the host.

This page documents the `Dockerfile` that assembles an Ubuntu 24.04 image containing Claude Code, the `uv` Python manager, Node 22, `ripgrep`, and supporting tools, plus the tiny `entrypoint.sh` that injects per-project environment variables before executing the requested command. The design deliberately keeps the image generic and the entrypoint minimal so that project-specific needs are handled either by ad-hoc `docker exec` or by thin child Dockerfiles.

## Purpose and Usage Context

The image exists to give Claude Code a consistent Linux execution environment (Node runtime, search tools, Python package manager) without polluting the host or requiring every workspace to ship its own full toolchain image. The worktree is always bind-mounted at `/workdir`; the container never becomes the source of truth for code or git history.

Superset invokes `.superset/launch.sh`, which performs preflight checks, prepares host credentials, and ultimately runs:

```bash
docker run --rm -it \
  -v "$PWD:/workdir" \
  -v "$HOME/.claude:/home/claude/.claude" \
  ... \
  -u "$(id -u):$(id -g)" \
  claude-sandbox:latest claude ...
```

On subsequent attachments the same script uses `docker exec` but explicitly calls the entrypoint again so that `/workdir/.env` is re-sourced.

Sources: [README.md:1-15](), [.superset/launch.sh:111-114](), [.superset/launch.sh:250]()

## Dockerfile Structure

The Dockerfile is intentionally linear and comment-heavy. It proceeds through five clear stages.

### Base Image and Core Toolchain

```dockerfile
FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive \
    LANG=C.UTF-8 \
    LC_ALL=C.UTF-8 \
    PATH="/home/claude/.local/bin:$PATH"

RUN apt-get update && apt-get install -y --no-install-recommends \
        ca-certificates curl gnupg \
        git openssh-client \
        build-essential pkg-config \
        less vim-tiny \
        ripgrep jq fd-find \
        python3 python3-pip python3-venv \
    && ln -s "$(command -v fdfind)" /usr/local/bin/fd \
    && rm -rf /var/lib/apt/lists/*
```

`ripgrep` (`rg`) and `fd` are installed because Claude Code's bash and file-search tools rely on them. The `fd` symlink preserves the conventional `fd` command name.

Sources: [Dockerfile:1-22]()

### uv and Node + Claude Code Installation

`uv` is installed via its official bootstrap script directly into `/usr/local/bin` so it is available to every user without shell-profile modifications:

```dockerfile
RUN curl -fsSL https://astral.sh/uv/install.sh \
    | env UV_INSTALL_DIR=/usr/local/bin UV_NO_MODIFY_PATH=1 sh
```

Node 22 is obtained from NodeSource and Claude Code is installed globally:

```dockerfile
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
    && apt-get install -y --no-install-recommends nodejs \
    && rm -rf /var/lib/apt/lists/* \
    && npm install -g @anthropic-ai/claude-code
```

An optional scientific Python stack is left commented out to keep the default image small.

Sources: [Dockerfile:24-36]()

### Non-root User and Runtime Configuration

The image creates a dedicated `claude` user at UID 1000:

```dockerfile
RUN userdel -r ubuntu 2>/dev/null || true \
    && useradd --create-home --uid 1000 --shell /bin/bash claude \
    && chmod 0777 /home/claude

RUN git config --system --add safe.directory /workdir

WORKDIR /workdir
```

The 0777 mode on `/home/claude` and the explicit `safe.directory` entry allow the container to be started with the host user's UID/GID (via `-u $(id -u):$(id -g)`) while still permitting writes into bind-mounted locations that carry host ownership. The stock `ubuntu` user is removed so that UID 1000 can be claimed cleanly.

Sources: [Dockerfile:45-58]()

### Entrypoint Wiring

The final stage copies and registers the entrypoint:

```dockerfile
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod 0755 /usr/local/bin/entrypoint.sh

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["claude"]
```

Sources: [Dockerfile:60-65]()

## Entrypoint Script

The script is deliberately minimal (14 lines):

```sh
#!/bin/sh
set -eu

if [ -f /workdir/.env ]; then
    set -a
    . /workdir/.env
    set +a
fi

exec "$@"
```

It exports every variable defined in `/workdir/.env` (typically `ANTHROPIC_API_KEY`, model overrides, project-specific secrets) into the environment of the process that follows. Using `exec` replaces the shell so that signals and exit status are passed through cleanly. The `set -eu` guarantees that a missing `.env` file or a later failure aborts early.

Because `docker exec` bypasses the image `ENTRYPOINT`, `.superset/launch.sh` explicitly invokes `/usr/local/bin/entrypoint.sh` on re-attach paths as well.

Sources: [entrypoint.sh:1-14](), [.superset/launch.sh:111-114]()

## Build Context Hygiene

`.dockerignore` excludes everything that should never enter the image layers:

```
.git
.gitignore
.superset/
README.md
node_modules
__pycache__
*.pyc
.venv
venv
.env
.env.*
!.env.example
...
```

Only `entrypoint.sh` is ever `COPY`ed. This keeps builds fast and prevents accidental leakage of secrets or host-specific files into the published image.

Sources: [.dockerignore:1-21]()

## Key Runtime Behaviors

| Aspect                  | Implementation                                                                 | Reason |
|-------------------------|--------------------------------------------------------------------------------|--------|
| Worktree location       | `-v "$PWD:/workdir"`                                                           | Single source of truth for Superset diff viewer |
| User identity           | `-u "$(id -u):$(id -g)"`                                                       | Host UID/GID on bind mounts; no permission surprises |
| Environment injection   | `entrypoint.sh` sources `/workdir/.env` on both `run` and `exec`               | Per-project secrets without image rebuilds |
| Re-attachment           | `docker exec ... /usr/local/bin/entrypoint.sh ...`                             | Re-source `.env` and keep interactive session |
| Default command         | `CMD ["claude"]`                                                               | `launch.sh` can omit arguments for plain interactive start |
| Image extension         | Child `Dockerfile` starting `FROM claude-sandbox:latest`                       | Project-specific packages without forking the base |

Sources: [README.md:113-129](), [.superset/launch.sh:118-128]()

## Container Startup Flow

```mermaid
sequenceDiagram
    participant S as Superset / launch.sh
    participant D as Docker
    participant E as entrypoint.sh
    participant C as claude

    S->>D: docker run ... -v $PWD:/workdir -u hostuid ... claude-sandbox claude [prompt]
    D->>E: ENTRYPOINT /usr/local/bin/entrypoint.sh
    E->>E: if /workdir/.env exists: set -a; source; set +a
    E->>C: exec claude [prompt]
    Note over C: Claude Code runs with project env vars

    S->>D: docker exec ... /usr/local/bin/entrypoint.sh claude ...
    D->>E: entrypoint.sh (re-invoked)
    E->>E: re-source /workdir/.env
    E->>C: exec claude ...
```

This diagram shows why the entrypoint must be re-invoked on `docker exec` and why `/workdir/.env` is always read from the bind mount rather than baked into the image.

## Design Rationale and Limitations

- The image stays intentionally generic; heavy dependencies (large Python stacks, custom CLIs) belong in per-project `Dockerfile.project` files that extend the base.
- No secrets or host configuration ever enter the image layers because `.dockerignore` and the bind-mount strategy keep them out.
- The UID-1000 + world-writable home trick trades a small amount of image surface for seamless host-container UID interoperability; the actual runtime user is the host identity thanks to the `-u` flag.
- Rebuilding is only required when the base toolchain changes; most day-to-day customization happens via `docker exec` (ephemeral) or child images (persistent).

Sources: [Dockerfile:45-58](), [README.md:62-87](), [SETUP.md:132-151]()

## Summary

The combination of a single, well-documented `Dockerfile` and a 14-line entrypoint that reliably sources `/workdir/.env` gives Superset workspaces a reproducible Claude Code sandbox while preserving host-side file ownership, git history, and per-project secrets. All behavior is directly verifiable from the five core files listed at the top of this page.

Sources: [Dockerfile:1-65](), [entrypoint.sh:1-14]()
