Traefik v3로 Docker Compose 앱 5개 운영하기
하나의 IP와 443 포트에서 Traefik v3를 사용해 5개 앱을 운영하는 방법을 설명합니다. Host 규칙 설정과 Let's Encrypt 자동 인증서 발급은 물론, acme.json 파일 권한 문제로 컨테이너가 시작되지 않는 치명적인 오류 해결책을 포함합니다.
단일 IP, 5개의 애플리케이션, 1개의 443 포트
VPS에는 단일 공인 IPv4 주소와 단일 TCP 443 포트가 있습니다. 여기에 Gitea, 애플리케이션의 스테이징 복사본, 내부 대시보드, 상태 페이지, 웹훅 수신기를 모두 올리고 싶다면 5개의 호스트 이름을 하나의 서버에서 운영해야 합니다. 리버스 프록시는 :80 및 :443 포트를 점유하고 모든 요청의 Host 헤더를 읽어 적절한 컨테이너로 전달하는 프로세스입니다. Traefik은 이 역할을 수행하며, 사용자가 직접 certbot을 실행할 필요 없이 각 호스트 이름에 대한 인증서를 자동으로 발급하고 갱신합니다. Nginx와 Caddy 또한 동일한 5개의 호스트 이름을 완벽하게 처리할 수 있으므로, 아직 선택 전이라면 인증서 처리 방식과 애플리케이션별 설정 비용을 기준으로 세 가지 프록시를 비교해 보는 것이 좋습니다.
Traefik과 Nginx의 server {} 블록을 구분 짓는 핵심은 설정이 어디에서 오는가입니다. Nginx는 파일을 수정하고 리로드해야 하며, 인증서 수명 주기는 별도의 작업으로 남습니다. 이는 certbot을 사용하여 Nginx에서 Let's Encrypt 인증서를 발급할 때 따르는 워크플로우와 같으며, 갱신 타이머는 웹 서버 외부에서 별도로 동작합니다. 반면 Traefik의 Docker 프로바이더는 Docker 이벤트 스트림을 감시하고 컨테이너의 레이블(labels)을 읽습니다. Host() 규칙 레이블이 포함된 컨테이너를 시작하면 1초 이내에 라우팅이 가능해지고, 컨테이너를 중지하면 경로는 즉시 사라집니다. 이것이 바로 함정이기도 합니다. 설정이 5개의 컨테이너 레이블에 분산되어 존재하기 때문에, 레이블을 잘못 입력하면 오류 메시지 없이 컨테이너가 라우팅 대상에서 제외되며 Traefik은 아무런 경고도 하지 않습니다.
네 가지 핵심 개념
- Entrypoints는 수신 대기 중인 소켓입니다. 두 가지를 정의합니다.
web는:80에서,websecure는:443에서 대기합니다. - Routers는 요청(
Host(...))을 매칭하여 서비스에 연결합니다. 인증서는tls.certresolver을 통해 라우터 단위로 요청됩니다. - Services는 백엔드이며, Docker 네트워크 내부에서 컨테이너와 해당 컨테이너가 수신 대기하는 포트를 의미합니다.
- Middlewares는 라우터와 서비스 사이에 위치하며 기본 인증, IP 허용 목록, 헤더 재작성, 리다이렉트 등을 수행합니다.
이 네 가지 개념은 사용자가 직접 수동으로 작성해야 할 작업을 Traefik이 명명한 것입니다. 라우터는 server_name에 해당하고, 서비스는 proxy_pass 대상이며, 미들웨어는 nginx 리버스 프록시 서버 블록을 한 줄씩 작성할 때 직접 설정하는 헤더 및 인증 지시어와 같습니다.
정적 설정(entrypoints, providers, ACME)은 Traefik 명령줄이나 traefik.yml을 통해 전달되며, 이를 변경하려면 Traefik을 재시작해야 합니다. 동적 설정(routers, services, middlewares)은 컨테이너 레이블을 통해 전달되며 실시간으로 다시 불러옵니다. 이 두 가지를 혼동하는 것이 "설정 플래그가 작동하지 않는다"는 문제가 발생하는 주된 원인입니다.
Compose 파일
proxy라는 이름의 공유 Docker 네트워크가 중추 역할을 합니다. Traefik은 두 컨테이너가 모두 이 네트워크에 연결되어 있을 때만 통신할 수 있습니다.
name: edge
networks:
proxy:
name: proxy
services:
traefik:
image: traefik:v3.5
restart: unless-stopped
command:
- --providers.docker=true
- --providers.docker.exposedByDefault=false
- --providers.docker.network=proxy
- --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443
- --entryPoints.web.http.redirections.entryPoint.to=websecure
- --entryPoints.web.http.redirections.entryPoint.scheme=https
- --certificatesresolvers.le.acme.email=you@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.tlschallenge=true
# while you iterate, point at staging so a mistake costs nothing:
# - --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
- --api.dashboard=true
- --log.level=INFO
- --accesslog=true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)
- traefik.http.routers.dashboard.entrypoints=websecure
- traefik.http.routers.dashboard.tls.certresolver=le
- traefik.http.routers.dashboard.service=api@internal
- traefik.http.routers.dashboard.middlewares=dashboard-auth
- traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$REPLACE$$THIS
gitea:
image: gitea/gitea:1 # major-only pin keeps this demo copy-pasteable; pin an exact release in production
restart: unless-stopped
volumes:
- ./gitea:/data
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.gitea.rule=Host(`git.example.com`)
- traefik.http.routers.gitea.entrypoints=websecure
- traefik.http.routers.gitea.tls.certresolver=le
- traefik.http.services.gitea.loadbalancer.server.port=3000docker compose up -d를 먼저 설정하고, 이어서 docker compose logs -f traefik를 설정합니다. 추가되는 각 애플리케이션은 gitea 블록을 복사하여 고유한 라우터 이름, Host(), 내부 포트를 지정하면 됩니다. TLS와 백업을 포함한 Docker 기반 Nextcloud 설치도 같은 방식으로 구성할 수 있습니다. 컨테이너의 외부 포트 노출을 제거하고 proxy에 연결한 뒤, 라우터 레이블을 통해 호스트 이름과 인증서를 관리하십시오.
다음 다섯 가지 세부 사항은 반드시 숙지해야 합니다.
exposedByDefault=false 옵션을 사용하면 traefik.enable=true 레이블이 붙기 전까지 컨테이너가 Traefik에 노출되지 않습니다. 이 옵션을 생략하면 확인을 위해 잠시 실행한 postgres와 같은 일회성 컨테이너까지 모두 라우팅 대상이 됩니다.
providers.docker.network=proxy은 컨테이너가 여러 네트워크에 연결된 경우 Traefik이 사용할 네트워크를 지정합니다. 이를 생략하면 Traefik이 잘못된 컨테이너 IP를 선택할 수 있으며, 이 경우 애플리케이션 오류처럼 보이는 502 에러가 발생합니다.
loadbalancer.server.port=3000은 컨테이너 내부 포트입니다. 예를 들어 Gitea는 내부에서 3000번 포트를 사용합니다. 모든 애플리케이션 컨테이너는 포트를 외부로 노출하지 않으며, 오직 Traefik만 포트를 노출한다는 점에 유의하십시오.
web 엔트리포인트의 리다이렉트 설정은 일반 텍스트 요청을 HTTPS로 308 리다이렉트합니다. 80번 포트는 항상 열려 있어야 합니다. ACME HTTP 챌린지와 호스트 이름만 입력하는 사용자를 위해 필요하기 때문입니다.
basic-auth 해시에서 $$이 두 번 반복되는 것은 오타가 아니라 Compose의 이스케이프 처리입니다. htpasswd -nbB admin 'your-password'(apache2-utils 패키지)로 해시를 생성한 뒤, 모든 $을 두 번씩 입력하십시오.
인증서와 acme.json의 함정
tlschallenge=true은 TLS-ALPN-01을 선택합니다. Let's Encrypt가 443 포트로 서버에 접속하면 Traefik이 TLS 핸드셰이크 과정에서 챌린지에 응답합니다. 대안으로 80 포트를 사용하는 HTTP-01 방식이 있으며, Traefik의 command: 목록에서 tlschallenge 줄을 다음 두 줄로 교체하면 됩니다.
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web두 방식 모두 가능합니다. 공통적으로 호스트네임에 대한 공용 DNS가 이미 VPS를 가리키고 있어야 하며, 인증 기관이 외부에서 해당 도메인으로 접속할 수 있어야 합니다. 먼저 A(및 AAAA) 레코드를 생성하고 dig +short git.example.com으로 확인한 뒤 Traefik을 시작하십시오.
이제 많은 사용자가 저녁 시간을 허비하게 만드는 함정에 대해 설명합니다. Traefik은 ACME 계정 키와 발급된 모든 인증서를 하나의 acme.json 파일에 저장합니다. 이 파일이 그룹이나 다른 사용자에게 읽기 권한이 부여된 상태라면, Traefik은 다음과 유사한 메시지를 출력하고 종료됩니다.
error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600가장 깔끔한 해결책은 위에서 언급한 방식입니다. 디렉터리를 바인드 마운트하여 Traefik이 올바른 권한 모드로 직접 파일을 생성하게 하십시오. 만약 touch을 사용하여 acme.json를 직접 생성했다면, umask 설정에 의해 644 권한이 부여되었을 것입니다. 호스트에서 다음 명령으로 수정하십시오.
chmod 600 ./letsencrypt/acme.json
docker compose restart traefik해당 디렉터리는 애플리케이션 볼륨과 함께 백업하십시오. 파일을 잃어버려도 인증서를 재발급받으면 되지만, 5개의 호스트네임 인증서를 한꺼번에 재발급받으면 속도 제한(rate limit)에 걸릴 수 있습니다.
설정 반복 과정에서는 스테이징 CA를 사용하십시오. caserver 줄의 주석을 해제하여 모든 경로가 정상 작동하는지 확인한 뒤, 다시 주석 처리하고 acme.json를 삭제하여 운영용 인증서를 새로 요청하십시오. Let's Encrypt 운영 서버는 동일한 호스트네임 세트에 대해 주당 5개의 중복 인증서 발급만 허용하며, 동일한 이름에 대한 반복적인 검증 실패 시 속도를 제한합니다. 스테이징 서버는 신뢰할 수 없는 인증서를 발급하므로 브라우저에서 경고가 표시되는데, 이 경고가 바로 설정이 성공했다는 신호이며 제한 또한 훨씬 관대합니다.
대시보드는 제어 인터페이스이지 데모가 아닙니다
대부분의 퀵스타트 가이드는 --api.insecure=true 설정을 사용하는데, 이는 인증 없이 8080 포트로 대시보드를 노출합니다. 공인 IP가 할당된 서버에서 이 설정을 사용하면 스캔을 시도하는 누구에게나 라우팅 토폴로지, 호스트 이름, 미들웨어 이름, 백엔드 포트 정보가 그대로 노출됩니다.
위의 traefik 서비스에 적용된 레이블은 이에 대한 대안입니다. 대시보드를 다른 일반적인 애플리케이션처럼 실제 호스트 이름과 TLS를 사용하여 basicauth 뒤에서 라우팅하는 방식입니다. service=api@internal는 라우터와 Traefik의 내장 API를 연결하는 역할을 합니다. 여기에 왼쪽에서 오른쪽 순으로 적용되는 IP 허용 목록을 추가하여 보안을 더욱 강화하십시오. 사무실 IP 주소가 유동적이라면, 동일한 VPS에 직접 호스팅하는 WireGuard VPN에서 할당하는 서브넷 범위로 설정하고 터널을 통해서만 대시보드에 접근하도록 구성하십시오.
- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-auth다섯 개 이상의 애플리케이션이 각자의 계정을 필요로 하게 되면 하나의 basicauth 비밀번호를 공유하는 방식은 더 이상 방어 수단이 될 수 없습니다. 동일한 미들웨어 슬롯에 forwardauth을 적용하면 인증 권한을 직접 호스팅하는 싱글 사인온 서버인 Authentik으로 위임할 수 있습니다. 이렇게 하면 대시보드를 포함한 모든 경로가 하나의 로그인 절차 뒤에 배치되며, 단일 지점에서 접근 권한을 즉시 취소할 수 있습니다.
Docker 소켓은 root 권한과 같습니다
/var/run/docker.sock는 호스트의 /을 마운트하는 컨테이너를 생성할 수 있는 API입니다. 이 API에 접근하는 것은 해당 머신의 root 권한을 갖는 것과 같으며, Traefik은 레이블을 읽기 위해 이 권한이 필요합니다.
마운트 시 :ro를 유지하되, 이것이 무엇을 의미하는지 명확히 이해해야 합니다. 이는 소켓 파일을 읽기 전용으로 만들 뿐, 소켓을 통해 전달되는 POST 요청을 막지는 못합니다. 진정한 완화책은 Traefik에 직접 소켓을 넘기지 않고, 그 사이에 필터링 프록시를 두는 것입니다.
dockerproxy:
image: tecnativa/docker-socket-proxy # pin the current tag
restart: unless-stopped
environment:
CONTAINERS: 1
NETWORKS: 1
POST: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxyTraefik에서 소켓 볼륨을 제거하고 프로바이더가 프록시를 가리키도록 설정하십시오.
--providers.docker.endpoint=tcp://dockerproxy:2375Traefik은 컨테이너와 네트워크에 대한 읽기 권한은 유지하면서, 무언가를 생성할 수 있는 능력은 상실하게 됩니다.
방화벽, 포트, 그리고 흔히 잘못 설정하는 규칙
두 개의 포트와 SSH를 개방합니다:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableDocker의 게시된 포트는 ufw를 우회합니다. Docker는 자체 iptables 규칙을 삽입하며, 이는 ufw 체인보다 먼저 평가됩니다. 따라서 ports: ["3000:3000"]으로 시작된 컨테이너는 ufw에서 거부(deny) 규칙을 설정했더라도 인터넷에서 접근할 수 있습니다. 이에 대한 방어는 방화벽 설정이 아닌 구조적인 접근이 필요합니다. Traefik을 통해서만 포트를 게시하고, 다른 모든 컨테이너에는 networks: [proxy]만 부여하십시오. 호스트에 반드시 접근해야 하는 서비스가 있다면, "127.0.0.1:3000:3000"을 사용하여 루프백(loopback)에 바인딩하십시오.
문제 해결: 실제로 마주하게 될 오류
404 page not found, Traefik이 반환하는 오류입니다. 일치하는 라우터가 없습니다. 발생 가능성이 높은 순서대로 나열하면 다음과 같습니다. 컨테이너에 traefik.enable=true가 누락되었거나(exposedByDefault=false 설정 필요), Host() 규칙이 입력한 이름과 일치하지 않는 경우, 또는 라벨 간의 라우터 이름이 서로 다른 경우(routers.gitea.rule와 routers.gitea.entrypoints은 동일한 단어여야 합니다)입니다. 혹은 호스트 이름을 백틱(`) 대신 따옴표로 감쌌을 수 있습니다. Traefik v3의 매처 내부에는 반드시 백틱을 사용해야 합니다.
502 Bad Gateway. 라우터는 일치했으나 백엔드에 도달할 수 없는 상태입니다. 거의 대부분 컨테이너가 proxy 네트워크에 연결되지 않은 경우이므로 docker inspect -f '{{json .NetworkSettings.Networks}}' gitea를 확인하십시오. 다른 원인으로는 잘못된 loadbalancer.server.port 설정이 있습니다. 퍼블리시된 포트를 지정했거나, 애플리케이션이 다른 포트에서 대기 중일 수 있습니다. 로그에 시도 내용이 기록됩니다: dial tcp 172.18.0.5:8080: connect: connection refused.
브라우저 경고가 발생하며, 인증서가 TRAEFIK DEFAULT CERT로 발급되었습니다. 해당 호스트 이름에 대한 인증서가 존재하지 않아 Traefik이 자체 서명된 기본 인증서를 제공한 것입니다. ACME 관련 로그를 확인하십시오:
unable to obtain ACME certificate for domains "git.example.com" ...
acme: error: 400 ... DNS problem: NXDOMAIN looking up A for git.example.comDNS가 아직 해당 서버를 가리키고 있지 않습니다. 레코드를 수정하고 TTL이 만료될 때까지 기다린 뒤 Traefik을 재시작하십시오.
Invalid response from http://git.example.com/.well-known/acme-challenge/... (HTTP 챌린지 관련): 외부에서 80번 포트로 Traefik에 접근할 수 없는 상태입니다. 보통 ufw가 아닌 VPS 앞단의 제공자 수준 방화벽이 원인입니다.
인증서가 발급되지 않으며, Cloudflare의 오렌지색 구름(프록시)이 활성화된 경우. Cloudflare가 엣지에서 TLS를 종료하므로 TLS-ALPN-01 챌린지가 통과할 수 없습니다. 발급 과정 중에는 레코드를 DNS-only로 변경하거나, API 토큰을 사용하여 DNS-01 챌린지로 전환하십시오. 와일드카드 인증서를 발급할 수 있는 유일한 방법은 DNS-01 챌린지뿐입니다.
리다이렉트 루프. Traefik 앞단에서 이미 TLS를 종료하고 평문(plaintext)을 :80으로 전달하고 있습니다. 이때 엔트리포인트 리다이렉트 설정이 다시 HTTPS로 요청을 보내면서 루프가 발생합니다. 두 리다이렉트 설정 중 하나를 제거하십시오.
서비스 유지 관리
Docker 유닛은 부팅 시 활성화되어야 하며(systemctl is-enabled docker), restart: unless-stopped 설정을 통해 재부팅 후에도 스택이 자동으로 복구되도록 합니다. 명시적인 제어를 원한다면 docker compose -f /srv/edge/compose.yml up -d를 RemainAfterExit=yes 옵션과 함께 실행하는 작은 systemd 유닛을 작성하여 systemctl status edge 및 실행 순서를 제어할 수 있습니다.
Traefik 태그는 특정 버전으로 고정하십시오(traefik:v3.5, latest 사용 금지). v2에서 v3로의 업그레이드 과정에서 규칙 문법과 공급자(provider) 명칭이 변경되었으므로, 자동 업데이트(latest)가 활성화되어 있으면 이해할 수 없는 설정 파일을 불러와 서비스가 중단될 수 있습니다. 업그레이드는 신중하게 진행하십시오. 마이그레이션 노트를 읽고, 태그를 변경한 뒤, docker compose up -d traefik 명령을 실행하고, 로그를 모니터링해야 합니다. 여전히 v2 태그를 사용 중이라면 Traefik v2에서 v3로의 마이그레이션 가이드를 통해 모든 명칭 변경 사항, 호환 모드, 그리고 인증서를 유지하며 롤백하는 방법을 확인할 수 있습니다.
./letsencrypt 파일과 각 애플리케이션의 데이터 볼륨을 백업하십시오. Traefik은 compose 파일에서 다시 생성할 수 없는 별도의 상태 정보를 저장하지 않습니다.
규모가 커질 때 발생하는 문제
첫 번째 한계는 처리량이 아니라 단일 서버 구성입니다. VPS 한 대에서 실행되는 Traefik 하나는 5개 애플리케이션의 단일 장애 지점이 됩니다. 또한 acme.json은 플랫 파일 저장소를 사용하므로, 두 개의 Traefik 인스턴스가 동시에 쓰기를 시도하면 데이터가 손상됩니다. 규모를 확장하려면 인증서 저장소를 파일 외부로 옮기거나, 다른 곳에서 TLS를 종료해야 합니다.
두 번째는 장기 연결 문제입니다. Server-sent events, 대용량 업로드, 느린 클라이언트는 엔트리포인트의 응답 타임아웃 제한에 걸릴 수 있습니다. 이때는 --entryPoints.websecure.transport.respondingTimeouts.readTimeout과 그 하위 설정인 writeTimeout, idleTimeout을 조정해야 합니다. WebSockets는 별도의 추가 설정 없이 그대로 통과합니다.
세 번째는 디스크 문제입니다. --accesslog=true는 표준 출력(stdout)으로 로그를 기록하는데, Docker의 json-file 드라이버는 제한을 두지 않으면 이를 영구적으로 보관합니다. Traefik 서비스에 logging.options.max-size를 설정하거나, 접근 로그를 별도 파일로 기록하고 로그 로테이션을 적용하십시오.
이 모든 과정에 오케스트레이터는 필요하지 않습니다. 다만 실제 IP를 보유하고 80번과 443번 포트가 외부로 열려 있는, 직접 제어 가능한 서버가 필요할 뿐입니다. 작은 VPS 한 대가 전체 의존성 목록의 전부입니다.
FAQ
Traefik을 사용해도 certbot이 필요한가요?
아니요. Traefik의 ACME 리졸버가 라우팅하는 모든 호스트네임에 대해 인증서를 요청하고 갱신하며, 이를 acme.json에 저장합니다. Certbot은 nginx나 다른 서버가 직접 TLS를 종료할 때 사용하는 적절한 도구입니다. 동일한 호스트네임에 두 도구를 모두 실행하면 Let's Encrypt의 속도 제한(rate limit)만 소진하게 됩니다.
컨테이너가 Traefik을 통해 404 오류를 반환하는 이유는 무엇인가요?
Traefik이 반환하는 404는 요청과 일치하는 라우터가 전혀 없음을 의미합니다. 컨테이너에 traefik.enable=true이 설정되어 있는지(exposedByDefault=false이 설정된 경우 필수), Host() 값이 입력한 이름과 일치하는지, 그리고 해당 앱의 모든 레이블에서 라우터 이름이 동일한지 확인하십시오. Traefik v3는 매처(matcher) 내부에서 따옴표 대신 백틱(backtick)을 사용해야 합니다.
여기서 404와 502의 차이는 무엇인가요?
404는 라우팅이 전혀 이루어지지 않았음을 의미하며, 502는 라우터는 일치했으나 백엔드가 연결을 거부했음을 의미합니다. 502 오류의 일반적인 원인은 컨테이너가 proxy 네트워크에 연결되지 않았거나, loadbalancer.server.port이 컨테이너 내부에서 앱이 수신 대기하는 포트가 아닌 외부로 노출된 포트를 가리키고 있는 경우입니다. 접근 로그(access log)에는 Traefik이 연결을 시도한 정확한 주소가 기록됩니다.
Docker 소켓을 읽기 전용으로 마운트하는 것만으로 충분한가요?
:ro 플래그는 소켓 파일 자체를 읽기 전용으로 만들 뿐 그 뒤의 API까지 제한하지는 않습니다. POST 요청은 여전히 소켓을 통해 전달될 수 있으며, Docker API에 대한 접근 권한은 호스트의 root 권한과 동일합니다. 더 안전한 구성은 위에서 설명한 docker-socket-proxy 컨테이너를 사용하는 것이며, 이는 Traefik에 컨테이너 및 네트워크 읽기 권한만 노출하고 쓰기 작업은 완전히 차단합니다.
Traefik으로 와일드카드 인증서를 발급할 수 있나요?
DNS 제공업체의 API 토큰을 사용하는 DNS-01 챌린지를 통해서만 가능합니다. TLS-ALPN-01과 HTTP-01은 각각 단일 호스트네임만 검증하므로 와일드카드 인증서를 생성할 수 없습니다. 또한 Cloudflare와 같은 CDN이 VPS 앞단에서 TLS를 종료하여 다른 두 챌린지가 완료되지 않는 경우에도 DNS-01이 해결책입니다.