본문으로 건너뛰기

Docker로 Potato 실행하기

게시된 Docker 이미지로 Potato 프로젝트를 제공하고, 주석을 내 디스크에 보관하며, 컨테이너가 읽는 관리자 키와 환경 변수를 설정합니다.

이 페이지는 첫 docker run부터 운영 중인 연구를 새 릴리스로 옮기는 일까지, 컨테이너에서 Potato 주석 프로젝트를 실행하는 방법을 다룹니다. 자신의 컴퓨터나 서버에서 연구를 제공하면서 호스트에 Python을 설치하지 않고 Potato를 쓰려는 연구자와 연구실 관리자를 위한 페이지입니다. Potato는 이미지를 GitHub Container Registry에 ghcr.io/davidjurgens/potato로 게시하며, 이미지에는 서버와 그 의존성만 들어 있습니다. 컨테이너가 시작될 때 프로젝트 폴더가 /app에 마운트되므로, 이미지 하나로 모든 작업을 제공할 수 있습니다.

potato deploy는 이 명령이 설정해 주는 클라우드 호스트에서 같은 이미지를 실행하며, 설치된 Potato 버전에 맞는 태그를 고릅니다. 그 방법은 Potato를 로컬 또는 클라우드에서 실행하기가, 직접 관리하는 서버의 gunicorn, 리버스 프록시, TLS는 프로덕션 설정이 다룹니다.

이미지 받기

Potato의 numpy, pandas, scipy, scikit-learn 의존성을 pip로 설치하면 1 GB 머신에서는 메모리가 부족하고 2 GB 머신에서도 5분에서 10분이 걸립니다. 이미지에는 이 의존성이 이미 설치되어 있습니다.

bash
docker pull ghcr.io/davidjurgens/potato:2.14.0

이미지는 두 가지 변형으로 제공되며, 각각 움직이는 태그, 릴리스별 태그, 커밋별 태그가 있습니다.

태그내용
latest, 2.14.0Potato의 핵심 의존성, 백업 클라이언트(huggingface_hub와 boto3), SSO 로그인용 Authlib
latest-all, 2.14.0-all위 항목 전부와 함께 ai_support의 AI SDK, PDF와 DOCX 수집, parquet 내보내기, MySQL 드라이버를 담은 all extra
sha-<short>, sha-<short>-allmaster 브랜치의 커밋 하나

두 변형 모두 linux/amd64와 linux/arm64용으로 빌드되며, arm64 빌드는 Apple Silicon에서 네이티브로 실행됩니다. 어느 변형에도 수 기가바이트의 torch wheel이 딸린 vision extra는 들어 있지 않습니다. ONNX Runtime Web이 Potato 패키지 안에 들어 있으므로 브라우저 쪽 세그멘테이션은 이 extra 없이도 동작합니다.

하루 넘게 운영할 연구에는 버전 태그를 고정하십시오. latest 태그는 master 브랜치가 바뀔 때마다 다시 빌드되므로, 일주일 간격으로 docker pull을 두 번 실행하면 서로 다른 코드를 받을 수 있습니다. 2.14.0 같은 버전 태그는 릴리스 때 한 번 푸시되며 그 뒤로 다시 빌드되지 않습니다.

이미지를 직접 빌드하기

게시된 두 변형 어디에도 없는 extra가 필요하면 저장소를 클론해서 빌드하십시오. POTATO_EXTRAS 빌드 인수에는 Potato의 setup.py에 있는 extra를 하나 또는 쉼표로 구분해 여러 개 지정할 수 있습니다.

bash
git clone https://github.com/davidjurgens/potato
cd potato
docker build -t potato .
docker build --build-arg POTATO_EXTRAS=all -t potato:all .
docker build --build-arg POTATO_EXTRAS=all,saml -t potato:all-saml .

SAML 로그인용 saml extra는 all에 포함되지 않습니다. 이 extra가 컴파일 대상으로 삼는 libxmlsec1 라이브러리는 POTATO_EXTRAS에 saml이 있을 때만 Dockerfile이 설치합니다. 로컬 빌드는 potato:all 같은 태그로 실행하며, 이 페이지에서 ghcr.io/davidjurgens/potato:2.14.0이라고 쓴 자리에 그 태그를 넣으십시오.

프로젝트 실행하기

프로젝트 폴더에는 config.yaml과 설정이 이름을 부르는 모든 파일이 들어 있습니다. 이 페이지의 예시는 항목 세 개짜리 감성 분류 작업이며 구조는 다음과 같습니다.

text
myproject/
├── config.yaml
└── data/
    └── items.jsonl

설정의 모든 경로는 프로젝트 폴더를 기준으로 한 상대 경로입니다.

yaml
annotation_task_name: "Sentiment pilot"
task_dir: .
data_files:
  - data/items.jsonl
item_properties:
  id_key: id
  text_key: text
output_annotation_dir: annotation_output/
user_config:
  allow_all_users: true
annotation_schemes:
  - annotation_type: radio
    name: sentiment
    description: "What is the sentiment of this sentence?"
    labels: [positive, neutral, negative]

data/items.jsonl의 각 줄이 항목 하나입니다.

json
{"id": "1", "text": "The battery lasts all day."}
{"id": "2", "text": "The screen cracked within a week."}
{"id": "3", "text": "It arrived on Tuesday."}

myproject가 들어 있는 폴더에서 서버를 시작합니다.

bash
docker run -d --name potato -p 8000:7860 \
  -v "$PWD/myproject:/app" \
  ghcr.io/davidjurgens/potato:2.14.0

그러면 작업이 http://localhost:8000에서 열립니다. -p 8000:7860 인수는 내 컴퓨터의 8000번 포트를 컨테이너 안에서 서버가 듣는 7860번 포트에 연결합니다. -v 인수는 프로젝트 폴더를 /app에 마운트합니다. 호스트 쪽은 $PWD/myproject 같은 경로여야 합니다. myproject처럼 이름만 쓰면 Docker가 이를 새로 만들 빈 볼륨의 이름으로 읽기 때문입니다. docker logs -f potato는 서버 로그를 계속 보여 주고, docker stop potato는 서버를 멈춥니다.

설정 파일이 여러 개인 프로젝트는 /app을 기준으로 한 경로인 POTATO_CONFIG로 하나를 고릅니다.

bash
docker run -d --name potato -p 8000:7860 \
  -v "$PWD/myproject:/app" \
  -e POTATO_CONFIG=configs/pilot.yaml \
  ghcr.io/davidjurgens/potato:2.14.0

몇 주 동안 이어지는 연구 실행하기

몇 주 동안 이어지는 연구에는 재시작과 업데이트를 거쳐도 바뀌지 않는 세션 서명 키가 필요합니다. POTATO_SECRET_KEY가 없으면 서버가 시작될 때마다 새 무작위 키를 고르므로 모든 주석자가 로그아웃됩니다. 프로젝트 폴더 밖에 env 파일을 한 번 만들고, 모든 docker run에 같은 파일을 넘기십시오.

bash
printf 'POTATO_SECRET_KEY=%s\nPOTATO_ADMIN_API_KEY=%s\n' \
  "$(openssl rand -hex 32)" "$(openssl rand -hex 32)" > potato.env
chmod 600 potato.env
 
docker run -d --name potato -p 8000:7860 \
  -v "$PWD/myproject:/app" \
  --env-file potato.env \
  --restart unless-stopped \
  ghcr.io/davidjurgens/potato:2.14.0

관리자 키 섹션이 이 파일의 둘째 줄을 설명합니다. --restart unless-stopped는 충돌이나 호스트 재부팅 뒤에 컨테이너를 되살립니다.

이미지의 다른 명령

이미지 이름 뒤에 오는 것은 무엇이든 서버를 대체합니다. 이미지에 들어 있는 검증기로 설정을 확인하거나 컨테이너 안에서 셸을 열 수 있습니다.

bash
docker run --rm -v "$PWD/myproject:/app" \
  ghcr.io/davidjurgens/potato:2.14.0 potato validate config.yaml --strict
 
docker run --rm -it -v "$PWD/myproject:/app" \
  ghcr.io/davidjurgens/potato:2.14.0 sh

Linux의 파일 소유권

이미지의 서버는 uid 1000으로 실행되며, 바인드 마운트는 호스트에서 폴더가 가진 소유권을 그대로 유지합니다. Linux에서 내가 만든 폴더는 내 uid의 소유이고, root가 만든 폴더는 root의 소유입니다. 폴더의 소유자가 uid 1000이 아니면 서버는 폴더에 주석을 쓸 수 없습니다. 컨테이너는 서버를 시작하기 전에 폴더를 확인하고, 시작 도중에 실패하는 대신 ERROR: /app is not writable by uid 1000을 출력하며 멈춥니다. 아래 두 명령 중 하나로 해결됩니다.

bash
sudo chown -R 1000:1000 myproject                  # hand the folder to the container user
docker run --user "$(id -u):$(id -g)" ...          # or run the container as yourself

--user를 쓰면 Potato가 쓰는 주석과 관리자 키 파일도 내 계정의 소유가 됩니다. macOS와 Windows의 Docker Desktop은 바인드 마운트의 소유권을 무시하므로, Mac에서 동작하는 프로젝트가 Linux 서버에서는 여전히 실패할 수 있습니다. POTATO_ALLOW_READONLY_APP=1은 이 확인을 건너뜁니다. Potato는 주석, potato.log, SQLite 데이터베이스를 여전히 프로젝트 안에 쓰므로, 이 경로들이 별도의 마운트로 쓰기 가능할 때만 이 변수를 설정하십시오.

일부 호스트는 이미지에 지정된 사용자와 상관없이 모든 컨테이너를 root로 시작합니다. 컨테이너가 root로 시작되면 컨테이너는 /app을 이미지의 potato 사용자에게 넘기고 그 사용자로 서버를 시작합니다. root를 그대로 유지하려면 POTATO_RUN_AS_ROOT=1을 설정하십시오.

주석 보관하기

Potato는 연구가 수집하는 모든 것을 프로젝트 폴더에 씁니다. 주석은 output_annotation_dir 아래에 주석자별 디렉터리로 저장되고, 각 디렉터리에 그 주석자의 user_state.json이 있습니다. config.yaml 옆에는 SQLite 데이터베이스 두 개가 있습니다. project.sqlite는 메모, 코드북, 케이스, 검색 색인, 검토 워크플로를 담고, datasets.sqlite는 평가 데이터셋과 그 버전을 담습니다. 두 데이터베이스 모두 데이터 파일로부터 다시 만들 수 없습니다. 바인드 마운트를 쓰면 이 파일 전부가 내 디스크에 있고 컨테이너를 삭제해도 그대로 남습니다.

output_annotation_dir은 프로젝트 폴더 안으로 해석되어야 합니다. /data/annotations 같은 경로는 서버를 시작 단계에서 output_annotation_dir: Path '/data/annotations' resolves to '/data/annotations' which is outside the project directory '/app'라는 메시지와 함께 멈추게 합니다. annotation_output/ 같은 상대 경로를 쓰십시오. 이미지의 /data 볼륨도 프로젝트 밖에 있으며, Potato는 그곳에 아무것도 쓰지 않습니다.

실행 중인 컨테이너에서 데이터베이스를 꺼낼 때는 일반 파일로 복사하지 말고 SQLite의 백업 명령으로 복사하십시오. 서버가 데이터베이스에 쓰는 동안 project.sqlite는 WAL 모드로 동작하므로, 일반 복사는 손상되었거나 최근 작업이 빠진 사본을 아무 경고 없이 만들 수 있습니다. 이미지에는 스냅숏을 뜨는 sqlite3 도구가 들어 있습니다.

bash
docker exec potato sqlite3 /app/project.sqlite ".backup /tmp/snap.sqlite"
docker cp potato:/tmp/snap.sqlite ./project.sqlite

대부분의 관리형 컨테이너 호스트는 컨테이너가 재시작되면 컨테이너의 파일 시스템을 지웁니다. 그런 호스트에서는 프로젝트를 /app에 마운트한 영속 저장소에 두거나, 설정에 backup 블록을 추가하십시오. 이 블록은 주석과 데이터베이스 스냅숏을 일정에 따라 S3 버킷이나 Hugging Face 데이터셋에 복사하고, 서버가 빈 디스크로 시작되면 복원합니다. 이미지의 모든 변형에 이 블록이 필요로 하는 클라이언트가 들어 있습니다.

환경 변수

컨테이너는 시작할 때 다음 변수를 읽습니다.

변수기본값용도
POTATO_CONFIGconfig.yaml컨테이너 안의 설정 파일 경로이며 /app을 기준으로 함
PORT7860컨테이너 안에서 서버가 듣는 포트
GUNICORN_WORKERS1워커 프로세스 수이며 다른 값이면 컨테이너가 시작 단계에서 멈춤
GUNICORN_THREADS8워커 하나의 스레드 수이자 동시에 처리하는 요청 수
GUNICORN_TIMEOUT120gunicorn이 워커를 재시작하기 전에 워커가 응답 없이 있을 수 있는 시간(초)
POTATO_SECRET_KEY시작할 때마다 무작위세션 쿠키에 서명하며 설정의 secret_key가 우선함
POTATO_ADMIN_API_KEY생성된 파일관리자 API 키이며 설정의 admin_api_key가 우선함
POTATO_GENERATED_TEMPLATES_DIR시작할 때 선택Potato가 시작할 때 만드는 작업 템플릿을 쓰는 위치
POTATO_BUNDLE_URL설정 안 됨시작할 때 /app으로 내려받을 프로젝트 .tar.gz의 URL
POTATO_BUNDLE_SHA256설정 안 됨내려받은 프로젝트가 일치해야 하는 체크섬
POTATO_BUNDLE_TOKEN설정 안 됨다운로드와 함께 Authorization: Bearer로 전송됨
POTATO_RUN_AS_ROOT설정 안 됨1이면 컨테이너가 root로 시작될 때 서버를 root로 계속 실행함
POTATO_ALLOW_MULTIWORKER설정 안 됨1이면 GUNICORN_WORKERS가 1을 넘을 수 있으며 실제 연구에서는 주석을 잃게 됨
POTATO_ALLOW_READONLY_APP설정 안 됨1이면 /app에 쓸 수 있는지 확인하는 시작 단계의 검사를 건너뜀

컨테이너가 PORT를 바꾸면 -p의 오른쪽에도 같은 포트를 써야 합니다. 예를 들어 PORT=9000이면 -p 8000:9000입니다.

워커와 스레드 수

컨테이너는 gunicorn 워커를 둘 이상 두면 시작을 거부합니다. Potato는 항목 풀, 할당 대기열, 모든 주석자의 상태를 한 프로세스의 메모리에 보관합니다. 두 번째 워커는 이 셋의 사본을 따로 가지므로 첫 번째 워커가 이미 할당한 항목을 다시 할당하게 됩니다. 저장할 때마다 user_state.json이 통째로 다시 쓰이기 때문에, 마지막에 저장한 워커가 다른 워커의 주석을 아무 오류 없이 버립니다. 동시 요청을 늘리려면 대신 GUNICORN_THREADS를 올리십시오. 스레드는 상태의 사본 하나를 공유합니다.

생성된 템플릿

Potato는 시작할 때 각 작업의 주석 페이지용 템플릿을 만듭니다. 컨테이너 밖에서는 패키지 디렉터리에 쓸 수 있을 때마다 템플릿이 설치된 패키지 안에 들어갑니다. 이미지는 Potato를 root로 설치하고 서버를 uid 1000으로 실행하므로, 서버는 패키지 디렉터리에 쓸 수 없습니다. 그래서 Potato는 서버의 사용자만 읽고 쓸 수 있는 /tmp 아래의 디렉터리에 템플릿을 쓰며, 시작 로그에 그 디렉터리가 표시됩니다. Potato는 시작할 때마다 설정으로부터 템플릿을 다시 만들므로 그 디렉터리를 잃어도 손해가 없습니다. 템플릿을 tmpfs 마운트 같은 다른 곳에 두려면 POTATO_GENERATED_TEMPLATES_DIR을 설정하십시오.

시작할 때 프로젝트 내려받기

Render, Fly, Railway, ECS처럼 이미지만 실행하는 호스트에는 마운트할 폴더가 없습니다. POTATO_BUNDLE_URL을 설정하면 컨테이너는 프로젝트의 .tar.gz를 내려받고, 내려받은 파일을 POTATO_BUNDLE_SHA256과 대조해 확인한 뒤, 서버를 시작하기 전에 프로젝트를 /app에 풉니다. 컨테이너는 체크섬을 /app/.potato-bundle-sha에 기록하며, 영속 디스크에서 재시작할 때는 체크섬이 일치하는 다운로드를 건너뜁니다. potato deploy가 만든 번들에서는 프로젝트의 새 버전이 디스크에 이미 있는 주석 출력이나 SQLite 데이터베이스를 대체하지 않습니다. potato deploy up이 이 변수들을 대신 설정합니다.

컨테이너의 관리자 키

/admin의 관리자 대시보드와 관리자 API는 공유 키 하나를 받으며, 이 키는 X-API-Key 헤더로 보내거나 관리자 로그인 페이지에 입력합니다. Potato는 키를 최상위 admin_api_key 설정 키에서 먼저 읽고, 그다음에 POTATO_ADMIN_API_KEY 환경 변수에서 읽습니다. 컨테이너에서는 환경 변수를 쓰면 키가 config.yaml과 프로젝트를 담은 저장소에 들어가지 않습니다. 장기 연구 섹션의 potato.env 파일이 이 변수를 설정하므로, 관리자 키는 그 파일에 들어 있는 값입니다.

bash
grep POTATO_ADMIN_API_KEY potato.env

설정과 환경 어느 쪽에도 키가 없으면, 누군가 처음 /admin을 열거나 관리자 엔드포인트를 호출할 때 Potato가 키를 생성합니다. Potato는 생성한 키를 작업 디렉터리의 admin_api_key.txt에 씁니다. 컨테이너에서 작업 디렉터리는 마운트한 프로젝트 폴더인 /app입니다. 이 파일은 서버의 사용자만 읽을 수 있고, Potato는 재시작한 뒤에도 같은 파일을 읽습니다. 서버 로그에는 파일 이름이 나오지만 키는 출력되지 않습니다. 컨테이너를 통해 키를 읽으십시오.

bash
docker exec potato cat /app/admin_api_key.txt

프로젝트 폴더를 uid 1000에 넘긴 Linux 호스트에서는 내 계정이 sudo 없이 admin_api_key.txt를 읽을 수 없습니다. --user "$(id -u):$(id -g)"로 시작한 컨테이너는 이 파일을 내 계정의 소유로 씁니다. 이 키로 열리는 내용은 관리자 대시보드 페이지를 참고하십시오.

컨테이너의 디버그 모드

연구를 제공하는 컨테이너에서는 debug: false를 유지하십시오. 설정에 debug: true가 있으면 Potato는 두 기능의 동작을 바꿉니다.

  • 키 없이 열리는 관리자 페이지. 관리자 대시보드와 관리자 API는 X-Forwarded-For, X-Real-IP, Forwarded 헤더가 없는 루프백 주소의 요청에는 키를 요구하지 않습니다. Docker의 기본 브리지 네트워크에서는 공개된 포트로 들어온 요청이 브리지의 주소에서 컨테이너에 도달하므로 여전히 키가 필요합니다. --network host에서는 호스트 머신의 요청이 루프백 요청이므로 키가 필요 없습니다. 같은 네트워크 네임스페이스에 있으면서 전달 헤더를 붙이지 않는 리버스 프록시는, 프록시가 넘겨 주는 모든 요청을 로컬 요청처럼 보이게 만듭니다.
  • 공유 디버그 계정. debug_phase도 설정되어 있으면 모든 방문자가 하나의 공유 계정 debug_user로 로그인하고 해당 단계로 건너뜁니다. 이 계정은 관리자가 아니지만 모든 방문자가 이 계정의 답을 공유합니다.

서버는 debug가 켜져 있으면 시작할 때마다 경고를 기록합니다. docker exec로 컨테이너 안에서 실행한 명령은 루프백 요청입니다. 따라서 디버그 모드에서 docker exec potato curl -s http://127.0.0.1:7860/admin/health는 키 없이 관리자 상태 보고서를 반환합니다.

상태 확인

GET /health는 인증이 필요 없으며 서버가 떠 있는지만 알려 줍니다. 이 경로는 서버가 준비되면 {"status": "ok"}를 반환하고 서버가 아직 데이터를 불러오는 중에는 상태 코드 503을 반환하므로, 로드 밸런서나 오케스트레이터가 이 경로를 준비 상태 확인으로 폴링할 수 있습니다. 이미지 자체의 HEALTHCHECK는 /health를 30초마다 폴링하고 큰 작업을 위해 90초의 시작 유예 기간을 두며, 결과는 docker ps에 표시됩니다. GET /admin/health는 작업 이름과 항목 관리자 및 주석자 관리자의 상태를 알려 주며, 이 경로에는 X-API-Key 헤더로 보내는 관리자 키가 필요합니다.

Docker Compose

Potato 저장소는 Compose 파일을 제공하지 않습니다. 프로덕션 설정에는 프로젝트 폴더를 /app에 마운트하고 세션 키와 관리자 키를 환경에서 읽으면서 이 이미지를 nginx 뒤에서 실행하는 Compose 파일이 있습니다.

새 버전으로 업데이트하기

Potato를 업데이트하면 컨테이너는 교체되지만 모든 주석과 데이터베이스가 들어 있는 프로젝트 폴더는 그대로 남습니다. 전환하기 전에 새 릴리스를 받고, 새 릴리스의 검증기로 설정을 확인하고, 프로젝트 데이터베이스의 스냅숏을 뜨십시오. 2.14.0은 옮겨 갈 릴리스로 바꾸십시오.

bash
docker pull ghcr.io/davidjurgens/potato:2.14.0
docker run --rm -v "$PWD/myproject:/app" \
  ghcr.io/davidjurgens/potato:2.14.0 potato validate config.yaml --strict
docker exec potato sqlite3 /app/project.sqlite ".backup /tmp/snap.sqlite"
docker cp potato:/tmp/snap.sqlite ./project-before-update.sqlite

그런 다음 실행 중인 컨테이너를 같은 마운트와 같은 env 파일을 쓰는 새 태그의 컨테이너로 교체합니다.

bash
docker stop potato
docker rm potato
docker run -d --name potato -p 8000:7860 \
  -v "$PWD/myproject:/app" \
  --env-file potato.env \
  --restart unless-stopped \
  ghcr.io/davidjurgens/potato:2.14.0

potato.env가 POTATO_SECRET_KEY를 그대로 유지하므로 주석자는 전환 뒤에도 로그인된 상태로 남고, 관리자 키도 바뀌지 않습니다.

문제 해결

ERROR: config file not found: config.yaml. 서버가 찾는 위치에 프로젝트가 마운트되지 않았습니다. 오류는 -v 인수와 비교할 수 있도록 /app의 내용을 나열합니다. 목록이 비어 있으면 대개 -v myproject:/app처럼 이름만 쓴 경우입니다. Docker는 이름만 쓰면 내 폴더가 아니라 새 빈 볼륨으로 읽습니다.

ERROR: /app is not writable by uid 1000. 서버가 프로젝트 폴더에 쓸 수 없습니다. 폴더를 uid 1000에 넘기거나 컨테이너를 --user로 실행하십시오. Linux의 파일 소유권에 두 명령이 모두 있습니다.

output_annotation_dir ... is outside the project directory '/app'. 설정이 주석 출력을 마운트한 폴더 밖으로 지정하고 있습니다. annotation_output/ 같은 상대 경로를 쓰십시오.

재시작 뒤에 모두 로그아웃됩니다. 서명 키가 설정되어 있지 않아 서버가 시작할 때 새 무작위 키를 골랐습니다. env 파일에 POTATO_SECRET_KEY를 설정하고 모든 docker run에 그 파일을 넘기십시오.

컨테이너가 GUNICORN_WORKERS를 언급하는 오류와 함께 종료됩니다. 워커 수가 1보다 큽니다. GUNICORN_WORKERS=1로 설정하거나 변수를 지우고, 대신 GUNICORN_THREADS를 올리십시오.

관리자 로그인 페이지가 모든 키를 거부합니다. 설정의 admin_api_key가 POTATO_ADMIN_API_KEY보다 우선하고, 둘 다 admin_api_key.txt보다 우선합니다. 먼저 설정을 확인한 다음 docker exec potato printenv POTATO_ADMIN_API_KEY를 실행하십시오.

더 읽을거리

구현 세부 사항은 소스 문서를 참고하십시오.