프로덕션 설정
gunicorn, nginx 또는 Apache, TLS 종료, Docker, 세션 유지, 그리고 공유 서버에 필요한 접근 제어를 갖추어 Potato를 프로덕션 환경에 배포합니다.
여러 사용자, 데이터 영속성, 적절한 보안을 갖추어 실제 주석 작업에 Potato를 배포합니다.
서버 구성
기본 프로덕션 구성
server:
host: 0.0.0.0
port: 8000
debug: falseserver 블록은 host, port, debug만 받으며 그 밖의 키는 받지 않습니다. 워커 수와 타임아웃은 설정 파일이 아니라 Potato를 실행하는 WSGI 서버에서 지정합니다. 디버그 모드는 인증을 우회하므로 프로덕션에서는 debug를 꺼 두십시오.
HTTPS 사용
Potato 설정에는 TLS 항목이 없으므로 인증서를 설정 파일에서 구성하지 않습니다. TLS는 Potato 앞에 둔 리버스 프록시에서 종료하거나(아래 nginx와 Apache 섹션이 이 방식을 구성합니다), "SSL 사용"에서 보여 주는 것처럼 인증서를 gunicorn에 직접 넘기십시오.
Gunicorn으로 실행하기
Potato는 gunicorn에서 워커를 정확히 하나만 두고 실행하며, 동시성이 더 필요하면 스레드 수를 늘리십시오.
gunicorn "potato.flask_server:create_app('config.yaml')" \
--workers 1 \
--threads 8 \
--bind 0.0.0.0:8000 \
--timeout 120워커를 하나로 두는 것은 성능 조정이 아니라 정확성을 위한 요구 사항입니다. Potato는 항목 풀, 할당 대기열, 모든 주석자의 상태를 프로세스 안의 메모리에 보관합니다. 두 번째 워커는 이 세 가지의 사본을 따로 가지므로 첫 번째 워커가 이미 할당한 인스턴스를 다시 내주고, user_state.json은 저장할 때마다 통째로 다시 쓰이기 때문에 마지막에 저장한 워커가 다른 워커의 주석을 덮어써 없앱니다. 이 손실은 어디에도 보고되지 않으며 주석은 그냥 사라집니다. 스레드는 이 상태의 사본 하나를 공유하므로, --workers와 달리 --threads는 안전하게 동시성을 높입니다.
SSL 사용
gunicorn "potato.flask_server:create_app('config.yaml')" \
--workers 1 \
--threads 8 \
--bind 0.0.0.0:443 \
--certfile /path/to/cert.pem \
--keyfile /path/to/key.pem리버스 프록시 설정
Nginx 구성
upstream potato {
server 127.0.0.1:8000;
}
server {
listen 80;
server_name annotation.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name annotation.example.com;
ssl_certificate /etc/letsencrypt/live/annotation.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/annotation.example.com/privkey.pem;
location / {
proxy_pass http://potato;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Long annotation sessions
proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_send_timeout 300;
}
}Apache 구성
<VirtualHost *:443>
ServerName annotation.example.com
SSLEngine on
SSLCertificateFile /path/to/cert.pem
SSLCertificateKeyFile /path/to/key.pem
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8000/
ProxyPassReverse / http://127.0.0.1:8000/
<Proxy *>
Order allow,deny
Allow from all
</Proxy>
</VirtualHost>URL 경로 아래에 마운트하기
Potato를 도메인 루트가 아닌 https://host/app1/에서 제공하려면 한 단계가 더 필요합니다. 주석 페이지는 /static/...과 /updateinstance를 공개 루트 기준으로 요청하므로, 경로 접두사 아래에서는 페이지 틀은 그대로 표시되는데 에셋이 404를 반환하고 주석이 저장되지 않습니다.
Potato에 공개 접두사를 알려 주십시오. 프록시를 직접 관리하고 프록시가 전달 헤더를 보낼 수 있다면 다음과 같이 합니다.
export POTATO_PROXY_FIX=1
potato start config.yaml -p 8000location /app1/ {
proxy_pass http://127.0.0.1:8000/; # trailing slash strips /app1/
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /app1;
}앱에 그 프록시를 통해서만 접근할 수 있을 때에만 POTATO_PROXY_FIX를 켜십시오. 이 설정은 전달 헤더를 신뢰하므로, 내부 포트에 직접 접근할 수 있는 클라이언트는 접두사를 위조해 생성되는 모든 URL을 오염시킬 수 있습니다.
헤더를 추가할 수는 없지만 마운트 경로를 알고 있다면 경로를 직접 지정하십시오. 이때도 프록시는 전달하기 전에 접두사를 제거해야 합니다.
export POTATO_URL_PREFIX=/app1
potato start config.yaml -p 8000세션이 없는 브라우저에서 로그인해 보며 확인하십시오. 접두사가 빠졌을 때 가장 먼저 깨지는 페이지가 로그인 양식입니다.
라이브 에이전트 뷰어와 라이브 코딩 뷰어는 Server-Sent Events로 스트리밍하는데, 버퍼링하는 프록시는 스트림이 끝날 때까지 이를 붙잡아 둡니다. 해당 경로에서는 버퍼링을 끄십시오.
location /app1/api/ {
proxy_pass http://127.0.0.1:8000/api/;
proxy_set_header Host $host;
proxy_buffering off;
proxy_read_timeout 3600s;
}Docker 배포
Potato는 미리 빌드한 이미지 ghcr.io/davidjurgens/potato를 GitHub Container Registry에 게시합니다. 이 이미지에는 Potato와 그 의존성만 들어 있습니다. config.yaml과 데이터가 담긴 프로젝트 폴더를 /app에 마운트하므로, 이미지 하나로 모든 작업을 제공할 수 있습니다.
docker run -p 8000:7860 \
-v "$PWD/myproject:/app" \
-e POTATO_SECRET_KEY="$(openssl rand -hex 32)" \
ghcr.io/davidjurgens/potato:latestlatest 태그와 2.10.3 같은 릴리스 태그에는 핵심 의존성만 들어 있습니다. -all 태그(latest-all, 2.10.3-all)는 AI SDK, 형식 리더, 내보내기와 인증용 추가 패키지를 더하며, 비전 모델은 포함하지 않습니다.
컨테이너는 위 gunicorn 섹션에서 설명한 이유로 gunicorn 워커 하나와 스레드 여덟 개로 작업을 제공합니다. GUNICORN_WORKERS를 1보다 크게 설정하면 POTATO_ALLOW_MULTIWORKER=1도 함께 설정하지 않는 한 컨테이너가 시작 단계에서 멈추며, 이 두 설정을 함께 쓰면 실제 연구에서 주석을 잃게 됩니다. 동시성을 높이려면 GUNICORN_THREADS를 늘리십시오. POTATO_CONFIG는 config.yaml이 아닌 다른 설정 파일을 지정하고, PORT는 컨테이너 안의 포트를 7860에서 다른 값으로 바꿉니다.
컨테이너는 uid 1000으로 실행되며 주석 출력, potato.log, SQLite 데이터베이스를 /app에 씁니다. Linux에서 바인드 마운트한 프로젝트 폴더의 소유자가 다른 사용자이면, 서버는 부팅 시 문제를 명시하는 오류를 내며 멈춥니다. sudo chown -R 1000:1000 myproject로 폴더 소유권을 uid 1000에 넘기십시오. Mac의 Docker Desktop은 바인드 마운트의 소유권을 무시하므로 이 오류는 Linux에서만 나타납니다.
이미지 이름 뒤에 오는 인수는 서버 명령을 대체하므로, 일회성 점검에 유용합니다.
docker run -v "$PWD/myproject:/app" \
ghcr.io/davidjurgens/potato:latest potato validate config.yaml --strictDocker Compose
services:
potato:
image: ghcr.io/davidjurgens/potato:latest
ports:
- "8000:7860"
volumes:
- ./myproject:/app
environment:
- POTATO_SECRET_KEY=${POTATO_SECRET_KEY}
- POTATO_ADMIN_API_KEY=${POTATO_ADMIN_API_KEY}
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./certs:/etc/ssl/certs
depends_on:
- potato
restart: unless-stoppedCompose 네트워크 안에서 nginx는 서비스 이름과 컨테이너 포트로 Potato에 접근합니다. 따라서 위 리버스 프록시 섹션의 upstream 블록은 server 127.0.0.1:8000; 대신 server potato:7860;이 됩니다.
프로젝트 폴더는 이미지에 복사되지 않고 마운트되므로, Potato가 그 안에 쓴 주석은 컨테이너를 교체해도 호스트에 남습니다.
접근 제어
가입할 수 있는 사람 제한하기
Potato는 기본적으로 자가 가입을 허용합니다. 공개 서버에서는 가입할 수 있는 사람을 명시하십시오.
user_config:
allow_all_users: false
users:
- annotator1@example.com
- annotator2@example.com두 키 모두 user_config 아래에 속합니다. 최상위에 쓰면 무시되며, 그러면 겉보기에는 접근 목록이 있는데 실제로는 누구나 들어올 수 있습니다.
관리자 접근
admin_api_key는 X-API-Key 헤더로 보내는 공유 키입니다. 직접 설정하지 않으면 Potato가 키를 생성해 {task_dir}/admin_api_key.txt에 기록하므로, 프로덕션 호스트에서는 직접 설정하고 저장소에 넣지 마십시오.
admin_api_key: "your-shared-key"
rbac:
enabled: true
user_role_assignments:
lead@example.com: adminrbac를 구성하면 지정된 사람은 공유 키를 주고받는 대신 평소처럼 로그인해 /admin에 접근합니다. 전체 권한 목록은 사용자 관리를 참고하십시오.
세션
secret_key: "a-long-random-string"
session_timeout_minutes: 60
persist_sessions: true
session_lifetime_days: 7
max_session_seconds: 7200secret_key는 세션 쿠키에 서명하며, 대신 POTATO_SECRET_KEY에서 가져올 수도 있습니다. 이 값이 없으면 Potato는 시작할 때마다 새 키를 생성하므로, 재시작할 때마다 작업 중인 모든 사람이 로그아웃됩니다. 배포할 때마다 재시작하는 서버에서 주석 손실을 가장 자주 일으키는 설정이 바로 이것입니다.
주석 출력
output_annotation_dir: /var/potato/annotations/
export_annotation_format: [csv, jsonl]
auto_export_interval: 300Potato는 output_annotation_dir 아래에 주석자마다 디렉터리 하나를 만들고, 각 디렉터리에 그 사람의 user_state.json을 둡니다. export_annotation_format은 주기적 내보내기가 쓰는 형식을 지정하며, 값이 비어 있으면 자동 내보내기를 전혀 하지 않습니다. auto_export_interval은 내보내기 사이의 간격(초)이며 기본값은 60입니다.
데이터베이스 백엔드
주석은 기본적으로 디스크에 저장됩니다. 데이터베이스가 필요한 배포에서는 database.type에 mysql 또는 file을 지정할 수 있습니다.
database:
type: mysql
host: localhost
port: 3306
database: potato_db
username: potato
password: ${DB_PASSWORD}키 이름은 name과 user가 아니라 database와 username입니다. 연결 문자열은 대신 POTATO_DB_CONNECTION에서 가져올 수 있으며, 그러면 자격 증명이 설정 파일에 들어가지 않습니다.
드라이버는 pip install 'potato-annotation[mysql]'로 설치하십시오. 2.10.2 이전에는 MySQL 백엔드가 한 번도 켜지지 않았고, MySQL을 지정한 설정도 아무 알림 없이 주석을 디스크에 저장했습니다. 2.10.2부터는 드라이버가 없거나 서버에 연결할 수 없으면 시작이 중단되며, 내보내기, 백업, potato deploy pull은 데이터베이스를 읽습니다.
상태 확인과 로깅
Potato는 모든 배포에서 GET /health를 제공하며, 이 경로는 인증이 필요 없으므로 로드 밸런서나 오케스트레이터의 준비 상태 프로브로 쓸 수 있습니다.
curl -sf https://annotation.example.com/health로그 상세 수준은 별도의 로깅 블록이 아니라 설정 파일이나 명령줄에서 지정합니다.
verbose: true
debug_log: serverdebug_log는 all, ui, server, none 중 하나를 받습니다. 특정 하위 시스템을 추적할 때는 POTATO_LOGGERS 환경 변수로 출력을 지정한 로거로 좁힐 수 있습니다.
확장
Potato는 전체 항목 집합을 메모리에 올린 단일 프로세스로 실행되므로, 서버 수가 아니라 머신 사양을 늘려 확장하십시오. 로드 밸런서 뒤에 복제본을 여러 개 두어도 작업이 나뉘지 않습니다. 각 복제본이 할당 대기열과 주석자 상태의 사본을 따로 가지므로, 위 gunicorn 섹션에서 설명한 데이터 손실이 그대로 재현됩니다.
데이터셋과 함께 늘어나는 자원은 메모리입니다. 항목이 백만 개 정도가 되면 페이로드를 SQLite 캐시로 옮길 수 있습니다.
item_store:
backend: paged # default: memory
cache_size: 2048이 설정을 켜기 전에 측정하십시오. 페이징으로 절약되는 메모리는 비전 항목에서 약 28%, 텍스트에서 약 51%로, 몇 배 수준이 아닙니다. 또한 코퍼스를 불러오는 속도가 약 2.5배 느려집니다. 이 파일은 부팅할 때마다 데이터 파일에서 다시 만들어지는 캐시이므로 삭제해도 안전합니다.
대규모 프로젝트에서 실제로 효과가 있는 수단은 per_annotator_quota로 주석자 한 명에게 할당되는 양에 상한을 두는 것, batch_assignment로 코퍼스를 여러 코호트에 나누는 것, 그리고 시작할 때 모든 데이터를 불러오는 대신 data_directory로 데이터 파일을 점진적으로 추가하는 것입니다.
백업 전략
자동 백업
#!/bin/bash
# backup.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR=/var/backups/potato
# Backup annotations
tar -czf $BACKUP_DIR/annotations_$DATE.tar.gz /var/potato/annotations/
# Backup config
cp /var/potato/config.yaml $BACKUP_DIR/config_$DATE.yaml
# Keep only last 7 days
find $BACKUP_DIR -mtime +7 -deletecrontab에 추가합니다.
0 2 * * * /var/potato/backup.sh보안 체크리스트
- 유효한 인증서로 프록시에서 TLS 종료
- 재시작 후에도 세션이 유지되도록
secret_key또는POTATO_SECRET_KEY설정 - 생성된 파일을 그대로 쓰지 말고
admin_api_key직접 설정 -
user_config.users로 가입 제한 - 디버그 모드는 인증을 우회하므로
debug는 꺼 둠 - gunicorn은 워커 하나로 실행
- 자격 증명은 설정 파일이 아니라 환경 변수에 보관
-
POTATO_PROXY_FIX는 신뢰할 수 있는 프록시 뒤에서만 켬 - 주석 출력 디렉터리를 정기적으로 백업
- 관리자 역할을 가진 사람 검토
환경 변수
# .env file
POTATO_SECRET_KEY=your-super-secret-key
POTATO_ADMIN_API_KEY=your-admin-key
POTATO_DB_CONNECTION=mysql://potato:password@localhost:3306/potato_db서명 키는 다음 명령으로 생성합니다.
python3 -c 'import secrets; print(secrets.token_hex(32))'Docker Compose에서 로드합니다.
services:
potato:
env_file:
- .env문제 해결
로그 확인
# Gunicorn logs
journalctl -u potato -f
# Docker logs
docker-compose logs -f potato일반적인 문제
502 Bad Gateway: Potato가 실행 중이고 프록시가 전달하는 포트에 바인딩되어 있는지 확인하십시오.
배포 후 모두 로그아웃됨: secret_key가 설정되지 않아 시작할 때 새 서명 키가 생성된 것입니다.
주석 누락: 워커 수를 확인하십시오. gunicorn 워커가 둘 이상이면 아무 보고 없이 주석이 사라집니다.
경로 접두사 아래에서 에셋 404: POTATO_PROXY_FIX 또는 POTATO_URL_PREFIX를 설정하고, 프록시가 전달하기 전에 접두사를 제거하는지 확인하십시오.
대규모 코퍼스에서 느림: 서버를 늘리기 전에 메모리를 늘리고, 항목이 약 백만 개를 넘으면 item_store.backend: paged를 고려하십시오.
다음 단계
구현 세부 사항은 리버스 프록시 가이드와 확장 가이드를 참고하십시오.