AI agent가 코드를 생성하고 실행하는 시대가 왔다. Claude Code, Cursor, Devin 같은 도구들이 로컬 환경에서 직접 코드를 실행하는데, 솔직히 처음에는 좀 무서웠다. Agent가 rm -rf를 실행하거나, 시스템 파일을 건드리거나, 외부에 민감한 데이터를 전송하면 어쩌나 하는 생각이 들었다.
해결책은 간단하다. Docker 컨테이너 안에서 실행하면 된다. 컨테이너 내부에서 뭘 하든 호스트 시스템에는 영향이 없고, 문제가 생기면 컨테이너를 날리고 새로 만들면 그만이다. 이 글에서는 MacBook M1 32GB RAM 환경을 기준으로, AI agent용 Docker 샌드박스를 처음부터 끝까지 구축하는 방법을 정리한다.
목차
Docker Desktop M1 설치 및 Apple Silicon 설정
2024년 이후로 Docker Desktop의 Apple Silicon 지원이 많이 안정화됐다. 예전에는 M1에서 Docker를 돌리면 뭔가 하나씩 안 되는 게 있었는데, 지금은 대부분의 이미지가 ARM64 네이티브로 제공된다. 관련 내용은 Claude Code 권한 보안 설정에서도 다루고 있다.
설치 방법
Docker Desktop 공식 사이트에서 Apple Silicon용 .dmg 파일을 받아 설치한다. Homebrew로도 가능하다:
# Homebrew Cask로 설치 (권장)
$ brew install --cask docker
# 설치 후 Docker Desktop 앱 실행
$ open /Applications/Docker.app
# 설치 확인
$ docker --version
Docker version 27.5.1, build 9f9e405
$ docker compose version
Docker Compose version v2.32.4
# Apple Silicon 아키텍처 확인
$ docker info | grep -i arch
Architecture: aarch64
# 테스트 컨테이너 실행
$ docker run --rm hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
Architecture: aarch64가 출력되면 ARM64 네이티브로 정상 동작하는 것이다. 만약 x86_64로 표시된다면 Rosetta를 통해 에뮬레이션되고 있는 것이니 설정을 확인해야 한다.
초기 설정에서 챙겨야 할 것들
Docker Desktop을 처음 실행하면 설정 마법사가 뜬다. 여기서 두 가지를 확인한다:
- Use Virtualization framework: 반드시 체크. Apple의 네이티브 가상화 프레임워크를 사용해서 성능이 크게 좋아진다.
- Use Rosetta for x86_64/amd64 emulation: 체크 해제가 기본이고, 필요할 때만 켜는 것을 추천한다. 아래에서 자세히 다룬다.
Rosetta 에뮬레이션 이슈와 ARM 네이티브 이미지 선택

M1 Mac에서 Docker를 쓸 때 가장 많이 부딪히는 문제가 아키텍처 불일치이다. Docker Hub의 많은 이미지가 linux/amd64만 지원하는 경우가 아직 있다. 이런 이미지를 M1에서 그냥 실행하면 이런 경고가 뜬다:
$ docker run --rm postgres:14
WARNING: The requested image's platform (linux/amd64) does not match
the detected host platform (linux/arm64/v8) and no specific platform
was requested.
# 이 경고가 뜨면 Rosetta 에뮬레이션으로 실행되는 것
# 성능이 20~40% 정도 떨어집니다
# ARM64 네이티브 이미지를 명시적으로 지정하는 방법
$ docker run --rm --platform linux/arm64 postgres:16
# postgres:16은 ARM64 네이티브 이미지를 제공합니다
# 이미지의 지원 아키텍처 확인
$ docker manifest inspect python:3.12-slim | grep architecture
"architecture": "amd64",
"architecture": "arm64",
# arm64가 있으면 M1 네이티브 지원
내가 AI agent용 샌드박스를 만들면서 겪은 실제 이슈가 하나 있었다. chromadb라는 벡터 DB의 공식 이미지가 한동안 amd64만 지원해서, M1에서 실행하면 메모리를 비정상적으로 많이 먹었다. 이런 경우에는 해당 프로젝트의 GitHub에서 ARM64 빌드를 확인하거나, 직접 Dockerfile을 작성해서 ARM64 베이스 이미지 위에 올리는 게 낫다.
이미지 선택 원칙
AI agent 환경을 구축할 때 이미지 선택 기준을 정리하면 이렇다:
- 베이스 이미지는 반드시
linux/arm64지원 여부 확인 python:3.12-slim,node:22-slim등 공식 slim 이미지는 대부분 ARM64 지원- Alpine 이미지(
python:3.12-alpine)는 ARM64를 지원하지만, 일부 Python 패키지의 C 확장이 빌드 안 되는 경우가 있어서slim을 추천 - 커뮤니티 이미지를 쓸 때는 Docker Hub 페이지에서 OS/ARCH 탭을 확인
메모리/CPU 할당 최적화 (32GB 기준)
MacBook M1 32GB RAM 환경에서 Docker에 리소스를 얼마나 줄 것인지는 꽤 중요한 문제이다. 너무 많이 주면 macOS 자체가 느려지고, 너무 적게 주면 컨테이너 안에서 OOM(Out of Memory) 에러가 터진다.
Docker Desktop > Settings > Resources에서 설정한다. 내가 여러 조합을 시도해보고 안정적이었던 설정은 이렇다:
| 항목 | 추천 값 | 비고 |
|---|---|---|
| Memory | 12GB | 32GB 중 12GB. LLM 추론까지 돌리려면 16GB |
| CPU | 6 cores | M1의 8코어 중 6개. 호스트에 2개는 남겨둬야 함 |
| Swap | 4GB | 메모리 부족 시 버퍼 역할 |
| Disk image size | 100GB | AI 관련 이미지가 크므로 여유있게 |
주의할 점은 Docker Desktop이 할당받은 메모리를 항상 점유하는 건 아니라는 것이다. Apple Virtualization Framework를 사용하면 실제 사용량만큼만 물리 메모리를 소비한다. 다만 상한선을 12GB로 잡아두면, 컨테이너가 폭주해도 macOS가 멈추는 상황은 막을 수 있다.
개인적으로는 AI agent 작업 중에 VS Code, 브라우저, Slack까지 동시에 띄워놓기 때문에 Docker에 16GB 이상은 주지 않는다. 만약 Docker만 집중적으로 사용하는 상황이라면 20GB까지 올려도 괜찮다.
AI agent용 Dockerfile 작성
AI agent가 코드를 실행하려면 Python과 Node.js가 둘 다 필요한 경우가 많다. Python은 데이터 분석, ML 추론, API 호출에, Node.js는 웹 스크래핑, 프론트엔드 빌드, 각종 npm 도구에 쓰인다. 하나의 컨테이너에 두 런타임을 함께 넣되, 이미지 크기가 과도하게 커지지 않도록 multi-stage build를 활용한다.
# AI Agent Sandbox Dockerfile
# 타겟: Apple Silicon (linux/arm64)
FROM python:3.12-slim AS base
# 시스템 패키지 설치
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
build-essential \
jq \
wget \
unzip \
&& rm -rf /var/lib/apt/lists/*
# Node.js 22 LTS 설치 (ARM64 네이티브)
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
# Python 가상환경 생성
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# 자주 쓰는 Python 패키지 미리 설치
COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt
# 작업 디렉터리 설정
WORKDIR /workspace
# 비root 사용자 생성 (보안)
RUN useradd -m -s /bin/bash agent \
&& chown -R agent:agent /workspace /opt/venv
USER agent
# 기본 셸 설정
SHELL ["/bin/bash", "-c"]
CMD ["bash"]
위 Dockerfile과 함께 사용할 requirements.txt는 이렇게 구성한다:
# requirements.txt — AI agent 기본 패키지
requests==2.32.3
httpx==0.28.1
beautifulsoup4==4.12.3
pandas==2.2.3
numpy==2.2.1
pydantic==2.10.5
python-dotenv==1.0.1
tiktoken==0.8.0
openai==1.59.7
anthropic==0.42.0
langchain-core==0.3.30
chromadb==0.6.3
pytest==8.3.4
핵심 포인트는 useradd로 비root 사용자를 만들어서 컨테이너 내에서도 권한을 제한하는 것이다. AI agent가 apt-get install이나 시스템 파일 수정을 시도해도 권한 에러로 차단된다. 이게 샌드박스의 첫 번째 보안 레이어이다.
이미지 빌드 및 테스트
# ARM64 네이티브로 빌드
$ docker build --platform linux/arm64 -t ai-sandbox:latest .
# 빌드 시간: M1 기준 약 3~5분 (네트워크 상태에 따라 다름)
# 이미지 크기 확인
$ docker images ai-sandbox
REPOSITORY TAG IMAGE ID CREATED SIZE
ai-sandbox latest a3b7c9d2e1f0 10 seconds ago 1.87GB
# 컨테이너 진입 테스트
$ docker run --rm -it ai-sandbox:latest
agent@c4f2a1b3d5e6:/workspace$ python --version
Python 3.12.8
agent@c4f2a1b3d5e6:/workspace$ node --version
v22.13.0
agent@c4f2a1b3d5e6:/workspace$ whoami
agent
agent@c4f2a1b3d5e6:/workspace$ sudo apt-get update
bash: sudo: command not found
# sudo가 없으므로 시스템 변경 불가 — 의도된 동작
볼륨 마운트로 프로젝트 파일 공유
컨테이너를 삭제하면 내부 데이터가 전부 사라진다. AI agent가 생성한 코드나 결과물을 호스트에서도 접근하려면 볼륨 마운트가 필요하다. 다만 마운트 범위를 최소화하는 것이 보안상 중요하다.
# 프로젝트 디렉터리만 마운트 (읽기/쓰기)
$ docker run --rm -it \
-v $(pwd)/project:/workspace/project \
ai-sandbox:latest
# 읽기 전용 마운트 (agent가 수정하면 안 되는 데이터)
$ docker run --rm -it \
-v $(pwd)/reference-data:/workspace/data:ro \
-v $(pwd)/output:/workspace/output \
ai-sandbox:latest
# 절대로 하면 안 되는 것: 홈 디렉터리 전체 마운트
# $ docker run -v $HOME:/workspace/home ai-sandbox:latest
# ↑ SSH 키, .env 파일, 브라우저 쿠키 등 모든 민감 정보가 노출됨
마운트 전략을 정리하면 이렇다:
- 작업 디렉터리 (
/workspace/project): 읽기/쓰기 마운트. Agent가 코드를 생성하고 실행하는 공간 - 참조 데이터 (
/workspace/data): 읽기 전용 마운트. 학습 데이터, 설정 파일 등 - 출력 디렉터리 (
/workspace/output): 읽기/쓰기 마운트. 실행 결과물 저장 - 비밀 정보: 볼륨 마운트 대신
--env-file이나 Docker secrets 사용
M1 Mac에서 볼륨 마운트 시 성능 이슈가 있을 수 있다. Docker Desktop 4.30 이후로 VirtioFS가 기본 파일 시스템으로 설정되면서 많이 개선됐지만, node_modules처럼 파일 수가 수만 개인 디렉터리를 마운트하면 여전히 느린다. 이 경우 node_modules는 컨테이너 내부에만 두고 마운트에서 제외하는 것이 좋다:
# node_modules를 마운트에서 제외하는 패턴
$ docker run --rm -it \
-v $(pwd)/project:/workspace/project \
-v /workspace/project/node_modules \
ai-sandbox:latest
# 두 번째 -v에 호스트 경로가 없으면 anonymous volume이 생성됨
# 호스트의 node_modules와 컨테이너의 node_modules가 분리됩니다
네트워크 격리 설정
AI agent 샌드박스에서 가장 신경 써야 할 부분이 네트워크이다. Agent가 생성한 코드가 외부 서버에 데이터를 전송하거나, 악의적인 다운로드를 시도할 수 있으니까.
# 네트워크 완전 차단 (가장 엄격)
$ docker run --rm -it --network none ai-sandbox:latest
agent@abc123:/workspace$ curl https://google.com
curl: (6) Could not resolve host: google.com
# 외부 접근 완전 불가
# 내부 네트워크만 허용 (컨테이너 간 통신은 가능)
$ docker network create --internal ai-internal-net
$ docker run --rm -it --network ai-internal-net ai-sandbox:latest
agent@def456:/workspace$ curl https://google.com
curl: (6) Could not resolve host: google.com
# 외부는 차단되지만, 같은 네트워크의 다른 컨테이너에는 접근 가능
# 특정 도메인만 허용하는 방법 (iptables 활용)
# 이건 컨테이너 내부가 아니라 호스트에서 설정합니다
$ docker run --rm -it \
--cap-drop=ALL \
--security-opt=no-new-privileges \
ai-sandbox:latest
실무에서 내가 사용하는 전략은 이렇다. 코드 생성 단계에서는 네트워크를 열어두되, 코드 실행 단계에서는 --network none으로 완전히 차단한다. Agent가 pip install이나 npm install이 필요하면 코드 생성 단계에서 의존성을 설치하고, 실행은 격리된 환경에서 하는 방식이다.
Linux capabilities 제한
네트워크 차단과 함께 컨테이너의 Linux capabilities도 최소화하면 보안이 더 강화된다:
# 모든 capabilities 제거 후 필요한 것만 추가
$ docker run --rm -it \
--cap-drop=ALL \
--cap-add=CHOWN \
--cap-add=SETUID \
--cap-add=SETGID \
--security-opt=no-new-privileges \
--read-only \
--tmpfs /tmp:size=512m \
-v $(pwd)/project:/workspace/project \
ai-sandbox:latest
# --read-only: 컨테이너 파일시스템을 읽기 전용으로 마운트
# --tmpfs /tmp: /tmp만 쓰기 가능 (크기 제한)
# --security-opt=no-new-privileges: 권한 상승 차단
docker-compose로 multi-container 환경
AI agent가 단순히 코드만 실행하는 게 아니라, 벡터 DB에 임베딩을 저장하거나, 로컬 LLM을 호출하는 경우도 있다. 이런 복합 환경은 docker-compose로 관리하는 게 편하다.
# docker-compose.yml
version: "3.9"
services:
# AI Agent 실행 환경
sandbox:
build:
context: .
dockerfile: Dockerfile
platform: linux/arm64
volumes:
- ./project:/workspace/project
- ./output:/workspace/output
networks:
- ai-internal
deploy:
resources:
limits:
cpus: "4"
memory: 8G
reservations:
memory: 2G
stdin_open: true
tty: true
# 벡터 데이터베이스 (ChromaDB)
vectordb:
image: chromadb/chroma:0.6.3
platform: linux/arm64
volumes:
- chroma-data:/chroma/chroma
networks:
- ai-internal
deploy:
resources:
limits:
memory: 2G
environment:
- IS_PERSISTENT=TRUE
- ANONYMIZED_TELEMETRY=FALSE
# Redis (캐싱, 세션 관리)
redis:
image: redis:7.4-alpine
platform: linux/arm64
networks:
- ai-internal
deploy:
resources:
limits:
memory: 512M
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
networks:
ai-internal:
internal: true # 외부 인터넷 접근 차단
driver: bridge
volumes:
chroma-data:
이 구성의 핵심은 internal: true 네트워크이다. 세 컨테이너끼리는 서로 통신할 수 있지만, 외부 인터넷에는 접근할 수 없다. sandbox 컨테이너에서 http://vectordb:8000으로 ChromaDB에 접근하고, redis:6379로 Redis에 접근할 수 있지만, https://evil-server.com으로는 데이터를 보낼 수 없다.
# 전체 환경 시작
$ docker compose up -d
[+] Running 4/4
✔ Network ai-sandbox_ai-internal Created
✔ Container ai-sandbox-redis-1 Started
✔ Container ai-sandbox-vectordb-1 Started
✔ Container ai-sandbox-sandbox-1 Started
# sandbox 컨테이너에 접속
$ docker compose exec sandbox bash
agent@sandbox:/workspace$ # 내부 서비스 접근 테스트
agent@sandbox:/workspace$ curl http://vectordb:8000/api/v1/heartbeat
{"nanosecond heartbeat":1712345678901234567}
agent@sandbox:/workspace$ redis-cli -h redis ping
PONG
agent@sandbox:/workspace$ curl https://google.com
curl: (6) Could not resolve host: google.com
# 외부 접근 차단 확인
# 리소스 사용량 실시간 모니터링
$ docker compose stats
NAME CPU % MEM USAGE / LIMIT
ai-sandbox-sandbox-1 0.32% 245.1MiB / 8GiB
ai-sandbox-vectordb-1 0.15% 187.3MiB / 2GiB
ai-sandbox-redis-1 0.08% 12.4MiB / 512MiB
# 전체 환경 종료 및 정리
$ docker compose down
$ docker compose down -v # 볼륨까지 삭제 (데이터 초기화)
Claude Code를 Docker 안에서 실행하기
Claude Code는 기본적으로 호스트 머신에서 실행되지만, Docker 컨테이너 안에서 실행하면 한 단계 더 격리된 환경을 만들 수 있다. 특히 Claude Code의 --dangerously-skip-permissions 모드를 사용할 때, 컨테이너 안에서 실행하면 실수로 호스트 시스템이 손상되는 것을 방지할 수 있다.
# Dockerfile.claude-code
FROM node:22-slim
# 시스템 패키지
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
curl \
python3 \
python3-pip \
python3-venv \
&& rm -rf /var/lib/apt/lists/*
# Claude Code 설치
RUN npm install -g @anthropic-ai/claude-code
# 작업 디렉터리
WORKDIR /workspace
# 비root 사용자
RUN useradd -m -s /bin/bash developer \
&& chown -R developer:developer /workspace
USER developer
CMD ["claude"]
실행할 때는 API 키를 환경변수로 전달한다:
# Claude Code를 Docker 안에서 실행
$ docker build -f Dockerfile.claude-code -t claude-sandbox .
$ docker run --rm -it \
-e ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} \
-v $(pwd)/project:/workspace/project \
--network none \
claude-sandbox
# Claude Code가 컨테이너 내부에서 실행됨
# /workspace/project 안에서만 파일을 읽고 쓸 수 있음
# 네트워크는 차단되어 있지만, Claude API 호출이 필요하므로
# 실제로는 API 서버만 허용하는 네트워크 설정이 필요합니다
# Anthropic API만 허용하는 네트워크 설정
$ docker network create claude-net
# claude-net에서 Anthropic API만 허용하려면
# 프록시 컨테이너를 앞에 두는 방법이 현실적입니다
$ docker run --rm -it \
-e ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} \
-v $(pwd)/project:/workspace/project \
--network claude-net \
claude-sandbox
솔직히 말하면 네트워크를 완전히 차단하면 Claude Code가 API 서버에 접근할 수 없어서 동작하지 않는다. 현실적인 방법은 claude-net 네트워크에 Squid 같은 프록시를 두고, api.anthropic.com만 화이트리스트로 허용하는 것이다. 이 부분은 환경에 따라 설정이 달라지므로, 우선은 일반 네트워크로 실행하되 볼륨 마운트 범위를 최소화하는 것부터 시작하는 것을 추천한다.
컨테이너 리소스 모니터링
AI agent가 코드를 실행하면 예상치 못한 리소스 소모가 발생할 수 있다. 무한 루프, 메모리 누수, 디스크 폭주 같은 상황을 빠르게 감지하려면 모니터링이 필수이다.
# 실시간 리소스 모니터링 (docker stats)
$ docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}"
NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
ai-sandbox 34.52% 1.23GiB / 8GiB 15.38% 1.2kB / 0B 45MB / 12MB
vectordb 2.31% 312.4MiB / 2GiB 15.26% 856B / 456B 23MB / 8MB
redis 0.12% 18.7MiB / 512MiB 3.65% 234B / 123B 4MB / 1MB
# 특정 컨테이너만 모니터링
$ docker stats ai-sandbox --no-stream
# --no-stream: 한 번만 출력하고 종료
# 컨테이너 내부에서 프로세스 확인
$ docker exec ai-sandbox ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
agent 1 0.0 0.0 4624 3712 pts/0 Ss 10:23 0:00 bash
agent 45 98.2 2.1 345612 178432 pts/0 R 10:25 1:23 python train.py
# CPU 98% 사용하는 프로세스 발견 → 필요시 강제 종료
$ docker exec ai-sandbox kill 45
# 컨테이너 자체를 즉시 중지 (비상 상황)
$ docker kill ai-sandbox
# 디스크 사용량 확인
$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 12 5 8.234GB 4.123GB (50%)
Containers 3 3 245.1MB 0B (0%)
Local Volumes 4 3 1.567GB 234MB (14%)
Build Cache 23 0 2.345GB 2.345GB
# 사용하지 않는 리소스 정리
$ docker system prune -a --volumes
WARNING! This will remove:
- all stopped containers
- all networks not used by at least one container
- all anonymous volumes not used by at least one container
- all images without at least one container associated to them
- all build cache
Total reclaimed space: 6.7GB
한 가지 팁을 주자면, docker compose에서 deploy.resources.limits를 설정해두면 컨테이너가 지정된 리소스 이상을 사용하지 못한다. 메모리 제한을 초과하면 커널이 OOM Killer로 프로세스를 종료한다. 이게 결국 agent가 무한정 리소스를 소모하는 것을 막는 하드 리밋 역할을 한다.
AI가 생성한 코드를 안전하게 실행하는 패턴
지금까지 설명한 Docker 설정을 종합해서, AI agent가 생성한 코드를 안전하게 실행하는 워크플로를 정리한다. 나는 이 패턴을 “일회용 컨테이너 패턴”이라고 부르고 있다.
패턴 1: 일회용 컨테이너로 코드 실행
#!/bin/bash
# run-sandbox.sh — AI가 생성한 코드를 안전하게 실행
SCRIPT_FILE=$1
TIMEOUT=${2:-30} # 기본 30초 타임아웃
if [ -z "$SCRIPT_FILE" ]; then
echo "Usage: ./run-sandbox.sh [timeout_seconds]"
exit 1
fi
# 스크립트 파일을 임시 디렉터리에 복사
TEMP_DIR=$(mktemp -d)
cp "$SCRIPT_FILE" "$TEMP_DIR/script.py"
# 일회용 컨테이너에서 실행
docker run --rm \
--name "sandbox-$(date +%s)" \
--network none \
--cap-drop=ALL \
--security-opt=no-new-privileges \
--read-only \
--tmpfs /tmp:size=256m,noexec \
--memory=2g \
--cpus=2 \
--pids-limit=100 \
-v "$TEMP_DIR:/workspace/run:ro" \
-v "$TEMP_DIR/output:/workspace/output" \
ai-sandbox:latest \
timeout "$TIMEOUT" python /workspace/run/script.py
EXIT_CODE=$?
# 결과 확인
if [ $EXIT_CODE -eq 124 ]; then
echo "TIMEOUT: 스크립트가 ${TIMEOUT}초 내에 완료되지 않았습니다."
elif [ $EXIT_CODE -ne 0 ]; then
echo "ERROR: 종료 코드 $EXIT_CODE"
else
echo "SUCCESS"
ls -la "$TEMP_DIR/output/" 2>/dev/null
fi
# 임시 디렉터리 정리
rm -rf "$TEMP_DIR"
이 스크립트의 보안 레이어를 하나씩 분석하면:
--network none: 외부 통신 완전 차단. 데이터 유출 방지--cap-drop=ALL: 모든 Linux capabilities 제거--read-only: 컨테이너 파일시스템 쓰기 차단--tmpfs /tmp:size=256m,noexec: /tmp에만 쓰기 허용, 실행 파일 생성 불가--memory=2g: 메모리 상한 2GB--cpus=2: CPU 2코어 제한--pids-limit=100: 프로세스 수 제한 (fork bomb 방지)timeout: 실행 시간 제한- 스크립트 파일 읽기 전용 마운트
패턴 2: Python 래퍼로 실행 결과 캡처
쉘 스크립트 대신 Python으로 래퍼를 만들면 실행 결과를 구조화된 형태로 받을 수 있다:
# sandbox_runner.py
import subprocess
import json
import tempfile
import os
from pathlib import Path
from dataclasses import dataclass, asdict
@dataclass
class SandboxResult:
success: bool
stdout: str
stderr: str
exit_code: int
timed_out: bool
execution_time_ms: int
def run_in_sandbox(
code: str,
timeout: int = 30,
memory_limit: str = "2g",
network: bool = False,
) -> SandboxResult:
"""AI가 생성한 Python 코드를 Docker 샌드박스에서 실행"""
with tempfile.TemporaryDirectory() as tmpdir:
# 코드를 파일로 저장
script_path = Path(tmpdir) / "script.py"
script_path.write_text(code, encoding="utf-8")
output_dir = Path(tmpdir) / "output"
output_dir.mkdir()
# Docker 실행 명령어 구성
cmd = [
"docker", "run", "--rm",
"--network", "none" if not network else "bridge",
"--cap-drop=ALL",
"--security-opt=no-new-privileges",
"--memory", memory_limit,
"--cpus", "2",
"--pids-limit", "100",
"-v", f"{tmpdir}/script.py:/workspace/script.py:ro",
"-v", f"{output_dir}:/workspace/output",
"ai-sandbox:latest",
"timeout", str(timeout),
"python", "/workspace/script.py",
]
import time
start = time.monotonic()
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout + 10, # Docker 오버헤드 고려
)
elapsed = int((time.monotonic() - start) * 1000)
return SandboxResult(
success=result.returncode == 0,
stdout=result.stdout[:10000], # 출력 크기 제한
stderr=result.stderr[:5000],
exit_code=result.returncode,
timed_out=result.returncode == 124,
execution_time_ms=elapsed,
)
except subprocess.TimeoutExpired:
elapsed = int((time.monotonic() - start) * 1000)
return SandboxResult(
success=False,
stdout="",
stderr="Execution timed out",
exit_code=-1,
timed_out=True,
execution_time_ms=elapsed,
)
# 사용 예시
if __name__ == "__main__":
ai_generated_code = """
import math
result = [math.factorial(i) for i in range(20)]
for i, val in enumerate(result):
print(f"{i}! = {val}")
"""
result = run_in_sandbox(ai_generated_code, timeout=10)
print(json.dumps(asdict(result), indent=2, ensure_ascii=False))
이 래퍼를 사용하면 AI agent가 생성한 코드의 실행 결과를 안전하게 받아볼 수 있다. 성공 여부, 표준 출력, 에러 메시지, 실행 시간까지 구조화된 형태로 반환된다.
실전에서 주의할 점
이 패턴을 실제로 운영하면서 알게 된 몇 가지 주의사항이다:
- Docker 이미지 캐싱: 매번 새 컨테이너를 만들지만 이미지는 캐싱되므로 시작 시간이 1~2초 수준이다. 이미지를 미리 빌드해두면 체감 속도가 빠르다.
- 파일 시스템 정리:
--rm플래그를 빠뜨리면 중단된 컨테이너가 디스크에 쌓인다. 주기적으로docker container prune을 실행하자. - 로그 크기 제한: Agent가 무한 출력하는 코드를 생성할 수 있다.
stdout의 크기를 코드 레벨에서 잘라내는 것이 안전하다. - 시크릿 주입: API 키 같은 비밀값을 컨테이너에 전달할 때는
--env보다--env-file을 사용하자. 환경변수는docker inspect로 볼 수 있기 때문에, 컨테이너가 남아있으면 노출될 수 있다.
AI agent를 활용하면서 가장 중요한 것은 결국 “통제 가능한 환경”을 만드는 것이다. Docker는 그 목적에 가장 적합한 도구이고, Apple Silicon Mac에서도 이제 충분히 성숙한 환경을 제공한다. 처음에는 설정이 번거롭게 느껴질 수 있지만, 한 번 구축해두면 어떤 코드든 걱정 없이 실행할 수 있다는 안도감이 상당하다. 자세한 내용은 Docker Desktop Mac 설치 가이드를 참고하자.