10. 설치·설정·배포
구독·결제 API 서버를 로컬 개발 환경에서 띄우는 절차부터 docker로 운영 배포하는 절차까지 정리한다. 핵심 원칙은 하나다 — PostgreSQL(DB)은 개발·배포 둘 다 별도 docker(외부)로 따로 구성하고, compose에는 포함하지 않는다. 운영 compose(docker-compose.prod.yml)는 nginx·app·redis 3개만 띄운다.
🔗함께 보기: 서비스 API
1. 요구사항·구성 요소
1.1 요구사항
| 항목 | 버전·비고 |
|---|---|
| Python | 3.13 이상 (pyproject.toml:6 — requires-python = ">=3.13") |
| uv | 패키지·가상환경 관리(로컬 개발에서 앱 직접 실행 시 사용) |
| Docker / Docker Compose | redis·운영 스택 기동, 별도 PostgreSQL 컨테이너 구성 |
| PostgreSQL | 16 권장. asyncpg 드라이버로 접속(별도 docker 또는 외부 관리형 DB) |
| Redis | 7 (세션·nonce·레이트리밋·킬스위치 캐시) |
1.2 구성 요소
운영 스택은 클라우드 단일 인스턴스에 세 컨테이너를 띄우고, DB는 외부에 둔다.
인터넷 ──443/80──▶ nginx(TLS 종단) ──8000──▶ app(payment_system) ──▶ 외부 PostgreSQL
└──▶ redis(세션·캐시)
| 컨테이너 | 이미지·역할 | 외부 노출 |
|---|---|---|
nginx |
nginx:1.27-alpine — HTTPS 입구(TLS 종단)·리버스 프록시 |
80 / 443 (유일하게 공개) |
app |
Dockerfile 빌드(FastAPI/uvicorn). 상태 없음(stateless) |
없음(내부 8000, nginx만 접근) |
redis |
redis:7-alpine — 세션·nonce·레이트리밋 캐시 |
없음(내부 6379) |
| PostgreSQL | 컨테이너에 없음 — 별도 docker 또는 외부 관리형 DB(예: Azure Database for PostgreSQL)를 DATABASE_URL로 연결 |
외부 |
💡참고: 회사 클라우드는 MS Azure 기준이며, 운영 DB는 관리형(Azure Database for PostgreSQL)을 권장한다. 돈 기록(DB)을 앱과 분리해 백업·보안·확장을 단순화한다(
docker/README.md).
2. 로컬 개발
호스트에서 앱을 uv run uvicorn으로(핫리로드) 직접 실행하고, Redis만 docker로 띄운다. DB는 별도 docker(외부) 로 따로 구성해 앱이 그 엔드포인트로 연결한다.
2.1 절차
- 저장소를 받고 의존성을 설치한다.
- 외부 PostgreSQL를 별도 docker로 띄운다(아래 예:
payment-postgres, 호스트 5432). - 개발용 Redis를
docker compose up -d로 띄운다(payment-dev프로젝트, 호스트 6380). .env(또는.env.dev)에 접속·비밀값을 채운다.- 마이그레이션을 적용한 뒤 앱을 실행한다.
2.2 명령
의존성 설치:
uv sync
외부 PostgreSQL을 별도 docker로 기동(예시 — DB는 compose에 없으므로 직접 띄운다):
docker run -d --name payment-postgres \
-e POSTGRES_USER=payment \
-e POSTGRES_PASSWORD='Payment!2002' \
-e POSTGRES_DB=payment \
-p 5432:5432 postgres:16
개발용 Redis 기동(docker-compose.yml, 프로젝트명 payment-dev, 호스트 127.0.0.1:6380만 노출):
docker compose up -d
마이그레이션 적용 후 앱 실행(핫리로드):
uv run alembic upgrade head
uv run uvicorn app.main:app --reload --port 8000
💡참고:
docker-compose.yml은 Redis만 띄운다. 포트는 루프백(127.0.0.1:6380:6379)에만 바인딩해 외부 노출을 막는다(docker-compose.yml).
2.3 .env 핵심 값
cp .env.example .env.dev 후 값을 채운다. 로드 순서는 .env → .env.<APP_ENV>(뒤가 우선)이며, APP_ENV 미지정 시 dev로 동작한다(.env.example).
ENVIRONMENT=dev
BASE_URL=http://localhost:8000
# 별도 docker로 띄운 외부 Postgres(asyncpg 드라이버) — 호스트 5432
DATABASE_URL=postgresql+asyncpg://payment:Payment!2002@localhost:5432/payment
# 개발용 redis(docker-compose.yml) — 호스트 6380
REDIS_URL=redis://localhost:6380/0
# AES-256-GCM 키(base64 32바이트)
# python -c "import base64,os;print(base64.b64encode(os.urandom(32)).decode())"
ENCRYPTION_KEY=
# 토스 시크릿 키: dev=test_sk_*
TOSS_SECRET_KEY=test_sk_xxxx
💡팁:
ENCRYPTION_KEY는 위 한 줄로 새로 생성한다. 분실·변경 시 기존 암호화 데이터(카드 빌링키 등)를 복호화할 수 없다.
3. 환경변수
.env.example 기준 주요 변수. .env(공통)에 두는 값과 환경별(.env.dev/.env.prod)로 덮어쓰는 값이 나뉜다.
| 변수 | 설명 | 예시·기본 |
|---|---|---|
ENVIRONMENT / APP_ENV |
실행 환경. prod면 docs 비공개·세션 쿠키 secure 등 보안 강화 |
dev / prod |
BASE_URL |
서버 공개 URL(이메일 링크 등) | http://localhost:8000 / https://도메인 |
DATABASE_URL |
외부 PostgreSQL 접속(반드시 asyncpg 드라이버) |
postgresql+asyncpg://payment:...@localhost:5432/payment |
REDIS_URL |
Redis 접속(운영은 compose가 redis://redis:6379/0으로 덮어씀) |
redis://localhost:6380/0 |
ENCRYPTION_KEY |
AES-256-GCM 키(base64 32바이트). 필수 | (직접 생성) |
TOSS_SECRET_KEY |
토스 시크릿 키(dev=test_sk_*, prod=live_sk_*). 필수 |
test_sk_xxxx |
TOSS_API_BASE_URL |
토스 API 베이스 URL | https://api.tosspayments.com |
TRUST_PROXY / TRUST_PROXY_HOPS |
리버스 프록시 뒤면 true + XFF hop 수(nginx 1단=1, 앞에 LB 더 있으면 2) |
false / 1 |
WEBHOOK_IP_CHECK_ENABLED |
토스 발신 IP 외 웹훅 거부(운영 true 권장) |
true |
SWAGGER_ID / SWAGGER_PW |
/docs HTTP Basic 계정. 비우면 docs 404 |
admin / (강력값) |
SCHEDULER_ENABLED / SCHEDULER_INTERVAL_MINUTES |
구독 자동 갱신 배치 사용·주기 | true / 5 |
DB_POOL_SIZE / DB_MAX_OVERFLOW |
DB 커넥션 풀(총 최대 = 합) | 10 / 20 |
⚠️중요:
ENCRYPTION_KEY·TOSS_SECRET_KEY·DB 비밀번호 등 비밀값은 Git에 커밋하지 않는다..env.example만 추적하고, 실제 값은.env/.env.dev/.env.prod에 둔다.참고: 운영에서는
REDIS_URL·TRUST_PROXY·APP_ENV를 compose가 컨테이너 네트워크 기준으로 주입하므로.env.prod에 적지 않아도 된다(적어도 compose 값이 우선,docker-compose.prod.yml:24).
4. 테스트
테스트도 외부 DB를 사용한다. 외부 payment-postgres에 payment_test 데이터베이스를 만들어 두면 된다(운영 DB와 별개). 기본 접속값은 tests/conftest.py:18에 정의되어 있고, 다르면 TEST_DATABASE_URL로 덮어쓴다.
테스트용 DB 생성(외부 Postgres 컨테이너 안에서):
docker exec -it payment-postgres createdb -U payment payment_test
전체·부분 테스트 실행:
uv run pytest # 전체
uv run pytest tests/integration/ # 통합
uv run pytest tests/e2e/ # E2E(어드민 화면)
다른 DB로 실행할 때:
TEST_DATABASE_URL=postgresql+asyncpg://payment:Payment!2002@localhost:5432/payment_test uv run pytest
⚠️주의: 테스트 DB는 운영 DB와 별개(
payment_test)다. 기본값은 외부payment-postgres(host 5432, userpayment)를 가리킨다(tests/conftest.py:14-18).
5. 운영 배포(docker)
운영 스택은 docker-compose.prod.yml(프로젝트명 payment_system)로 nginx·app·redis 3개를 띄운다. DB는 외부다.
5.0 새 리눅스 서버에 처음 설치하기 (전체 절차)
아무것도 없는 새 리눅스 서버(예: Ubuntu 22.04)에 처음부터 올리는 순서다. 아래 1~9를 차례로 따라가면 된다. (1~3은 서버 준비, 4~6은 설정·실행, 7~9는 마무리)
- Docker 설치 — Docker Engine + Compose 플러그인을 설치하고 현재 사용자를 docker 그룹에 넣는다(이후 로그아웃·재로그인).
curl -fsSL https://get.docker.com | sudo sh # Docker Engine + compose 플러그인 sudo usermod -aG docker $USER # sudo 없이 docker 사용(재로그인 필요) docker --version && docker compose version # 설치 확인 - 코드 가져오기 — 저장소를 서버에 클론(또는 복사)한다.
Git을 쓰지 않으면git clone <이-저장소-URL> payment_system cd payment_systemscp -r ./payment_system user@서버:/opt/처럼 통째로 복사해도 된다. - 외부 PostgreSQL 준비 — DB는 컴포즈에 없으므로 따로 마련한다. 관리형 DB(Azure Database for PostgreSQL 등)를 쓰거나, 같은 서버/별도 서버에 postgres 컨테이너를 띄운다. 빈
payment데이터베이스와 접속 계정만 있으면 되고, 스키마(테이블)는 앱이 자동 생성한다(아래 6단계의 마이그레이션). DB 방화벽에서 앱 서버 → DB(5432)만 허용한다.💡
참고: 리눅스에서 같은 호스트의 DB에 붙을 때는DATABASE_URL의 호스트를host.docker.internal로 둔다. compose에extra_hosts: "host.docker.internal:host-gateway"가 이미 있어 컨테이너가 호스트에 닿는다. - 환경설정
.env.prod작성 —.env.example를 참고해 채운다. 필수:DATABASE_URL(외부 DB),ENCRYPTION_KEY(새로 생성),TOSS_SECRET_KEY(라이브),BASE_URL(도메인),SWAGGER_ID/PW. 상세는 아래 5.1 참고.# AES-256-GCM 키 생성(운영용으로 새로): python3 -c "import base64,os;print(base64.b64encode(os.urandom(32)).decode())" - TLS 인증서 배치 —
docker/nginx/certs/에fullchain.pem·privkey.pem을 둔다(Let's Encrypt 권장, 도메인 없으면 자체 서명). 명령은 아래 5.1 참고. - 실행 — 빌드·기동(+ 자동 마이그레이션):
컨테이너가 켜지면 엔트리포인트가docker compose -f docker-compose.prod.yml up -d --build docker compose -f docker-compose.prod.yml logs -f app # 마이그레이션·기동 로그 확인alembic upgrade head로 외부 DB에 테이블을 자동 생성한 뒤 앱을 띄운다. - 최초 관리자(SYSTEM_ADMIN) 생성 — 로그인하려면 첫 관리자 계정을 만들어야 한다. 앱 컨테이너 안에서 CLI로 생성한다.
docker compose -f docker-compose.prod.yml exec app \ python -m app.cli create-admin --email admin@yourco.com --password '강력한비밀번호!' - 방화벽 개방 — 외부에서 들어오는 포트는 80·443만 연다(앱 8000·redis는 비공개). 예(ufw):
sudo ufw allow 80,443/tcp && sudo ufw enable - 동작 확인 —
https://도메인/접속 → 로그인 화면.docker compose -f docker-compose.prod.yml ps로 3개 컨테이너가 healthy인지,/docs(Swagger, SWAGGER_ID/PW)와 관리자 로그인이 되는지 확인한다.
⚠️주의:
.env.prod와 TLS 키 같은 비밀값은 Git에 커밋하지 말 것.ENCRYPTION_KEY는 분실·변경 시 기존 암호화 데이터(빌링키 등)를 복호화할 수 없으니 안전하게 보관한다.팁: 코드 업데이트 후 재배포는
git pull→docker compose -f docker-compose.prod.yml up -d --build. 새 마이그레이션이 있으면 기동 시 자동 적용된다.
5.1 사전 준비
.env.example를 참고해.env.prod를 채운다(Git 미추적).- TLS 인증서를
docker/nginx/certs/에 배치한다. DATABASE_URL이 외부 관리형 DB 엔드포인트를 가리키는지 확인한다.
.env.prod 최소 필수값(docker/README.md):
ENVIRONMENT=prod
BASE_URL=https://your-domain.example.com
# 외부 관리형 Postgres — 반드시 asyncpg 드라이버(필요 시 ?ssl=require)
DATABASE_URL=postgresql+asyncpg://USER:PASSWORD@your-db-host:5432/payment
ENCRYPTION_KEY=
TOSS_SECRET_KEY=live_sk_xxxx
SWAGGER_ID=admin
SWAGGER_PW=강력한값
WEBHOOK_IP_CHECK_ENABLED=true
TLS 인증서 — docker/nginx/certs/에 fullchain.pem·privkey.pem 두 파일을 둔다.
Let's Encrypt(운영 권장):
sudo certbot certonly --webroot -w docker/nginx/certbot-www -d your-domain.example.com
sudo cp /etc/letsencrypt/live/your-domain.example.com/fullchain.pem docker/nginx/certs/
sudo cp /etc/letsencrypt/live/your-domain.example.com/privkey.pem docker/nginx/certs/
자체 서명(도메인 없이 우선 띄워볼 때):
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout docker/nginx/certs/privkey.pem \
-out docker/nginx/certs/fullchain.pem \
-subj "/CN=localhost"
5.2 실행
docker compose -f docker-compose.prod.yml up -d --build
마이그레이션은 자동이다. 컨테이너 시작 시 엔트리포인트(docker/entrypoint.sh)가 alembic upgrade head로 외부 DB 스키마를 최신(head)까지 적용한 뒤 uvicorn을 실행한다.
5.3 로그·상태·종료
docker compose -f docker-compose.prod.yml logs -f app # 마이그레이션·기동 로그
docker compose -f docker-compose.prod.yml ps # 컨테이너 상태
docker compose -f docker-compose.prod.yml down # 중지(redis 볼륨은 유지)
재배포(업데이트):
git pull && docker compose -f docker-compose.prod.yml up -d --build
💡참고: 외부에 열리는 포트는 nginx의 80/443뿐이다.
app(8000)·redis(6379)는 publish하지 않아 docker 내부 네트워크에서만 컨테이너 이름으로 서로를 호출한다(docker-compose.prod.yml).
6. 운영 주의사항
- 컴포즈 프로젝트명 분리: 운영 스택은
name: payment_system(nginx·redis·app 3개), 개발 인프라는name: payment-dev(redis만)로 격리되어 있다. 같은 프로젝트명으로 섞으면 운영 컨테이너에 개발용 컨테이너가 끼어드는 사고가 난다(docker-compose.prod.yml:18,docker-compose.yml). - DB는 개발·배포 둘 다 별도 docker(외부): compose에 postgres가 없다. 앱·테스트 모두 외부 DB 엔드포인트로 연결한다.
DATABASE_URL을localhost/127.0.0.1로 두면 컨테이너 자신을 가리켜 접속에 실패한다 — 실제 배포는 외부 DB 호스트로, 로컬에서 호스트의 DB docker에 붙일 때는host.docker.internal:5432를 사용한다(docker/README.md). - 비밀값 관리:
ENCRYPTION_KEY(운영 전용 새 값)·TOSS_SECRET_KEY(live_sk_*)·SWAGGER_PW등은.env.prod에 두고 Git에 커밋하지 않는다. - 마이그레이션 head 적용: 엔트리포인트가 매 기동 시
alembic upgrade head를 실행한다. 앱을 여러 대로 확장하면 마이그레이션 중복을 피하기 위해 한 대만 기본값으로 두고 나머지는RUN_MIGRATIONS=0으로 띄운다(docker/entrypoint.sh). - 클라이언트 IP: nginx가
X-Forwarded-For를 세팅하고 앱은TRUST_PROXY=true·TRUST_PROXY_HOPS=1로 읽는다. 앞단에 LB가 하나 더 있으면TRUST_PROXY_HOPS=2로 올린다(웹훅·어드민 IP 화이트리스트 정합에 중요). - 인증서 갱신:
certbot renew후docker compose -f docker-compose.prod.yml exec nginx nginx -s reload. - 세션 보안(추가 키 없음): 어드민 세션은 별도 쿠키 서명키 없이 Redis에 저장된 무작위 세션 토큰(
secrets.token_urlsafe(32))으로 동작한다(app/services/auth.py). 따라서SECRET_KEY류의 환경변수는 없으며, redis를 내부 네트워크 전용으로 두는 것(포트 미공개)이 곧 세션 보호다.
7. 백업·복구·롤백
돈 기록(DB)은 외부 관리형 PostgreSQL에 있으므로 백업·복구는 1차적으로 관리형 DB 기능(Azure Database for PostgreSQL의 자동 백업·특정 시점 복구 PITR)에 위임한다. 그 위에, 배포·마이그레이션 직전 수동 스냅샷을 안전망으로 권장한다.
7.1 DB 백업 (pg_dump)
# DB에 접근 가능한 호스트(또는 앱 컨테이너)에서 — 배포/마이그레이션 직전 권장
pg_dump "postgresql://USER:PASSWORD@DB_HOST:5432/payment" -Fc -f payment_$(date +%Y%m%d_%H%M).dump
-Fc(커스텀 포맷)은pg_restore로 부분/선택 복구가 가능하다.- 관리형 DB의 자동 백업·PITR과 함께 쓰는 것을 권장한다(자동 백업은 일상 보호, 수동 덤프는 배포 직전 롤백 안전망).
7.2 DB 복구 (pg_restore)
# 주의: 운영 DB에 덮어쓰기 전, 반드시 현재 상태를 먼저 백업한다.
pg_restore --clean --if-exists -d "postgresql://USER:PASSWORD@DB_HOST:5432/payment" payment_YYYYMMDD_HHMM.dump
7.3 마이그레이션 롤백 (alembic downgrade)
엔트리포인트는 기동 시 alembic upgrade head만 수행한다(docker/entrypoint.sh). 직전 배포의 스키마 변경을 되돌리려면 앱 컨테이너에서 downgrade를 직접 실행한다.
docker compose -f docker-compose.prod.yml exec app alembic current # 현재 리비전 확인
docker compose -f docker-compose.prod.yml exec app alembic downgrade -1 # 한 단계 되돌리기
# 특정 리비전으로: docker compose -f docker-compose.prod.yml exec app alembic downgrade <revision>
- 데이터 손실 주의: 컬럼/테이블을 제거하는 downgrade는 그 데이터를 함께 지운다. 반드시 7.1 백업을 먼저 떠 두고, 코드 롤백(이전 이미지 재배포)으로 해결 가능한지 먼저 검토한다.
- 스키마-이미지 정합: 새 이미지 배포는 자동으로
upgrade head까지 올린다. 이전 이미지로 되돌릴 때는, 그 이미지가 기대하는 리비전까지 먼저 downgrade해야 앱과 스키마가 맞는다.
7.4 Redis 데이터
redis는 세션·nonce·레이트리밋·킬스위치 캐시다. 명명 볼륨(redis-data)에 스냅샷이 남지만, 유실되어도 재로그인 수준의 영향이며 결제·구독 데이터(DB)와는 무관하다. 별도 백업 대상이 아니다.
🔗함께 보기: 서비스 API