서비스 학습
SOURCE · be/Dockerfile · 2026-09-06

FastAPI를 작고 제한된 Linux process로 실행한다

Mac의 Python 환경에 의존하지 않도록 runtime을 image에 고정하고, application을 root가 아닌 전용 사용자로 실행한다.

Python 3.13.7 slimnon-rootUvicornhealthcheck

Frontend container와 목적은 같지만 내용은 다르다

Frontend는 Node로 build한 정적 파일을 Nginx가 제공했지만 Backend는 요청마다 Python 코드를 계속 실행한다. 따라서 최종 image에도 Python interpreter, FastAPI, Uvicorn과 application source가 남아 있어야 한다.

BasePython slim

Linux + interpreter

Dependencypip install

고정 version

Sourceapp/

서비스 코드

ProcessUvicorn

ASGI HTTP server

현재 전체 코드

# syntax=docker/dockerfile:1

FROM python:3.13.7-slim AS runtime

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

RUN groupadd --system app && useradd --system --gid app --home-dir /app app

COPY requirements.txt ./
RUN pip install --no-cache-dir --requirement requirements.txt

COPY app ./app

USER app
EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=2)"]

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

구간별 해설

PYTHONDONTWRITEBYTECODEPYTHONUNBUFFERED

첫 설정은 container filesystem에 __pycache__ bytecode를 만들지 않게 한다. 두 번째 설정은 stdout과 stderr log를 오래 buffer하지 않아 docker logs에서 즉시 관찰할 수 있게 한다.

Dependency를 source보다 먼저 복사하는 이유

requirements.txt가 같으면 application source를 수정해도 느린 pip install layer를 재사용할 수 있다. Frontend Dockerfile에서 package lock을 먼저 복사한 것과 같은 cache 원리다.

USER app이 중요한 이유

Container 내부 root도 잘못된 mount나 kernel 취약점과 결합하면 피해 범위를 키울 수 있다. Web process에는 system 설정을 바꿀 권한이 필요 없으므로 전용 system user로 최소 권한을 준다. 실제 검증에서 container의 configured user는 app이었다.

왜 healthcheck에 curl을 설치하지 않았나?

Python image에는 표준 library urllib.request가 이미 있다. Health 요청 하나를 위해 별도 OS package를 추가하지 않아 image와 공격 표면을 늘리지 않는다.

0.0.0.0과 loopback 공개의 차이

현재 구성에서는 Uvicorn을 0.0.0.0에 bind해 loopback뿐 아니라 container의 네트워크 interface에서도 받게 한다. 반드시 모든 interface를 열어야 하는 것은 아니지만, container의 127.0.0.1만 listen하면 일반적인 Docker port publishing으로 들어오는 요청을 받지 못한다. 반면 host에서는 -p 127.0.0.1:8000:8000으로 Mac loopback에만 공개해 같은 네트워크의 다른 기기가 아직 접근하지 못하게 한다.

Build context

.dockerignore는 무엇을 보내지 않을지 정한다

docker build ./be의 마지막 인수는 그 폴더 전체를 build context로 Docker engine에 보낸다는 뜻이다. 보내는 순간 COPY로 쓰지 않는 파일까지 전송되고, layer cache 판정에도 영향을 준다. 이 파일은 무엇을 보내지 않을지 정한다.

__pycache__
*.py[cod]
.pytest_cache
.venv
.env
.git
.DS_Store
tests
README.md
제외이유
__pycache__ · *.py[cod]host에서 만들어진 bytecode다. container의 Python 버전과 다를 수 있고 image 안에서 다시 만들어진다.
.venv가장 중요한 항목이다. macOS용으로 설치된 가상환경이며 Linux container에서는 동작하지 않는다. 크기도 커서 build가 눈에 띄게 느려진다. 의존성은 image 안에서 requirements.txt로 다시 설치한다.
.pytest_cache직전 실행 기록일 뿐 실행에 필요하지 않다.
.env보안상 가장 중요하다. 실제 설정값이 담기며 앞으로 database 암호와 인증 열쇠가 들어갈 파일이다. image에 구워지면 그 image를 받은 누구나 읽을 수 있다. 설정은 실행 시점에 환경변수로 주입한다.
.git전체 이력이 들어 있어 크고, 과거 commit에 지워진 비밀이 남아 있을 수 있다.
.DS_StoremacOS Finder가 만드는 파일로 Linux에서 의미가 없다.
testsproduction image는 애플리케이션을 실행하기만 한다. 테스트는 image를 만들기 전에 개발 환경과 CI에서 실행한다. 다만 이 때문에 docker run으로 container 안에서 pytest를 돌릴 수는 없다.
README.md사람이 읽는 문서이며 실행에 필요하지 않다.

FE와 다른 점이 하나 있다. fe/.dockerignoreDockerfile*을 제외하지만 여기서는 제외하지 않는다. 두 경우 모두 build 결과에는 영향이 없다 — Dockerfile 자체는 engine이 따로 읽으며 COPY 대상이 아니기 때문이다. 목록을 맞추면 더 일관되지만 동작이 달라지지는 않는다.

제외가 실제로 적용됐는지는 build 첫 줄의 context 크기로 확인할 수 있다. .venv가 포함되면 수백 MB, 제외되면 수십 KB 수준이다.

초기 구현의 실제 Docker·Gemma 검증 결과

Build성공

local-moe-be:step2

Statehealthy
Userapp
InferenceHTTP 200

Host Gemma

이 기록은 연결 분리 전 Python 3.13 Linux image에서 application과 HTTPX2 Adapter가 import되고, host.docker.internal을 통해 macOS loopback TurboFieldfare의 readiness와 실제 16-token 제한 생성까지 성공한다는 것을 보여 준다. 이번 문서 검토에서 Gemma 생성이나 이 과거 image의 health 상태를 다시 측정한 것은 아니다.

docker run --rm --name local-moe-be-step2 \ -p 127.0.0.1:8000:8000 \ -e APP_INFERENCE_BASE_URL=http://host.docker.internal:8080 \ local-moe-be:step2

보완 후 확인한 범위는 별도다

연결 분리·오류 분류 보완 뒤 같은 tag의 이미지를 다시 빌드했고, 네트워크 없는 임시 container의 ASGI TestClient로 /healthz=200, /readyz=503/unavailable, 새 오류 schema 및 lifespan 정리를 확인했다. 이 검사는 Uvicorn의 공개 port·Docker HEALTHCHECK·실제 host Gemma 생성의 재측정과 다르다.

Dockerfile의 Python tag와 requirements의 직접 의존성 버전 지정은 환경 차이를 줄이지만, base image digest와 모든 간접 의존성을 고정한 완전한 lock은 아니다. 따라서 다른 시점의 build가 byte 단위까지 동일하다고 보장하지 않는다.

자주 발생할 수 있는 오류

증상원인확인
Container가 즉시 종료module import 또는 설정 검증 실패docker logs
Host에서 연결 거부-p 누락 또는 Uvicorn이 127.0.0.1에 bindpublish와 --host 0.0.0.0
계속 startingstart period 또는 health 명령 실패docker inspect health log
Permission deniedapp user가 쓰기 금지 경로에 파일 생성서비스는 stdout·외부 volume 사용
← 이전system.py다음 →Backend 2단계