Home Linux AWS Cloud Docker Python AI
클릭하여 터미널 활성화

Docker 기반 Loki + Promtail 로그 수집 구축 Prometheus 환경에 로그 시각화 얹기

// Nginx · HTTPS · SSL · 리버스 프록시 · Docker · Rocky Linux · 보안
Nginx 웹 서버에 자체 서명 SSL 인증서를 적용하고, Grafana와 Prometheus를 Nginx 리버스 프록시 뒤에 통합하여 단일 HTTPS 엔드포인트(443)로 모든 서비스에 접근할 수 있도록 구성했다.
📋 이 글에서 다루는 것
OpenSSL로 자체 서명 SSL 인증서를 생성하고 Nginx에 적용하는 방법
HTTP → HTTPS 301 리다이렉트와 TLS 1.2/1.3 보안 설정
Grafana와 Prometheus를 Nginx 리버스 프록시로 서브패스(/grafana/, /prometheus/) 통합
HSTS, X-Frame-Options 등 보안 헤더 설정과 방화벽 포트 최소화
Docker 크로스 네트워크 구성으로 분리된 스택 간 통신 해결
1. Overview — Before vs After

지금까지 모든 서비스가 각자 포트를 열고 HTTP로 통신했다. Grafana는 9300, Prometheus는 9090, 웹 앱은 8080. 포트를 외우고 다녀야 하고, 데이터는 평문으로 날아다닌다. 이번에 Nginx를 HTTPS 게이트웨이로 바꿔서, 443 포트 하나로 모든 서비스에 접근하고 나머지 포트는 방화벽에서 차단한다.

항목BeforeAfter
프론트엔드http://10.211.55.10:8080https://10.211.55.10/
APIhttp://10.211.55.10:8080/api/https://10.211.55.10/api/
Grafanahttp://10.211.55.10:9300https://10.211.55.10/grafana/
Prometheushttp://10.211.55.10:9090https://10.211.55.10/prometheus/
SSL없음NEW TLS 1.2/1.3 자체서명 인증서
HTTP 접속 시그냥 응답HTTPS로 301 리다이렉트
보안 헤더없음NEW HSTS, X-Frame-Options, XSS Protection
외부 노출 포트8080, 9090, 930080, 443만 (최소화)
Before — 방화벽
firewall-cmd
ports: 8080/tcp 3000/tcp 9090/tcp 9300/tcp
After — 방화벽
firewall-cmd
services: http ssh
ports: 443/tcp
rich rules:
  rule family="ipv4" port port="9100" protocol="tcp" reject
  rule family="ipv4" port port="8081" protocol="tcp" reject

2. Architecture
Before — 포트 분산 구조
Client ├── :8080 ──→ [Nginx] ──→ [Backend :8000] ├── :9300 ──→ [Grafana] └── :9090 ──→ [Prometheus] (전부 HTTP, 포트 분산)
After — 단일 HTTPS 엔드포인트
Client │ ├── :80 (HTTP) ──→ 301 Redirect ──→ :443 │ └── :443 (HTTPS + SSL) │ ├── / ──→ [Static HTML] 프론트엔드 ├── /api/ ──→ [Backend :8000] API 서버 ├── /grafana/ ──→ [Grafana :3000] 모니터링 대시보드 └── /prometheus/ ──→ [Prometheus :9090] 메트릭 쿼리 [SSL: TLS 1.2/1.3, Self-Signed Certificate] [Headers: HSTS, X-Frame-Options, X-XSS-Protection]
Docker Network 구성

Nginx는 웹 앱 스택(docker-lab_default)에 속하고, Grafana와 Prometheus는 모니터링 스택(monitoring_default)에 속한다. 서로 다른 Docker 네트워크라 기본적으로 통신이 안 된다. Nginx에 monitoring_default 네트워크를 external: true로 추가 연결해서 해결한다.

[docker-lab_default 네트워크] ├── nginx-proxy (80, 443) └── backend-app (8000) │ │ monitoring_default 네트워크 연결 (external)[monitoring_default 네트워크] ├── grafana (3000) ├── prometheus (9090) ├── node-exporter (9100) ├── cadvisor (8080) ├── loki (3100) └── promtail
💡 핵심 — Docker Compose는 스택별로 격리된 네트워크를 만든다. 다른 스택의 컨테이너에 접근하려면 external: true로 해당 네트워크를 연결해야 한다. 이걸 모르면 Nginx → Grafana 프록시에서 "502 Bad Gateway"가 뜬다.

3. Step 1 — SSL 인증서 생성

HTTPS를 적용하려면 SSL/TLS 인증서가 필요하다. 인증서는 "이 서버가 진짜 이 서버가 맞다"를 증명하는 디지털 신분증이다. 보통 CA(인증기관)에서 발급받지만, 내부 서버나 개발 환경에서는 직접 서명한 인증서(Self-Signed)를 만들어 쓴다. CA 서명이 없어서 브라우저가 "안전하지 않음" 경고를 띄우지만, 암호화 자체는 CA 서명 인증서와 완전히 동일하게 동작한다.

-nodes 옵션이 중요한데, 이걸 안 쓰면 개인키에 비밀번호가 걸려서 Nginx가 시작할 때마다 비밀번호를 입력해야 한다. 서버 재부팅이나 컨테이너 재시작 시 자동 복구가 안 되기 때문에 운영 환경에서는 보통 -nodes를 사용한다.

인증서 생성
$ sudo mkdir -p /opt/docker-lab/ssl

$ sudo openssl req -x509 -nodes -days 365 \
  -newkey rsa:2048 \
  -keyout /opt/docker-lab/ssl/server.key \
  -out /opt/docker-lab/ssl/server.crt \
  -subj '/C=KR/ST=Seoul/L=Seoul/O=EndofLinux/CN=rocky.local'
옵션 설명
옵션설명
-x509자체 서명 인증서 생성 (CSR 대신 바로 인증서)
-nodes개인키 암호화 없음 (서버 자동 시작용)
-days 365유효기간 1년
-newkey rsa:20482048비트 RSA 키 새로 생성
-keyout개인키 저장 경로
-out인증서 저장 경로
-subj인증서 정보 (국가/지역/조직/도메인)
생성 결과 확인
인증서 확인
$ sudo openssl x509 -in /opt/docker-lab/ssl/server.crt -noout -subject -dates

subject=C=KR, ST=Seoul, L=Seoul, O=EndofLinux, CN=rocky.local
notBefore=Mar  8 10:22:53 2026 GMT
notAfter=Mar  8 10:22:53 2027 GMT
파일 구조
/opt/docker-lab/ssl/
/opt/docker-lab/ssl/
├── server.crt    # 인증서 (공개키 포함)
└── server.key    # 개인키
자체 서명 vs Let's Encrypt
항목자체 서명 (Self-Signed)Let's Encrypt
비용무료무료
브라우저 신뢰❌ 경고 표시✅ 신뢰됨
발급 조건없음공인 도메인 + 외부 접근 필요
유효기간자유 설정90일 (자동 갱신)
용도내부/개발/테스트프로덕션

4. Step 2 — Nginx HTTPS 설정

이 파일은 /opt/docker-lab/nginx.conf이고, Nginx가 어떤 포트에서 듣고, 어떤 경로를 어디로 보낼지 정의하는 핵심 설정 파일이다. 컨테이너 내부 /etc/nginx/conf.d/default.conf에 마운트된다.

이번 변경의 핵심은 세 가지다. 첫째, HTTP 80번 서버 블록을 추가해서 모든 HTTP 요청을 HTTPS로 리다이렉트한다 — 사용자가 실수로 http://로 접속해도 자동으로 https://로 넘어가게 하는 것이다. 둘째, HTTPS 443번 서버 블록에 SSL 인증서와 보안 프로토콜을 설정한다. 셋째, 기존에 각자 포트를 열고 있던 Grafana(9300)와 Prometheus(9090)를 서브패스 프록시로 통합해서 443번 포트 하나로 접근할 수 있게 한다.

Before — HTTP only
nginx.conf (기존)
upstream backend {
    server app:8000;
}

server {
    listen 80;
    server_name _;

    location / {
        root /usr/share/nginx/html;
        index index.html;
    }

    location /api/ {
        proxy_pass http://backend/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
After — HTTPS + 리버스 프록시 통합
nginx.conf (최종)
upstream backend {
    server app:8000;
}

# HTTP → HTTPS 리다이렉트
server {
    listen 80;
    server_name _;
    return 301 https://$host$request_uri;
}

# HTTPS 서버
server {
    listen 443 ssl;
    server_name _;

    # SSL 인증서
    ssl_certificate     /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    # SSL 프로토콜 (TLS 1.0/1.1 비활성화)
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # 보안 헤더
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options SAMEORIGIN always;
    add_header X-XSS-Protection "1; mode=block" always;

    # 프론트엔드
    location / {
        root /usr/share/nginx/html;
        index index.html;
    }

    # API 리버스 프록시
    location /api/ {
        proxy_pass http://backend/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Grafana 리버스 프록시
    location /grafana/ {
        proxy_pass http://grafana:3000/grafana/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        # WebSocket 지원 (Grafana Live용)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # Prometheus 리버스 프록시
    location /prometheus/ {
        proxy_pass http://prometheus:9090/prometheus/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
SSL 프로토콜 설정 상세
설정설명
ssl_protocolsTLSv1.2 TLSv1.3TLS 1.0/1.1 비활성화 (취약)
ssl_ciphersECDHE-*강력한 암호화 스위트만 허용
ssl_prefer_server_cipherson서버 측 암호화 우선
ssl_session_cacheshared:SSL:10mSSL 세션 캐시 (성능 향상)
ssl_session_timeout10m세션 재사용 타임아웃
보안 헤더 설명
헤더설명
Strict-Transport-Securitymax-age=315360001년간 HTTPS만 사용 강제 (HSTS)
X-Content-Type-OptionsnosniffMIME 타입 스니핑 방지
X-Frame-OptionsSAMEORIGINiframe 삽입 방지 (클릭재킹 방어)
X-XSS-Protection1; mode=blockXSS 공격 감지 시 페이지 차단
리버스 프록시 헤더 설명
헤더설명
Host원본 호스트명 전달
X-Real-IP실제 클라이언트 IP 전달
X-Forwarded-For프록시 체인 IP 전달
X-Forwarded-Proto원본 프로토콜 (https) 전달
Upgrade / ConnectionWebSocket 지원 (Grafana Live용)
⚠️ Grafana WebSocket — Grafana Live 기능(실시간 대시보드 업데이트)은 WebSocket을 사용한다. proxy_http_version 1.1Upgrade/Connection 헤더가 없으면 Live 기능이 동작하지 않는다.

5. Step 3 — Docker Compose 수정

여기서 수정해야 하는 파일이 세 개다. 각 파일이 뭔지 먼저 짚고 넘어간다.

파일경로역할
docker-compose.yml (웹 앱)/opt/docker-lab/Nginx + Backend 컨테이너 정의. 포트와 네트워크 설정
docker-compose.yml (모니터링)/opt/monitoring/Prometheus + Grafana + Loki 등 8개 컨테이너 정의
prometheus.yml/opt/monitoring/Prometheus가 메트릭을 어디서 수집할지 정의하는 스크래핑 설정
docker-lab (웹 앱) — docker-compose.yml

이 파일은 Nginx와 Backend 앱을 정의한다. 핵심 변경은 세 가지다: 포트를 8080에서 표준 80/443으로 변경, SSL 인증서 디렉토리를 :ro(읽기 전용)로 마운트, 그리고 모니터링 스택 네트워크에 연결하는 것이다. 네트워크 연결이 왜 필요한가 — Docker Compose는 스택별로 격리된 네트워크를 만들기 때문에, Nginx가 다른 스택에 있는 Grafana나 Prometheus에 접근하려면 external: true로 해당 네트워크를 명시적으로 연결해야 한다.

/opt/docker-lab/docker-compose.yml (최종)
services:
  app:
    build: .
    container_name: backend-app
    networks:
      - default
    restart: unless-stopped

  nginx:
    image: nginx:latest
    container_name: nginx-proxy
    ports:
      - "80:80"            # HTTP → HTTPS 리다이렉트용
      - "443:443"          # HTTPS 메인 포트
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - ./index.html:/usr/share/nginx/html/index.html
      - ./ssl:/etc/nginx/ssl:ro     # SSL 인증서 (읽기 전용)
    depends_on:
      - app
    networks:
      - default
      - monitoring_default          # Grafana/Prometheus 접근용
    restart: unless-stopped

networks:
  monitoring_default:
    external: true                           # 모니터링 스택 네트워크 연결
변경 사항
변경설명
ports: 8080:8080:80, 443:443표준 HTTP/HTTPS 포트 사용
NEW ssl 볼륨./ssl:/etc/nginx/ssl:ro (읽기 전용)
NEW monitoring_default 네트워크Grafana/Prometheus 컨테이너 접근용 외부 네트워크
monitoring (모니터링 스택) — 변경 사항

이 파일은 /opt/monitoring/docker-compose.yml이다. Grafana와 Prometheus에 서브패스 설정을 추가한다. 왜 필요한가 — Nginx가 /grafana/ 경로를 Grafana로 프록시 해줘도, Grafana 자체가 "나는 / 루트에서 서비스되고 있다"고 생각하면 CSS/JS 리소스를 /public/에서 찾으려고 한다. 실제로는 /grafana/public/이어야 하니까 404가 뜬다. Grafana에게 "너는 /grafana/ 경로에서 서비스되고 있다"를 알려줘야 하는 것이다. Prometheus도 마찬가지다.

모니터링 스택 변경분
# Prometheus — 서브패스 설정 추가
  prometheus:
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--web.external-url=/prometheus/'
      - '--web.route-prefix=/prometheus/'

# Grafana — 서브패스 설정 추가
  grafana:
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=EofGrafana2026!
      - GF_SERVER_ROOT_URL=https://10.211.55.10/grafana/
      - GF_SERVER_SERVE_FROM_SUB_PATH=true
서브패스 설정 정리
서비스설정설명
Prometheus--web.external-url=/prometheus/외부 URL 경로 인식
Prometheus--web.route-prefix=/prometheus/내부 라우팅 프리픽스
GrafanaGF_SERVER_ROOT_URL서브패스 URL 설정
GrafanaGF_SERVER_SERVE_FROM_SUB_PATH=true서브패스 모드 활성화
Prometheus 설정 변경 — prometheus.yml

이 파일은 /opt/monitoring/prometheus.yml이고, Prometheus가 어떤 타겟에서 메트릭을 수집할지 정의하는 스크래핑 설정이다. 서브패스를 적용하면 Prometheus 자기 자신의 메트릭 엔드포인트도 경로가 바뀐다. 기본 /metrics/prometheus/metrics로 변경되기 때문에, 자체 수집 job의 metrics_path를 수정해야 한다. 안 하면 Prometheus가 자기 자신의 상태를 수집하지 못해서 해당 메트릭이 누락된다.

prometheus.yml 변경분
  - job_name: 'prometheus'
    metrics_path: '/prometheus/metrics'    # 서브패스 모드 메트릭 경로
    static_configs:
      - targets: ['localhost:9090']

6. Step 4 — 방화벽 설정

지금까지 Nginx가 HTTPS 게이트웨이 역할을 하도록 설정했다. 그런데 방화벽에서 기존 포트를 안 닫으면 의미가 없다. 누군가 http://10.211.55.10:9090으로 Prometheus에 직접 접근할 수 있으면, 리버스 프록시를 거치지 않고 SSL 없이 평문 통신이 가능하다. HTTPS로 통합한 의미가 사라진다. 따라서 80(리다이렉트용)과 443(HTTPS)만 열고 나머지는 전부 차단해야 한다.

방화벽 설정
# HTTPS 포트 개방
$ sudo firewall-cmd --permanent --add-port=443/tcp

# HTTP 서비스 허용 (80 → 리다이렉트용)
$ sudo firewall-cmd --permanent --add-service=http

# 기존 포트 제거
$ sudo firewall-cmd --permanent --remove-port=8080/tcp
$ sudo firewall-cmd --permanent --remove-port=9300/tcp
$ sudo firewall-cmd --permanent --remove-port=9090/tcp

# 적용
$ sudo firewall-cmd --reload
Before vs After
포트BeforeAfter
8080/tcp허용 (Nginx HTTP)제거
9090/tcp허용 (Prometheus)제거
9300/tcp허용 (Grafana)제거
80/tcp없음NEW HTTP → HTTPS 리다이렉트
443/tcp없음NEW HTTPS 전체 서비스
최종 방화벽 상태
firewall-cmd --list-all
public (active)
  services: dhcpv6-client http ssh
  ports: 443/tcp
  rich rules:
    rule family="ipv4" port port="9100" protocol="tcp" reject
    rule family="ipv4" port port="8081" protocol="tcp" reject

7. Step 5 — 서비스 재시작 및 검증
재시작
# 모니터링 스택 재시작
$ cd /opt/monitoring && sudo docker compose up -d --force-recreate

# 웹 앱 스택 재시작
$ cd /opt/docker-lab && sudo docker compose up -d --force-recreate
검증 결과
테스트
# HTTP → HTTPS 리다이렉트
$ curl -sk -o /dev/null -w '%{http_code} → %{redirect_url}' http://localhost
301 → https://localhost/

# HTTPS 프론트엔드
$ curl -sk -o /dev/null -w '%{http_code}' https://localhost
200

# API
$ curl -sk https://localhost/api/
{"status": "ok", "message": "Hello from Backend!", "server": "Rocky Linux 9.7"}

# Grafana (302 = 로그인 페이지 리다이렉트, 정상)
$ curl -sk -o /dev/null -w '%{http_code}' https://localhost/grafana/
302

# Prometheus (302 = graph 페이지 리다이렉트, 정상)
$ curl -sk -o /dev/null -w '%{http_code}' https://localhost/prometheus/
302

# SSL 인증서 확인
$ echo | openssl s_client -connect localhost:443 2>/dev/null | openssl x509 -noout -subject -issuer -dates
subject=C=KR, ST=Seoul, L=Seoul, O=EndofLinux, CN=rocky.local
issuer=C=KR, ST=Seoul, L=Seoul, O=EndofLinux, CN=rocky.local
notBefore=Mar  8 10:22:53 2026 GMT
notAfter=Mar  8 10:22:53 2027 GMT
✓ 전체 검증 통과 — HTTP→HTTPS 리다이렉트, 프론트엔드, API, Grafana, Prometheus 모두 정상. SSL 인증서도 정상 적용.

8. File Structure (최종)
전체 파일 구조
/opt/docker-lab/
├── docker-compose.yml        # 웹 앱 서비스 정의
├── Dockerfile                # Python 백엔드 이미지
├── app.py                    # Python API 서버
├── nginx.conf                # Nginx HTTPS + 리버스 프록시 설정
├── index.html                # 프론트엔드 페이지
└── ssl/                      # SSL 인증서              ← NEW
    ├── server.crt             # 인증서 (공개키)
    └── server.key             # 개인키

/opt/monitoring/
├── docker-compose.yml        # 모니터링 스택 정의
├── prometheus.yml            # Prometheus 설정
├── loki-config.yml           # Loki 설정
└── promtail-config.yml       # Promtail 설정

9. Service Endpoints (최종)
ServiceURL인증
Frontendhttps://10.211.55.10/-
Backend APIhttps://10.211.55.10/api/-
Grafanahttps://10.211.55.10/grafana/admin / EofGrafana2026!
Prometheushttps://10.211.55.10/prometheus/-

10. Container Summary (최종)
ContainerImageInternal Port접근 경로
nginx-proxynginx:latest80, 443/ (HTTPS 엔트리포인트)
backend-appdocker-lab-app8000/api/
grafanagrafana/grafana:11.4.03000/grafana/
prometheusprom/prometheus9090/prometheus/
node-exporterprom/node-exporter9100내부 전용
cadvisorcadvisor/cadvisor8080내부 전용
lokigrafana/loki:3.4.23100내부 전용
promtailgrafana/promtail:3.4.2-내부 전용

11. SSL/TLS 개념 정리
인증서 구조
[인증서 (server.crt)] ├── 공개키 (Public Key) ├── 주체 정보 (Subject: C=KR, O=EndofLinux, CN=rocky.local) ├── 발급자 (Issuer: 자체 서명이므로 Subject와 동일) ├── 유효기간 (2026-03-08 ~ 2027-03-08) └── 서명 (Signature) [개인키 (server.key)] └── 비밀키 (Private Key) — 서버만 보유
HTTPS 통신 과정
1. ClientServer: "HTTPS 연결 요청" 2. ServerClient: "인증서(공개키) 전달" 3. Client: 인증서 검증 (자체서명이라 브라우저 경고) 4. ClientServer: 공개키로 대칭키 암호화 전송 5. 이후: 대칭키로 암호화 통신
프로덕션 인증서 선택 가이드
환경인증서방법
개발/테스트자체 서명openssl req -x509
프로덕션Let's Encryptcertbot --nginx
기업상용 CADigiCert, Sectigo 등 구매

12. Troubleshooting
문제원인해결
브라우저 "안전하지 않음" 경고자체 서명 인증서 (CA 미신뢰)고급 → 계속 진행 (정상 동작)
Grafana 404서브패스 미설정GF_SERVER_SERVE_FROM_SUB_PATH=true
Prometheus 404서브패스 미설정--web.external-url=/prometheus/
Nginx → Grafana 연결 실패네트워크 격리monitoring_default 외부 네트워크 연결
HTTP 접속 시 페이지 안 뜸리다이렉트 설정 누락return 301 https://$host$request_uri
서브패스 적용 후 No dataData Source URL 미수정URL에 /prometheus 서브패스 추가

13. Post-Setup Issue — Grafana Dashboard No Data

HTTPS + 리버스 프록시 통합 작업을 전부 끝내고 Grafana에 접속했더니 대시보드에 No data가 떴다. 모든 패널이 비어 있었다. 서브패스를 적용할 때 빠뜨리기 쉬운 함정이 여기 있었다.

증상

Grafana 대시보드 접속 자체는 https://10.211.55.10/grafana/로 정상 동작하지만, CPU·메모리·디스크 등 모든 메트릭 패널이 No data를 표시했다.

원인

Prometheus에 --web.route-prefix=/prometheus/ 서브패스를 적용했으면, Prometheus의 API 경로가 /api/v1/query에서 /prometheus/api/v1/query로 바뀐다. 그런데 Grafana의 Prometheus Data Source URL은 기존 http://prometheus:9090 그대로 남아있었기 때문에, Grafana가 엉뚱한 경로로 메트릭을 요청하고 있었던 것이다.

항목Before (문제)After (해결)
Data Source URLhttp://prometheus:9090http://prometheus:9090/prometheus
메트릭 경로/api/v1/query (404)/prometheus/api/v1/query (200)
진단 과정

먼저 서버 시간이 맞는지 확인한다. 이전에 시간 동기화 문제가 있었기 때문에 습관적으로 체크하는 것이다. 시간이 맞으면 Data Source URL을 확인한다.

진단
# 1. 서버 시간 확인
$ date -u
Sun Mar  8 10:38:34 UTC 2026
# → 정상 (Mac과 동일)

# 2. Grafana Data Source URL 확인
$ curl -s -u 'admin:EofGrafana2026!' \
  http://localhost:9300/grafana/api/datasources \
  | python3 -c "import sys,json; [print(d['name'], d['url']) for d in json.load(sys.stdin)]"

Loki http://loki:3100
Prometheus http://prometheus:9090        # ← /prometheus 누락 발견!
❌ 원인 발견 — Prometheus URL에 서브패스 /prometheus가 빠져 있었다. Grafana가 http://prometheus:9090/api/v1/query로 요청하지만, Prometheus는 /prometheus/api/v1/query에서만 응답한다.
해결

Grafana API로 Data Source URL을 수정한다. 웹 UI에서도 할 수 있지만, 재현 가능한 명령어로 남겨두는 게 좋다.

Data Source URL 수정
# Data Source UID 확인
$ DS_UID=$(curl -s -u 'admin:EofGrafana2026!' \
  http://localhost:9300/grafana/api/datasources \
  | python3 -c "import sys,json; [print(d['uid']) for d in json.load(sys.stdin) if d['name']=='Prometheus']")

# URL 업데이트 (서브패스 추가)
$ curl -s -X PUT -u 'admin:EofGrafana2026!' \
  -H 'Content-Type: application/json' \
  http://localhost:9300/grafana/api/datasources/uid/$DS_UID \
  -d '{
    "name": "Prometheus",
    "type": "prometheus",
    "access": "proxy",
    "url": "http://prometheus:9090/prometheus",
    "isDefault": true
  }'
검증
검증
$ curl -s -u 'admin:EofGrafana2026!' \
  'http://localhost:9300/grafana/api/datasources/proxy/uid/efewvb6wfudq8e/api/v1/query?query=node_load1' \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['data']['result'][0]['value'][1])"

0.31
# → 정상 반환, 대시보드 데이터 복구 확인
✓ 해결 완료 — 메트릭 데이터가 정상적으로 반환되고, Grafana 대시보드의 모든 패널이 다시 표시된다.
교훈 — 서브패스 적용 시 수정해야 할 3곳

서비스에 서브패스(sub-path)를 적용할 때는 서비스 자체 + 리버스 프록시 + 연동 클라이언트 세 곳을 모두 수정해야 한다. 하나라도 빠지면 장애가 발생한다.

Prometheus 서브패스 적용 시 수정 필요한 곳: 1. Prometheus 자체 → --web.external-url=/prometheus/ → --web.route-prefix=/prometheus/ 2. Nginx 리버스 프록시 → proxy_pass http://prometheus:9090/prometheus/; 3. Grafana Data Source ← 이걸 빠뜨려서 No data 발생 → url: http://prometheus:9090/prometheus
⚠️ 핵심 — 리버스 프록시 뒤에 서비스를 숨길 때, 프록시 설정만 고치고 끝내면 안 된다. 해당 서비스를 참조하는 모든 클라이언트(Grafana, 모니터링 도구, CI/CD 등)의 URL도 같이 바꿔야 한다.

14. Management Commands
관리 명령어 모음
# SSL 인증서 만료일 확인
$ openssl x509 -in /opt/docker-lab/ssl/server.crt -noout -dates

# SSL 인증서 상세 정보
$ openssl x509 -in /opt/docker-lab/ssl/server.crt -noout -text

# Nginx 설정 테스트
$ sudo docker exec nginx-proxy nginx -t

# Nginx 리로드 (재시작 없이 설정 적용)
$ sudo docker exec nginx-proxy nginx -s reload

# HTTPS 연결 테스트
$ curl -sk https://10.211.55.10/
$ curl -sk https://10.211.55.10/api/
$ curl -sk https://10.211.55.10/grafana/
$ curl -sk https://10.211.55.10/prometheus/

# SSL 핸드셰이크 디버깅
$ openssl s_client -connect 10.211.55.10:443

# 인증서 갱신 (1년 후)
$ sudo openssl req -x509 -nodes -days 365 \
  -newkey rsa:2048 \
  -keyout /opt/docker-lab/ssl/server.key \
  -out /opt/docker-lab/ssl/server.crt \
  -subj '/C=KR/ST=Seoul/L=Seoul/O=EndofLinux/CN=rocky.local'
$ sudo docker exec nginx-proxy nginx -s reload

15. 자주 묻는 질문 (FAQ)
Q. 자체 서명 인증서는 실제로 암호화가 되나?
암호화는 정상 동작한다. 자체 서명과 CA 서명의 차이는 "신뢰 여부"이지 "암호화 여부"가 아니다. 브라우저가 경고를 띄우는 이유는 인증서 발급자가 신뢰할 수 있는 CA 목록에 없기 때문이지, 암호화가 안 되기 때문이 아니다. 내부 서버나 개발 환경에서는 자체 서명으로 충분하다.
Q. TLS 1.0/1.1을 왜 비활성화하나?
TLS 1.0은 BEAST, POODLE 등 알려진 취약점이 있고, TLS 1.1도 더 이상 안전하지 않다. PCI DSS 등 보안 규정에서도 TLS 1.2 이상을 요구한다. Nginx에서 ssl_protocols TLSv1.2 TLSv1.3;으로 설정하면 취약한 프로토콜을 차단할 수 있다.
Q. Grafana를 서브패스(/grafana/)로 운영할 때 주의할 점은?
반드시 GF_SERVER_ROOT_URLGF_SERVER_SERVE_FROM_SUB_PATH=true 두 가지를 모두 설정해야 한다. 하나만 설정하면 CSS/JS 리소스 경로가 꼬여서 로그인 화면은 뜨지만 대시보드가 깨진다. Nginx의 proxy_pass URL 끝에도 반드시 /grafana/를 붙여야 한다.
Q. Docker 크로스 네트워크는 왜 필요한가?
Docker Compose는 스택별로 격리된 네트워크를 생성한다. Nginx는 docker-lab_default, Grafana는 monitoring_default 네트워크에 속하기 때문에 기본적으로 서로 통신이 불가능하다. Nginx의 docker-compose.ymlmonitoring_default 네트워크를 external: true로 추가하면 두 네트워크를 동시에 연결할 수 있다.
Q. Let's Encrypt로 전환하려면 어떻게 하나?
공인 도메인이 있고 외부에서 80번 포트에 접근 가능하면, certbot --nginx 명령어로 자동 발급과 Nginx 설정 수정이 가능하다. Docker 환경에서는 certbot 컨테이너를 추가하거나, nginx-proxy + letsencrypt-companion 이미지를 사용하는 방법이 일반적이다.
Q. 서브패스 적용 후 Grafana에서 No data가 뜨면?
Prometheus에 서브패스를 적용했으면 Grafana의 Data Source URL도 같이 수정해야 한다. http://prometheus:9090http://prometheus:9090/prometheus로 변경한다. 서브패스 적용 시 서비스 자체, 리버스 프록시, 연동 클라이언트(Data Source) 세 곳을 모두 수정해야 한다.

📌 이 글 핵심 요약
OpenSSL로 자체 서명 SSL 인증서를 생성하고 Nginx에 적용하여 HTTPS를 구성했다
HTTP 접속은 301 리다이렉트로 HTTPS로 자동 전환되고, TLS 1.2/1.3만 허용한다
Grafana(/grafana/)와 Prometheus(/prometheus/)를 Nginx 리버스 프록시로 통합하여 443 포트 하나로 접근한다
HSTS, X-Frame-Options 등 보안 헤더를 추가하고, 방화벽에서 불필요한 포트를 차단했다
Docker 크로스 네트워크(external: true)로 분리된 스택 간 통신을 해결했다
서브패스 적용 시 서비스 자체 + 프록시 + 연동 클라이언트(Data Source) 세 곳 모두 수정 필수