[Docker 시리즈 10편] Nginx 컨테이너로 정적 사이트와 프록시 다루기

English version

8~9편에서 컨테이너 상태를 지키는 법을 익혔다면, 이번에는 실제 웹 요청을 처리하는 Nginx 컨테이너를 파헤쳐 볼 차례입니다. Docker 안에서 Nginx를 쓰면 정적 파일 서빙과 리버스 프록시 기능을 가볍게 묶어 둘 수 있습니다. 고등학생 입장에서도 "파일을 보여 주고, 일부 요청은 다른 서버로 넘긴다"는 큰 그림만 이해하면 금방 따라올 수 있습니다.

이 글의 흐름

  1. Docker에서 Nginx 컨테이너를 올리는 가장 기본 흐름
  2. 정적 파일을 빠르게 서빙하기 위한 gzip·캐시 설정
  3. 리버스 프록시가 무엇인지, 어떻게 설정하는지
  4. 실습으로 확인하는 curl/브라우저 테스트
  5. 정적 사이트와 API를 함께 다룰 때의 확장 감각

읽기 카드

  • 예상 소요 시간: 18분
  • 사전 준비: HTTP 기본 구조, 헤더 의미
  • 읽고 나면: 프론트엔드 정적 사이트를 Nginx 하나로 안전하게 공개하는 구성 요소를 설명할 수 있습니다.

Docker에서 Nginx 컨테이너 시작하기

가장 기본적인 정적 사이트 컨테이너를 만들려면 두 파일이면 충분합니다.

FROM nginx:alpine
COPY ./public /usr/share/nginx/html
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
services:
  web:
    build: .
    ports:
      - "8080:80"

docker compose up -d --build를 실행하면 로컬 ./public 폴더 안의 HTML/CSS/JS가 컨테이너에 복사되고, 브라우저에서 http://localhost:8080으로 접속할 수 있습니다. 이때 가장 핵심이 되는 설정 키워드는 아래 네 가지입니다.

  • listen 80: 컨테이너 안에서 몇 번 포트를 열지 지정합니다.
  • root /usr/share/nginx/html: 어떤 폴더의 파일을 보여 줄지 정합니다.
  • location: 특정 URL 규칙에 다른 동작을 적용합니다.
  • proxy_pass: 받은 요청을 다른 서버로 전달합니다.

이 네 가지를 이해하면 나머지 디테일은 필요할 때마다 찾아 붙일 수 있습니다.

정적 파일 서빙: gzip과 캐시 헤더

정적 사이트는 용량과 전송 시간이 곧 체감 속도입니다. 최소한 아래 두 가지는 꼭 넣어 보세요.

gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/javascript application/json;

location ~* \.(js|css|png|jpg|svg|woff2)$ {
  expires 1y;
  add_header Cache-Control "public, immutable";
}
  • gzip: 텍스트 파일을 압축해서 전송 시간을 줄입니다.
  • Cache-Control: 파일 이름 뒤에 해시가 붙어 있다면 1년짜리 캐시도 안전하게 쓸 수 있습니다.

학생 프로젝트에서는 CDN을 붙이기 어렵기 때문에, 이렇게 웹 서버 레벨에서 직접 최적화를 걸어 두면 체감 속도가 크게 개선됩니다.

리버스 프록시 이해하기

Nginx는 정적 파일만 내주는 도구가 아닙니다. 동일한 도메인에서 다른 컨테이너를 숨기고 싶을 때 리버스 프록시로 활용할 수 있습니다.

location ^~ /api/ {
  proxy_pass http://backend:4000/;
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-Proto $scheme;
}
  • /api/... 요청은 모두 backend라는 Compose 서비스로 전달됩니다.
  • 브라우저는 프런트와 백엔드가 같은 도메인인 것처럼 느끼므로 CORS 설정이 단순해집니다.

여기서 한 가지 자주 헷갈리는 포인트가 있습니다. proxy_pass 뒤 URL 끝의 슬래시(/) 유무에 따라 전달되는 경로가 달라질 수 있습니다. 초보자 단계에서는 우선 예시를 그대로 따라 쓰고, 경로가 이상하게 붙으면 슬래시 위치를 먼저 의심하면 됩니다.

아주 단순하게 보면 아래처럼 기억해도 좋습니다.

location /api/ {
  proxy_pass http://backend:4000/;
}
  • 요청이 /api/hello라면 백엔드에는 보통 /hello에 가깝게 전달됩니다.
location /api/ {
  proxy_pass http://backend:4000;
}
  • 요청이 /api/hello라면 백엔드에는 /api/hello 형태로 더 많이 남습니다.

즉, 경로가 중복되거나 빠지는 이상한 현상이 보이면 proxy_pass 끝 슬래시부터 먼저 확인하면 됩니다.

이 구조를 그림으로 그리면 이렇게 됩니다.

브라우저 -> localhost:8080/api -> nginx 컨테이너 -> backend 컨테이너

backend 이름은 Compose 네트워크 안에서 다른 컨테이너를 찾는 DNS 이름입니다. docker compose ps로 서비스 이름을 확인하고, proxy_pass http://<서비스명>:<포트>/ 형태로 연결하면 됩니다. 반대로 호스트 브라우저에서는 backend:4000으로 직접 접근하지 않고, 보통 localhost:8080처럼 Nginx가 열어 둔 공개 포트로 접속합니다.

또 하나 기억할 점은 rootalias 차이입니다. 지금 글에서는 root를 써서 문서 루트를 정했습니다. root는 "기본 폴더를 하나 정해 두고 그 아래에서 파일을 찾는 방식"이고, alias는 "특정 경로만 완전히 다른 폴더로 바꿔 연결하는 방식"에 가깝습니다.

예를 들면 아래처럼 생각할 수 있습니다.

  • root /usr/share/nginx/html; 이고 요청이 /images/logo.png라면 보통 /usr/share/nginx/html/images/logo.png를 찾습니다.
  • location /images/ { alias /data/assets/; }라면 같은 요청을 /data/assets/logo.png처럼 더 직접 연결합니다.

초반에는 root로 시작하고, 나중에 정적 자산 경로를 세밀하게 나눌 때 alias를 배우면 충분합니다.

손으로 확인하는 테스트

설정을 바꿨다면 바로바로 테스트해 보세요. 아래 명령만으로 대부분의 문제를 잡을 수 있습니다.

curl -I http://localhost:8080          # 정적 파일 헤더 확인
curl -I http://localhost:8080/app.js   # 캐시 헤더 확인
curl -I http://localhost:8080/api/health
  • 첫 번째 명령으로 gzip이 적용됐는지, Content-Type이 맞는지 확인합니다.
  • 두 번째 명령으로 캐시 헤더와 만료 시점을 살펴봅니다.
  • 세 번째 명령으로 프록시 요청이 백엔드까지 제대로 전달되는지 확인합니다. 502가 출력된다면 프록시 경로나 백엔드 포트를 다시 확인하면 됩니다.

정적 사이트와 API를 함께 다룰 때의 확장 감각

처음에는 정적 파일만 서빙해도 충분하지만, 프로젝트가 커지면 /api, /comments, /auth 같은 경로를 다른 서비스로 넘겨야 할 수 있습니다. 이때 Nginx는 정적 파일 서버이면서 동시에 앞단 정리자 역할을 합니다. 즉, "파일은 직접 보여 주고, 일부 요청은 다른 컨테이너로 넘긴다"는 감각을 익히는 것이 핵심입니다. 다음 글에서는 개발 컨테이너에서 코드를 어떤 볼륨 전략으로 공유할지 살펴봅니다.

💬 댓글

이 글에 대한 의견을 남겨주세요