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

Rocky Linux에 Docker로 웹 서비스 + 모니터링 구축하기 | Nginx · Prometheus · Grafana 완전 가이드

// Docker · Nginx · Prometheus · Grafana · Rocky Linux · 모니터링
Rocky Linux 9.7 + Docker Compose로 웹 서비스 아키텍처와 Prometheus/Grafana 모니터링 스택 구축하기

프로젝트 개요

Rocky Linux 서버에 Docker 기반 인프라 환경을 구축하여, 실무에서 사용하는 웹 서비스 아키텍처와 모니터링 시스템을 구현한 랩 프로젝트다. 각 설정이 왜 필요한지, 어떤 파일을 어디에 만드는지까지 차근차근 짚고 넘어간다.

구성 목표
Step 1Docker 설치Completed
Step 2Nginx 컨테이너 배포Completed
Step 3Nginx 리버스 프록시 + Python 백엔드Completed
Step 4Prometheus + 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)를 한번에 정의하고 실행하는 입문 파일
DockerfilePython 백엔드를 컨테이너 이미지로 빌드하는 빌드 스크립트
app.pyJSON을 반환하는 Python HTTP API 서버
nginx.confNginx가 어떤 경로를 백엔드로 전달할지 정의하는 프록시 설정
index.html/ 경로로 접속했을 때 보이는 정적 HTML

 app.py 생성

백엔드 API 서버다. 0.0.0.0으로 바인딩하는 게 중요한데, localhost127.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.ymlPrometheus가 메트릭을 어디서, 얼마마다 수집할지 정의
docker-compose.ymlPrometheus, 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 초기 설정 순서
1ID / password으로 로그인
2Connections → Data Sources → Add → Prometheus
3URL: http://prometheus:9090 입력 (localhost X)
4Save & Test → 연결 확인
5Dashboards → Import → ID 1860 입력
62~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 #모니터링