Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.claude/worktrees
23 changes: 19 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ ARG BASE_IMAGE=ubuntu:24.04
FROM ${BASE_IMAGE}

ARG NODE_VERSION=22
ARG EXTRA_PACKAGES=""
ARG EXTRA_APT_PACKAGES=""
ARG EXTRA_NPM_PACKAGES=""
ARG ENABLE_SUDO=false
ARG INSTALL_CLAUDE=true
ARG INSTALL_CODEX=true

USER root

Expand All @@ -17,7 +20,7 @@ RUN apt-get -qq update > /dev/null && apt-get -qq -o=Dpkg::Use-Pty=0 install -y
sudo \
python3 \
python3-pip \
${EXTRA_PACKAGES} \
${EXTRA_APT_PACKAGES} \
> /dev/null && rm -rf /var/lib/apt/lists/*

# Install Node.js
Expand All @@ -26,7 +29,19 @@ RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - 2>/de
&& rm -rf /var/lib/apt/lists/*

# Install Claude Code CLI
RUN npm install -g @anthropic-ai/claude-code
RUN if [ "$INSTALL_CLAUDE" = "true" ]; then \
npm install -g @anthropic-ai/claude-code; \
fi

# Install OpenAI Codex CLI
RUN if [ "$INSTALL_CODEX" = "true" ]; then \
npm install -g @openai/codex; \
fi

# Install extra global npm packages
RUN if [ -n "$EXTRA_NPM_PACKAGES" ]; then \
npm install -g $EXTRA_NPM_PACKAGES; \
fi

# SSH configuration
RUN mkdir /var/run/sshd \
Expand All @@ -38,7 +53,7 @@ RUN mkdir /var/run/sshd \
&& echo "HostKey /etc/ssh/host-keys/ssh_host_rsa_key" >> /etc/ssh/sshd_config \
&& echo "HostKey /etc/ssh/host-keys/ssh_host_ecdsa_key" >> /etc/ssh/sshd_config \
&& echo "HostKey /etc/ssh/host-keys/ssh_host_ed25519_key" >> /etc/ssh/sshd_config \
&& echo "AcceptEnv ANTHROPIC_API_KEY ANTHROPIC_BASE_URL" >> /etc/ssh/sshd_config
&& echo "AcceptEnv ANTHROPIC_API_KEY ANTHROPIC_BASE_URL OPENAI_API_KEY" >> /etc/ssh/sshd_config

# Grant passwordless sudo if enabled
RUN if [ "$ENABLE_SUDO" = "true" ]; then \
Expand Down
53 changes: 34 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# claude-server
# agentic-coding-container

A Docker image that runs an SSH server with [Claude Code](https://github.com/anthropics/claude-code) pre-installed, intended for use as a remote development environment.
A Docker image that runs an SSH server with AI coding agents pre-installed, intended for use as a remote development environment. Compatible with the ssh feature of the Claude desktop app allowing for development on a remote machine, for example a kubernetes node with a GPU.

Ships with [Claude Code](https://github.com/anthropics/claude-code) and [OpenAI Codex](https://github.com/openai/codex) out of the box — either can be toggled off at build time.

## What it does

- Runs an OpenSSH server accessible via public key authentication
- Installs Claude Code CLI globally via npm
- Installs Claude Code and Codex CLIs globally via npm (configurable)
- Runs as the `ubuntu` user
- SSH host keys are persisted via a mounted volume so they survive container restarts

## Build

```sh
docker build -t claude-server .
docker build -t agentic-coding-container .
```

### Build arguments
Expand All @@ -21,11 +23,21 @@ docker build -t claude-server .
|---|---|---|
| `BASE_IMAGE` | `ubuntu:24.04` | Base image to build from |
| `NODE_VERSION` | `22` | Node.js major version to install |
| `EXTRA_PACKAGES` | — | Space-separated extra apt packages to install |
| `EXTRA_APT_PACKAGES` | — | Space-separated extra apt packages to install |
| `EXTRA_NPM_PACKAGES` | — | Space-separated extra global npm packages to install |
| `ENABLE_SUDO` | `false` | Grant the `ubuntu` user passwordless `sudo` (grants full root access — use with caution) |
| `INSTALL_CLAUDE` | `true` | Install [Claude Code](https://github.com/anthropics/claude-code) CLI |
| `INSTALL_CODEX` | `true` | Install [OpenAI Codex](https://github.com/openai/codex) CLI |

```sh
docker build --build-arg BASE_IMAGE=ubuntu:22.04 --build-arg NODE_VERSION=20 -t claude-server .
# Custom base image and Node version
docker build --build-arg BASE_IMAGE=ubuntu:22.04 --build-arg NODE_VERSION=20 -t agentic-coding-container .

# Claude Code only
docker build --build-arg INSTALL_CODEX=false -t agentic-coding-container .

# Codex only
docker build --build-arg INSTALL_CLAUDE=false -t agentic-coding-container .
```

## Run
Expand All @@ -39,9 +51,10 @@ Two mounts are required:
docker run -d \
-p 22:22 \
-e ANTHROPIC_API_KEY=sk-ant-... \
-v claude-host-keys:/etc/ssh/host-keys \
-e OPENAI_API_KEY=sk-... \
-v host-keys:/etc/ssh/host-keys \
-v /path/to/authorized_keys:/etc/ssh/authorized_keys/authorized_keys:ro \
claude-server
agentic-coding-container
```

### Environment variables
Expand All @@ -51,14 +64,15 @@ docker run -d \
| `PORT` | `22` | Port sshd listens on |
| `ANTHROPIC_API_KEY` | — | Injected into the `ubuntu` user's environment for Claude Code (written to `.bashrc`/`.profile` in plaintext) |
| `ANTHROPIC_BASE_URL` | — | Optional custom Anthropic API base URL (passed through via SSH `AcceptEnv`) |
| `OPENAI_API_KEY` | — | Injected into the `ubuntu` user's environment for Codex CLI (written to `.bashrc`/`.profile` in plaintext) |

### Connecting

```sh
ssh ubuntu@<host>
```

## Claude Desktop app and sandboxing
## Desktop app sandboxing

The Claude Desktop app runs in a sandbox that may block outbound SSH connections. If the app cannot reach your server directly, use a local port forward so it connects via `localhost` instead.

Expand All @@ -68,7 +82,7 @@ The Claude Desktop app runs in a sandbox that may block outbound SSH connections
ssh -N -L 2222:<host>:22 ubuntu@<host>
```

Then point Claude Desktop at `localhost:2222`.
Then point the desktop app at `localhost:2222`.

**Persistent forward with autossh:**

Expand All @@ -94,8 +108,8 @@ autossh -M 0 -N -L 2222:<host>:22 ubuntu@<host>
The manifest defaults to `ghcr.io/n1mmy/claude-server:main`. To use a custom image:

```sh
docker build -t your-registry/claude-server:latest .
docker push your-registry/claude-server:latest
docker build -t your-registry/agentic-coding-container:latest .
docker push your-registry/agentic-coding-container:latest
```

Then update `image:` in `k8s-manifest.yaml` to match.
Expand All @@ -108,11 +122,12 @@ kubectl create secret generic ssh-authorized-keys \
-n claude-workspace
```

#### 3. Set your Anthropic API key
#### 3. Set your API keys

```sh
kubectl create secret generic anthropic-credentials \
--from-literal=api-key=sk-ant-... \
kubectl create secret generic ai-credentials \
--from-literal=anthropic-api-key=sk-ant-... \
--from-literal=openai-api-key=sk-... \
-n claude-workspace
```

Expand All @@ -125,21 +140,21 @@ kubectl apply -f k8s-manifest.yaml
#### 5. Get the SSH address

```sh
kubectl get svc claude-ssh -n claude-workspace
kubectl get svc agentic-coding-ssh -n claude-workspace
# Note the EXTERNAL-IP
```

For local clusters without LoadBalancer:

```sh
kubectl port-forward svc/claude-ssh 2222:22 -n claude-workspace
kubectl port-forward svc/agentic-coding-ssh 2222:22 -n claude-workspace
# Then SSH to localhost:2222
```

#### 6. Connect

```sh-config
Host claude-k8s
Host agentic-coding
HostName <EXTERNAL-IP>
User ubuntu
Port 22
Expand All @@ -148,7 +163,7 @@ Host claude-k8s

### GPU nodes

To request a GPU, add to the container resources in `manifests.yaml`:
To request a GPU, add to the container resources in `k8s-manifest.yaml`:

```yaml
resources:
Expand Down
6 changes: 5 additions & 1 deletion entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@ else
echo "WARNING: No authorized_keys found at /etc/ssh/authorized_keys/authorized_keys"
fi

# Write ANTHROPIC_API_KEY into ubuntu's environment so SSH sessions pick it up
# Write API keys into ubuntu's environment so SSH sessions pick them up
if [ -n "$ANTHROPIC_API_KEY" ]; then
echo "export ANTHROPIC_API_KEY='$ANTHROPIC_API_KEY'" >> /home/ubuntu/.bashrc
echo "export ANTHROPIC_API_KEY='$ANTHROPIC_API_KEY'" >> /home/ubuntu/.profile
fi
if [ -n "$OPENAI_API_KEY" ]; then
echo "export OPENAI_API_KEY='$OPENAI_API_KEY'" >> /home/ubuntu/.bashrc
echo "export OPENAI_API_KEY='$OPENAI_API_KEY'" >> /home/ubuntu/.profile
fi

echo "SSH server starting..."
exec /usr/sbin/sshd -D -e ${PORT:+-p $PORT}
43 changes: 26 additions & 17 deletions k8s-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,21 @@ data:
authorized_keys: REPLACE_WITH_BASE64_ENCODED_PUBLIC_KEY

---
# Secret: Anthropic API key
# Secret: AI provider API keys
apiVersion: v1
kind: Secret
metadata:
name: anthropic-credentials
name: ai-credentials
namespace: claude-workspace
type: Opaque
stringData:
# Set your API key here or use: kubectl create secret generic anthropic-credentials
# --from-literal=api-key=sk-ant-... -n claude-workspace
api-key: REPLACE_WITH_ANTHROPIC_API_KEY
# Set your API keys here or use:
# kubectl create secret generic ai-credentials \
# --from-literal=anthropic-api-key=sk-ant-... \
# --from-literal=openai-api-key=sk-... \
# -n claude-workspace
anthropic-api-key: REPLACE_WITH_ANTHROPIC_API_KEY
openai-api-key: REPLACE_WITH_OPENAI_API_KEY

---
# PVC: project workspace (code, configs)
Expand All @@ -49,29 +53,29 @@ spec:
# storageClassName: standard # uncomment and set to match your cluster's storage class

---
# Deployment: Claude Code SSH server
# Deployment: Agentic Coding SSH server
apiVersion: apps/v1
kind: Deployment
metadata:
name: claude-server
name: agentic-coding-server
namespace: claude-workspace
labels:
app: claude-server
app: agentic-coding-server
spec:
replicas: 1
selector:
matchLabels:
app: claude-server
app: agentic-coding-server
strategy:
type: Recreate # Required: volumes are ReadWriteOnce
template:
metadata:
labels:
app: claude-server
app: agentic-coding-server
spec:
containers:
- name: claude
image: ghcr.io/n1mmy/claude-server:main
- name: agentic-coding
image: ghcr.io/n1mmy/agentic-coding-container:main
imagePullPolicy: IfNotPresent

ports:
Expand All @@ -82,8 +86,13 @@ spec:
- name: ANTHROPIC_API_KEY
valueFrom:
secretKeyRef:
name: anthropic-credentials
key: api-key
name: ai-credentials
key: anthropic-api-key
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: ai-credentials
key: openai-api-key

volumeMounts:
# SSH authorized keys (read-only, mounted from secret)
Expand Down Expand Up @@ -157,14 +166,14 @@ spec:
apiVersion: v1
kind: Service
metadata:
name: claude-ssh
name: agentic-coding-ssh
namespace: claude-workspace
labels:
app: claude-server
app: agentic-coding-server
spec:
type: LoadBalancer # Change to NodePort for bare metal, or ClusterIP + port-forward for local
selector:
app: claude-server
app: agentic-coding-server
ports:
- name: ssh
port: 22
Expand Down
Loading