💡 요약 정리
- Python 3.12 + FastAPI ≥0.115 + uvicorn ≥0.32 + uv 0.5+ + systemd 환경의 운영 매뉴얼입니다.
- DB는 PostgreSQL 17 또는 MariaDB 11.4 중 선택하여 사용할 수 있습니다.
- 앱은
127.0.0.1:8000에서 listen하며, 외부 접속은 nginx 리버스 프록시를 통해 처리됩니다.
- 배포 시 systemd unit 파일과 env 파일은 절대 업로드하지 마세요.
1. 환경 매니페스트
| 항목 | 값 |
|---|
| OS | Ubuntu 24.04 LTS |
| 언어 / 런타임 | Python 3.12 |
| 프레임워크 | FastAPI ≥0.115 + uvicorn ≥0.32 |
| 패키지 매니저 | uv 0.5+ |
| 프로세스 매니저 | systemd |
| 앱 listen | 127.0.0.1:8000 (외부 직접 접근 차단) |
| 리버스 프록시 | nginx 1.24 (:80/:443, Let's Encrypt 자동) |
| 앱 디렉토리 | /opt/[프로젝트] (appuser:appuser 0750) |
| 환경변수 | /etc/[프로젝트]/env (root:appuser 0640) |
| 로그 | /var/log/[프로젝트] |
| 서비스 단위 | [프로젝트].service |
| 주 코드 | /opt/[프로젝트]/main.py (FastAPI app 객체) |
DB 매니페스트 (선택 옵션)
| 항목 | PostgreSQL 17 | MariaDB 11.4 |
|---|
| 인증 | peer auth (sudo psql) | unix_socket auth (sudo mariadb) |
| 포트 | 5432 | 3306 |
| 서비스명 | postgresql@17-main | mariadb |
| 백업 | pg_dump | mariadb-dump |
| 클라이언트 라이브러리 | psycopg[binary] 또는 asyncpg | pymysql 또는 aiomysql |
예시: 프로젝트 이름 변수 설정
본 가이드의 [프로젝트]는 자리 치환입니다. 셸에 다음 한 줄을 박아두시면 그대로 복사·붙여넣기가 가능합니다.
PROJECT_NAME=myapp; export PROJECT_NAME
2. 서버 접속
2-1. SSH (root)
ssh root@아이디.mycafe24.com
2-2. SFTP / rsync (코드 업로드)
appuser는 SSH 키 미설정. root로 업로드 후 chown하시는 방법을 권장합니다.
| STEP | 명령 |
|---|
| 업로드 (rsync) | rsync -avz --exclude='.venv/' --exclude='__pycache__/' ./ root@아이디.mycafe24.com:/opt/$PROJECT_NAME/ |
| 권한 회복 | ssh root@아이디.mycafe24.com "chown -R appuser:appuser /opt/$PROJECT_NAME" |
2-3. appuser로 작업 (디버깅)
| 작업 | 명령 |
|---|
| 전환 | sudo -u appuser bash |
| 이동 | cd /opt/$PROJECT_NAME |
3. 환경 확인
| 확인 항목 | 명령 |
|---|
| 디렉토리 확인 | ls -la /opt/$PROJECT_NAME |
| 환경변수 파일 | sudo cat /etc/$PROJECT_NAME/env |
| 서비스 활성 | sudo systemctl is-active $PROJECT_NAME |
| 서비스 상세 | sudo systemctl status $PROJECT_NAME |
| 로그 (최근 50줄) | sudo journalctl -u $PROJECT_NAME -n 50 |
| 포트 listen (127.0.0.1만 정상) | ss -tlnp | grep 8000 |
| 헬스 체크 (서버 내부) | curl -sf http://127.0.0.1:8000/ |
| 외부 도메인 응답 | curl -sI https://아이디.mycafe24.com/ |
| uvicorn 워커 확인 | ps aux | grep uvicorn |
4. DB 직접 접속
4-1. PostgreSQL 17 — 슈퍼유저 (peer auth)
sudo -u postgres psql
| 작업 | 명령 |
|---|
| DB 목록 | \l |
| 사용자 목록 | \du |
| DB 전환 | \c [DB명] |
| 테이블 목록 | \dt |
| 종료 | \q |
4-2. MariaDB 11.4 — 슈퍼유저 (unix_socket auth)
sudo mariadb
| 작업 | 명령 |
|---|
| DB 목록 | SHOW DATABASES; |
| 사용자 목록 | SELECT user, host FROM mysql.user; |
| DB 전환 | USE [DB명]; |
| 테이블 목록 | SHOW TABLES; |
| 종료 | EXIT; |
4-3. 앱 사용자로 접속 (TCP)
| DB | 접속 명령 |
|---|
| PostgreSQL | psql "postgresql://[DB사용자]:[비밀번호]@127.0.0.1:5432/[DB명]" |
| MariaDB | mariadb -u [DB사용자] -p [DB명] |
비밀번호는 /etc/[프로젝트]/env의 DB_PASSWORD 값을 사용하시면 됩니다.
5. 코드 배포 워크플로우
5-A. 배포 시 위험 회피
자체 코드를 통째로 업로드하면 자동 구성된 DB · 보안 · systemd 설정이 사라져 서비스가 깨질 수 있습니다. 아래 위험 요소를 반드시 확인하신 후 배포를 진행해 주세요.
위험 5종 매트릭스
| # | 위험 | 증상 | FastAPI 안전 패턴 |
|---|
| 1 | DB 환경변수 참조 깨짐 | HTTP 200은 떠도 모든 DB 쿼리 실패 (KeyError, NameError) | os.environ["DATABASE_URL"] 또는 os.environ.get("DATABASE_URL") 코드 에서 그대로 사용 |
| 2 | 외부 IP에 listen | 외부에서 :8000 직접 접속 가능 → nginx 우회 | uvicorn --host 127.0.0.1 (systemd unit ExecStart) |
| 3 | 엔트리 포인트 변경 | systemd가 시작 시 파일 못 찾음 ("could not find") | /opt/[프로젝트]/main.py + app = FastAPI() 위치 유지 |
| 4 | 의존성 동기화 누락 | "ImportError" / "ModuleNotFoundError" | 업로드 후 uv sync 또는 uv add <패키지> |
| 5 | systemd unit 덮어씀 | env 변수 주입 안 됨 → DB 연결 실패 | [프로젝트].service 절대 업로드 대상 포함 금지 |
안전한 코드 패턴
main.py에 다음 핵심 요소를 그대로 유지하시면 됩니다.
| 요소 | 코드 |
|---|
| import | from fastapi import FastAPI + import os |
| 앱 객체 (이름 유지 필수) | app = FastAPI() |
| 환경변수 참조 | DATABASE_URL = os.environ["DATABASE_URL"] |
| 루트 라우트 | @app.get("/") + def root(): return {"status": "ok"} |
절대 업로드 금지 파일
-
/etc/systemd/system/[프로젝트].service
-
/etc/[프로젝트]/env
3가지 배포 방법 (안전 순서)
| 방법 | 설명 | 적합 |
|---|
| 1. 변경 파일만 업로드 | rsync로 .py 파일만 (가장 안전) | 일상 배포 |
| 2. 소스 통째 + 서버 빌드 | .venv/, __pycache__/ 제외 필수 | 큰 변경, 의존성 추가 |
| 3. 외부 설정 분리 | DB 등은 env 파일만, 코드는 자유 | 권장 베이스 |
배포 후 4가지 체크
| # | 확인 사항 | 명령 |
|---|
| 1 | 활성 상태 | sudo systemctl is-active $PROJECT_NAME |
| 2 | 127.0.0.1 listen | ss -tlnp | grep 8000 | grep '127.0.0.1' |
| 3 | 헬스 OK | curl -sf http://127.0.0.1:8000/health |
| 4 | 외부 응답 | curl -sI https://[도메인]/ |
5-B. FastAPI 배포 7단계
| STEP | 작업 | 명령 |
|---|
| 1 | 빌드 | FastAPI는 인터프리터 — 빌드 단계 없음 |
| 2 | 업로드 (rsync) | rsync -avz --exclude='.venv/' --exclude='__pycache__/' ./ root@아이디.mycafe24.com:/opt/$PROJECT_NAME/ |
| 3 | 권한 회복 | chown -R appuser:appuser /opt/$PROJECT_NAME |
| 4 | 의존성 동기화 | sudo -u appuser uv sync --directory /opt/$PROJECT_NAME |
| 5 | 마이그 레이션 (Alembic 등 사용 시) | env 로드 후 uv run alembic upgrade head |
| 6 | 재시작 | sudo systemctl restart $PROJECT_NAME |
| 7 | 검증 | sudo journalctl -u $PROJECT_NAME -n 30 -f |
자주 만나는 실수
| 실수 | 결과 | 회복 |
|---|
.venv/ 통째 업로드 | 다른 Python 버전 .venv가 덮어씌워져 ImportError | rm -rf /opt/$PROJECT_NAME/.venv && sudo -u appuser uv sync |
app 객체명 변경 | systemd 시작 실패 | main.py에 app = FastAPI() 복원 |
| Pydantic 동기화 누락 | ValidationError | 모델 import 경로 확인, restart |
5-C. 환영 페이지 끄고 내 코드 띄우기 (필독)
설치 직후 도메인으로 접속하면 "서버가 정상 동작 중입니다" 환영 페이지가 표시됩니다. 이는 Nginx가 /var/www/cafe24-welcome/index.html을 우선 서빙하기 때문입니다. 본인 코드의 / 라우트가 보이게 하려면 이 환영 파일 1개만 정리하면 됩니다.
5-C-1. 환영 페이지 끄기
| 방법 | 명령 |
|---|
| 방법 A — 삭제 | sudo rm /var/www/cafe24-welcome/index.html |
방법 B — 백업 후 비활성 (원복 가능) | sudo mv /var/www/cafe24-welcome/index.html /var/www/cafe24-welcome/index.html.bak |
| 현재 상태 확인 | ls /var/www/cafe24-welcome/ |
5-C-2. 확인
도메인 재접속 또는 curl -sI https://[도메인]/ 결과로 본인 앱이 응답하는지 확인합니다. 캐시가 남으면 시크릿 창 또는 강제 새로고침을 권장합니다.
Tip: 환영 파일을 백업해두면 트러블슈팅 시 "Nginx는 살아있나" 확인용으로 다시 활성화할 수 있습니다.
6. DB 연동 실전 코드
SQLAlchemy 2.x + asyncpg (PostgreSQL)
db.py에 다음 요소를 그대로 정의하시면 됩니다.
| 요소 | 코드 |
|---|
| import | from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker + import os |
| URL | DATABASE_URL = os.environ["DATABASE_URL"] |
| engine | create_async_engine(DATABASE_URL, pool_size=5, max_overflow=10, pool_pre_ping=True) |
| session | async_sessionmaker(engine, expire_on_commit=False) |
SQLAlchemy 2.x + aiomysql (MariaDB)
DATABASE_URL을 mysql+aiomysql://user:pass@127.0.0.1:3306/dbname?charset=utf8mb4 형식으로 사용하시면 됩니다. 나머지 코드는 동일합니다.
CRUD 라우터 예시
FastAPI + Depends 기반 비동기 세션 패턴입니다.
| 구성 | 코드 |
|---|
| 의존성 | from fastapi import FastAPI, Depends + from sqlalchemy import select + from sqlalchemy.ext.asyncio import AsyncSession |
| 세션 generator | async def get_session(): async with AsyncSessionLocal() as session: yield session |
| list 라우트 | @app.get("/items") + async def list_items(session: AsyncSession = Depends(get_session)): |
| 쿼리 실행 | result = await session.execute(select(Item)) |
| 응답 | return result.scalars().all() |
7. 정적 파일 / 미디어
FastAPI는 정적 파일을 nginx에 직접 위임하는 패턴이 권장됩니다. nginx 설정 파일에 /static/·/media/ location을 추가하시고, 변경 후 아래 명령으로 적용해 주세요.
sudo nginx -t && sudo systemctl reload nginx
8. 로그 / 모니터링
| 로그 종류 | 명령 |
|---|
| 실시간 (systemd) | sudo journalctl -u $PROJECT_NAME -f |
| 최근 100줄 | sudo journalctl -u $PROJECT_NAME -n 100 |
| 1시간 이내 | sudo journalctl -u $PROJECT_NAME --since "1 hour ago" |
| 에러만 | sudo journalctl -u $PROJECT_NAME -p err |
| nginx access | sudo tail -f /var/log/nginx/[프로젝트]_access.log |
| nginx error | sudo tail -f /var/log/nginx/[프로젝트]_error.log |
9. HTTPS / 도메인
9-1. 무료 도메인 (자동)
아이디.mycafe24.com은 SSL 자동 — 별도 작업 불필요합니다.
9-2. 보유 도메인 SSL
보유 도메인의 HTTPS 적용은 SSL 별도 구매 필요합니다. 카페24 SSL 인증서를 구매하신 후 나의 서비스 관리에서 설치해 주세요.
10. 트러블슈팅 + FAQ
트러블슈팅 매트릭스
| 증상 | 원인 | 해결 |
|---|
| HTTP 502 Bad Gateway | uvicorn 다운 / 포트 미스매치 | systemctl status $PROJECT_NAME + journalctl -u $PROJECT_NAME -n 50 |
| ImportError / ModuleNotFoundError | 의존성 미동기화 | sudo -u appuser uv sync + restart |
| KeyError: 'DATABASE_URL' | env 미주입 (수동 실행) | set -a; source /etc/$PROJECT_NAME/env; set +a 후 실행 |
| Connection refused (DB) | DB 미기동 | PG: systemctl status postgresql@17-main / Maria: systemctl status mariadb |
| password authentication failed | env 비밀번호 불일치 | sudo cat /etc/$PROJECT_NAME/env로 확인, 매뉴얼 로그인 시도 |
| Worker timeout (503) | 동기 블로킹 | async/await 누락 검토 |
FAQ
| 질문 | 답변 |
|---|
| uv add 후 ImportError가 안 사라져요 | systemd 프로세스가 import 캐시 보유. restart 전 uv sync 실행 필수 |
| uvicorn 직접 실행 시 KeyError | systemd 컨텍스트 외부에선 env 미주입. 수동 실행 시 set -a; source /etc/$PROJECT_NAME/env; set +a 후 명령 실행 |
| 외부에서 :8000으로 접속 안 됩니다 | 의도된 동작입니다. 외부 노출은 nginx :80/:443. ss -tlnp | grep 8000이 127.0.0.1만 보이면 정상 |
| /etc/[프로젝트]/env에 평문 비밀번호 노출 우려 | 모드 0640 root:appuser. systemd EnvironmentFile이 부팅 시 주입. 더 강한 격리 원하시면 sops/vault 사용 가능 |
11. 추가 운영 / 커스터마이징
nginx 변경 안전 시퀀스, DB 백업, 보안, 성능 튜닝 등 공통 운영 사항은 공통 운영·커스터마이징 가이드 페이지를 참조해 주세요. 요금제·SSL·도메인·트래픽 등 일반 문의는 FAQ · 트러블슈팅 페이지를 확인하시면 됩니다.