개요
Ghost는 전문 퍼블리싱을 위한 오픈소스 블로그 플랫폼입니다. 블로그 작성, 이메일 뉴스레터, 유료 멤버십/구독을 하나의 플랫폼에서 제공합니다. Medium의 셀프호스팅 대안으로, 깔끔한 에디터와 빠른 성능이 특징입니다.
| 항목 | 내용 |
|---|
| GitHub | https://github.com/TryGhost/Ghost |
| 공식 사이트 | https://ghost.org |
| 문서 | https://ghost.org/docs |
| 라이선스 | MIT |
| GitHub Stars | 48K+ |
Ghost란?
왜 Ghost인가?
WordPress
→ 플러그인 많지만 무거움
→ 보안 취약점 많음
→ 블로깅에 집중하기 어려움
Ghost
→ 블로깅에 집중
→ 빠르고 깔끔
→ 뉴스레터 + 유료구독 내장
→ 현대적 기술 스택 (Node.js)
핵심 기능
"Publishing + Newsletter + Membership"
1. 블로그 작성 - 깔끔한 에디터
2. 이메일 뉴스레터 - Mailgun 연동
3. 유료 구독 - Stripe 결제
4. 멤버십 관리 - 무료/유료 회원
아키텍처
┌─────────────────────────────────────────┐
│ Ghost │
│ ┌─────────────────────────────────┐ │
│ │ Admin Panel (글 작성/관리) │ │
│ ├─────────────────────────────────┤ │
│ │ Theme Engine (Handlebars) │ │
│ ├─────────────────────────────────┤ │
│ │ Content API (헤드리스 모드) │ │
│ ├─────────────────────────────────┤ │
│ │ Members & Subscriptions │ │
│ └─────────────────────────────────┘ │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ MySQL / MariaDB (Production) │
│ SQLite (Development) │
└─────────────────────────────────────────┘
주요 기능
✍️ 콘텐츠 작성
| 기능 | 설명 |
|---|
| 에디터 | 깔끔한 마크다운 에디터 |
| 카드 | 이미지, 비디오, 코드 카드 |
| 예약 발행 | 게시 일정 설정 |
| SEO | 메타태그, OG 자동 |
| 태그/카테고리 | 콘텐츠 분류 |
📧 뉴스레터
| 기능 | 설명 |
|---|
| 이메일 발송 | 게시물 → 뉴스레터 |
| 구독자 관리 | 이메일 리스트 |
| Mailgun 연동 | 대량 발송 |
| 구독 폼 | 이메일 수집 |
💰 멤버십 & 구독
| 기능 | 설명 |
|---|
| 무료 멤버 | 이메일 구독 |
| 유료 멤버 | Stripe 결제 |
| 티어 | 월간/연간 플랜 |
| 콘텐츠 제한 | 유료 회원 전용 |
🎨 테마
| 기능 | 설명 |
|---|
| 기본 테마 | Casper (기본) |
| 마켓플레이스 | 유/무료 테마 |
| 커스텀 테마 | Handlebars 기반 |
🔌 통합
| 서비스 | 용도 |
|---|
| Mailgun | 이메일 발송 |
| Stripe | 결제 |
| Unsplash | 이미지 |
| Zapier | 자동화 |
시스템 요구 사항
| 항목 | 최소 | 권장 |
|---|
| CPU | 1 코어 | 2+ 코어 |
| RAM | 1GB | 2GB+ |
| 저장소 | 10GB | 20GB+ |
| 포트 | 2368 | – |
Docker 설치
방법 1: 기본 설치 (SQLite, 개발용)
docker run -d \
--name ghost \
-p 2368:2368 \
-v ghost-content:/var/lib/ghost/content \
ghost:5-alpine
접속: http://localhost:2368 관리자: http://localhost:2368/ghost
방법 2: Docker Compose + MySQL (프로덕션)
# docker-compose.yml
services:
ghost:
image: ghost:5-alpine
container_name: ghost
restart: unless-stopped
ports:
- "2368:2368"
environment:
url: http://localhost:2368
database__client: mysql
database__connection__host: ghost-db
database__connection__user: ghost
database__connection__password: ghost_password
database__connection__database: ghost
volumes:
- ./content:/var/lib/ghost/content
depends_on:
- ghost-db
ghost-db:
image: mysql:8
container_name: ghost-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: ghost
MYSQL_USER: ghost
MYSQL_PASSWORD: ghost_password
volumes:
- ./data/mysql:/var/lib/mysql
방법 3: MariaDB 사용
# docker-compose.yml
services:
ghost:
image: ghost:5-alpine
container_name: ghost
restart: unless-stopped
ports:
- "2368:2368"
environment:
url: http://localhost:2368
database__client: mysql
database__connection__host: ghost-db
database__connection__user: ghost
database__connection__password: ghost_password
database__connection__database: ghost
volumes:
- ./content:/var/lib/ghost/content
depends_on:
- ghost-db
ghost-db:
image: mariadb:10
container_name: ghost-db
restart: unless-stopped
environment:
MARIADB_ROOT_PASSWORD: root_password
MARIADB_DATABASE: ghost
MARIADB_USER: ghost
MARIADB_PASSWORD: ghost_password
volumes:
- ./data/mariadb:/var/lib/mysql
방법 4: 프로덕션 (실제 도메인)
# docker-compose.yml
services:
ghost:
image: ghost:5-alpine
container_name: ghost
restart: unless-stopped
ports:
- "2368:2368"
environment:
# 필수: 실제 도메인
url: https://blog.yourdomain.com
# 데이터베이스
database__client: mysql
database__connection__host: ghost-db
database__connection__user: ghost
database__connection__password: ${DB_PASSWORD}
database__connection__database: ghost
# 메일 (Mailgun)
mail__transport: SMTP
mail__options__service: Mailgun
mail__options__auth__user: ${MAILGUN_USER}
mail__options__auth__pass: ${MAILGUN_PASSWORD}
volumes:
- ./content:/var/lib/ghost/content
depends_on:
- ghost-db
ghost-db:
image: mysql:8
container_name: ghost-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ghost
MYSQL_USER: ghost
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- ./data/mysql:/var/lib/mysql
.env 파일:
DB_PASSWORD=your_secure_password
DB_ROOT_PASSWORD=your_root_password
MAILGUN_USER=postmaster@yourdomain.com
MAILGUN_PASSWORD=your_mailgun_api_key
방법 5: Mailgun 뉴스레터 설정
# docker-compose.yml
services:
ghost:
image: ghost:5-alpine
container_name: ghost
restart: unless-stopped
ports:
- "2368:2368"
environment:
url: https://blog.yourdomain.com
# Database
database__client: mysql
database__connection__host: ghost-db
database__connection__user: ghost
database__connection__password: ghost_password
database__connection__database: ghost
# Mailgun 설정
mail__transport: SMTP
mail__options__service: Mailgun
mail__options__host: smtp.mailgun.org
mail__options__port: 587
mail__options__secure: "false"
mail__options__auth__user: postmaster@mg.yourdomain.com
mail__options__auth__pass: your-mailgun-smtp-password
mail__from: "Blog <noreply@yourdomain.com>"
# Bulk Email (뉴스레터용)
bulkEmail__mailgun__apiKey: your-mailgun-api-key
bulkEmail__mailgun__domain: mg.yourdomain.com
bulkEmail__mailgun__baseUrl: https://api.mailgun.net/v3
volumes:
- ./content:/var/lib/ghost/content
depends_on:
- ghost-db
ghost-db:
image: mysql:8
container_name: ghost-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: ghost
MYSQL_USER: ghost
MYSQL_PASSWORD: ghost_password
volumes:
- ./data/mysql:/var/lib/mysql
방법 6: Stripe 결제 연동
# docker-compose.yml
services:
ghost:
image: ghost:5-alpine
container_name: ghost
restart: unless-stopped
ports:
- "2368:2368"
environment:
url: https://blog.yourdomain.com
# Database
database__client: mysql
database__connection__host: ghost-db
database__connection__user: ghost
database__connection__password: ghost_password
database__connection__database: ghost
# Stripe 설정 (관리자 패널에서도 설정 가능)
# 기본적으로 Admin > Settings > Membership에서 설정
volumes:
- ./content:/var/lib/ghost/content
depends_on:
- ghost-db
ghost-db:
image: mysql:8
container_name: ghost-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: ghost
MYSQL_USER: ghost
MYSQL_PASSWORD: ghost_password
volumes:
- ./data/mysql:/var/lib/mysql
방법 7: Nginx 리버스 프록시
# docker-compose.yml
services:
ghost:
image: ghost:5-alpine
container_name: ghost
restart: unless-stopped
environment:
url: https://blog.yourdomain.com
database__client: mysql
database__connection__host: ghost-db
database__connection__user: ghost
database__connection__password: ghost_password
database__connection__database: ghost
volumes:
- ./content:/var/lib/ghost/content
depends_on:
- ghost-db
networks:
- ghost-net
ghost-db:
image: mysql:8
container_name: ghost-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: ghost
MYSQL_USER: ghost
MYSQL_PASSWORD: ghost_password
volumes:
- ./data/mysql:/var/lib/mysql
networks:
- ghost-net
nginx:
image: nginx:alpine
container_name: ghost-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- ghost
networks:
- ghost-net
networks:
ghost-net:
driver: bridge
nginx.conf:
events {
worker_connections 1024;
}
http {
upstream ghost {
server ghost:2368;
}
server {
listen 80;
server_name blog.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name blog.yourdomain.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
location / {
proxy_pass http://ghost;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
방법 8: Traefik + HTTPS
# docker-compose.yml
services:
ghost:
image: ghost:5-alpine
container_name: ghost
restart: unless-stopped
environment:
url: https://blog.yourdomain.com
database__client: mysql
database__connection__host: ghost-db
database__connection__user: ghost
database__connection__password: ghost_password
database__connection__database: ghost
volumes:
- ./content:/var/lib/ghost/content
labels:
- "traefik.enable=true"
- "traefik.http.routers.ghost.rule=Host(`blog.yourdomain.com`)"
- "traefik.http.routers.ghost.entrypoints=websecure"
- "traefik.http.routers.ghost.tls.certresolver=letsencrypt"
- "traefik.http.services.ghost.loadbalancer.server.port=2368"
depends_on:
- ghost-db
networks:
- traefik-net
- ghost-net
ghost-db:
image: mysql:8
container_name: ghost-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: ghost
MYSQL_USER: ghost
MYSQL_PASSWORD: ghost_password
volumes:
- ./data/mysql:/var/lib/mysql
networks:
- ghost-net
networks:
traefik-net:
external: true
ghost-net:
driver: bridge
환경 변수
필수 설정
| 변수 | 설명 |
|---|
url | 사이트 URL (필수!) |
database__client | mysql 또는 sqlite3 |
database__connection__host | DB 호스트 |
database__connection__user | DB 사용자 |
database__connection__password | DB 비밀번호 |
database__connection__database | DB 이름 |
메일 설정
| 변수 | 설명 |
|---|
mail__transport | SMTP |
mail__options__service | Mailgun, SendGrid 등 |
mail__options__host | SMTP 호스트 |
mail__options__port | SMTP 포트 |
mail__options__auth__user | SMTP 사용자 |
mail__options__auth__pass | SMTP 비밀번호 |
mail__from | 발신 주소 |
뉴스레터 (Bulk Email)
| 변수 | 설명 |
|---|
bulkEmail__mailgun__apiKey | Mailgun API 키 |
bulkEmail__mailgun__domain | Mailgun 도메인 |
bulkEmail__mailgun__baseUrl | Mailgun API URL |
초기 설정
1. 시작
docker compose up -d
2. 관리자 계정 생성
http://localhost:2368/ghost 접속
- 사이트 제목, 관리자 정보 입력
- 첫 번째 사용자가 Owner(소유자)
3. 사이트 설정
- Settings → General
- Settings → Design
- Settings → Membership
- Stripe 연동 (유료 구독 시)
- 가격 설정
- Settings → Email newsletter
4. 첫 글 작성
- Posts → New post
- 제목, 본문 작성
- 이미지 추가 (드래그 앤 드롭)
- Publish → 발행
Content API (헤드리스 모드)
Ghost를 헤드리스 CMS로 사용하려면 Content API를 활용합니다.
API 키 생성
- Settings → Integrations
- “Add custom integration”
- Content API Key 복사
API 호출
// Next.js 예시
const GHOST_URL = 'https://blog.yourdomain.com';
const GHOST_KEY = 'your_content_api_key';
// 모든 게시물 조회
const res = await fetch(
`${GHOST_URL}/ghost/api/content/posts/?key=${GHOST_KEY}`
);
const data = await res.json();
// 특정 게시물 조회
const post = await fetch(
`${GHOST_URL}/ghost/api/content/posts/slug/${slug}/?key=${GHOST_KEY}`
);
백업 및 복원
데이터 구조
./
├── content/ # Ghost 콘텐츠
│ ├── data/ # SQLite DB (개발용)
│ ├── images/ # 업로드 이미지
│ ├── themes/ # 테마
│ └── settings/ # 설정
└── data/
└── mysql/ # MySQL 데이터
백업
# 컨테이너 정지
docker compose down
# 전체 백업
tar -czvf ghost-backup-$(date +%Y%m%d).tar.gz content/ data/
# 관리자 패널에서 백업
# Settings → Labs → Export content
# 재시작
docker compose up -d
복원
tar -xzvf ghost-backup.tar.gz
docker compose up -d
# 관리자 패널에서 복원
# Settings → Labs → Import content
업데이트
# 최신 이미지
docker compose pull
# 재시작
docker compose down
docker compose up -d
주의: 메이저 버전 업그레이드 시 릴리스 노트 확인
트러블슈팅
URL 문제
Ghost는 url 환경 변수가 매우 중요합니다:
environment:
url: https://blog.yourdomain.com # 실제 접속 URL과 일치해야 함
이미지 업로드 안 됨
# 권한 확인
sudo chown -R 1000:1000 ./content
DB 연결 오류
- MySQL이 먼저 시작되었는지 확인
database__connection__host가 컨테이너 이름인지 확인
뉴스레터 발송 안 됨
- Mailgun 설정 확인
- DNS SPF, DKIM 레코드 확인
대안 비교
| 기능 | Ghost | WordPress | Substack | Medium |
|---|
| 오픈소스 | ✅ | ✅ | ❌ | ❌ |
| 셀프호스팅 | ✅ | ✅ | ❌ | ❌ |
| 뉴스레터 | ✅ 내장 | 플러그인 | ✅ | ❌ |
| 유료구독 | ✅ 내장 | 플러그인 | ✅ | ✅ |
| 에디터 | 깔끔 | 복잡 | 깔끔 | 깔끔 |
| 속도 | 빠름 | 느림 | 빠름 | 빠름 |
| 테마 | 적음 | 매우 많음 | 제한 | 없음 |
선택 가이드
| 용도 | 추천 |
|---|
| 블로그 + 뉴스레터 | Ghost |
| 복잡한 웹사이트 | WordPress |
| 빠른 시작 (SaaS) | Substack |
| 수익화 중심 | Ghost, Substack |
활용 사례
1. 개인 블로그
구성:
- Ghost 기본 설정
- Casper 테마
- 무료 멤버십 (이메일 구독)
2. 뉴스레터 비즈니스
구성:
- Mailgun 연동
- Stripe 유료 구독
- 무료/유료 티어
3. 기업 블로그
구성:
4. 헤드리스 CMS
구성:
- Content API 활용
- Next.js/Gatsby 프론트엔드
- 정적 사이트 생성
마무리
Ghost는 블로깅에 집중한 현대적 퍼블리싱 플랫폼입니다. 뉴스레터와 유료 구독이 내장되어 있어 콘텐츠 크리에이터에게 최적화되어 있습니다.
핵심 장점
| 장점 | 설명 |
|---|
| 깔끔한 에디터 | 글쓰기에 집중 |
| 뉴스레터 내장 | Mailgun 연동 |
| 유료구독 내장 | Stripe 결제 |
| 빠른 성능 | Node.js 기반 |
| SEO 최적화 | 자동 메타태그 |
| Content API | 헤드리스 CMS 가능 |