FastAPI 배포 — Docker와 Gunicorn
내 노트북에서 잘 돌던 앱을 진짜 서비스로 내보낼 차례. 컨테이너로 묶고, 여러 워커로 띄우고, 시크릿은 환경변수로 숨긴다.
지금까지 우리는 fastapi dev main.py 한 줄로 서버를 띄우고, 브라우저에서 /docs 를 열어 잘 돈다는 걸 확인해 왔다. 그런데 그 서버는 내 컴퓨터에서만 살아 있다. 노트북을 닫으면 꺼지고, 친구는 접속할 수 없다. 이제 이 앱을 인터넷 어디에서도 24시간 도는 진짜 서비스로 내보낼 차례다.
11편은 배포(deployment)를 다룬다. 핵심 질문은 셋이다 — 개발 서버를 그대로 띄우면 왜 안 되나, 앱을 어떻게 한 덩어리로 포장(Docker)하나, 그리고 요청이 몰려도 버티게 어떻게 여러 일꾼(워커)을 둘 것인가. 클라우드 종류는 AWS·GCP·Fly.io 등 천차만별이지만, 그 아래 깔린 원리는 모두 같다. 그 공통 원리를 잡는다.
1. 개발 서버 vs 프로덕션 서버
가장 먼저 깨야 할 습관 — fastapi dev 와 --reload 는 개발 전용이다. 프로덕션에서는 절대 쓰지 않는다. 편해서 무심코 그대로 배포하기 쉬운데, 이건 명백한 사고다.
왜 그런가. --reload 옵션은 파일이 바뀔 때마다 코드를 다시 읽어 들이려고 디스크를 계속 감시한다. 개발할 땐 코드를 고치면 바로 반영돼서 천국이지만, 운영 환경에서는 그저 쓸데없이 CPU 를 먹고 메모리가 새는 위험일 뿐이다. 게다가 개발 서버는 기본적으로 프로세스 하나로 돈다. 한 명의 일꾼이 모든 요청을 줄 세워 처리하니, 요청이 조금만 몰려도 줄이 길어진다.
| 구분 | 개발 서버 | 프로덕션 서버 |
|---|---|---|
| 실행 명령 | fastapi dev / --reload | uvicorn --workers · gunicorn |
| 워커 수 | 1개 (단일 프로세스) | CPU 코어 기준 여러 개 |
| 자동 리로드 | 켬 (편의) | 끔 (위험·낭비) |
| 목표 | 빠른 피드백 | 안정성·처리량 |
정리하면 프로덕션은 두 가지가 필요하다. 여러 개의 워커(요청을 나눠 처리하는 일꾼)와, 워커가 죽으면 다시 살려 주는 프로세스 매니저다. 이걸 깔끔하게 묶어 주는 그릇이 바로 Docker 다.
2. Dockerfile 작성하기
Docker 는 앱과 그 앱이 필요로 하는 모든 것(파이썬 버전, 라이브러리, 코드)을 컨테이너라는 한 상자에 담는 기술이다. "내 컴퓨터에선 됐는데요?" 라는 영원한 변명을 없애 준다 — 상자째로 옮기니 어디서든 똑같이 돈다. 그 상자를 어떻게 만들지 적어 둔 설계도가 Dockerfile 이다.
한 줄씩 의미를 보자. FROM python:3.12-slim 은 "파이썬 3.12 가 깔린 가벼운 리눅스에서 시작" 이라는 뜻이다. slim 은 군더더기를 뺀 작은 이미지라 빌드가 빠르고 보안 표면도 줄어든다. WORKDIR /code 는 컨테이너 안의 작업 폴더를 정한다.
여기서 가장 중요한 한 수 — requirements.txt 를 코드보다 먼저 복사하고 설치하는 순서다. Docker 는 변하지 않은 단계를 캐시로 재사용한다. 라이브러리 목록은 거의 안 바뀌고 코드는 자주 바뀌니, 의존성 설치를 위로 빼 두면 코드만 고쳤을 때 무거운 pip install 을 건너뛴다. 빌드가 몇 배 빨라진다.
마지막 CMD 가 컨테이너가 켜질 때 실행할 명령이다. --host 0.0.0.0 은 "컨테이너 바깥에서 들어오는 접속도 받겠다" 는 뜻으로, 이게 없으면 외부에서 접속이 안 된다. 이제 이미지를 빌드하고 실행한다.
-p 80:80 의 앞쪽은 내 서버(호스트)의 포트, 뒤쪽은 컨테이너 안의 포트다. 이 다리를 놓아야 브라우저가 컨테이너 안의 FastAPI 까지 닿는다. 이제 이 이미지는 어떤 클라우드에 던져도 똑같이 돈다.
3. 여러 워커와 Gunicorn
컨테이너 안에서 uvicorn 을 그냥 띄우면 여전히 워커가 하나다. 코어가 여러 개인 서버라면 일꾼을 늘려 동시에 더 많은 요청을 받는 게 이득이다. 가장 간단한 방법은 uvicorn 에 --workers 를 주는 것.
두 방법의 차이는 누가 워커를 관리하느냐다. Gunicorn 은 오랜 시간 검증된 프로세스 매니저로, 워커 하나가 죽으면 즉시 새로 띄워 주고, 무중단으로 워커를 재시작하는 등 운영 기능이 탄탄하다. -k uvicorn.workers.UvicornWorker 가 "각 워커는 uvicorn 으로 돌려라" 는 지정이고, -w 4 가 워커 수다. FastAPI 는 ASGI 앱이라 반드시 이 uvicorn 워커 클래스를 끼워야 한다는 점을 잊지 말자.
요즘은 컨테이너 하나에 워커를 몰아넣기보다, 컨테이너당 워커 1~2개로 가볍게 두고 컨테이너 자체를 여러 개 띄우는 방식(쿠버네티스·오토스케일링)도 많다. 어느 쪽이든 "단일 프로세스로는 절대 운영하지 않는다" 는 원칙은 같다.
4. 환경변수·보안·헬스체크
마지막은 운영의 디테일이다. 코드는 그대로지만 운영 환경에서만 챙겨야 하는 것들이 있다.
시크릿은 환경변수로
DB 접속 주소, SECRET_KEY, 외부 API 키 같은 민감한 값을 코드에 직접 적어 두면 안 된다. 대신 환경변수로 주입하고, 코드에서는 os.environ 으로 읽는다. 같은 이미지를 개발·운영에 그대로 쓰면서 값만 바꿀 수 있어 유연하다.
Dockerfile 에 ENV SECRET_KEY=abcd 처럼 키를 박거나, .env 파일을 깃에 커밋하면 그 비밀은 이미지·이력에 영원히 남는다. 나중에 코드에서 지워도 깃 히스토리에서 복원된다. 시크릿은 반드시 실행 시점에 환경변수로 주입하고, .env 는 .gitignore 와 .dockerignore 에 넣어 둔다.
헬스체크 엔드포인트
운영 환경은 "이 서버 아직 살아 있나?" 를 주기적으로 묻는다. 로드밸런서나 쿠버네티스가 응답이 없는 컨테이너를 자동으로 빼고 새로 띄우려면, 가볍게 200 을 돌려주는 /health 같은 엔드포인트가 있어야 한다.
HTTPS 는 리버스 프록시에 맡긴다
마지막으로 보안 접속(HTTPS) 처리다. FastAPI 앱이 직접 인증서를 다루게 하지 말고, 앞단에 Nginx 같은 리버스 프록시를 두어 HTTPS 종료·인증서 갱신을 위임하는 게 표준이다. 프록시가 바깥의 HTTPS 요청을 받아 내부의 평문 HTTP 로 FastAPI 에 전달한다. 앱은 비즈니스 로직에만 집중하고, 보안·압축·정적 파일 같은 무거운 일은 프록시가 맡는 역할 분담이다.
요약
11편의 핵심을 한 번에. 개발 서버(fastapi dev·--reload)는 절대 운영에 쓰지 않는다. 앱은 Dockerfile 로 컨테이너에 포장하되 requirements.txt 를 먼저 복사해 캐시를 살리고, 실행은 Gunicorn + uvicorn 워커 여러 개로 한다(워커 수는 코어 기준으로 시작해 측정으로 조절). 시크릿은 코드·이미지·깃이 아니라 환경변수로 주입하고, /health 로 생존을 알리며, HTTPS 는 리버스 프록시에 위임한다. 이 다섯 가지면 어떤 클라우드에서도 통한다.
다음 편 예고 — 미니 프로젝트: To-Do API
마지막 12편. 지금까지 배운 라우터·DB·인증·배포를 모두 합쳐, 처음부터 끝까지 동작하는 To-Do API 하나를 완성한다.