테마 전환

셀프 호스팅 Dev Sandbox: Docker와 Go로 preview URL 환경 만들기

Easton editorial illustration: code bundle entering an isolated Docker sandbox and exiting through a guarded preview-URL portal

"sandboxed README는 Go 제어 플레인, Docker, Traefik, SQLite, preview URL, idle stop, production hardening 경계를 설명합니다."

"Docker resource constraints 문서는 명시적으로 설정하지 않으면 container에 CPU나 memory 제한이 없다고 설명합니다."

"Docker Sandboxes 문서는 microVM, 독립 Docker daemon, network isolation, credential isolation을 더 강한 security model로 제시합니다."

"Traefik Docker provider는 Docker labels에서 routing 설정을 읽고 Host rule로 container service에 route할 수 있습니다."

PR마다 독립 preview 환경을 열 때 흔한 선택은 Vercel이나 Netlify입니다. 하지만 비용, private network 안의 데이터, infrastructure 통제권이 더 중요하다면 단일 Docker host와 Go 제어 플레인으로 초기 요구를 대체할 수 있습니다. 각 sandbox는 독립 preview URL을 갖고, resource에는 상한이 있으며, security boundary도 명확합니다. Kubernetes도 multi-node도 필요 없습니다.

판단표: 어떤 조건에서 어떤 방식을 쓸까

“셀프 호스팅 preview 환경” 요구를 받으면 많은 사람이 먼저 shell script로 docker run을 반복하거나, 반대로 바로 Kubernetes를 떠올립니다. 먼저 판단표로 나눕니다.

상황추천 방식이유
내부 팀의 동시 preview가 10명 미만이고 trust boundary가 팀 안에 있음단일 host Docker + Go 제어 플레인resource density를 제어하기 쉽고, 구조가 단순하며, K8s cluster 운영이 필요 없음
내부 팀의 동시 preview가 20명 초과이거나 multi-node HA가 필요함K8s + Namespace isolation단일 host로는 부족하며 node 간 scheduling과 rolling upgrade가 필요함
외부 사용자 또는 불신 코드 실행, 예를 들어 autonomous agentmicroVM, 예: Docker Sandboxes / FirecrackerDocker socket은 host root에 준하는 권한이며 불신 workload와 섞으면 안 됨
persistence가 필요 없는 단순 static previewshell script + random port동작은 하지만 preview URL이 안정적이지 않고 resource limit과 security boundary가 약함

판단 기준은 세 가지입니다.

team size: 단일 host는 10명 이하의 동시 내부 preview에 적합합니다. 대략 sandbox 하나당 512 MB RAM + 0.5 CPU를 잡습니다. 16 GB memory host라면 20개 안팎의 sandbox를 버틸 수 있지만, 그 이후에는 여유가 줄어듭니다. 이 density를 넘으면 K8s scheduling이나 microVM으로 node를 나눠야 합니다.

trust boundary: 내부 팀원과 신뢰할 수 있는 사용자는 Docker container에서 실행할 수 있습니다. 하지만 외부의 낯선 사용자나 autonomous agent가 임의 코드를 실행한다면 Docker socket 방식은 충분히 안전하지 않습니다. Docker 공식 Sandboxes는 microVM isolation을 사용하고, 각 sandbox가 별도 Docker daemon, filesystem, network를 가집니다.

upgrade path: 처음에는 단일 host Docker로 시작해 요구와 resource density를 검증합니다. 동시 실행이 단일 host를 넘거나 multi-node availability가 필요해지면 K8s로 이동합니다. trust boundary가 “내부 팀”에서 “외부 사용자”로 바뀌면 microVM으로 이동합니다.

tastyeffectco/sandboxes README는 이 단일 host Docker 방식이 AI app-builder, agent platform, coding playground에 맞는다고 설명합니다. microVM 수준의 격리가 아니라, Go 제어 플레인이 단일 host Docker 위에서 container를 만들고 preview URL을 공개하는 방식입니다.

아키텍처 분해: 제어 플레인의 핵심 구성요소

단일 host Docker preview 환경은 docker run을 한 번 실행하는 것으로 끝나지 않습니다. lifecycle, route registration, resource cleanup을 관리하는 제어 플레인이 필요합니다. tastyeffectco/sandboxes의 architecture는 여섯 module로 나뉩니다.

Go 제어 플레인(sandboxd): container 안에서 실행되고 host Docker socket과 data directory를 mount합니다. Docker CLI를 통해 sandbox container lifecycle을 관리합니다. 생성, 시작, 중지, 삭제입니다. 모든 sandbox metadata는 SQLite에 저장되고 source of truth가 됩니다.

Docker socket mount: Docker daemon으로 들어가는 제어 플레인의 입구입니다. sandboxd/var/run/docker.sock을 mount하여 container 생성과 관리 권한을 얻습니다. 이 지점이 전체 architecture의 권한 경계입니다. Docker socket을 mount한 제어 플레인은 host에 대해 매우 높은 권한을 가집니다.

Traefik labels registration: 각 sandbox container가 시작될 때 제어 플레인은 Docker labels로 Traefik routing 설정을 주입합니다. Traefik은 reverse proxy로 동작하며 labels에서 route rule을 발견하고 *.preview.example.com 요청을 해당 container로 전달합니다.

SQLite metadata storage: 각 sandbox에는 unique ID와 대응 directory가 있습니다. metadata는 SQLite에 저장됩니다. workspace는 SANDBOXED_DATA_DIR/workspaces/ 아래에 있고, sandbox마다 subdirectory를 만들어 source code, config, artifact를 저장합니다.

idle reaper와 pressure reaper: idle reaper는 sandbox container의 idle time을 확인하고 threshold를 넘으면 container를 stop하여 RAM을 회수합니다. pressure reaper는 host memory pressure를 감시하고 memory가 부족해질 때 일부 sandbox를 stop하여 host OOM을 피합니다. 이 두 reaper가 resource cleanup의 핵심입니다.

wake path: idle reaper가 sandbox container를 stop한 뒤, preview URL의 첫 access가 container를 깨워야 합니다. Traefik catch-all이 request를 제어 플레인으로 넘기고, 제어 플레인은 container를 시작한 뒤 app이 ready될 때까지 warming page를 반환합니다.

최소 구성은 Go 제어 플레인 container, Docker socket mount, Traefik, SQLite, idle reaper입니다. local quick start에는 Docker Engine과 Compose plugin이 필요합니다.

Preview URL 구현

preview URL의 핵심은 random port가 아니라 안정적인 domain입니다. 각 sandbox는 독립적인 {sandbox_id}.preview.example.com hostname을 가집니다.

Traefik Docker provider 설정

Traefik은 Docker provider를 통해 container labels에서 routing 설정을 발견할 수 있습니다. 최소 설정 예시입니다.

# traefik.yml
providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false

exposedByDefault: false는 명시적으로 labels가 붙은 container만 Traefik이 발견한다는 뜻입니다.

Host rule과 Docker labels

제어 플레인은 sandbox container를 만들 때 labels를 주입합니다. 예시:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.sandbox123.rule=Host(`sandbox123.preview.example.com`)"
  - "traefik.http.routers.sandbox123.entrypoints=websecure"
  - "traefik.http.services.sandbox123.loadbalancer.server.port=3000"

Host rulesandbox123.preview.example.com 요청을 해당 container로 route합니다. loadbalancer.server.port는 container 내부 application이 listen하는 port를 지정합니다.

wake-on-request path

idle reaper가 sandbox container를 stop한 뒤, preview URL의 첫 access가 wake flow를 시작합니다.

  1. DNS가 Traefik으로 resolve됩니다. *.preview.example.com wildcard DNS가 필요합니다
  2. Traefik은 해당 sandbox route rule을 찾지만 container는 stopped 상태입니다
  3. Traefik catch-all이 request를 제어 플레인의 wake handler로 전달합니다
  4. 제어 플레인이 sandbox container를 시작하고 warming page를 반환합니다
  5. container가 ready되면 Traefik은 이후 request를 container로 직접 전달합니다

catch-all의 핵심은 모든 sandbox router보다 낮은 priority의 fallback router를 두는 것입니다.

labels:
  - "traefik.http.routers.catch-all.rule=HostRegexp(`{subdomain:[a-z0-9-]+}.preview.example.com`)"
  - "traefik.http.routers.catch-all.priority=1"
  - "traefik.http.routers.catch-all.service=wake-service"

sandbox route와 match되지 않는 request는 catch-all로 떨어지고, 제어 플레인이 wake logic을 처리합니다.

Security boundary: Docker socket 권한

Docker socket을 mount하는 것은 제어 플레인에 host root에 준하는 힘을 주는 일입니다. 이것이 이 architecture의 security baseline입니다. 내부 팀과 신뢰할 수 있는 사용자에게는 맞지만, 불신 코드 실행에는 맞지 않습니다.

Docker socket의 risk

Docker 공식 문서는 daemon에 attack surface가 있다고 설명합니다. Docker API를 안전하지 않게 공개하면 remote non-root user가 host root access를 얻을 수 있습니다. /var/run/docker.sock을 mount한 container는 Docker CLI로 container를 만들고, 수정하고, 삭제할 수 있습니다. 직접 만든 container를 통해 host filesystem과 network에도 접근할 수 있습니다.

즉 다음을 의미합니다.

  • 제어 플레인 container는 host에 대해 매우 높은 권한을 가집니다
  • 제어 플레인과 불신 workload를 같은 host에서 섞지 않습니다
  • sandbox container 안의 code도 제어 플레인에 영향을 줄 수 있다면 host에 간접 영향을 줄 수 있습니다

불신 scenario의 경계

단일 host Docker + Go 제어 플레인이 맞는 경우:

  • 내부 팀원을 위한 preview 환경
  • 신뢰할 수 있는 사용자용 coding playground
  • AI app-builder나 agent platform의 internal validation environment

맞지 않는 경우:

  • 외부의 알 수 없는 사용자가 실행하는 임의 코드
  • host를 신뢰할 수 없는 autonomous agent의 production 실행 환경
  • 강한 격리가 필요한 multi-tenant platform

trust boundary가 “내부 팀”에서 “외부 사용자”로 바뀌면 Docker 공식 Sandboxes나 Firecracker microVM으로 이동해야 합니다. Docker Sandboxes의 security model에는 hypervisor isolation, 독립 network, 독립 Docker daemon, 독립 filesystem, credential isolation이 포함됩니다. 각 sandbox는 host Docker daemon을 공유하는 container가 아니라 완전한 microVM입니다.

Production hardening checklist

production deployment에서는 다음 경계를 보강합니다.

  • network isolation: 제어 플레인과 sandbox container를 전용 network에서 실행하고 business network와 섞지 않습니다
  • API authentication: local quick start는 인증이 없을 수 있습니다. production에서는 token 또는 다른 auth mechanism을 켭니다
  • minimum exposure: preview URL은 Traefik reverse proxy로 공개하고 Docker API port는 직접 공개하지 않습니다
  • TLS: preview URL에는 wildcard TLS certificate를 사용하고 plaintext 전송을 피합니다
  • logs and monitoring: 제어 플레인의 API request와 container lifecycle event를 기록하고 alert로 연결합니다

더 강한 security boundary가 필요하다면 AI Agent sandbox 실전 guide에서 gVisor, Firecracker, Kubernetes 경계 방식을 비교합니다.

Resource limits: memory / CPU / PIDs

Docker container는 default로 resource constraint가 없습니다. 제한을 걸지 않으면 sandbox container 하나가 host RAM과 CPU를 소모해 다른 sandbox와 host process에 영향을 줍니다. multi-tenant preview 환경에서는 container별 hard limit이 최소 조건입니다.

Docker default behavior

Docker 공식 문서는 container가 kernel scheduler가 허용하는 범위에서 host resource를 사용할 수 있다고 설명합니다. --memory--cpus를 명시하지 않으면 container는 비어 있는 host resource를 가져갈 수 있습니다.

memory hard limit

--memory는 container가 사용할 수 있는 memory 상한을 설정합니다. 예시:

docker run --memory="512m" --memory-swap="512m" sandbox-image

--memory-swap은 memory + swap 상한입니다. --memory-swap--memory와 같으면 container는 swap을 사용하지 않습니다.

memory limit을 초과하면 OOM killer가 container process를 종료할 수 있습니다. host OOM이 발생하면 다른 container와 host process에도 영향을 줄 수 있습니다.

CPU share limit

--cpus는 container가 사용할 수 있는 CPU 양을 설정합니다. 예시:

docker run --cpus="0.5" sandbox-image

container는 최대 0.5 CPU만 사용할 수 있습니다. 여러 sandbox가 동시에 실행될 때 CPU share limit은 container 하나가 모든 CPU를 점유하는 일을 막습니다.

Process count limit

--pids-limit은 fork bomb을 막는 데 사용합니다. 예시:

docker run --pids-limit=100 sandbox-image

container는 최대 100개의 process만 만들 수 있습니다. 한도를 넘으면 fork()가 실패합니다.

Compose configuration example

Docker Compose에서는 deploy.resources.limits에 설정합니다.

services:
  sandbox:
    image: sandbox-image
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 512M
          pids: 100

Compose의 deploy.resources는 Docker Swarm mode에서 적용됩니다. 단일 host Docker에서는 --memory, --cpus, --pids-limit을 직접 전달하거나 docker-compose --compatibility로 실행합니다.

제어 플레인은 sandbox container를 만들 때 이런 limit parameter를 주입해야 합니다. 사용자 수동 설정에 의존하지 않는 편이 안전합니다.

Operations: image cache와 Docker Hub rate limit

많은 sandbox를 자주 만들고 삭제하면 image pull이 bottleneck이 됩니다. Docker Hub에는 pull rate limit과 abuse rate limit이 있고, account type과 plan에 따라 policy가 바뀝니다. production에서는 sandbox image pull을 매번 public Docker Hub에 맡기지 않는 구조가 필요합니다.

Docker Hub rate limit

Docker 공식 문서는 anonymous user, authenticated user, team account마다 pull rate limit이 다르다고 설명합니다. 상한을 넘으면 pull request가 거부됩니다. 구체적인 숫자는 policy에 따라 바뀌므로 본문에 고정하지 말고 Docker Hub usage and limits를 확인합니다.

multi-sandbox에서는 다음이 문제가 됩니다.

  • sandbox를 자주 만들면 container start마다 image pull이 생깁니다
  • idle reaper로 stop한 뒤 다시 시작하면 image가 다시 필요할 수 있습니다
  • 같은 image가 여러 sandbox에서 반복적으로 pull됩니다

Image pre-warming과 cache strategy

production environment에는 다음 조치가 필요합니다.

Image pre-warming: 제어 플레인을 시작하기 전에 자주 쓰는 image를 local에 pull해 둡니다. sandbox 시작 시 pull 대기 시간을 줄입니다.

Private registry: 자주 쓰는 image를 Harbor, AWS ECR, GCP Artifact Registry 같은 private registry에 push합니다. 제어 플레인은 public Docker Hub가 아니라 private registry에서 pull합니다.

Docker Hub login: Docker Hub에서 pull해야 한다면 authenticated account를 사용해 적절한 pull allowance를 얻습니다. Docker는 production에서 anonymous pull에 의존하지 말고 login할 것을 권장합니다.

Image cache: Docker daemon은 이미 pull한 image layer를 cache합니다. sandbox container를 자주 삭제하고 재생성한다면 cleanup이 유용한 layer를 너무 공격적으로 삭제하지 않게 해야 합니다.

Internal registry acceleration

host가 private network에 있다면 enterprise network의 Docker pull timeout troubleshooting을 참고해 registry mirror나 proxy를 설정합니다. image pull은 production platform의 일부로 다뤄야 합니다.

Troubleshooting checklist: preview URL에 접근할 수 없음

preview URL이 열리지 않으면 이 5단계를 순서대로 확인합니다.

1. DNS가 Traefik을 가리키는가

wildcard DNS 설정을 확인합니다. *.preview.example.com의 A record 또는 CNAME은 Traefik이 실행되는 host IP를 가리켜야 합니다.

판정에는 dig 또는 nslookup을 사용합니다.

dig sandbox123.preview.example.com

반환된 IP는 다른 address가 아니라 Traefik host IP여야 합니다.

2. Traefik이 container labels를 발견했는가

Traefik Docker provider 설정과 container labels를 확인합니다.

판정에는 Traefik dashboard나 logs를 사용합니다.

docker logs traefik-container | grep "sandbox123"

Traefik logs에는 sandbox123의 route rule이 발견되었다는 내용이 있어야 합니다. 없다면 다음을 확인합니다.

  • traefik.enable=true label이 있는지
  • exposedByDefault: false 설정이 올바른지
  • Traefik이 Docker socket을 올바르게 mount했는지

3. container가 실행 중인가

sandbox container 상태를 확인합니다.

판정에는 docker ps를 사용합니다.

docker ps | grep sandbox123

container가 stopped라면 idle reaper가 중지했거나 wake path가 restart에 실패했을 수 있습니다. preview URL에 access하면 제어 플레인의 wake handler가 container를 시작하고 warming page를 반환해야 합니다. 실패하면 제어 플레인 logs를 확인합니다.

4. application listen address

container 안 application의 listen address와 port를 확인합니다.

container에 들어가 listen port를 확인합니다.

docker exec sandbox123 netstat -tuln

application은 127.0.0.1:3000이 아니라 0.0.0.0:3000에서 listen해야 합니다. Docker 공식 문서는 127.0.0.1 또는 ::1에 bind된 port는 Docker host에서만 접근 가능하며 외부 request가 도달하지 않는다고 설명합니다.

application이 localhost에만 listen한다면 application config를 바꾸거나 적절한 network mode로 container를 실행합니다.

5. Port binding check

Traefik 설정 port와 container application port가 일치하는지 확인합니다.

Traefik labels는 다음과 같을 수 있습니다.

- "traefik.http.services.sandbox123.loadbalancer.server.port=3000"

container 안 application은 3000 port에서 listen해야 합니다. Traefik이 3000을 가리키는데 application이 8080에서 listen한다면 request는 실패합니다.

port가 일치하지 않으면 Traefik labels 또는 application config를 수정합니다.

Conclusion

단일 host Docker와 Go 제어 플레인으로 셀프 호스팅 preview 환경을 만들 수 있습니다. 각 sandbox는 독립 preview URL을 갖고, resource limit을 설정할 수 있으며, security boundary도 명확합니다. 다만 적용 조건은 있습니다. 내부 팀, 신뢰할 수 있는 사용자, 소규모 동시 실행이 전제입니다. trust boundary가 외부의 낯선 사용자로 확장되거나 동시 실행이 단일 host를 넘으면 microVM 또는 K8s로 이동해야 합니다.

핵심 module을 다시 보면, 판단표는 방식을 빠르게 고르게 하고, architecture breakdown은 제어 플레인, Traefik, SQLite, reaper가 어떻게 협력하는지 보여 줍니다. Preview URL은 Traefik Host rule과 Docker labels에 의존합니다. security boundary는 Docker socket이 host root에 준하는 권한임을 강조합니다. resource limit은 multi-tenant 환경의 최소 조건입니다. image cache는 Docker Hub rate limit에 대비하는 운영이고, troubleshooting checklist는 preview URL이 열리지 않을 때의 분기점입니다.

다음 단계:

  • 내부 팀의 소규모 preview → 단일 host Docker + Go 제어 플레인으로 resource density를 검증합니다
  • 외부 사용자 또는 high-risk scenario → Docker 공식 Sandboxes나 Firecracker microVM으로 이동합니다
  • self-hosted CI Runner → GitHub Actions self-hosted runner 실전 guide를 참고해 private infra를 구성합니다
  • preview 환경 안의 application deployment → Next.js Docker self-hosting 실전을 참고해 sandbox 안에서 application을 실행합니다

셀프 호스팅 Dev Sandbox MVP 구축 절차

단일 Docker host에서 시작해 내부 preview 환경에 리소스 제한과 접근 제어를 넣는 절차입니다.

⏱️ Estimated time: 4 hr

  1. 1

    Step 1: 격리 모델 선택

    신뢰할 수 있는 팀과 작은 preview workload라면 단일 host Docker로 시작합니다. 알 수 없는 사용자나 불신 코드를 다룬다면 microVM, 별도 host, Kubernetes를 선택합니다.
  2. 2

    Step 2: 제어 플레인 준비

    sandbox metadata, lifecycle operation, reaper, wake-on-request를 담당하는 작은 Go service를 실행합니다.
  3. 3

    Step 3: Traefik discovery 설정

    `exposedByDefault: false`로 Docker provider를 활성화하고, traffic을 받을 sandbox container에만 labels를 붙입니다.
  4. 4

    Step 4: 안정적인 preview URL 할당

    `*.preview.example.com` 같은 wildcard DNS를 사용하고, 각 `{sandbox_id}.preview.example.com` host를 app을 제공하는 container port로 route합니다.
  5. 5

    Step 5: workspace 영속화

    각 sandbox를 `SANDBOXED_DATA_DIR/workspaces/` 또는 동등한 host directory에 저장해 `docker stop`이 사용자 파일을 삭제하지 않게 합니다.
  6. 6

    Step 6: 리소스 제한 추가

    각 sandbox에 memory, CPU, PIDs 제한을 설정합니다. runaway build 하나가 host 전체를 고갈시키지 못하게 합니다.
  7. 7

    Step 7: production entry point 잠그기

    Docker API를 공개하지 않습니다. API auth, TLS, preview link access control, network separation, audit logs를 추가합니다.
  8. 8

    Step 8: image와 registry 운영 계획

    자주 쓰는 image를 미리 받아 두고, 필요하면 Docker Hub에 login합니다. sandbox 생성 빈도가 높다면 private registry나 cache를 사용합니다.

FAQ

Dev Sandbox와 일반 Docker Compose는 무엇이 다른가요?
Compose는 고정된 service 묶음을 오래 실행하는 데 적합합니다. Dev Sandbox 제어 플레인은 요청에 따라 환경을 만들고, 시작하고, 멈추고, 깨우고, 삭제하며, 각 환경에 안정적인 preview URL을 주고 제품 backend가 관리할 상태를 기록합니다.
처음부터 Kubernetes를 쓰지 않는 이유는 무엇인가요?
multi-node scheduling, high availability, 표준 network policy, platform governance가 필요하다면 Kubernetes가 더 맞습니다. 신뢰할 수 있는 내부 팀에서 제품 loop를 검증하는 단계라면 단일 Docker host로 충분한 경우가 많습니다.
Docker container 격리로 모르는 사용자의 임의 코드를 실행해도 되나요?
그 용도에서는 강한 경계로 보지 않는 편이 안전합니다. Docker socket 기반 제어 플레인은 신뢰할 수 있는 사용자에게 적합합니다. 불신 임의 코드는 microVM, 전용 VM, gVisor, Kata, Firecracker 또는 최소한 tenant별 host로 옮겨야 합니다.
preview URL은 HTTPS가 꼭 필요한가요?
로컬 `*.localhost` 테스트는 HTTP로 시작할 수 있습니다. 공개 preview domain에서는 token, form, business data를 입력할 수 있으므로 HTTPS를 써야 합니다. wildcard certificate를 쓰면 sandbox마다 인증서를 발급하지 않아도 됩니다.
idle stop 이후 파일이 사라지나요?
workspace가 persistent host directory에 저장되어 있다면 사라지지 않습니다. `docker stop`은 리소스를 해제하지만 파일은 유지합니다. destroy와 purge를 분리해 container만 지우는지 workspace도 지우는지 명확히 해야 합니다.
Docker Hub rate limit이 이 구조에 영향을 주나요?
영향을 줍니다. sandbox를 자주 만들면 image pull이 늘어납니다. production에서는 필요할 때 Docker Hub에 인증하고, 자주 쓰는 image를 pre-warm하며, private registry나 image cache를 고려해야 합니다.

6분 읽기 · 게시일: 2026년 6월 5일 · 수정일: 2026년 7월 14일

시리즈 읽기 경로1편 중 1편

Docker 실전 가이드

이 시리즈의 첫 글을 읽고 있습니다. 다음 글로 이어가거나 시리즈 허브에서 전체 경로를 확인하세요.

시리즈 허브 보기

이전

이 시리즈의 시작입니다.

다음

현재 이 시리즈의 최신 글입니다.

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog