Ghost Docker 설치 가이드: 블로그 & 뉴스레터 플랫폼




개요

Ghost는 전문 퍼블리싱을 위한 오픈소스 블로그 플랫폼입니다. 블로그 작성, 이메일 뉴스레터, 유료 멤버십/구독을 하나의 플랫폼에서 제공합니다. Medium의 셀프호스팅 대안으로, 깔끔한 에디터와 빠른 성능이 특징입니다.

항목내용
GitHubhttps://github.com/TryGhost/Ghost
공식 사이트https://ghost.org
문서https://ghost.org/docs
라이선스MIT
GitHub Stars48K+

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자동화

시스템 요구 사항

항목최소권장
CPU1 코어2+ 코어
RAM1GB2GB+
저장소10GB20GB+
포트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__clientmysql 또는 sqlite3
database__connection__hostDB 호스트
database__connection__userDB 사용자
database__connection__passwordDB 비밀번호
database__connection__databaseDB 이름

메일 설정

변수설명
mail__transportSMTP
mail__options__serviceMailgun, SendGrid
mail__options__hostSMTP 호스트
mail__options__portSMTP 포트
mail__options__auth__userSMTP 사용자
mail__options__auth__passSMTP 비밀번호
mail__from발신 주소

뉴스레터 (Bulk Email)

변수설명
bulkEmail__mailgun__apiKeyMailgun API 키
bulkEmail__mailgun__domainMailgun 도메인
bulkEmail__mailgun__baseUrlMailgun API URL

초기 설정

1. 시작

docker compose up -d

2. 관리자 계정 생성

  • http://localhost:2368/ghost 접속
  • 사이트 제목, 관리자 정보 입력
  • 첫 번째 사용자가 Owner(소유자)

3. 사이트 설정

  1. Settings → General
    • 사이트 제목, 설명
    • 타임존, 언어
  2. Settings → Design
    • 테마 선택/업로드
    • 네비게이션 설정
  3. Settings → Membership
    • Stripe 연동 (유료 구독 시)
    • 가격 설정
  4. Settings → Email newsletter
    • Mailgun 연동
    • 뉴스레터 디자인

4. 첫 글 작성

  1. Posts → New post
  2. 제목, 본문 작성
  3. 이미지 추가 (드래그 앤 드롭)
  4. Publish → 발행

Content API (헤드리스 모드)

Ghost를 헤드리스 CMS로 사용하려면 Content API를 활용합니다.

API 키 생성

  1. Settings → Integrations
  2. “Add custom integration”
  3. 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 레코드 확인

대안 비교

기능GhostWordPressSubstackMedium
오픈소스
셀프호스팅
뉴스레터✅ 내장플러그인
유료구독✅ 내장플러그인
에디터깔끔복잡깔끔깔끔
속도빠름느림빠름빠름
테마적음매우 많음제한없음

선택 가이드

용도추천
블로그 + 뉴스레터Ghost
복잡한 웹사이트WordPress
빠른 시작 (SaaS)Substack
수익화 중심Ghost, Substack

활용 사례

1. 개인 블로그

구성:

  • Ghost 기본 설정
  • Casper 테마
  • 무료 멤버십 (이메일 구독)

2. 뉴스레터 비즈니스

구성:

  • Mailgun 연동
  • Stripe 유료 구독
  • 무료/유료 티어

3. 기업 블로그

구성:

  • 커스텀 테마
  • 다중 작성자
  • SEO 최적화

4. 헤드리스 CMS

구성:

  • Content API 활용
  • Next.js/Gatsby 프론트엔드
  • 정적 사이트 생성

마무리

Ghost는 블로깅에 집중한 현대적 퍼블리싱 플랫폼입니다. 뉴스레터와 유료 구독이 내장되어 있어 콘텐츠 크리에이터에게 최적화되어 있습니다.

핵심 장점

장점설명
깔끔한 에디터글쓰기에 집중
뉴스레터 내장Mailgun 연동
유료구독 내장Stripe 결제
빠른 성능Node.js 기반
SEO 최적화자동 메타태그
Content API헤드리스 CMS 가능



댓글 남기기