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

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

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

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. 참고 자료