// Docker · Nginx · Prometheus · Grafana · Rocky Linux · 모니터링
Rocky Linux 9.7 + Docker Compose로 웹 서비스 아키텍처와 Prometheus/Grafana 모니터링 스택 구축하기
프로젝트 개요
Rocky Linux 서버에 Docker 기반 인프라 환경을 구축하여, 실무에서 사용하는 웹 서비스 아키텍처와 모니터링 시스템을 구현한 랩 프로젝트다. 각 설정이 왜 필요한지, 어떤 파일을 어디에 만드는지까지 차근차근 짚고 넘어간다.
구성 목표
| Step 1 | Docker 설치 | Completed |
| Step 2 | Nginx 컨테이너 배포 | Completed |
| Step 3 | Nginx 리버스 프록시 + Python 백엔드 | Completed |
| Step 4 | Prometheus + Grafana 모니터링 | Completed |
Step 1. Docker 설치
별도로 만드는 파일 없음. 모두 명령어로만 진행된다.
1-1. Docker repo 추가
Rocky Linux에는 Docker 공식 전용 repo가 없다. Docker는 RHEL 계열에 CentOS repo를 공유하도록 가이드하고 있어서, Rocky도 같은 repo를 쓰면 된다. RPM 패키지 호환이 되기 때문이다.
# Rocky Linux 전용 repo가 없어서 CentOS repo 사용
$ dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
1-2. Docker 설치
docker-ce는 Docker 엔진 본체이고, containerd.io는 컨테이너 런타임, docker-compose-plugin은 Compose v2다. 네 개를 한 번에 설치해야 Docker가 정상 동작한다.
$ dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
1-3. 서비스 등록 및 docker 그룹 추가
start만 하면 서버 재부팅 후 Docker가 꺺진다. enableusermod
는 일반 계정이 sudo 없이 docker 명령어를 쓰기 위함이다. Docker 소켓은 root 권한이 필요해서 그룹 비소속 시 메듰 명령어마다 sudo를 붙여야 한다.
$ systemctl start docker && systemctl enable docker
$ usermod -aG docker admin
# 그룹 추가 후 로그아웃 → 재로그인 필요
⚠️
docker 그룹 소속 = 사실상 root 권한과 동일하다. 컨테이너를 통해 호스트 파일시스템 접근이 가능하기 때문이다. 운영 서버에서는 필요한 계정에만 부여하는 게 안전하다.
Step 2. 앱 스택 — Nginx 리버스 프록시 + Python 백엔드
이 스택은 /opt/docker-lab/ 디렉토리 안에 파일 5개를 만드는 구조다. 먼저 디렉토리를 만들고 각 파일을 여다.
# 작업 디렉토리 생성
$ mkdir -p /opt/docker-lab && cd /opt/docker-lab
만들어야 할 파일 5개
| docker-compose.yml | 컨테이너 2개(app, nginx)를 한번에 정의하고 실행하는 입문 파일 |
| Dockerfile | Python 백엔드를 컨테이너 이미지로 빌드하는 빌드 스크립트 |
| app.py | JSON을 반환하는 Python HTTP API 서버 |
| nginx.conf | Nginx가 어떤 경로를 백엔드로 전달할지 정의하는 프록시 설정 |
| index.html | / 경로로 접속했을 때 보이는 정적 HTML |
app.py 생성
백엔드 API 서버다. 0.0.0.0으로 바인딩하는 게 중요한데, localhost나 127.0.0.1로 바인딩하면 컨테이너 간 네트워크에서 Nginx가 백엔드를 찾지 못한다. 컨테이너 기반 환경에서 자주 하는 실수다.
# /opt/docker-lab/app.py
$ cat > app.py << 'EOF'
from http.server import HTTPServer, BaseHTTPRequestHandler
import json, datetime
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
data = {
"status": "ok", "message": "Hello from Backend!",
"server": "Rocky Linux 9.7",
"time": datetime.datetime.now().isoformat()
}
self.wfile.write(json.dumps(data).encode())
HTTPServer(('0.0.0.0', 8000), Handler).serve_forever()
EOF
Dockerfile 생성
app.py를 컨테이너 이미지로 폴캐는 뱌드 설계도다. python:3.11-slim을 쓰는 건 이미지 크기 때문이다. python:3.11 전체는 ~900MB인데 slim은 ~150MB 수준이다. CMD를 배열 형태로 쓰는 이유도 있다 — 셰 형태로 쓰면 SIGTERM 시그널이 Python 프로세스에 직접 전달되지 않아 컨테이너가 요바르게 종료되지 않는다.
# /opt/docker-lab/Dockerfile
$ cat > Dockerfile << 'EOF'
FROM python:3.11-slim
WORKDIR /app # COPY 시 기준가 됨. 경로 혼선 방지
COPY app.py . # 필요한 파일만 복사해서 이미지 크기 최소화
EXPOSE 8000 # 문서화 목적. 포트 오픈은 Compose ports에서 진행
CMD ["python", "app.py"]# exec form 사용. 셰 형태보다 시그널 처리 안정적
EOF
nginx.conf 생성
Nginx가 어떤 URL을 백엔드로 넘길지 정의하는 파일이다. 이 파일을 컨테이너 내부 /etc/nginx/conf.d/default.conf에 마운트할 것이라 파일명은 상관없지만 nginx.conf로 얼맞춰서 관리하기 쉽게 하는 게 일반적이다.
Proxy Headers가 왜 중요한가 — 프록시를 거치면 백엔드는 붙은 IP가 Nginx 컨테이너 IP로 보인다. 헤더를 넣지 않으면 백엔드 로그에 진짜 클라이언트 IP가 찍히지 않아 IP 차단이나 액세스 제어가 무력해진다.
# /opt/docker-lab/nginx.conf
$ cat > nginx.conf << 'EOF'
upstream backend {
server app:8000; # IP 대신 서비스명(app) 사용 → 컨테이너 IP 바뀐어도 자동 인식
}
server {
listen 80;
server_name _; # 와일드카드 → 어떤 도메인으로 접속해도 받겠다
location / { # /api/ 이외 요청은 정적 HTML 반환
root /usr/share/nginx/html;
index index.html;
}
location /api/ { # /api/로 시작하는 요청만 백엔드로 프록시
proxy_pass http://backend/;
proxy_set_header Host $host; # 원래 호스트명 전달
proxy_set_header X-Real-IP $remote_addr; # 진짜 클라이언트 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 프록시 책이 여러단일 때 IP 체인
proxy_set_header X-Forwarded-Proto $scheme; # HTTP/HTTPS 정보 (SSL 오프로드 환경에서 중요)
}
}
EOF
index.html 생성
Nginx가 / 경로를 요청받았을 때 반환할 정적 HTML이다. 로컬파일을 컨테이너 내부 /usr/share/nginx/html/index.html에 마운트하기 때문에, 이미지를 매번 빌드하지 않아도 수정사항이 바로 반영된다.
# /opt/docker-lab/index.html
$ cat > index.html << 'EOF'
<h1>Docker Lab - Frontend</h1>
<p><a href="/api/">Backend API 테스트</a></p>
EOF
docker-compose.yml 생성
위에서 만든 파일 4개를 결합해 컨테이너 2개를 하나의 스택으로 엮는 파일이다. depends_on: app은 app 컨테이너가 먼저 시작된 후 nginx가 시작되도록 순서를 보장한다. 없으면 nginx가 아직 떠있지 않은 백엔드로 프록시를 넣다가 에러가 발생할 수 있다.
# /opt/docker-lab/docker-compose.yml
$ cat > docker-compose.yml << 'EOF'
services:
app:
build: . # 현재 디렉토리의 Dockerfile로 빌드
container_name: backend-app
restart: unless-stopped # 크래시시 자동 재기동, 수동 stop하면 유지
nginx:
image: nginx:latest
container_name: nginx-proxy
ports:
- "8080:80" # 호스트 8080 → 컨테이너 80. 백엔드 포트는 열지 않음
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf # 이미지 안 구워도 설정 동적 반영
- ./index.html:/usr/share/nginx/html/index.html
depends_on:
- app # app이 먼저 떠있어야 nginx 시작
restart: unless-stopped
EOF
실행 및 확인
# 빌드 + 백그라운드 실행
$ docker compose up -d --build
# 백엔드 API 확인
$ curl http://localhost:8080/api/
{"status": "ok", "message": "Hello from Backend!", "server": "Rocky Linux 9.7", ...}
# 프론트엔드 확인
$ curl -o /dev/null -w '%{http_code}' http://localhost:8080/
200
Step 3. 모니터링 스택 — Prometheus + Grafana
별도 디렉토리 /opt/monitoring/에 파일 2개를 만드는 구조다. 앱 스택과 분리하는 이유는 하나 다운될 때 다른 스택에 영향을 안 부으려기 위함이다.
# 디렉토리 생성
$ mkdir -p /opt/monitoring && cd /opt/monitoring
만들어야 할 파일 2개
| prometheus.yml | Prometheus가 메트릭을 어디서, 얼마마다 수집할지 정의 |
| docker-compose.yml | Prometheus, Grafana, node-exporter, cAdvisor 4개 컨테이너 정의 |
prometheus.yml 생성
Prometheus는 Pull 방식이다. 수집 대상이 Prometheus에게 데이터를 밀어넣는 게 아니라, Prometheus가 수집 대상에게 직접 찾아가서 데이터를 가져온다. 이 파일에서 ‘어디에 찾아가서 수집할지’를 정의한다.
타겟을 IP가 아닌 서비스명으로 쓰는 이유가 있다 — Docker Compose는 서비스명으로 내부 DNS를 자동 생성하기 때문에 컨테이너을 재시작해도 IP가 바뀐어도 이름으로 찾아갈 수 있다. 한 가지 주의할 점은 cAdvisor 타겟에 cadvisor:8080을 쓰는데, 호스트 포트(8081)가 아니라 컨테이너 내부 포트(8080)를 써야 한다는 점이다.
# /opt/monitoring/prometheus.yml
$ cat > prometheus.yml << 'EOF'
global:
scrape_interval: 15s # 수집 주기. 짧으면 정밀하지만 수집 부하 상승
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090'] # Prometheus 자체 메트릭 수집
- job_name: 'node'
static_configs:
- targets: ['node-exporter:9100'] # 서비스명 DNS. 호스트 OS 메트릭
- job_name: 'docker'
static_configs:
- targets: ['cadvisor:8080'] # 호스트포트(8081)이 아니라 컨테이너 내부포트(8080)
EOF
docker-compose.yml 생성
node-exporter가 /proc, /sys를 마운트하는 이유가 있다. 컨테이너는 기본적으로 OS와 노드가 분리되어 있어서 내부에서 호스트 메트릭을 읽지 못한다. 호스트의 /proc과 /sys를 마운트해서 node-exporter가 호스트 OS 리소스를 읽도록 하는 게 핵심 구조다. 물론 :ro 플래그로 읽기 전용으로 마운트해서 실수로 혹시 쓰기가 발생하는 상황을 차단한다.
# /opt/monitoring/docker-compose.yml
$ cat > docker-compose.yml << 'EOF'
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
ports: ["9090:9090"]
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml # 위에서 만든 설정 마운트
- prometheus_data:/prometheus # 수집 데이터 영속화. 컨테이너 재시작해도 데이터 유지
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: grafana
ports: ["3000:3000"]
environment:
- GF_SECURITY_ADMIN_PASSWORD=password # 최초 관리자 비밀번호 설정 (첫 로그인 후 변경 권장)
volumes:
- grafana_data:/var/lib/grafana # 대시보드 설정 영속화. 재시작해도 만든 대시보드 유지
depends_on: [prometheus]
restart: unless-stopped
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
ports: ["9100:9100"]
volumes:
- /proc:/host/proc:ro # 호스트 프로세스 정보. :ro = 읽기전용으로 마운트해 실수 쉽기 의헄 차단
- /sys:/host/sys:ro # 호스트 커널/하드웨어 정보
- /:/rootfs:ro # 디스크 사용량 메트릭을 위해 호스트 루트 마운트
command:
- '--path.procfs=/host/proc' # 마운트한 경로를 node-exporter에 알려주는 옵션
- '--path.sysfs=/host/sys'
- '--path.rootfs=/rootfs'
restart: unless-stopped
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
ports: ["8081:8080"]
volumes:
- /:/rootfs:ro # 컨테이너 메트릭 수집을 위해 호스트 FS 마운트
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro # Docker 데이터를 읽어 컨테이너별 리소스 분석
restart: unless-stopped
volumes:
prometheus_data: # Docker가 관리하는 Named Volume. 컨테이너 삭제해도 데이터 보존
grafana_data:
EOF
실행 및 Grafana 연결
# 모니터링 스택 실행
$ docker compose up -d
실행 후 http://10.211.55.10:3000으로 접속하면 Grafana 로그인 화면이 나온다. Data Source 연결 시 여기서 가장 자주 하는 실수가 있다 — URL에 localhost:9090을 입력하면 안 된다. Grafana 컨테이너 자신의 localhost를 찾기 때문이다. 반드시 http://prometheus:9090으로 입력해야 한다.
Grafana 초기 설정 순서
| 1 | ID / password으로 로그인 |
| 2 | Connections → Data Sources → Add → Prometheus |
| 3 | URL: http://prometheus:9090 입력 (localhost X) |
| 4 | Save & Test → 연결 확인 |
| 5 | Dashboards → Import → ID 1860 입력 |
| 6 | 2~3분 대기 후 시간 범위 Last 15m 설정하면 메트릭 표시 |
관리 명령어 모음
# 전체 컨테이너 상태 확인
$ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
# 앱 스택
$ cd /opt/docker-lab && docker compose up -d --build
$ cd /opt/docker-lab && docker compose down
$ cd /opt/docker-lab && docker compose logs -f
# 모니터링 스택
$ cd /opt/monitoring && docker compose up -d
$ cd /opt/monitoring && docker compose down
# 리소스 사용량 확인
$ docker stats --no-stream
Tags:
#Docker
#Nginx
#Prometheus
#Grafana
#RockyLinux
#모니터링