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:
parent
bfcb79a890
commit
4605a62d90
3 changed files with 64 additions and 2 deletions
11
Dockerfile
11
Dockerfile
|
|
@ -13,13 +13,22 @@ RUN pacman -Syu --noconfirm \
|
|||
ripgrep \
|
||||
nodejs \
|
||||
npm \
|
||||
curl \
|
||||
sudo && \
|
||||
groupadd -g ${GID} ${USERNAME} && \
|
||||
useradd -m -u ${UID} -g ${GID} -s /bin/bash ${USERNAME} && \
|
||||
echo "${USERNAME} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \
|
||||
npm install -g @anthropic-ai/claude-code && \
|
||||
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}
|
||||
|
||||
WORKDIR /home/${USERNAME}
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -14,6 +14,7 @@ the host.
|
|||
- **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)
|
||||
- **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
|
||||
|
||||
## 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
|
||||
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
|
||||
|
||||
The container home at `~/.local/share/claude-container/container-home/` is mounted
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ class ClaudeContainer:
|
|||
return self._get_xdg_data_home() / 'claude-container' / 'container-home'
|
||||
|
||||
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)
|
||||
if self.container_exists() and self.container_running():
|
||||
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),
|
||||
])
|
||||
|
||||
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:
|
||||
logger.info(f"Creating container '{self.container_name}'")
|
||||
self._run([
|
||||
|
|
@ -118,7 +155,7 @@ class ClaudeContainer:
|
|||
)
|
||||
|
||||
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"):
|
||||
val = os.environ.get(var)
|
||||
if val:
|
||||
|
|
|
|||
Reference in a new issue