Skip to content

Commit 2c2aa52

Browse files
docs(blog): add late March 2026 engineering articles
主要变更: - 添加 HagiCode 技能系统技术分析 - 添加 HagiCode Soul 平台技术分析 - 添加 Docker AI CLI 用户隔离实践指南 - 添加 HagiCode Desktop P2P 加速架构详解 - 添加 HagiCode GLM-5.1 多 CLI 集成指南 - 添加 GLM-5.1 全支持与 Gemini CLI 集成 - 添加语音与图片上传的多模态输入实践 Co-Authored-By: Hagicode <[email protected]> Signed-off-by: newbe36524 <[email protected]>
1 parent 52901a9 commit 2c2aa52

7 files changed

Lines changed: 2512 additions & 0 deletions

src/content/docs/en/blog/2026-03-24-hagicode-skill-system-technical-analysis.mdx

Lines changed: 422 additions & 0 deletions
Large diffs are not rendered by default.

src/content/docs/en/blog/2026-03-25-hagicode-soul-platform-technical-analysis.mdx

Lines changed: 314 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
---
2+
title: "Running AI CLI Tools in Docker Containers: A Practical Guide to User Isolation and Persistent Volumes"
3+
date: 2026-03-26
4+
tags: [Docker, AI CLI, HagiCode, user isolation, persistent volumes, containerized deployment, Claude Code, Codex]
5+
---
6+
7+
## Running AI CLI Tools in Docker Containers: A Practical Guide to User Isolation and Persistent Volumes
8+
9+
> Integrating AI coding tools like Claude Code, Codex, and OpenCode into containerized environments sounds simple, but there are hidden complexities everywhere. This article takes a deep dive into how the HagiCode project solves core challenges in Docker deployments, including user permissions, configuration persistence, and version management, so you can avoid the common pitfalls.
10+
11+
## Background
12+
13+
When we decided to run AI coding CLI tools inside Docker containers, the most intuitive thought was probably: "Aren't containers just root? Why not install everything directly and call it done?" In reality, that seemingly simple idea hides several core problems that must be solved.
14+
15+
First, **security restrictions are the first hurdle**. Take Claude CLI as an example: it explicitly forbids running as the root user. This is a mandatory security check, and if root is detected, it refuses to start. You might think, can't I just switch users with the `USER` directive? It is not that simple. There is still a mapping problem between the non-root user inside the container and the user permissions on the host machine.
16+
17+
Second, **state persistence is the second trap**. Claude Code requires login, Codex has its own configuration, and OpenCode also has a cache directory. If you have to reconfigure everything every time the container restarts, the whole idea of "automation" loses its meaning. We need these configurations to persist beyond the lifecycle of the container.
18+
19+
The third problem is **permission consistency**. Can processes inside the container access configuration files created by the host user? UID/GID mismatches often cause file permission errors, and this is extremely common in real deployments.
20+
21+
These problems may look independent, but in practice they are tightly connected. During HagiCode's development, we gradually worked out a practical solution. Next, I will share the technical details and the lessons learned from those pitfalls.
22+
23+
## About HagiCode
24+
25+
The solution shared in this article comes from our practical experience in the [HagiCode](https://hagicode.com) project. HagiCode is an open-source AI-assisted programming platform that integrates multiple mainstream AI coding assistants, including Claude Code, Codex, and OpenCode. As a project that needs cross-platform and highly available deployment, HagiCode has to solve the full range of challenges involved in containerized deployment.
26+
27+
If you find the technical solution in this article valuable, that is a sign HagiCode has something real to offer in engineering practice. In that case, the [HagiCode official website](https://hagicode.com) and [GitHub repository](https://github.com/HagiCode-org/site) are both worth following.
28+
29+
## Why can't we just use root?
30+
31+
There is a common misunderstanding here: Docker containers run as root by default, so why not just install the tools as root? If you think that way, Claude CLI will quickly teach you otherwise.
32+
33+
```bash
34+
# Run Claude CLI directly as root? No.
35+
docker run --rm -it --user root myimage claude
36+
# Output: Error: This command cannot be run as root user
37+
```
38+
39+
This is a hard security restriction in Claude CLI. The reason is simple: these CLI tools read and write sensitive user configuration, including API tokens, local caches, and even scripts written by the user. Running them with root privileges introduces too much risk.
40+
41+
So the question becomes: how can we satisfy the CLI's security requirements while keeping container management flexible? We need to change the way we think about it: **instead of switching users at runtime, create a dedicated user during the image build stage**.
42+
43+
## Creating a dedicated user: more than just changing a name
44+
45+
You might think that adding a single `USER` line to the Dockerfile is enough. That is indeed the simplest approach, but it is not robust enough.
46+
47+
### Static creation vs. dynamic mapping
48+
49+
HagiCode's approach is to create a `hagicode` user with UID 1000, which usually matches the default user on most host machines:
50+
51+
```dockerfile
52+
RUN groupadd -o -g 1000 hagicode && \
53+
useradd -o -u 1000 -g 1000 -s /bin/bash -m hagicode && \
54+
mkdir -p /home/hagicode/.claude && \
55+
chown -R hagicode:hagicode /home/hagicode
56+
```
57+
58+
But this only solves the built-in user inside the image. What if the host user is UID 1001? You still need to support dynamic mapping when the container starts.
59+
60+
docker-entrypoint.sh contains the key logic:
61+
62+
```bash
63+
if [ -n "$PUID" ] && [ -n "$PGID" ]; then
64+
if ! id hagicode >/dev/null 2>&1; then
65+
groupadd -g "$PGID" hagicode
66+
useradd -u "$PUID" -g "$PGID" -s /bin/bash -m hagicode
67+
fi
68+
fi
69+
```
70+
71+
The advantage of this design is clear: **use the default UID 1000 at image build time, then adjust dynamically at runtime through the `PUID` and `PGID` environment variables**. No matter what UID the host user has, ownership of configuration files remains correct.
72+
73+
## The design philosophy of persistent volumes
74+
75+
Each AI CLI tool has its own preferred configuration directory, so they need to be mapped one by one:
76+
77+
| CLI Tool | Path in Container | Named Volume |
78+
|---------|-----------|--------|
79+
| Claude | `/home/hagicode/.claude` | `claude-data` |
80+
| Codex | `/home/hagicode/.codex` | `codex-data` |
81+
| OpenCode | `/home/hagicode/.config/opencode` | `opencode-config-data` |
82+
83+
Why use named volumes instead of bind mounts? Three reasons:
84+
85+
1. **Simpler management**: Named volumes are managed automatically by Docker, so you do not need to create host directories manually.
86+
2. **Permission isolation**: The initial contents of the volumes are created by the user inside the container, avoiding permission conflicts with the host.
87+
3. **Independent migration**: Volumes can exist independently of containers, so data is not lost when images are upgraded.
88+
89+
docker-compose-builder-web automatically generates the corresponding volume configuration:
90+
91+
```yaml
92+
volumes:
93+
claude-data:
94+
codex-data:
95+
opencode-config-data:
96+
97+
services:
98+
hagicode:
99+
volumes:
100+
- claude-data:/home/hagicode/.claude
101+
- codex-data:/home/hagicode/.codex
102+
- opencode-config-data:/home/hagicode/.config/opencode
103+
user: "${PUID:-1000}:${PGID:-1000}"
104+
```
105+
106+
Pay attention to the `user` field here: `PUID` and `PGID` are injected through environment variables to ensure that processes inside the container run with an identity that matches the host user. This detail matters because permission issues are painful to debug once they appear.
107+
108+
## Version management: baked-in versions with runtime overrides
109+
110+
Pinning Docker image versions is essential for reproducibility. But in real development, we often need to test a newer version or urgently fix a bug. If we had to rebuild the image every time, the workflow would be far too inefficient.
111+
112+
HagiCode's strategy is **fixed versions as the default, with runtime overrides as an extension mechanism**. It is a pragmatic engineering compromise between stability and flexibility.
113+
114+
Dockerfile.template pins versions here:
115+
116+
```dockerfile
117+
USER hagicode
118+
WORKDIR /home/hagicode
119+
120+
# Configure the global npm install path
121+
RUN mkdir -p /home/hagicode/.npm-global && \
122+
npm config set prefix '/home/hagicode/.npm-global'
123+
124+
# Install CLI tools using pinned versions
125+
RUN npm install -g @anthropic-ai/[email protected] && \
126+
npm install -g @openai/[email protected] && \
127+
npm install -g [email protected] && \
128+
npm cache clean --force
129+
```
130+
131+
docker-entrypoint.sh supports runtime overrides:
132+
133+
```bash
134+
install_cli_override_if_needed() {
135+
local package_name="$2"
136+
local override_version="$5"
137+
138+
if [ -n "$override_version" ]; then
139+
gosu hagicode npm install -g "${package_name}@${override_version}"
140+
fi
141+
}
142+
143+
# Example usage
144+
install_cli_override_if_needed "" "@anthropic-ai/claude-code" "" "" "${CLAUDE_CODE_CLI_VERSION}"
145+
```
146+
147+
This lets you test a new version through an environment variable without rebuilding the image:
148+
149+
```bash
150+
docker run -e CLAUDE_CODE_CLI_VERSION=2.2.0 myimage
151+
```
152+
153+
This design is practical because nobody wants to rebuild an image every time they test a new feature.
154+
155+
## Automatic configuration injection
156+
157+
In addition to configuring CLI tools manually, some scenarios require automatic configuration injection. The most typical example is an API token.
158+
159+
```bash
160+
if [ -n "$ANTHROPIC_AUTH_TOKEN" ]; then
161+
mkdir -p /home/hagicode/.claude
162+
cat > /home/hagicode/.claude/settings.json <<EOF
163+
{
164+
"env": {
165+
"ANTHROPIC_AUTH_TOKEN": "${ANTHROPIC_AUTH_TOKEN}"
166+
}
167+
}
168+
EOF
169+
chown -R hagicode:hagicode /home/hagicode/.claude
170+
fi
171+
```
172+
173+
Two things matter here: **pass sensitive information through environment variables instead of hard-coding it into the image**, and make sure the ownership of configuration files is set correctly, otherwise the CLI tools will not be able to read them.
174+
175+
## Best practices and a pitfall checklist
176+
177+
### Permission mismatch problems
178+
179+
This is the easiest trap to fall into. The host user has UID 1001, while the container uses 1000, so files created on one side cannot be accessed on the other.
180+
181+
```bash
182+
# Correct approach: make the container match the host user
183+
docker run \
184+
-e PUID=$(id -u) \
185+
-e PGID=$(id -g) \
186+
myimage
187+
```
188+
189+
This issue is very common, and it can be frustrating the first time you run into it.
190+
191+
### Configuration disappears after container restart
192+
193+
If you find yourself logging in again after every restart, check whether you forgot to mount a persistent volume:
194+
195+
```yaml
196+
volumes:
197+
- claude-data:/home/hagicode/.claude
198+
```
199+
200+
Nothing is more frustrating than carefully setting up a configuration only to see it disappear.
201+
202+
### The right way to upgrade versions
203+
204+
Do not run `npm install -g` directly inside a running container. The correct approaches are:
205+
206+
1. Set an environment variable to trigger override installation.
207+
2. Or rebuild the image.
208+
209+
```bash
210+
# Option 1: runtime override
211+
docker run -e CLAUDE_CODE_CLI_VERSION=2.2.0 myimage
212+
213+
# Option 2: rebuild the image
214+
docker build -t myimage:v2 .
215+
```
216+
217+
There is more than one road to Rome, but some roads are smoother than others.
218+
219+
### Security hardening checklist
220+
221+
- Pass API tokens through environment variables instead of writing them into the image.
222+
- Set configuration file permissions to `600`.
223+
- Always run the application as a non-root user.
224+
- Update CLI versions regularly to fix security vulnerabilities.
225+
226+
Security is always important, but the real challenge is consistently enforcing it in practice.
227+
228+
## Extending support for new CLI tools
229+
230+
If you want to support a new CLI tool in the future, there are only three steps:
231+
232+
1. **Dockerfile.template**: add the installation step.
233+
2. **docker-entrypoint.sh**: add the version override logic.
234+
3. **docker-compose-builder-web**: add the persistent volume mapping.
235+
236+
This template-based design makes extension simple without changing the core logic.
237+
238+
## Conclusion
239+
240+
Running AI CLI tools in Docker containers involves three core challenges: **user permissions, configuration persistence, and version management**. By combining dedicated users, named-volume isolation, and environment-variable-based overrides, the HagiCode project built a deployment architecture that is both secure and flexible.
241+
242+
Key design points:
243+
244+
- **User isolation**: Create a dedicated user during the image build stage, with runtime support for dynamic `PUID`/`PGID` mapping.
245+
- **Persistence strategy**: Each CLI tool gets its own named volume, so restarts do not affect configuration.
246+
- **Version flexibility**: Fixed defaults ensure reproducibility, while runtime overrides provide room for testing.
247+
- **Automated configuration**: Sensitive configuration can be injected automatically through environment variables.
248+
249+
This solution has been running stably in the HagiCode project for some time, and I hope it offers useful reference points for developers with similar needs.
250+
251+
## Copyright Notice
252+
253+
Thank you for reading. If you found this article useful, you are welcome to like, bookmark, and share it.
254+
This content was created with AI-assisted collaboration, and the final content was reviewed and confirmed by the author.
255+
- Author: [newbe36524](https://www.newbe.pro)
256+
- Original article: [https://docs.hagicode.com/blog/2026-03-26-docker-ai-cli-user-isolation-guide/](https://docs.hagicode.com/blog/2026-03-26-docker-ai-cli-user-isolation-guide/)
257+
- Copyright: Unless otherwise stated, all articles in this blog are licensed under BY-NC-SA. Please include the source when reposting.

0 commit comments

Comments
 (0)