
여러 도메인을 안전하고 깔끔하게 운영하는 실전형 구조 정리
Docker 환경에서 여러 서비스를 운영하다 보면, 각 컨테이너마다 포트를 따로 열고 Nginx를 여러 번 두는 방식이 점점 복잡해집니다. 이럴 때 Traefik을 리버스 프록시로 두면, 도메인 기반 라우팅, 자동 인증서 발급, 공통 보안 헤더 적용, IP 제한, 기본 방화벽 성격의 접근 통제를 훨씬 단순하게 구성할 수 있습니다.
- Traefik + Docker Compose 최적화 구성
- 여러 도메인을 운영할 때 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"
여기서 가장 중요한 부분은 web → websecure로의 자동 리다이렉트입니다.
즉
- 사용자가
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를 받아 인증서를 자동으로 발급하고 갱신합니다.
운영에서는 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
보안 헤더를 중앙에서 일괄 적용하는 설정입니다.
주요 의미는 다음과 같습니다.
stsSecondsHSTS 유지 시간stsIncludeSubdomains하위 도메인에도 HSTS 적용stsPreload브라우저 preload 목록 고려contentTypeNosniffMIME sniffing 방지browserXssFilter일부 브라우저의 XSS 방어 보조referrerPolicyReferer 정보 노출 범위 제어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.yml의 dashboardAuth.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.comexample.com/apiexample.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으로 지정하면 서비스가 없다고 나옵니다.
라우터 이름 중복
라우터 이름과 서비스 이름은 유니크해야 합니다.
예
shopadminapi
이런 식으로 명확히 구분하는 것이 좋습니다.
ACME 파일 권한 오류
acme.json 권한이 잘못되면 인증서 저장에 실패할 수 있습니다.
IP 제한 오동작
앞에 또 다른 프록시가 있으면 IP 제한이 예상과 다르게 동작할 수 있습니다.
CSP 과도 설정
보안 헤더를 너무 강하게 적용하면 사이트 기능이 깨질 수 있습니다.
정리
Traefik + Docker Compose 구성의 핵심은 다음 한 줄로 정리할 수 있습니다.
Traefik은 앞단에서 라우팅과 보안을 담당하고, 앱 컨테이너는 내부 네트워크에서만 서비스하게 만든다.
이 구조의 장점은 매우 분명합니다.
- 서비스 추가가 쉬움
- 도메인별 라우팅이 단순함
- HTTPS 자동화 가능
- 공통 보안 정책을 중앙에서 관리 가능
- 내부 서비스와 외부 서비스를 명확히 분리 가능
특히 여러 도메인을 운영하는 환경에서는 nginx를 도메인마다 무조건 나눌 필요가 없고, 서비스 성격에 따라 nginx 1개 + 다도메인, 도메인별 분리, Path 기반 분기 중에서 선택하면 됩니다. 운영 관점에서 가장 중요한 것은 “편리함”보다 “통제 가능성”입니다. Traefik은 그 통제 가능성을 아주 깔끔하게 제공해 주는 도구입니다.

댓글