PC 없이 24시간 자동 운영을 위한 Docker 기반 배포 절차. Ubuntu 22.04+ VPS 기준으로 작성되었다.
- VPS 준비
- Docker 설치
- 레포 클론 + .env 구성
- secrets/sa.json 마운트 절차
- 빌드·기동
- 스케줄 확인
- 로그 확인
- 수동 실행
- dry_run 스모크 테스트
- 업데이트
- 대본 생성 경로 선택
- CI docker build 잡 추가 참고
| 항목 | 최소 사양 |
|---|---|
| OS | Ubuntu 22.04 LTS 이상 |
| RAM | 1 GB 이상 (권장 2 GB) |
| 디스크 | 10 GB 이상 (영상 파일 보관 시 더 필요) |
| 아웃바운드 포트 | 443, 80 (HTTPS/HTTP) |
| 인바운드 포트 | 불필요 — Telegram 봇은 폴링(아웃바운드) 방식이므로 서버 포트 개방이 필요 없다 |
팁: DigitalOcean Droplet 기본 플랜($6/월, 1vCPU·1GB RAM)으로 충분히 동작한다.
# Docker 공식 설치 스크립트 (Ubuntu)
curl -fsSL https://get.docker.com | sh
# 현재 사용자를 docker 그룹에 추가 (재로그인 필요)
sudo usermod -aG docker $USER
newgrp docker
# Docker Compose v2 포함 여부 확인
docker compose version설치 후 docker --version 과 docker compose version 이 정상 출력되면 준비 완료.
# 레포 클론
git clone https://github.com/wjdtjddns98/Auto-advertisement-Solution.git nutti
cd nutti
# .env 파일 생성 (.env.example 복사 후 편집)
cp .env.example .env
nano .env # 또는 vi .env# ── Telegram (검수 게이트) ──────────────────────────────────────────────────
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
TELEGRAM_CHAT_ID=your_telegram_chat_id
# ── AI 대본 생성 (11번 섹션에서 옵션 A / B 선택) ────────────────────────────
# 옵션 A: ANTHROPIC_API_KEY 직접 사용 (서버 환경 권장)
ANTHROPIC_API_KEY=sk-ant-...
# 옵션 B: claude CLI 방식 → 11번 섹션 참고 (아래 줄 주석 처리)
# ANTHROPIC_API_KEY= ← 비워두고 volumes에 ~/.claude 마운트
# ── fal.ai (영상·시작 프레임 생성) ───────────────────────────────────────────
# FAL_KEY 하나로 시작 프레임(FLUX.1 Kontext)·영상(Veo 3.1, veo_fal) 모두 처리한다.
FAL_KEY=your_fal_key
# ── Google Sheets ────────────────────────────────────────────────────────────
GOOGLE_SHEETS_ID=your_sheet_id
GOOGLE_SERVICE_ACCOUNT_JSON=/app/secrets/sa.json
# ── YouTube ──────────────────────────────────────────────────────────────────
YOUTUBE_CLIENT_ID=your_youtube_client_id
YOUTUBE_CLIENT_SECRET=your_youtube_client_secret
YOUTUBE_REFRESH_TOKEN=your_youtube_refresh_token
# ── Instagram ────────────────────────────────────────────────────────────────
INSTAGRAM_ACCESS_TOKEN=your_instagram_access_token
INSTAGRAM_ACCOUNT_ID=your_instagram_account_id
# ── 파이프라인 동작 설정 ──────────────────────────────────────────────────────
NUTTI_DRY_RUN=false # 실 운영 시 false
NUTTI_STATE_PATH=/app/data/pipeline_state.json # 볼륨 마운트 경로와 일치
NUTTI_REVIEW_STORE_PATH=/app/data/reviews.json # 볼륨 마운트 경로와 일치
NUTTI_MEDIA_DIR=/app/data/media # 영상 파일 저장 경로
# ── 마스코트 이미지 (실 운영 시 설정) ─────────────────────────────────────────
NUTTI_MASCOT_IMAGE=/app/assets/mascot.png # 컨테이너 내 경로 그대로 사용
.env파일은 절대 git commit 하지 않는다 —.gitignore에 이미 포함되어 있다.
Google Sheets 기록 기능에 필요한 서비스 계정 키 파일을 설정한다.
# secrets 디렉터리 생성 (git에서 제외됨)
mkdir -p secrets
# Google Cloud Console에서 다운로드한 sa.json을 복사
cp ~/downloads/your-sa-file.json secrets/sa.json
# 퍼미션 보호 (선택 사항)
chmod 600 secrets/sa.jsondocker-compose.yml의 볼륨 설정(./secrets:/app/secrets:ro)에 의해 컨테이너 내부
/app/secrets/sa.json으로 읽기 전용 마운트된다. docker-compose.yml의 environment
블록이 GOOGLE_SERVICE_ACCOUNT_JSON=/app/secrets/sa.json(절대경로)으로 오버라이드하므로,
.env의 값과 무관하게 컨테이너 내부에서는 항상 절대경로로 동작한다.
주의:
secrets/디렉터리는.gitignore와.dockerignore양쪽에 제외 설정이 되어 있어 이미지·커밋에 포함되지 않는다.
# ① 데이터 디렉터리 사전 생성 — nutti 유저(uid=1000)가 쓸 수 있도록 소유권 설정
# (이 단계를 생략하면 Docker가 root 소유로 디렉터리를 생성하여 첫 실행 시 PermissionError 발생)
mkdir -p data/media secrets
sudo chown -R 1000:1000 data
# ② 이미지 빌드 + 전체 서비스 백그라운드 기동
docker compose up --build -d
# 기동 상태 확인
docker compose ps예상 출력:
NAME IMAGE STATUS
nutti-nutti-1 nutti:latest running
nutti-scheduler-1 mcuadros/ofelia running
nutti(파이프라인 워커)·scheduler(Ofelia cron 스케줄러)가
모두 running 상태이면 정상이다.
# Ofelia 로그에서 cron 등록 확인
docker compose logs scheduler정상 기동 시 아래와 같은 로그가 출력된다:
scheduler | New job registered "run-pipeline" - "nutti run" - "0 0 0 * * *"
0 0 0 * * *(6필드: 초 분 시 → UTC 00:00:00 = KST 09:00)가 보이면 스케줄 등록 완료.
⚠ 크론 형식 주의(2026-06-30 실측): Ofelia(robfig/cron)는 **6필드 "초 분 시 일 월 요일"**로 해석한다. 5필드 "0 0 * * *"는 매일 09시가 아니라 엉뚱하게 동작한다(무인 검증에서 5필드 "50 1 * * *"가 01:50에 발동하지 않음을 확인). 스케줄은 반드시 6필드로 작성한다.
# nutti 파이프라인 실시간 로그
docker compose logs -f nutti
# Ofelia 스케줄러 로그
docker compose logs -f scheduler
# 전체 서비스 로그
docker compose logs -f스케줄 없이 즉시 파이프라인을 1회 실행하려면 runner 서비스를 사용한다.
runner는 Dockerfile의 ENTRYPOINT ["nutti"] 를 그대로 사용하는 one-shot 서비스다.
nutti 서비스는 Ofelia job-exec 대기용으로 entrypoint가 오버라이드되어 있어
docker compose run --rm nutti run "주제" 는 exit 127 로 실패한다.
# 실제 주제로 수동 실행 (실 API 호출)
docker compose --profile manual run --rm runner run "강아지 닭가슴살 간식, 하루 적정량은?"
# Reels 형식으로 수동 실행
docker compose --profile manual run --rm runner run "강아지 수제간식 레시피" --reels
# config 확인
docker compose --profile manual run --rm runner config--rm 플래그로 실행 완료 후 임시 컨테이너가 자동 삭제된다.
runner 서비스는 profiles: [manual] 로 선언되어 docker compose up -d 에는 포함되지 않는다.
주의:
--profile manual플래그는 반드시 명시한다. Docker Compose v2.20+ 는compose run시 대상 서비스의 프로파일을 자동 활성화하므로 생략해도 동작하지만, 그 미만 버전(Ubuntu 22.04 LTS의 apt 기본 패키지 등)에서는 생략 시no such service: runner오류가 발생한다. 모든 버전에서 동작하도록 항상 붙여 쓴다.
실제 API 키 없이 전체 파이프라인 흐름을 검증한다. 네트워크 호출이 발생하지 않으며 결정적(deterministic) 결과를 반환한다.
# dry_run 모드로 전체 사이클 실행
docker compose --profile manual run --rm -e NUTTI_DRY_RUN=true runner run "도커 스모크"완료: run=... 형식의 로그와 함께 exit 0으로 종료되면 이미지가 정상이다.
새 버전이 릴리스되면:
# 최신 코드 가져오기
git pull
# 이미지 재빌드 + 서비스 재시작 (다운타임 최소화)
docker compose up --build -d구버전 이미지 정리(선택):
docker image prune -fNutti는 Claude AI를 통해 대본을 생성한다. 컨테이너 환경에서 인증 방식은 두 가지 옵션이 있다.
가장 단순하고 안정적인 방법이다. .env에 API 키를 직접 설정한다.
ANTHROPIC_API_KEY=sk-ant-api03-...이 환경변수가 있으면 nutti는 Anthropic Python SDK를 통해 직접 API를 호출한다.
claude CLI 설치가 불필요하며, 재인증 없이 안정적으로 동작한다.
인증 우선순위: ANTHROPIC_API_KEY > CLAUDE_CODE_OAUTH_TOKEN > ~/.claude/.credentials.json
API 키 없이 Claude Max 구독으로 대본을 생성하는 방법이다. 이미지에 claude CLI가 포함돼
있고(Dockerfile에서 npm @anthropic-ai/claude-code 설치), ANTHROPIC_API_KEY가 비어 있으면
nutti가 claude -p로 폴백한다. 그 인증을 헤드리스 토큰 하나로 해결한다.
절차(2026-06-30 무인 1사이클 검증으로 동작 확인):
- 개발 머신에서 한 번만 헤드리스 토큰을 발급한다(브라우저 OAuth 1회):
claude setup-token # 출력: sk-ant-oat01-... .env에 토큰을 넣는다(ANTHROPIC_API_KEY는 비워둔다):ANTHROPIC_API_KEY= CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-...
- 끝.
env_file: .env가 컨테이너로 토큰을 전달하고, claude CLI가 이 환경변수로 인증한다. 추가 볼륨 마운트·credentials 파일 불필요.
중요 — 호스트 ~/.claude 파일 마운트는 쓰지 말 것(2026-06-30 실측 실패):
- 종전 안내(아래 폐기)였던
~/.claude/.credentials.json(claudeAiOauthOAuth 블록 포함)을CLAUDE_CONFIG_DIR로 마운트하는 방식은 Linux 컨테이너의 claude가 그 파일을 인증에 사용하지 못한다(Not logged in · Please run /login). 특히 Windows 호스트에서 만든 자격증명은 Linux claude가 못 읽는다. 반드시CLAUDE_CODE_OAUTH_TOKEN방식을 쓴다.
제약:
claude setup-token토큰도 무기한은 아니다 — 만료 시 재발급해.env를 갱신한다.
결론: API 키가 있으면 **옵션 A(ANTHROPIC_API_KEY)**가 가장 단순하다. Max 구독을 쓰면
**옵션 B(CLAUDE_CODE_OAUTH_TOKEN)**가 검증된 길이며, 호스트 ~/.claude 마운트는 쓰지 않는다.
참고 메모 — 아래는 GitHub Actions에 Docker 빌드 검증을 추가할 때의 예시이다. 실제 CI 파일(
.github/workflows/ci.yml) 수정은 별도 PR로 진행 권장.
# .github/workflows/ci.yml 에 추가할 job 예시
docker-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Docker 이미지 빌드 검증
run: docker build -t nutti:ci .
- name: dry_run 스모크 테스트
run: |
docker run --rm \
-e NUTTI_DRY_RUN=true \
-e NUTTI_STATE_PATH=/tmp/state.json \
-e NUTTI_REVIEW_STORE_PATH=/tmp/reviews.json \
nutti:ci run "CI 스모크"주의:
docker compose run --rm방식의 스모크 테스트는.env파일 존재를 전제로 하므로, CI 환경에서는-e플래그로 환경변수를 직접 주입하거나 GitHub Actions Secrets에서env_file을 동적 생성해야 한다.