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 도메인 추가
- Cloudflare 대시보드 접속
- Add a Site → 도메인 입력
- Free Plan 선택
- 네임서버를 Cloudflare로 변경 (도메인 등록기관에서)
3.2 DNS 레코드 설정
DNS → Records → Add 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/TLS → Overview
모드: Full (Strict) ← 선택
| 모드 | 설명 | 보안 |
|---|---|---|
| Off | SSL 없음 | ❌ |
| Flexible | Cloudflare↔사용자만 HTTPS | ⚠️ |
| Full | 서버에 자체서명 인증서 허용 | ⚠️ |
| Full (Strict) | 서버에 유효한 인증서 필수 | ✅ |
4. Origin CA 인증서 발급
Cloudflare Origin CA는 Cloudflare ↔ 서버 간 암호화에 사용되는 무료 인증서입니다. 최대 15년 유효합니다.
4.1 인증서 생성
SSL/TLS → Origin Server → Create 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 클릭 후 두 개의 텍스트가 표시됩니다:
- Origin Certificate →
example.com.pem - Private Key →
example.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 인증서 등록
- SSL Certificates → Add SSL Certificate → Custom
- 입력:
Name: example.com
Certificate Key: example.com.key 파일 업로드
Certificate: example.com.pem 파일 업로드
Intermediate Certificate: (비워둠)
- 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 관리 페이지에서: Hosts → Proxy Hosts → Add 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 접속
- Create your account 클릭
- 블로그 이름, 이름, 이메일, 비밀번호 입력
- Last step: Invite your team → I'll do this later 클릭
8.2 기본 설정
Settings (좌측 하단 톱니바퀴)에서:
| 설정 | 추천 값 |
|---|---|
| Title & description | 블로그 이름, 한 줄 설명 |
| Accent color | 브랜드 색상 (예: #06B6D4) |
| Publication cover | 선택 (로딩 속도 고려) |
| Site timezone | Asia/Seoul |
8.3 네비게이션 설정
Settings → Navigation
Primary navigation:
├── Home: /
├── About: /about
└── (필요에 따라 추가)
9. SMTP 설정 (이메일 발송)
Ghost는 로그인 인증, 구독 확인 등에 이메일을 사용합니다. Gmail SMTP를 설정합니다.
9.1 Gmail 앱 비밀번호 생성
- Google 계정 관리 접속
- 보안 → 2단계 인증 활성화
- 앱 비밀번호 생성
- 앱 선택: 메일, 기기 선택: 기타
- 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 이메일 테스트
- Ghost Admin → Settings → Staff
- 본인 프로필 클릭 → Email에서 주소 변경
- 인증 이메일 수신 확인
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 자동 백업 설정을 다룹니다.
시리즈 목차:
- Oracle Cloud 무료 서버 세팅
- Ghost 블로그 Docker 설치 ← 현재 글
- Ghost 블로그 백업 자동화
- 검색엔진 등록 (Google/Naver)
- Ghost 6.0 업그레이드
- Ghost ActivityPub 설정 (Fediverse)
- Ghost Analytics 설정 (Tinybird)