Ghost 블로그를 Docker로 설치하고 HTTPS 적용하기

Cloudflare, Nginx Proxy Manager, Ghost를 Docker로 구성하여 프로덕션급 블로그 인프라를 무료로 구축하는 방법을 설명합니다.

1. 아키텍처 개요

이번 글에서 구축할 인프라 구조입니다:

[사용자]
    ↓ HTTPS
[Cloudflare CDN] (Edge SSL)
    ↓ HTTPS
[Nginx Proxy Manager] (Origin SSL)
    ↓ HTTP (내부)
[Ghost Container] ← [MySQL Container]

구성 요소:

컴포넌트 역할 포트
Cloudflare CDN, DDoS 방어, Edge SSL -
Nginx Proxy Manager 리버스 프록시, Origin SSL 80, 443, 81
Ghost 블로그 애플리케이션 2368
MySQL Ghost 데이터베이스 3306 (내부)

2. 사전 준비

이 가이드를 따라하려면 다음이 필요합니다:

  • [x] Oracle Cloud 서버 (1편 참고)
  • [x] Docker 설치 완료
  • [x] 도메인 (예: example.com)
  • [x] Cloudflare 계정

3. Cloudflare 도메인 설정

3.1 도메인 추가

  1. Cloudflare 대시보드 접속
  2. Add a Site → 도메인 입력
  3. Free Plan 선택
  4. 네임서버를 Cloudflare로 변경 (도메인 등록기관에서)

3.2 DNS 레코드 설정

DNSRecordsAdd Record

Type: A
Name: @
Content: <서버 IP>
Proxy status: Proxied (주황 구름)
TTL: Auto

Type: A
Name: blog
Content: <서버 IP>
Proxy status: Proxied (주황 구름)
TTL: Auto

와일드카드 설정 (선택):

Type: A
Name: *
Content: <서버 IP>
Proxy status: Proxied (주황 구름)

팁: 와일드카드(*)를 설정하면 나중에 서브도메인 추가 시 DNS 설정 없이 바로 사용 가능합니다.

3.3 SSL/TLS 모드 설정

SSL/TLSOverview

모드: Full (Strict) ← 선택
모드 설명 보안
Off SSL 없음
Flexible Cloudflare↔사용자만 HTTPS ⚠️
Full 서버에 자체서명 인증서 허용 ⚠️
Full (Strict) 서버에 유효한 인증서 필수

4. Origin CA 인증서 발급

Cloudflare Origin CA는 Cloudflare ↔ 서버 간 암호화에 사용되는 무료 인증서입니다. 최대 15년 유효합니다.

4.1 인증서 생성

SSL/TLSOrigin ServerCreate Certificate

Private key and CSR: Generate with Cloudflare
Private key type: RSA (2048)
Hostnames:
  - example.com
  - *.example.com  # ← 와일드카드 포함
Certificate Validity: 15 years

4.2 인증서 저장

Create 클릭 후 두 개의 텍스트가 표시됩니다:

  1. Origin Certificateexample.com.pem
  2. Private Keyexample.com.key

중요: Private Key는 이 화면에서만 확인 가능합니다. 반드시 안전한 곳에 저장하세요!

4.3 서버에 인증서 업로드

# SSH 접속
ssh -i ~/.ssh/oracle-server-key ubuntu@<SERVER_IP>

# 인증서 디렉토리 생성
sudo mkdir -p /etc/ssl/cloudflare

# 인증서 저장 (nano 에디터 사용)
sudo nano /etc/ssl/cloudflare/example.com.pem
# 내용 붙여넣기 → Ctrl+O → Enter → Ctrl+X

sudo nano /etc/ssl/cloudflare/example.com.key
# 내용 붙여넣기 → Ctrl+O → Enter → Ctrl+X

# 개인키 권한 설정 (필수!)
sudo chmod 600 /etc/ssl/cloudflare/example.com.key

# 확인
ls -la /etc/ssl/cloudflare/

출력 예시:

-rw-r--r-- 1 root root 2156 Jan 18 10:00 example.com.pem
-rw------- 1 root root 1705 Jan 18 10:00 example.com.key

5. Nginx Proxy Manager 설치

Nginx Proxy Manager(NPM)는 웹 GUI로 리버스 프록시를 관리할 수 있는 도구입니다.

5.1 디렉토리 생성

mkdir -p ~/docker/nginx-proxy-manager
cd ~/docker/nginx-proxy-manager

5.2 docker-compose.yml 작성

cat > docker-compose.yml << 'EOF'
services:
  npm:
    image: jc21/nginx-proxy-manager:latest
    container_name: nginx-proxy-manager
    restart: unless-stopped
    ports:
      - "80:80"    # HTTP
      - "443:443"  # HTTPS
      - "81:81"    # 관리 페이지 (외부 노출 X)
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
      - /etc/ssl/cloudflare:/etc/ssl/cloudflare:ro  # ← Origin CA 인증서
    environment:
      - TZ=Asia/Seoul
EOF

5.3 컨테이너 실행

docker compose up -d

# 상태 확인
docker ps

출력 예시:

CONTAINER ID   IMAGE                             STATUS          PORTS
abc123def456   jc21/nginx-proxy-manager:latest   Up 10 seconds   0.0.0.0:80-81->80-81/tcp, 0.0.0.0:443->443/tcp

5.4 관리 페이지 접속 (SSH 터널)

81번 포트는 보안상 외부에 열지 않습니다. SSH 터널로 접속합니다.

# 로컬에서 실행
ssh -i ~/.ssh/oracle-server-key -L 8081:localhost:81 ubuntu@<SERVER_IP>

브라우저에서 http://localhost:8081 접속

초기 로그인:

Email: [email protected]
Password: changeme

첫 로그인 시 이메일과 비밀번호를 변경해야 합니다.

5.5 SSL 인증서 등록

  1. SSL CertificatesAdd SSL CertificateCustom
  2. 입력:
Name: example.com
Certificate Key: example.com.key 파일 업로드
Certificate: example.com.pem 파일 업로드
Intermediate Certificate: (비워둠)
  1. Save

팁: 서버에서 로컬로 인증서를 다운로드하려면:

scp -i ~/.ssh/oracle-server-key ubuntu@<SERVER_IP>:/etc/ssl/cloudflare/example.com.pem ~/Desktop/

6. Ghost Docker 설치

6.1 디렉토리 생성

mkdir -p ~/docker/ghost
cd ~/docker/ghost

6.2 docker-compose.yml 작성

cat > docker-compose.yml << 'EOF'
services:
  ghost:
    image: ghost:6-alpine
    container_name: ghost
    restart: unless-stopped
    ports:
      - "2368:2368"
    environment:
      url: https://blog.example.com  # ← 실제 도메인으로 변경
      database__client: mysql
      database__connection__host: ghost-db
      database__connection__user: ghost
      database__connection__password: YOUR_GHOST_DB_PASSWORD  # ← 변경 필수
      database__connection__database: ghost
    volumes:
      - ghost-content:/var/lib/ghost/content
    depends_on:
      - ghost-db
    networks:
      - ghost-network

  ghost-db:
    image: mysql:8.0
    container_name: ghost-db
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: YOUR_ROOT_PASSWORD  # ← 변경 필수
      MYSQL_DATABASE: ghost
      MYSQL_USER: ghost
      MYSQL_PASSWORD: YOUR_GHOST_DB_PASSWORD  # ← 위와 동일하게
    volumes:
      - ghost-db-data:/var/lib/mysql
    networks:
      - ghost-network

volumes:
  ghost-content:
  ghost-db-data:

networks:
  ghost-network:
    driver: bridge
EOF

비밀번호 생성 팁:

# 안전한 랜덤 비밀번호 생성
openssl rand -base64 24
# 예: Abc123XyzQwe456Rty789==

6.3 컨테이너 실행

docker compose up -d

# 상태 확인
docker ps

출력 예시:

CONTAINER ID   IMAGE              STATUS          PORTS                    NAMES
def456ghi789   ghost:6-alpine     Up 30 seconds   0.0.0.0:2368->2368/tcp   ghost
abc123def456   mysql:8.0          Up 35 seconds   3306/tcp, 33060/tcp      ghost-db

6.4 Ghost 로그 확인

docker logs ghost --tail 50

정상 부팅 시:

[INFO] Ghost booted in 5.234s
[INFO] Ghost is running in production mode...

6.5 로컬 테스트

curl localhost:2368

HTML이 반환되면 Ghost가 정상 작동 중입니다.


7. Nginx Proxy Manager에서 Proxy Host 설정

7.1 Docker Host IP 확인

ip route | grep docker0 | awk '{print $9}'
# 172.17.0.1

중요: Linux에서는 host.docker.internal 대신 172.17.0.1을 사용해야 합니다.

7.2 Proxy Host 추가

NPM 관리 페이지에서: HostsProxy HostsAdd Proxy Host

Details 탭:

항목
Domain Names blog.example.com
Scheme http
Forward Hostname / IP 172.17.0.1
Forward Port 2368
Block Common Exploits

SSL 탭:

항목
SSL Certificate example.com (앞서 등록한 인증서)
Force SSL
HTTP/2 Support
HSTS Enabled ❌ (Cloudflare가 처리)

Save 클릭

7.3 HTTPS 테스트

# 로컬에서 실행
curl -sI https://blog.example.com | head -5

정상 응답:

HTTP/2 200
content-type: text/html; charset=utf-8
cache-control: public, max-age=0

8. Ghost 초기 설정

8.1 관리자 계정 생성

브라우저에서 https://blog.example.com/ghost 접속

  1. Create your account 클릭
  2. 블로그 이름, 이름, 이메일, 비밀번호 입력
  3. Last step: Invite your teamI'll do this later 클릭

8.2 기본 설정

Settings (좌측 하단 톱니바퀴)에서:

설정 추천 값
Title & description 블로그 이름, 한 줄 설명
Accent color 브랜드 색상 (예: #06B6D4)
Publication cover 선택 (로딩 속도 고려)
Site timezone Asia/Seoul

8.3 네비게이션 설정

SettingsNavigation

Primary navigation:
├── Home: /
├── About: /about
└── (필요에 따라 추가)

9. SMTP 설정 (이메일 발송)

Ghost는 로그인 인증, 구독 확인 등에 이메일을 사용합니다. Gmail SMTP를 설정합니다.

9.1 Gmail 앱 비밀번호 생성

  1. Google 계정 관리 접속
  2. 보안2단계 인증 활성화
  3. 앱 비밀번호 생성
  4. 앱 선택: 메일, 기기 선택: 기타
  5. 16자리 비밀번호 저장

9.2 docker-compose.yml 수정

services:
  ghost:
    image: ghost:6-alpine
    # ... 기존 설정 ...
    environment:
      url: https://blog.example.com
      database__client: mysql
      database__connection__host: ghost-db
      database__connection__user: ghost
      database__connection__password: YOUR_GHOST_DB_PASSWORD
      database__connection__database: ghost
      # ↓ SMTP 설정 추가
      mail__transport: SMTP
      mail__options__service: Gmail
      mail__options__auth__user: [email protected]
      mail__options__auth__pass: YOUR_APP_PASSWORD  # ← 앱 비밀번호
      mail__from: '"블로그 이름" <[email protected]>'

9.3 컨테이너 재시작

cd ~/docker/ghost
docker compose down
docker compose up -d

9.4 이메일 테스트

  1. Ghost Admin → SettingsStaff
  2. 본인 프로필 클릭 → Email에서 주소 변경
  3. 인증 이메일 수신 확인

10. 트러블슈팅

10.1 502 Bad Gateway

원인: NPM이 Ghost 컨테이너에 연결 못함

# Ghost 실행 확인
docker ps | grep ghost

# Ghost 포트 확인
curl localhost:2368

# NPM 설정 확인
# Forward Hostname: 172.17.0.1 (host.docker.internal 아님!)
# Forward Port: 2368

10.2 521 Origin Down

원인: Cloudflare가 서버에 연결 못함

# 서버 방화벽 확인
sudo ufw status
# 80, 443 허용 확인

# Oracle Cloud 보안 목록 확인
# 80, 443 인바운드 규칙 있는지 확인

10.3 SSL 인증서 오류

원인: Origin CA 인증서 문제

# 인증서 유효성 확인
openssl x509 -in /etc/ssl/cloudflare/example.com.pem -text -noout | grep -E "(Issuer|Subject|Not)"

# NPM에서 인증서 재등록
# SSL Certificates → 삭제 후 재등록

10.4 Ghost 컨테이너 시작 실패

# 로그 확인
docker logs ghost

# 일반적인 원인:
# 1. MySQL 연결 실패 → 비밀번호 확인
# 2. url 설정 오류 → https:// 포함 확인
# 3. 포트 충돌 → 2368 사용 중인지 확인

10.5 이미지 업로드 실패

# 볼륨 권한 확인
docker exec ghost ls -la /var/lib/ghost/content

# 필요시 권한 수정
docker exec ghost chown -R node:node /var/lib/ghost/content

11. 핵심 개념 정리

개념 설명
Nginx Proxy Manager GUI 기반 리버스 프록시 관리 도구
Origin CA Cloudflare ↔ 서버 간 암호화용 무료 인증서
Full (Strict) 서버에 유효한 인증서 필수인 SSL 모드
Docker Network 컨테이너 간 통신을 위한 가상 네트워크
172.17.0.1 Linux Docker의 호스트 IP (bridge 네트워크)

12. 보안 체크리스트

  • [ ] Cloudflare SSL/TLS: Full (Strict) 모드
  • [ ] Origin CA 인증서: 15년 유효기간
  • [ ] NPM 관리 페이지: SSH 터널로만 접속
  • [ ] 데이터베이스: 외부 노출 X (Docker 내부 네트워크만)
  • [ ] Ghost Admin: HTTPS 필수
  • [ ] Gmail: 앱 비밀번호 사용 (2FA 활성화)

13. 공식 권장 방식과의 비교

Ghost 6.0부터 공식 문서는 Caddy 기반 Docker Compose를 권장합니다. 이 글은 NPM 환경에 최적화된 방식입니다.

항목 공식 권장 이 글 (NPM 기반)
웹서버 Caddy (자동 SSL) NPM + Cloudflare Origin CA
SSL 갱신 자동 (Let's Encrypt) 불필요 (15년 유효)
다른 서비스 통합 별도 설정 필요 NPM에서 통합 관리
Analytics Tinybird 자동 연동 Traffic Proxy 수동 설정

이 방식이 적합한 경우:

  • 이미 NPM으로 다른 서비스 관리 중
  • Cloudflare CDN/보안 사용 중
  • Ghost 외 다른 서비스도 운영 예정

Analytics와 ActivityPub 설정은 확장 시리즈에서 다룹니다.


14. FAQ

Q: Let's Encrypt 대신 Origin CA를 사용하는 이유는?
A: Cloudflare 프록시를 사용하면 Origin CA가 더 간편합니다. 15년 유효기간으로 갱신 걱정이 없고, 와일드카드 인증서도 무료입니다.

Q: Ghost 무료 버전의 제한은?
A: 셀프 호스팅 Ghost는 100% 무료이며 기능 제한이 없습니다. Ghost(Pro) 유료 플랜은 호스팅 + 관리 서비스입니다.

Q: MySQL 대신 SQLite를 사용할 수 있나요?
A: 가능하지만 권장하지 않습니다. MySQL이 성능과 안정성 면에서 더 좋고, 백업/복원도 편리합니다.

Q: 여러 Ghost 사이트를 운영할 수 있나요?
A: 네, 각 사이트별로 docker-compose.yml을 만들고 다른 포트를 사용하면 됩니다. NPM에서 도메인별로 라우팅합니다.

Q: 이미지는 어디에 저장되나요?
A: Docker 볼륨 ghost-content에 저장됩니다. /var/lib/docker/volumes/ghost_ghost-content/에서 확인 가능합니다.


15. 다음 단계

Ghost 블로그 설치가 완료되었습니다! 다음 글에서는 Ghost 자동 백업 설정을 다룹니다.

시리즈 목차:

  1. Oracle Cloud 무료 서버 세팅
  2. Ghost 블로그 Docker 설치 ← 현재 글
  3. Ghost 블로그 백업 자동화
  4. 검색엔진 등록 (Google/Naver)
  5. Ghost 6.0 업그레이드
  6. Ghost ActivityPub 설정 (Fediverse)
  7. Ghost Analytics 설정 (Tinybird)

16. 참고 자료