본문 바로가기

Docker 환경에서 여러 도메인을 안전하게 운영하는 Traefik 최적화 구성

728x90

여러 도메인을 안전하고 깔끔하게 운영하는 실전형 구조 정리

Docker 환경에서 여러 서비스를 운영하다 보면, 각 컨테이너마다 포트를 따로 열고 Nginx를 여러 번 두는 방식이 점점 복잡해집니다. 이럴 때 Traefik을 리버스 프록시로 두면, 도메인 기반 라우팅, 자동 인증서 발급, 공통 보안 헤더 적용, IP 제한, 기본 방화벽 성격의 접근 통제를 훨씬 단순하게 구성할 수 있습니다.

  1. Traefik + Docker Compose 최적화 구성
  2. 여러 도메인을 운영할 때 nginx 컨테이너를 어떻게 가져갈지에 대한 전략

왜 Traefik을 쓰는가

Traefik은 Docker와 궁합이 매우 좋은 리버스 프록시입니다. 가장 큰 장점은 컨테이너 라벨만으로 라우팅을 자동화할 수 있다는 점입니다. 기존 방식에서는 다음과 같은 작업이 필요했습니다.

  • Nginx 설정 파일을 직접 수정
  • 서비스 추가 시 별도 서버 블록 생성
  • 인증서 발급 및 갱신 자동화
  • 서비스별 보안 헤더, 인증, 접근 통제 각각 관리

반면 Traefik은 다음과 같이 단순화할 수 있습니다.

  • Docker 라벨로 도메인 라우팅 정의
  • Let’s Encrypt와 연동하여 인증서 자동 발급
  • 공통 보안 정책을 dynamic config로 분리
  • 내부망 전용 서비스는 IP 화이트리스트로 제한
  • 대시보드도 별도 라우터로 보호 가능

즉, Traefik은 단순한 프록시가 아니라, 컨테이너 기반 서비스 운영의 접점 역할을 해주는 도구라고 볼 수 있습니다.

기본 디렉토리 구조

아래처럼 디렉토리를 분리하면 운영과 유지보수가 훨씬 편해집니다.

traefik/
├── docker-compose.yml
├── traefik.yml          # static config
├── config/
│   └── dynamic.yml      # dynamic config
└── letsencrypt/
    └── acme.json        # 자동 생성 (chmod 600 필수)

이 구조의 핵심은 설정을 역할별로 나누는 것입니다.

  • docker-compose.yml
    실행 단위와 컨테이너 구성을 담당
  • traefik.yml
    Traefik의 시작 시점에 읽는 고정 설정
  • dynamic.yml
    실행 중 변경 가능한 middleware, TLS 옵션 등
  • acme.json
    Let’s Encrypt 인증서 저장소

이렇게 나누면, 나중에 서비스가 많아져도 구조가 흐트러지지 않습니다.

Traefik의 설정 구조 이해하기

Traefik은 설정을 크게 두 종류로 나눕니다.

Static Config

Traefik이 시작될 때 읽는 고정 설정입니다.

여기에는 다음이 들어갑니다.

  • entryPoints
  • providers
  • log / accessLog
  • API / dashboard
  • certificatesResolvers

즉, Traefik의 뼈대를 만드는 설정입니다.

Dynamic Config

실행 중 바뀔 수 있는 설정입니다.

여기에는 다음이 들어갑니다.

  • middlewares
  • TLS options
  • routers / services 일부
  • file provider로 불러오는 설정

즉, 실제 요청 처리 정책을 담는 영역입니다.

traefik.yml의 역할과 의미

아래는 static config의 핵심 요소입니다.

global:
  checkNewVersion: false
  sendAnonymousUsage: false

api:
  dashboard: true
  insecure: false

log:
  level: INFO
  format: json

accessLog:
  format: json
  fields:
    headers:
      defaultMode: drop

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true

  websecure:
    address: ":443"
    http:
      tls:
        certResolver: letsencrypt
      middlewares:
        - securityHeaders@file

certificatesResolvers:
  letsencrypt:
    acme:
      email: your-email@example.com
      storage: /letsencrypt/acme.json
      httpChallenge:
        entryPoint: web

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    network: traefik-net
  file:
    filename: /config/dynamic.yml
    watch: true

global

global:
  checkNewVersion: false
  sendAnonymousUsage: false
  • checkNewVersion: false
    시작할 때 새 버전 확인을 하지 않습니다.
  • sendAnonymousUsage: false
    익명 사용 통계를 보내지 않습니다.

운영 환경에서는 불필요한 외부 통신을 최소화하는 관점에서 적절한 선택입니다.

api

api:
  dashboard: true
  insecure: false

대시보드를 사용하되, insecure: false로 설정해서 외부에 무방비로 노출되지 않도록 합니다.

중요한 점은 Traefik 대시보드는 그냥 켜는 것만으로는 부족하고,
반드시 라우터와 인증, 접근 통제를 함께 붙여야 한다는 것입니다.

log와 accessLog

log:
  level: INFO
  format: json

accessLog:
  format: json
  fields:
    headers:
      defaultMode: drop
  • INFO 수준은 운영에 무난합니다.
  • JSON 형식은 SIEM, ELK, Loki 같은 로그 수집 시스템과 연동하기 좋습니다.
  • access log에서 헤더를 기본적으로 제외하면 민감 정보 노출을 줄일 수 있습니다.

특히 운영 환경에서는 헤더에 토큰, 쿠키, 인증 정보가 섞일 수 있으므로, 로그에 무엇을 남길지 신중하게 결정해야 합니다.

entryPoints

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true

  websecure:
    address: ":443"

여기서 가장 중요한 부분은 webwebsecure로의 자동 리다이렉트입니다.

  • 사용자가 http://로 접속하면
  • Traefik이 자동으로 https://로 넘겨줍니다.

이 방식은 개별 서비스마다 리다이렉트를 중복 설정하지 않아도 되기 때문에 운영이 매우 단순해집니다.

certificatesResolvers

certificatesResolvers:
  letsencrypt:
    acme:
      email: your-email@example.com
      storage: /letsencrypt/acme.json
      httpChallenge:
        entryPoint: web

Let’s Encrypt 인증서를 자동 발급받는 설정입니다.

핵심은 다음입니다.

  • email: ACME 계정 식별용 이메일
  • storage: 인증서와 계정 정보를 저장할 파일
  • httpChallenge.entryPoint: web: 80번 포트를 이용한 도메인 검증

즉, Traefik이 HTTP Challenge를 받아 인증서를 자동으로 발급하고 갱신합니다.

300x250

운영에서는 acme.json 권한 관리가 매우 중요합니다. 반드시 chmod 600으로 보호해야 합니다.

providers

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    network: traefik-net
  file:
    filename: /config/dynamic.yml
    watch: true

이 부분이 Traefik 운영의 핵심입니다.

Docker provider

Docker 컨테이너의 라벨을 읽어서 자동으로 라우팅 규칙을 생성합니다.

  • exposedByDefault: false
    매우 중요합니다.
    라벨이 없는 컨테이너는 외부에 자동 노출되지 않습니다.
  • network: traefik-net
    Traefik이 어느 네트워크에서 서비스들을 볼지 고정합니다.
File provider

공통 middleware나 TLS 정책을 별도 파일로 관리합니다.

  • watch: true
    파일이 변경되면 Traefik이 자동 반영합니다.

이 구조를 사용하면 공통 정책과 서비스별 라우팅을 분리할 수 있어 유지보수가 훨씬 쉬워집니다.

dynamic.yml의 역할과 의미

이 파일은 보안 정책과 TLS 정책을 중앙집중식으로 관리하는 곳입니다.

http:
  middlewares:

    securityHeaders:
      headers:
        forceSTSHeader: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 31536000
        contentTypeNosniff: true
        browserXssFilter: true
        referrerPolicy: "strict-origin-when-cross-origin"
        customFrameOptionsValue: "SAMEORIGIN"
        contentSecurityPolicy: "default-src 'self'"

    rateLimit:
      rateLimit:
        average: 100
        period: 1s
        burst: 50

    dashboardAuth:
      basicAuth:
        users:
          - "admin:$apr1$xxxxxxxx$xxxxxxxxxxxxxxxxxxxxxxxxxx"

    internalOnly:
      ipAllowList:
        sourceRange:
          - "192.168.0.0/16"
          - "10.0.0.0/8"
          - "172.16.0.0/12"

tls:
  options:
    default:
      minVersion: VersionTLS12
      cipherSuites:
        - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
        - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
      sniStrict: true

securityHeaders

보안 헤더를 중앙에서 일괄 적용하는 설정입니다.

주요 의미는 다음과 같습니다.

  • stsSeconds HSTS 유지 시간
  • stsIncludeSubdomains 하위 도메인에도 HSTS 적용
  • stsPreload 브라우저 preload 목록 고려
  • contentTypeNosniff MIME sniffing 방지
  • browserXssFilter 일부 브라우저의 XSS 방어 보조
  • referrerPolicy Referer 정보 노출 범위 제어
  • customFrameOptionsValue 클릭재킹 방지
  • contentSecurityPolicy 콘텐츠 로딩 범위 제한

이 중 CSP는 서비스에 따라 조정이 필요합니다.
너무 강하게 잡으면 외부 CDN, 폰트, 분석 스크립트가 깨질 수 있습니다.

rateLimit

요청 폭주에 대한 기본 방어책입니다.

rateLimit:
  rateLimit:
    average: 100
    period: 1s
    burst: 50

이 설정은 초당 평균 100 요청, 순간적으로는 50개의 버스트를 허용하는 형태입니다.
정확한 값은 서비스 특성에 맞게 조정해야 합니다.

예를 들어
  • 관리자 페이지: 더 엄격하게
  • 공개 API: 서비스 상황에 맞게
  • 정적 사이트: 비교적 넉넉하게

dashboardAuth

dashboardAuth:
  basicAuth:
    users:
      - "admin:$apr1$xxxxxxxx$xxxxxxxxxxxxxxxxxxxxxxxxxx"

대시보드에 Basic Auth를 거는 방식입니다.

운영에서는 이 인증만 단독으로 두기보다는 반드시 IP 제한과 함께 쓰는 것이 좋습니다.

  • 외부에서 바로 열리는 것을 막고
  • 내부망 또는 VPN에서만 접근 가능하게 만들고
  • 그 위에 Basic Auth를 한 번 더 얹는 방식이 이상적입니다.

internalOnly

내부망 서비스 전용 접근 제한입니다.

internalOnly:
  ipAllowList:
    sourceRange:
      - "192.168.0.0/16"
      - "10.0.0.0/8"
      - "172.16.0.0/12"

이 설정은 특정 IP 대역에서만 접근하도록 제한합니다.

활용 예시는 다음과 같습니다.

  • 관리자 페이지
  • 사내 전용 도구
  • 운영자 전용 대시보드
  • 검증용 내부 API

단, 이 기능은 앞단에 다른 프록시나 로드밸런서가 있을 경우 실제 클라이언트 IP를 어떻게 해석할지 별도 검토가 필요합니다.

tls.options.default

TLS 보안 수준을 정의합니다.

tls:
  options:
    default:
      minVersion: VersionTLS12
      sniStrict: true

이 설정은 다음 의미를 가집니다.

  • TLS 1.2 이상만 허용
  • SNI가 맞지 않으면 거부

즉, 보안이 약한 프로토콜을 배제하고 인증서와 호스트 매칭도 엄격히 검증합니다.

docker-compose.yml의 구조와 의미

이제 실제 실행 정의를 보겠습니다.

version: "3.9"

networks:
  traefik-net:
    external: true

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - traefik-net
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yml:/traefik.yml:ro
      - ./config:/config:ro
      - ./letsencrypt:/letsencrypt
    environment:
      - TZ=Asia/Seoul
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=Host(`traefik.your-domain.com`)"
      - "traefik.http.routers.dashboard.entrypoints=websecure"
      - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.middlewares=dashboardAuth@file,internalOnly@file"

Traefik 컨테이너

Traefik 자체가 프록시 역할을 하므로 80/443 포트를 직접 바인딩합니다.

중요한 포인트는 다음입니다.

  • restart: unless-stopped
    장애나 재시작 상황에 대비
  • no-new-privileges:true
    권한 상승 차단
  • docker.sock:ro
    읽기 전용으로 마운트
  • traefik-net
    앱 컨테이너와 동일 네트워크 사용

즉, Traefik 컨테이너는 외부 트래픽의 관문입니다.

대시보드 라우팅

- "traefik.http.routers.dashboard.rule=Host(`traefik.your-domain.com`)"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=dashboardAuth@file,internalOnly@file"

이 부분은 매우 중요합니다.

대시보드는 기본적으로 노출되면 안 됩니다.
반드시 다음이 필요합니다.

  • HTTPS
  • 인증
  • IP 제한

즉, 운영자 전용 경로로만 접근하게 해야 합니다.

예시 앱 컨테이너 구조

앱 A: 일반 웹 서비스

app-a:
  image: nginx:alpine
  container_name: app-a
  restart: unless-stopped
  networks:
    - traefik-net
  expose:
    - "80"
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.app-a.rule=Host(`app.your-domain.com`)"
    - "traefik.http.routers.app-a.entrypoints=websecure"
    - "traefik.http.routers.app-a.tls.certresolver=letsencrypt"
    - "traefik.http.routers.app-a.middlewares=rateLimit@file,securityHeaders@file"
    - "traefik.http.services.app-a.loadbalancer.server.port=80"

이 방식의 핵심은

  • 외부 포트는 직접 열지 않고
  • Traefik만 앞단에서 받고
  • 앱은 내부 네트워크로만 연결합니다.

즉, 서비스는 살아 있지만 외부에서는 직접 접근할 수 없습니다.

앱 B: 내부망 전용 서비스

app-b:
  image: nginx:alpine
  container_name: app-b
  restart: unless-stopped
  networks:
    - traefik-net
  expose:
    - "80"
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.app-b.rule=Host(`internal.your-domain.com`)"
    - "traefik.http.routers.app-b.entrypoints=websecure"
    - "traefik.http.routers.app-b.tls.certresolver=letsencrypt"
    - "traefik.http.routers.app-b.middlewares=internalOnly@file"
    - "traefik.http.services.app-b.loadbalancer.server.port=80"

내부 서비스는 대개 관리자 페이지, 운영 도구, 사내용 웹앱에 적합합니다.

특히
  • IP 제한
  • 인증 추가
  • 외부 공개 금지

가 핵심입니다.

초기 세팅 절차

실제로 처음 구성할 때는 아래 순서로 진행하면 됩니다.

네트워크 생성

docker network create traefik-net

이 네트워크는 Traefik과 앱 컨테이너가 서로 통신하기 위한 공통 네트워크입니다.

ACME 파일 생성 및 권한 설정

mkdir -p letsencrypt
touch letsencrypt/acme.json
chmod 600 letsencrypt/acme.json

이 파일에는 인증서 정보가 저장됩니다.
권한이 너무 열려 있으면 보안상 문제가 되므로 반드시 600으로 관리해야 합니다.

Basic Auth 해시 생성

htpasswd -nb admin yourpassword

이 명령으로 생성된 해시를 dynamic.ymldashboardAuth.users에 넣습니다.

중요한 점은 평문 비밀번호를 넣는 것이 아니라 반드시 해시 형태로 저장해야 한다는 것입니다.

서비스 실행

docker-compose up -d

로그 확인

docker-compose logs -f traefik

처음에는 다음 항목을 집중적으로 확인해야 합니다.

  • ACME 인증서 발급 성공 여부
  • 라우터 생성 여부
  • 대시보드 접근 여부
  • 앱 컨테이너 라우팅 성공 여부

여러 도메인을 운영할 때 nginx 컨테이너는 어떻게 가져갈까

이 부분이 실제 운영에서 가장 많이 고민되는 지점입니다.
결론부터 말하면, nginx 컨테이너를 도메인별로 무조건 나눌 필요는 없습니다.

구조는 크게 3가지로 생각할 수 있습니다.

패턴 1. nginx 1개 + 도메인 여러 개

정적 사이트 운영에 적합

예를 들어 다음과 같은 경우입니다.

  • 회사 소개 사이트
  • 캠페인 페이지
  • 내부용 정적 안내 페이지

이럴 때는 nginx 컨테이너 1개로도 충분합니다.

services:
  nginx:
    image: nginx:alpine
    container_name: nginx
    networks:
      - traefik-net
    volumes:
      - ./sites:/usr/share/nginx/html
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    expose:
      - "80"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.site-a.rule=Host(`site-a.com`)"
      - "traefik.http.routers.site-a.entrypoints=websecure"
      - "traefik.http.routers.site-a.tls.certresolver=letsencrypt"
      - "traefik.http.services.site-a.loadbalancer.server.port=80"
      - "traefik.http.routers.site-b.rule=Host(`site-b.com`)"
      - "traefik.http.routers.site-b.entrypoints=websecure"
      - "traefik.http.routers.site-b.tls.certresolver=letsencrypt"
      - "traefik.http.services.site-b.loadbalancer.server.port=80"

이 방식의 장점은 다음과 같습니다.

  • 컨테이너 수가 적다
  • 운영이 단순하다
  • 정적 파일 배포가 쉽다

단점은 서비스가 커질수록 nginx 설정이 복잡해질 수 있다는 점입니다.

패턴 2. 도메인별 컨테이너 분리

서비스가 독립적일 때 권장

예를 들어 다음과 같은 경우입니다.

  • 쇼핑몰 프론트
  • 관리자 백오피스
  • 외부 공개 API
  • 내부 운영 도구

이 경우는 각 서비스가 완전히 분리되는 것이 좋습니다.

장점은 다음과 같습니다.

  • 배포 주기 분리 가능
  • 장애 영향 범위 축소
  • 기술 스택을 각각 다르게 가져갈 수 있음
  • 권한과 접근 정책을 서비스별로 다르게 적용 가능

실무적으로는 대규모 운영 환경에서 가장 권장되는 방식입니다.

패턴 3. Path 기반 라우팅

한 도메인 아래에서 경로별로 분기

예를 들면 아래와 같습니다.

  • example.com
  • example.com/api
  • example.com/admin

이 방식은 한 도메인에서 프론트와 백엔드를 함께 운영할 때 유용합니다.

예시
labels:
  - "traefik.http.routers.web.rule=Host(`example.com`)"
  - "traefik.http.services.web.loadbalancer.server.port=3000"

  - "traefik.http.routers.api.rule=Host(`example.com`) && PathPrefix(`/api`)"
  - "traefik.http.routers.api.service=api-svc"
  - "traefik.http.services.api-svc.loadbalancer.server.port=8000"

  - "traefik.http.routers.admin.rule=Host(`example.com`) && PathPrefix(`/admin`)"
  - "traefik.http.routers.admin.middlewares=internalOnly@file"
  - "traefik.http.services.admin-svc.loadbalancer.server.port=8080"
  - "traefik.http.routers.admin.priority=10"

이 구조는 편리하지만, 경로 충돌과 우선순위 문제를 조심해야 합니다.
특히 /api/가 동시에 걸릴 수 있으므로 priority를 명시하는 습관이 좋습니다.

어떤 구조를 선택해야 할까

아래처럼 생각하면 됩니다.

상황 추천 구조
정적 사이트 여러 개 nginx 1개 + 도메인 여러 개
서비스가 완전히 독립적 도메인별 컨테이너 분리
프론트/백엔드가 한 도메인 아래 있음 Path 기반 라우팅
팀별로 운영이 분리되어 있음 컨테이너 분리
장애 격리를 중요하게 봄 컨테이너 분리

실무에서는 서비스 성격이 독립적이면 분리, 정적 콘텐츠면 하나의 nginx로 묶는 방식이 가장 관리하기 좋습니다.

운영 환경에서 꼭 확인해야 할 보안 포인트

Traefik은 편리한 만큼, 초기 설계가 보안에 큰 영향을 줍니다.
따라서 아래 항목은 반드시 점검해야 합니다.

의도치 않은 노출 방지

  • exposedByDefault: false 유지
  • 라벨 없는 컨테이너는 외부 노출 금지

관리 인터페이스 보호

  • 대시보드는 HTTPS만 허용
  • Basic Auth 적용
  • IP 제한 추가
  • 가능하면 VPN 내부에서만 접근

인증서와 키 관리

  • acme.json는 백업 및 권한 관리 필수
  • 개발 환경과 운영 환경의 인증서 저장소 분리 권장

로그 정책

  • access log에 민감한 헤더를 남기지 않기
  • 필요 시 별도 SIEM 연계
  • 인증 실패와 라우팅 실패를 구분해서 확인

TLS 정책

  • 최소 TLS 1.2 이상
  • 가능하면 TLS 1.3 허용
  • 오래된 프로토콜 차단

IP 제한

  • 운영자 전용 서비스는 IP Allow List 적용
  • 프록시/LB가 앞단에 있으면 실제 클라이언트 IP 기준 재검토

실무 적용 시 자주 발생하는 실수

포트 불일치

컨테이너가 실제로 듣는 포트와 Traefik에 지정한 포트가 다르면 라우팅이 실패합니다.

예를 들어 nginx는 기본적으로 80을 듣는데 80이 아닌 8080으로 지정하면 서비스가 없다고 나옵니다.

라우터 이름 중복

라우터 이름과 서비스 이름은 유니크해야 합니다.

  • shop
  • admin
  • api

이런 식으로 명확히 구분하는 것이 좋습니다.

ACME 파일 권한 오류

acme.json 권한이 잘못되면 인증서 저장에 실패할 수 있습니다.

IP 제한 오동작

앞에 또 다른 프록시가 있으면 IP 제한이 예상과 다르게 동작할 수 있습니다.

CSP 과도 설정

보안 헤더를 너무 강하게 적용하면 사이트 기능이 깨질 수 있습니다.

정리

Traefik + Docker Compose 구성의 핵심은 다음 한 줄로 정리할 수 있습니다.

Traefik은 앞단에서 라우팅과 보안을 담당하고, 앱 컨테이너는 내부 네트워크에서만 서비스하게 만든다.

이 구조의 장점은 매우 분명합니다.

  • 서비스 추가가 쉬움
  • 도메인별 라우팅이 단순함
  • HTTPS 자동화 가능
  • 공통 보안 정책을 중앙에서 관리 가능
  • 내부 서비스와 외부 서비스를 명확히 분리 가능

특히 여러 도메인을 운영하는 환경에서는 nginx를 도메인마다 무조건 나눌 필요가 없고, 서비스 성격에 따라 nginx 1개 + 다도메인, 도메인별 분리, Path 기반 분기 중에서 선택하면 됩니다. 운영 관점에서 가장 중요한 것은 “편리함”보다 “통제 가능성”입니다. Traefik은 그 통제 가능성을 아주 깔끔하게 제공해 주는 도구입니다.

728x90
그리드형(광고전용)

댓글