시놀로지 NAS에 Docker로 Ghost 설치하기 (Ghost 6.10.3-alpine + MySQL 8.0.44 + Nginx Proxy Manager HTTPS)

시놀로지 NAS에 Docker로 Ghost 설치하기 (Ghost 6.10.3-alpine + MySQL 8.0.44 + Nginx Proxy Manager HTTPS)
Photo by Kyle Larivee / Unsplash

이 글에서는 시놀로지 NAS(DSM)에서 컨테이너를 이용해 Ghost 블로그를 설치하고, Nginx Proxy Manager(NPM) 뒤에서 HTTPS 도메인으로 서비스하는 구성을 정리합니다.

구성 요약

  • Ghost: ghost:6.10.3-alpine
  • DB: mysql:8.0.44
  • 외부 서비스 도메인: https://blog.oopstorage.com
  • Ghost 외부 노출 포트: 2369 (호스트 2369 → 컨테이너 2368)
  • Reverse Proxy: Nginx Proxy Manager에서 SSL(HTTPS) 종단
  • 데이터 영속화: Ghost content / MySQL data 모두 volume로 보존

1. 준비사항

1) 사전 준비

  • 시놀로지 DSM에 Container Manager(Docker) 설치
  • 도메인 blog.oopstorage.com이 외부에서 NAS로 접근 가능하도록 DNS 설정
  • 라우터 포트포워딩 또는 리버스 프록시 접근 환경 준비(외부 노출 시)
  • (권장) Nginx Proxy Manager 컨테이너를 별도로 운영 중이어야 함

2) 설치 폴더 구성(권장)

NAS에 아래처럼 프로젝트 폴더를 준비합니다.

  • ghost/
    • docker-compose.yml
    • .env

2. 환경변수(.env) 분리

민감한 비밀번호/설정은 .env로 분리하면 관리가 편하고 안전합니다.

.env 파일 예시:

TZ=Asia/Seoul

MYSQL_DATABASE=ghost
MYSQL_USER=oOpsKorean
MYSQL_PASSWORD=CHANGE_ME_GHOST_DB_PASSWORD
MYSQL_ROOT_PASSWORD=CHANGE_ME_ROOT_PASSWORD
  • MYSQL_USERroot가 아닌 일반 사용자로 설정해야 합니다.
  • 비밀번호는 반드시 강력하게 설정하세요.

3. Docker Compose 작성

아래는 최종적으로 정상 구동에 성공한 docker-compose.yml 입니다.

version: "3.8"

services:
  db:
    image: mysql:8.0.44
    container_name: ghost-db
    restart: unless-stopped
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
      - --mysqlx=0
    environment:
      TZ: ${TZ}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    volumes:
      - ghost-db-data:/var/lib/mysql
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s
    networks:
      - ghost-net

  ghost:
    image: ghost:6.10.3-alpine
    container_name: ghost
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      # 호스트 2369 -> 컨테이너 2368(Ghost 기본)
      - "2369:2368"
    environment:
      TZ: ${TZ}
      NODE_ENV: production

      # 외부 공개 도메인(HTTPS)으로 고정 (NPM이 SSL 종단)
      url: "https://blog.oopstorage.com"

      database__client: mysql
      database__connection__host: db
      database__connection__user: ${MYSQL_USER}
      database__connection__password: ${MYSQL_PASSWORD}
      database__connection__database: ${MYSQL_DATABASE}

      # Reverse Proxy(NPM) 뒤 운영 시 필수 권장
      trust_proxy: "true"
    volumes:
      - ghost-content:/var/lib/ghost/content
    networks:
      - ghost-net

networks:
  ghost-net:

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

구성 포인트

  • DB가 준비되기 전에 Ghost가 먼저 뜨는 문제를 줄이기 위해 healthcheck + depends_on(condition: service_healthy)를 적용했습니다.
  • url은 반드시 실제 서비스 도메인인 https://blog.oopstorage.com으로 고정해야 합니다.
    • 내부 IP/포트를 넣어두면 리다이렉트/리소스 URL 생성이 꼬일 수 있습니다.
  • trust_proxy: "true"는 NPM처럼 리버스 프록시(SSL 종단) 뒤에서 Ghost가 “HTTPS 요청”을 올바르게 인지하도록 도와줍니다.

4. 컨테이너 실행

프로젝트 폴더에서 compose를 실행합니다.

docker compose up -d

(DSM UI를 사용한다면 Container Manager에서 프로젝트/스택으로 배포해도 동일합니다.)

실행 후 확인:

  • ghost-db 컨테이너: 정상 기동 및 healthcheck 통과
  • ghost 컨테이너: DB 연결 후 정상 기동

내부 포트 테스트(선택):

  • http://NAS_IP:2369 로 접속 시 Ghost 응답 확인
    (단, url이 HTTPS 도메인으로 고정되어 있으면 접속 시 HTTPS로 유도될 수 있습니다.)

5. Nginx Proxy Manager(NPM) 설정

NPM에서 Proxy Host를 생성합니다.

  • Domain Names: blog.oopstorage.com
  • Scheme: http
  • Forward Hostname / IP: NAS 내부 IP (예: 192.168.1.210)
  • Forward Port: 2369

SSL 탭:

  • Let’s Encrypt 인증서 발급
  • Force SSL 활성화

이후 외부에서 아래 주소로 접속:

  • https://blog.oopstorage.com

6. 한글 글꼴 적용 및 폰트 크기 조정

6.1. Code Injection 설정

  • Ghost Admin → Settings → Code Injection → Site Header
<!-- Prism.js CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs/themes/prism-twilight.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs/plugins/toolbar/prism-toolbar.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs/plugins/line-numbers/prism-line-numbers.min.css">
  • Ghost Admin → Settings → Code Injection → Site Footer
<!-- Prism.js Core -->
<script src="https://cdn.jsdelivr.net/npm/prismjs/prism.min.js" defer></script>

<!-- Prism.js Plugins -->
<script src="https://cdn.jsdelivr.net/npm/prismjs/plugins/autoloader/prism-autoloader.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/plugins/toolbar/prism-toolbar.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/plugins/line-numbers/prism-line-numbers.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/plugins/show-language/prism-show-language.min.js" defer></script>

<!-- Adds line numbers to code blocks -->
<script>
  window.addEventListener('DOMContentLoaded', (event) => {
    document.querySelectorAll('pre[class*=language-]').forEach(function (node) {
      node.classList.add('line-numbers');
    });
    Prism.highlightAll();
  });
</script>

7. 참고 및 주의사항

  • 어드민 에디터(글쓰기 화면)는 Code Injection이 적용되지 않음
  • 프론트엔드(블로그 방문자 화면)만 커스텀 가능