CH 27 · 배포 형태 선택
본장 목표
지금까지는 로컬에서 dsh web으로 dsh를 실행해 왔습니다 — 이것이 가장 흔한 형태지만 유일한 형태는 아닙니다. dsh는 주로 네 가지 실행 방식을 제공하며, 각기 다른 시나리오에 적합합니다:
- 로컬 Web UI: 앞선 튜토리얼에서 계속 보여줬던 방식
- Headless CLI: 인터페이스가 필요 없고, 한 줄 명령으로 작업을 실행하고 종료, 스크립트와 CI에 적합
- Python SDK: dsh를 자신의 Python 프로그램에 임베드, Agent가 코드의 함수 호출이 됨
- Docker 컨테이너화: 서버에서 돌아가는 컨테이너에 dsh를 설치, 팀 공유에 적합 — 이번 장에서 완전한 배포 튜토리얼을 제공합니다
읽고 나면 알게 됩니다: 자신의 필요에 어떤 방식을 써야 하는지, dsh web만 아는 것이 아니라.
먼저 정리: dsh는 왜 여러 형태가 있는가
CH 08에서 다룬 "모든 것은 플러그인"으로 돌아가 보면 — dsh의 핵심은 @deepseek-ai/dsh-base(모델 어댑터, 도구, 영속화, 샌드박스, 승인 등 저수준 능력)이고, 그 위에 서로 다른 bundle을 쌓으면 서로 다른 형태가 됩니다:
| 쌓는 bundle | 결과물 | 시작 방법 |
|---|---|---|
@deepseek-ai/dsh-web-app | 브라우저 인터페이스가 있는 Web UI | dsh web |
@deepseek-ai/dsh-headless | 인터페이스 없이, 실행 후 종료되는 CLI | dsh --profile headless "작업" |
| Python SDK 래퍼 | Python 프로그램에 임베드하여 호출 | pip install deepseek-harness-sdk |
| 위의 어느 형태든 컨테이너에 담기 | 서버에서 장기 실행 | docker compose up -d |
같은 베이스, 네 가지 착용법입니다. 이것도 "모든 것은 플러그인"의 또 다른 구현 — 형태 자체가 bundle의 조합입니다.
형태 1: 로컬 Web UI
앞선 CH 03부터 CH 04까지 계속 보여줬던 것이 바로 이것입니다. dsh web으로 시작, 브라우저에서 http://127.0.0.1:3080을 열면, 세션, 플러그인, 키가 모두 로컬에 저장됩니다.
개인 일상 사용, 플러그인 디버깅, 초보자에게 적합합니다. 한계는 컴퓨터가 켜져 있어야만 쓸 수 있다는 것 — 컴퓨터를 끄면 Agent도 멈추므로, 24시간 상시 가동이나 타인이随时 접속해야 하는 시나리오에는 적합하지 않습니다.
형태 2: Headless CLI
CH 05에서 이미 실습했습니다. 인터페이스 없이 작업을 주면 실행 후 결과를 출력하고 종료합니다.
dsh --profile headless "Summarize the current directory's README into three sentences"스크립트, CI/CD, 예약 작업(CH 15의 매일 AI 핫뉴스를 가져오는 작업이 바로 이것을 씁니다), 배치 처리에 적합합니다. 다회 대화가 필요한 시나리오에는 적합하지 않습니다.
한 가지 경계: headless 호출 한 번은 독립된 세션 하나이며, 끝나면 종료됩니다. 컨텍스트를 재사용하려면 Python SDK를 쓰거나, 파일 기반 핸드오프를 작성해야 합니다.
형태 3: Python SDK
dsh를 Python 코드의 라이브러리로 만들고, Agent는 함수 호출이 됩니다. pip install deepseek-harness-sdk로 설치하면, SDK에 Node 런타임이 포함되어 있어 대상 머신에 Node를 별도로 설치할 필요가 없습니다.
from deepseek_harness_sdk import DeepSeekHarness
dsh = DeepSeekHarness()
result = dsh.run("List the current directory's files, skip node_modules")
print(result.last_message)자신의 제품에 Agent를 임베드하거나, 세션을 세밀하게 제어(재사용, 이벤트 리스닝, 스트리밍 출력)하거나, 배치 작업이 컨텍스트를 공유해야 하는 경우에 적합합니다. 단순히 작업을 빠르게 한 번 실행하고 싶은 경우에는 적합하지 않습니다(headless 한 줄이면 충분).
한 가지 디테일: SDK와 Web UI는 서로 다른 독립 인스턴스이며, 실행 중인 Web UI를 공유할 수 없고, 각자 고유한 DSH_HOME을 가집니다.
형태 4: Docker 컨테이너화(완전 배포 튜토리얼)
dsh를 Docker 컨테이너에 설치해 서버에서 실행, 장기 가동과 팀 공유에 적합합니다.
서버로 가기 전에 두 마디: 서버가 없다면, 텐센트 클라우드 신규 사용자 행사에서 저가 경량 애플리케이션 서버를 자주 promotion하는데, 연간 100위안대로 dsh를 돌리기 충분합니다. 또한 텐센트 클라우드는 이제 인스턴스 생성 시 DeepSeek Harness 이미지를 직접 선택해 설치할 수 있으니, 손볼 여지가 있는 분은 스스로 연구해 보셔도 됩니다. 아래에서는 커뮤니티의 검증된 오픈소스 프로젝트로 완전한 배포 흐름을 시연합니다.
deepseek-harness-web-docker는 dsh + Caddy(리버스 프록시 + Basic Auth 인증)를 한 컨테이너로 패키징하여, 별도 설정 없이 바로 쓸 수 있고, 데이터는 영속화되며, 자동 헬스 체크까지 지원합니다.
이 프로젝트가 해결하는 문제
서버에서 공식 dsh web을 직접 쓰면 세 가지 통증이 있습니다:
- 인증이 없음 — dsh Web UI는 기본적으로 로그인 메커니즘이 없어서, 공망에 배포하면 그대로 노출됩니다
- 루프백만 청취 — 기본
127.0.0.1이므로, 설정을 바꿔야만 외부 접속이 가능 - 환경 관리가 번거로움 — Node 버전, 의존성, 데이터 디렉터리를 모두 직접 관리해야 함
이 프로젝트는 Caddy를 리버스 프록시로 사용하고 Basic Auth(사용자명/비밀번호 로그인)를 더해, dsh는 컨테이너 내부에서 루프백으로만 청취하고 외부 요청은 반드시 Caddy의 인증을 통과해야 들어올 수 있습니다. 데이터는 모두 호스트의 ./data 디렉터리에 마운트되므로, 컨테이너를 삭제해도 데이터는 보존됩니다.
1단계: 준비
Linux 서버 한 대(2코어 2G부터 시작), BaoTa(宝塔) 패널 설치. BaoTa AI는 모델을 먼저 설정해야 쓸 수 있습니다 — 왼쪽 메뉴의 AI → 상단 설정 → 사용자 정의 모델 추가 클릭, 모델 API Key를 입력.

모델 설정 후, BaoTa AI가 명령 실행을 도와줄 수 있습니다.
2단계: BaoTa AI 원클릭 배포
BaoTa 패널 왼쪽 메뉴에서 AI 클릭, 아래의 프롬프트를 전송:
Help me deploy DeepSeek Harness Web Docker, do this in order:
1. Confirm Docker and Docker Compose are installed, install if not
2. Project address is https://github.com/Xidong-AI/deepseek-harness-web-docker
3. Enter the project directory, cp .env.example .env
4. Edit .env: DSH_AUTH_USER=admin, DSH_AUTH_PASSWORD set a strong password, DEEPSEEK_API_KEY etc. tell me the file path before starting I'll fill it in myself
5. docker compose up -d to start
6. Give me a summary after verificationBaoTa AI가 도와줍니다. 4단계에서 .env 파일에서 API Key를 제외한 항목을 모두 설정한 뒤, 파일 경로를 알려줘서 직접 DEEPSEEK_API_KEY를 입력하게 합니다:

입력 후 "Done"이라고 답하면, AI는 docker compose up -d를 계속 실행해 시작하고, 헬스 체크로 검증한 뒤 마지막으로 완전한 요약을 줍니다:

컨테이너 상태가 healthy이고 Basic Auth가 동작 중(자격 증명 없이 401 반환)이면 배포 성공입니다.
3단계: 접속
브라우저에서 http://server-IP:3080을 열면, Basic Auth 로그인 창이 뜨고, .env에 설정한 사용자명과 비밀번호를 입력하면 dsh Web UI로 들어갈 수 있습니다.
브라우저 왼쪽 상단에 "Not Secure"(안전하지 않음)가 표시되는데 — 이는 현재 HTTP 프로토콜을 쓰고 있고 SSL 인증서가 없어서 그런 것이지, dsh 자체의 문제는 아닙니다. 정식 배포는 도메인 + HTTPS 바인딩을 권장합니다(다음 단계).

들어간 후는 로컬에서 쓰는 dsh와 완전히 동일합니다 — 작업 디렉터리를 선택하고, 메시지를 보냅니다.
4단계: 도메인 + HTTPS 바인딩
정식 환경에서는 도메인 + HTTPS를 반드시 바인딩해야 정상 동작합니다. http://server-IP로 직접 접속하면 작업 영역 선택 시 이런 오류가 납니다:

이유는 crypto.randomUUID()가 브라우저 Web API로, 표준에 따라 안전한 컨텍스트에서만 사용 가능합니다 — 즉 https:// 페이지, 또는 localhost/127.0.0.1. http://IP로 접속하는 것은 안전하지 않은 컨텍스트이므로 이 함수가 undefined가 되어, 프런트엔드가 세션을 생성하거나 작업 영역을 선택하는 순간 crypto.randomUUID is not a function을 던집니다.
따라서 도메인 + HTTPS 바인딩은 선택적 최적화가 아니라 반드시 해야 하는 것입니다.
전제 조건: 먼저 자신의 도메인 관리 콘솔(텐센트 클라우드 DNSPod, 알리바바 클라우드 万网 등)에서 도메인에 A 레코드 하나를 추가해 서버 공인 IP를 가리키게 합니다. 몇 분 기다려서 해석이生效되도록 합니다.
방금 하던 대화를 이어서(새로 열 필요 없음) 아래 내용을 BaoTa AI에 전송:
Help me bind a domain to the dsh I just deployed and apply for an SSL certificate and configure it for me
1. The domain is your-domain, already resolved to this server
2. Verify that https://your-domain is accessible, tell me the resultBaoTa AI가 자동으로 Caddy 설정을 수정하고, 포트를 열고, 컨테이너를 재시작하고, 인증서를 신청합니다. 완료 후 https://your-domain에 접속하면 브라우저 왼쪽 상단에 자물쇠 아이콘이 표시되고 "안전하지 않음"이 사라집니다.
인증서는 Let's Encrypt가 발급하고, Caddy가 자동 갱신하므로 수동 관리할 필요가 없습니다.
이후 유지보수
배포 후 일상적인 질문은 바로 BaoTa AI에 물어보면 됩니다, 예를 들어:
- "How to upgrade the dsh container to the latest version?"
- "How to view the dsh container's running logs?"
- "How to restart the dsh container?"
- "How to back up dsh's data?"
- "The dsh container is taking too much disk space, how to clean it up?"
실제 환경에 맞춰 해당 명령을 알려주고 실행까지 해줍니다. 데이터는 모두 프로젝트 디렉터리의 ./data/ 아래에 저장되어, 컨테이너를 삭제하거나 이미지를 업그레이드해도 데이터는 손실되지 않으며, 백업하려면 ./data 디렉터리 전체를 복사하면 됩니다.
컨테이너에 어떤 도구를 설치할 수 있는가
컨테이너에는 node 22, pnpm, python3, git, curl, jq, ripgrep, make/gcc(네이티브 모듈 컴파일용), Rust 등 자주 쓰는 도구가 사전 설치되어 있습니다. Agent는 x-cmd로 더 많은 도구를 스스로 설치할 수도 있고(root 불필요), 설치된 데이터도 ./data에 남아 재시작 후에도 유지됩니다.
주의 사항
- 현재 버전의 dsh는 다중 사용자 격리가 없습니다 — 모두 같은 Basic Auth로 로그인하면 같은 세션과 설정을 보게 됩니다. 팀이 공유해서 쓸 때는 민감한 정보를 저장하지 말거나, 각자 독립 인스턴스를 배포해야 합니다.
- API Key를 compose에 하드코딩하지 마세요 —
.env파일을 쓰세요, 프로젝트는 이미 git-ignore 처리해 두었습니다. - Agent는 컨테이너 안에서 일합니다 — 보이는 파일 시스템은 컨테이너 내부이지, 서버 자체가 아닙니다. 서버의 특정 디렉터리를 조작하게 하려면 docker-compose에 volume 마운트를 추가해 호스트 디렉터리를 컨테이너에 매핑해야 합니다.
- 비밀번호 변경:
.env의DSH_AUTH_PASSWORD를 수정한 다음docker compose up -d를 실행하면, 컨테이너 시작 시 자동으로 해시를 재생성합니다.
어떻게 선택하나: 한 표로 정리
| 본인 필요 | 선택 | 이유 |
|---|---|---|
| 매일 브라우저를 열어 Agent와 작업 | 로컬 Web UI | 인터페이스가 직관적, 궤적과 승인 확인 가능 |
| 스크립트 / CI / 예약 작업에 넣기 | Headless CLI | 한 줄 명령, 실행 후 종료 |
| Agent를 자신이 만든 프로그램에 임베드 | Python SDK | 함수 레벨 호출, 세션과 이벤트 제어 가능 |
| 서버에서 장기 실행 / 팀 공유 | Docker | 컨테이너화 배포, 자동 재시작, 데이터 마운트 |
| 배치 작업이 컨텍스트 공유 필요 | Python SDK | headless는 호출마다 독립 세션, SDK는 재사용 가능 |
실용적인 제안: 대부분의 분은 로컬 Web UI부터 시작하면 충분합니다. 자동화가 필요해지면 headless를 더하고, 제품에 임베드하려면 SDK를 쓰고, 서버에 올릴 일이 생기면 Docker를 쓰세요. 처음부터 네 가지를 모두 갖출 필요 없습니다 — dsh의 장점은 필요에 따라 전환하는 것이고, 기반은 동일한 한 세트입니다.
이 장에서 배운 것
아래 항목을 스스로 완수할 수 있으면 합격입니다:
- [ ] dsh의 네 가지 배포 형태를 알고 있다: 로컬 Web UI, Headless CLI, Python SDK, Docker
- [ ] 네 가지 형태의 시작 방법과 적용 시나리오를 알고 있다
- [ ] 필요에 따라 올바른 형태를 고를 수 있다 —
dsh web만 아는 것이 아니라 - [ ] Headless 호출 한 번이 독립 세션임을 알고 있고, 컨텍스트를 재사용하려면 SDK를 써야 함을 안다
- [ ] Python SDK가 Node 런타임을 포함하며, Web UI와는 독립 인스턴스임을 안다
- [ ] deepseek-harness-web-docker 프로젝트를 사용해 서버에 Basic Auth가 적용된 dsh를 배포할 수 있다
- [ ] 배포된 dsh에 도메인 + HTTPS를 바인딩할 수 있다(Caddy 자동 인증서)
- [ ] Docker 배포 시 주의 사항을 알고 있다: 다중 사용자 격리 없음, 키는 .env로, 컨테이너 내부 경로 차이
