Switch to native Claude Code installer and add update command

Replace the deprecated npm installation with the native installer. The
binary is installed to /usr/local/bin so it survives the home directory
bind mount at runtime.

Add a 'claude update' subcommand that rebuilds the image with the latest
Claude Code binary and removes all existing containers.

Disable the in-container auto-updater since the binary lives in the
read-only image layer.
This commit is contained in:
Jeena 2026-03-18 03:53:55 +00:00
parent bfcb79a890
commit 4605a62d90
3 changed files with 64 additions and 2 deletions

View file

@ -13,13 +13,22 @@ RUN pacman -Syu --noconfirm \
ripgrep \ ripgrep \
nodejs \ nodejs \
npm \ npm \
curl \
sudo && \ sudo && \
groupadd -g ${GID} ${USERNAME} && \ groupadd -g ${GID} ${USERNAME} && \
useradd -m -u ${UID} -g ${GID} -s /bin/bash ${USERNAME} && \ useradd -m -u ${UID} -g ${GID} -s /bin/bash ${USERNAME} && \
echo "${USERNAME} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \ echo "${USERNAME} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \
npm install -g @anthropic-ai/claude-code && \
pacman -Scc --noconfirm pacman -Scc --noconfirm
# Install Claude Code using the native installer, then copy the binary
# to a system-wide location so it survives the home directory bind mount
USER ${USERNAME}
WORKDIR /tmp
RUN curl -fsSL https://claude.ai/install.sh | bash && \
sudo cp ~/.local/bin/claude /usr/local/bin/claude && \
rm -rf ~/.local/share/claude ~/.local/bin/claude ~/.claude ~/.claude.json \
~/.cache/claude
USER ${USERNAME} USER ${USERNAME}
WORKDIR /home/${USERNAME} WORKDIR /home/${USERNAME}

View file

@ -14,6 +14,7 @@ the host.
- **Hard linking support**: Can hard link files like `~/.gitconfig` to share configurations with containers - **Hard linking support**: Can hard link files like `~/.gitconfig` to share configurations with containers
- Mounts only the current project directory (same absolute path inside container) - Mounts only the current project directory (same absolute path inside container)
- **Security boundary**: No access to SSH keys, passwords, or full `$HOME` (intentionally prevents remote code pushes) - **Security boundary**: No access to SSH keys, passwords, or full `$HOME` (intentionally prevents remote code pushes)
- **Easy updates**: `claude update` rebuilds the image with the latest Claude Code
- Simple shell function (`claude`) to launch interactively - Simple shell function (`claude`) to launch interactively
## Install ## Install
@ -55,6 +56,21 @@ The image is built automatically on first use if it does not already exist.
Claude Code starts inside the container with the current directory mounted and Claude Code starts inside the container with the current directory mounted and
set as the working directory. set as the working directory.
### Updating Claude Code
To update Claude Code to the latest version:
```
claude update
```
This rebuilds the Docker image with the latest Claude Code binary and removes
all existing containers. Containers are recreated automatically on the next
run. The persistent home directory is not affected.
The in-container auto-updater is disabled because Claude Code is installed in
the image layer. Use `claude update` to get new versions.
## Sharing host config files via hard links ## Sharing host config files via hard links
The container home at `~/.local/share/claude-container/container-home/` is mounted The container home at `~/.local/share/claude-container/container-home/` is mounted

View file

@ -41,6 +41,9 @@ class ClaudeContainer:
return self._get_xdg_data_home() / 'claude-container' / 'container-home' return self._get_xdg_data_home() / 'claude-container' / 'container-home'
def run(self, args: list[str]) -> None: def run(self, args: list[str]) -> None:
if args and args[0] == "update":
self.update()
return
self.container_home_path.mkdir(parents=True, exist_ok=True) self.container_home_path.mkdir(parents=True, exist_ok=True)
if self.container_exists() and self.container_running(): if self.container_exists() and self.container_running():
logger.error(f"Project '{self.project_path.name}' already has a running Claude container.") logger.error(f"Project '{self.project_path.name}' already has a running Claude container.")
@ -86,6 +89,40 @@ class ClaudeContainer:
str(self.docker_context_dir), str(self.docker_context_dir),
]) ])
def update(self) -> None:
logger.info("Updating Claude Code...")
logger.info("Removing existing containers...")
result = subprocess.run(
["docker", "ps", "-a", "--filter", "name=^cc-", "--format", "{{.Names}}"],
capture_output=True,
text=True,
)
for name in result.stdout.strip().splitlines():
if name:
logger.info(f" Removing container '{name}'")
subprocess.run(
["docker", "rm", "-f", name],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if self.image_exists():
logger.info(f"Removing image '{self.IMAGE}'...")
subprocess.run(
["docker", "rmi", self.IMAGE],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
logger.info("Rebuilding image with latest Claude Code...")
self._run([
"docker", "build", "--no-cache",
"--build-arg", f"USERNAME={self.host_username}",
"--build-arg", f"UID={self.host_uid}",
"--build-arg", f"GID={self.host_gid}",
"-t", self.IMAGE,
str(self.docker_context_dir),
])
logger.info("Update complete. Containers will be recreated on next run.")
def create_container(self) -> None: def create_container(self) -> None:
logger.info(f"Creating container '{self.container_name}'") logger.info(f"Creating container '{self.container_name}'")
self._run([ self._run([
@ -118,7 +155,7 @@ class ClaudeContainer:
) )
def exec_claude(self, args: list[str]) -> None: def exec_claude(self, args: list[str]) -> None:
env_args = [] env_args = ["-e", "DISABLE_AUTOUPDATER=1"]
for var in ("ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"): for var in ("ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"):
val = os.environ.get(var) val = os.environ.get(var)
if val: if val: