-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDockerfile.project
More file actions
297 lines (247 loc) · 11.5 KB
/
Dockerfile.project
File metadata and controls
297 lines (247 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# =============================================================================
# Project Container Dockerfile (Standalone)
# =============================================================================
# Fully standalone container for running Claude Agent SDK or OpenCode SDK.
# - SSH key baked in at build time (via BuildKit secrets)
# - Clones repo at runtime from GIT_REMOTE_URL
# - No volume mounts required
#
# Build command:
# DOCKER_BUILDKIT=1 docker build \
# --secret id=ssh_key,src=$HOME/.ssh/id_ed25519 \
# -f Dockerfile.project -t zerocoder-project .
#
# Environment variables at runtime:
# - GIT_REMOTE_URL: Git SSH URL to clone (required)
# - ANTHROPIC_API_KEY: API key for Claude
# - HOST_API_URL: Host API for beads sync
# - PROJECT_NAME: Project identifier
# - CONTAINER_TYPE: init or coding
FROM python:3.11-slim-bookworm
# Prevent interactive prompts
ENV DEBIAN_FRONTEND=noninteractive
# Disable beads daemon in containers (avoids stale lock file issues and sync conflicts)
ENV BD_NO_DAEMON=true
ENV BEADS_AUTO_START_DAEMON=false
# Install essentials and webapp development tools
RUN apt-get update && apt-get install -y \
git \
curl \
wget \
ca-certificates \
sudo \
procps \
jq \
tree \
zip \
unzip \
rsync \
build-essential \
openssh-client \
&& rm -rf /var/lib/apt/lists/*
# Install Node.js v22 (required for Vision MCP Server)
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
# Install pnpm and yarn globally
RUN npm install -g pnpm yarn
# Create non-root user (required for Claude Code --dangerously-skip-permissions)
# Use a different UID to avoid conflicts with existing ubuntu user
# The coder user has passwordless sudo for installing packages
RUN useradd --uid 1001 -m coder \
&& echo "coder ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# Install Claude Code CLI (required by Agent SDK)
# Install as coder user so it goes to /home/coder/.claude/
USER coder
RUN curl -fsSL https://claude.ai/install.sh | bash
USER root
# Add claude to PATH for subsequent build steps and runtime
ENV PATH="/home/coder/.local/bin:/home/coder/.claude/bin:${PATH}"
# Add Context7 MCP for documentation lookup (available to Claude agents)
USER coder
RUN claude mcp add context7 -- npx -y @upstash/context7-mcp
USER root
# Install Claude Agent SDK and dependencies
RUN pip install --no-cache-dir claude-agent-sdk requests
# Install uv for uvx command (needed for MiniMax MCP server)
RUN pip install --no-cache-dir uv
# =============================================================================
# SSH Key Setup (BuildKit secret)
# =============================================================================
# SSH key is mounted from host during build using BuildKit secrets
# This keeps the key out of image layers while allowing git clone
# Create .ssh directories with correct permissions
RUN mkdir -p /home/coder/.ssh && chmod 700 /home/coder/.ssh \
&& mkdir -p /root/.ssh && chmod 700 /root/.ssh
# Add GitHub to known_hosts at build time (prevents "authenticity" prompts)
RUN ssh-keyscan github.com >> /home/coder/.ssh/known_hosts 2>/dev/null \
&& chmod 644 /home/coder/.ssh/known_hosts \
&& cp /home/coder/.ssh/known_hosts /root/.ssh/known_hosts \
&& chmod 644 /root/.ssh/known_hosts
# Copy SSH key using BuildKit secret mount
# The key is only available during this RUN command, not stored in layers
RUN --mount=type=secret,id=ssh_key,uid=1001 \
if [ -f /run/secrets/ssh_key ]; then \
cp /run/secrets/ssh_key /home/coder/.ssh/id_ed25519 && \
chmod 600 /home/coder/.ssh/id_ed25519 && \
chown coder:coder /home/coder/.ssh/id_ed25519 && \
cp /run/secrets/ssh_key /root/.ssh/id_ed25519 && \
chmod 600 /root/.ssh/id_ed25519 && \
echo "SSH key installed successfully"; \
else \
echo "Warning: No SSH key secret provided"; \
fi
# Set ownership of .ssh directories
RUN chown -R coder:coder /home/coder/.ssh
# =============================================================================
# OpenCode SDK Setup (for GLM-4.7 model)
# =============================================================================
# Install OpenCode CLI
RUN curl -fsSL https://opencode.ai/install | bash \
&& mv /root/.opencode/bin/opencode /usr/local/bin/opencode 2>/dev/null || true
ENV PATH="/root/.opencode/bin:${PATH}"
# Copy OpenCode configuration and agent definitions
COPY opencode_config/ /app/opencode_config/
COPY opencode_agent_app.ts /app/opencode_agent_app.ts
COPY package.opencode.json /app/package.json
COPY tsconfig.opencode.json /app/tsconfig.opencode.json
# Install OpenCode SDK dependencies and build TypeScript
WORKDIR /app
RUN npm install && npm run build
# Copy OpenCode config to the location where OpenCode CLI expects it
# This is needed for the server to find the Z.ai provider configuration
RUN mkdir -p /home/coder/.config/opencode \
&& cp /app/opencode_config/config.json /home/coder/.config/opencode/config.json \
&& chown -R coder:coder /home/coder/.config
# Reset working directory
WORKDIR /project
# =============================================================================
# Claude Agent SDK Setup (for Claude models)
# =============================================================================
# Copy agent application and container scripts
COPY agent_app.py /app/agent_app.py
COPY container_scripts/feature_status.py /app/feature_status.py
COPY container_scripts/beads_commands.py /app/beads_commands.py
COPY container_scripts/setup_repo.sh /app/setup_repo.sh
COPY container_scripts/cleanup_session.sh /app/cleanup_session.sh
COPY container_scripts/beads_client.sh /app/beads_client.sh
RUN chmod +x /app/setup_repo.sh /app/cleanup_session.sh /app/beads_client.sh
# Create symlinks for beads_client.sh so agents can call it easily
# Both 'beads_client' and 'bd' point to the same script for compatibility
RUN ln -s /app/beads_client.sh /usr/local/bin/beads_client \
&& ln -s /app/beads_client.sh /usr/local/bin/bd
# Configure git for container operations (for root, during build)
RUN git config --global user.email "[email protected]" \
&& git config --global user.name "ZeroCoder Agent" \
&& git config --global init.defaultBranch main \
&& git config --global --add safe.directory /project
# Create directories for coder user
RUN mkdir -p /home/coder/.claude \
&& chown -R coder:coder /home/coder
# Configure git for coder user and set permissive umask
USER coder
RUN git config --global user.email "[email protected]" \
&& git config --global user.name "ZeroCoder Agent" \
&& git config --global init.defaultBranch main \
&& git config --global --add safe.directory /project \
&& echo "umask 002" >> ~/.bashrc \
&& echo "umask 002" >> ~/.profile
# Switch back to root for runtime operations
USER root
# Working directory is the project directory (cloned at runtime)
WORKDIR /project
# Make /project writable by coder user for git clone
RUN chown coder:coder /project
# Create entrypoint script
COPY <<EOF /entrypoint.sh
#!/bin/bash
set -e
# Set TZ environment variable from /etc/timezone for Node.js
# (Node.js doesn't read /etc/localtime, it needs TZ env var)
if [ -f /etc/timezone ]; then
export TZ=\$(cat /etc/timezone)
fi
# Limit Bun/Node heap to prevent OOM crashes (container has 4GB limit)
export NODE_OPTIONS="--max-old-space-size=3072"
export BUN_JSC_forceRAMSize="3221225472" # 3GB in bytes
# Set fully permissive umask so ALL new files are world readable/writable
# This ensures both host user and container user can access files
# 000 = files 666, dirs 777
umask 000
# Update Claude Code to latest version at startup
echo "Updating Claude Code to latest version..."
su - coder -c "claude update" 2>/dev/null || true
# Create agent log file (world-writable so coder user can write to it)
# This file is tailed to show agent activity in docker logs
touch /var/log/agent.log
chmod 666 /var/log/agent.log
echo "[$(date -Iseconds)] Container started" >> /var/log/agent.log
# =============================================================================
# Clone repository from GIT_REMOTE_URL
# =============================================================================
if [ -z "\$GIT_REMOTE_URL" ]; then
echo "[$(date -Iseconds)] ERROR: GIT_REMOTE_URL not set" >> /var/log/agent.log
echo "[$(date -Iseconds)] Container requires GIT_REMOTE_URL environment variable" >> /var/log/agent.log
exit 1
fi
echo "[$(date -Iseconds)] Cloning \$GIT_REMOTE_URL (branch: main)..." >> /var/log/agent.log
# Clone as coder user (always clone main branch)
if [ -d "/project/.git" ] || [ -f "/project/.git" ]; then
echo "[$(date -Iseconds)] Project already exists, pulling latest..." >> /var/log/agent.log
# Clean up stale git lock files (from crashed git operations)
if [ -f "/project/.git/index.lock" ] || [ -f "/project/.git/HEAD.lock" ] || [ -f "/project/.git/config.lock" ]; then
echo "[$(date -Iseconds)] Cleaning stale git locks..." >> /var/log/agent.log
rm -f /project/.git/index.lock
rm -f /project/.git/HEAD.lock
rm -f /project/.git/config.lock
find /project/.git/refs -name "*.lock" -delete 2>/dev/null || true
fi
su - coder -c "cd /project && git fetch origin && git reset --hard origin/main"
else
# Clone into temp dir first, then move contents (can't clone into non-empty /project)
su - coder -c "git clone --branch main \$GIT_REMOTE_URL /tmp/project-clone"
# Move all files including hidden ones
su - coder -c "mv /tmp/project-clone/* /tmp/project-clone/.[!.]* /project/ 2>/dev/null || true"
rm -rf /tmp/project-clone
fi
if [ ! -e "/project/.git" ]; then
echo "[$(date -Iseconds)] ERROR: Git clone failed - /project/.git not found" >> /var/log/agent.log
exit 1
fi
echo "[$(date -Iseconds)] Repository cloned successfully" >> /var/log/agent.log
# Clean up stale beads daemon lock files (prevents stack overflow bug)
if [ -f "/project/.beads/daemon.lock" ]; then
rm -f /project/.beads/daemon.lock
echo "[$(date -Iseconds)] Removed stale beads daemon.lock" >> /var/log/agent.log
fi
# Fix .beads directory permissions at startup (make world-writable)
if [ -d "/project/.beads" ]; then
chmod -R a+rwX /project/.beads 2>/dev/null || true
fi
# Fix prompts directory permissions at startup (make world-writable)
if [ -d "/project/prompts" ]; then
chmod -R a+rwX /project/prompts 2>/dev/null || true
fi
# Kill any running beads daemon (prevents sync conflicts)
if [ -f "/project/.beads/daemon.pid" ]; then
DAEMON_PID=\$(cat /project/.beads/daemon.pid 2>/dev/null)
if [ -n "\$DAEMON_PID" ] && kill -0 "\$DAEMON_PID" 2>/dev/null; then
echo "[$(date -Iseconds)] Stopping beads daemon (PID: \$DAEMON_PID)..." >> /var/log/agent.log
kill "\$DAEMON_PID" 2>/dev/null || true
sleep 1
fi
rm -f /project/.beads/daemon.pid /project/.beads/daemon.lock
fi
# Run repository setup script (syncs beads via host API)
if [ -f "/app/setup_repo.sh" ]; then
echo "[$(date -Iseconds)] Running repository setup (type: \${CONTAINER_TYPE:-coding})..." >> /var/log/agent.log
su - coder -c "cd /project && CONTAINER_TYPE=\${CONTAINER_TYPE:-coding} HOST_API_URL=\${HOST_API_URL:-} PROJECT_NAME=\${PROJECT_NAME:-} /app/setup_repo.sh"
fi
echo "[$(date -Iseconds)] Container ready, waiting for agent..." >> /var/log/agent.log
# Keep container running by tailing the agent log
# Agent processes will write to this file, making output visible in docker logs
exec tail -f /var/log/agent.log
EOF
RUN chmod +x /entrypoint.sh
# Default command - entrypoint keeps container alive
CMD ["/entrypoint.sh"]