Ghost 블로그 보안 패치 및 MySQL 8.0 마이그레이션 가이드 (Coolify)
Ghost 블로그의 치명적 DB 탈취 보안 취약점을 해결하고 MySQL 8.0으로 안전하게 마이그레이션하는 전체 과정을 단계별로 안내합니다. Docker Compose 설정부터 데이터 복구까지 확인하세요.
최근 발견된 Ghost 블로그의 치명적인 데이터베이스(DB) 탈취 보안 취약점을 해결하고, 구버전 MySQL에서 발생하던 "Unsupported Database" 경고를 제거하기 위해 DB를 MySQL 8.0으로 현대화하는 기술적 과정을 상세히 기록합니다.
이 가이드는 실제 운영 환경의 리스크를 최소화하기 위해 시놀로지 NAS(로컬) 환경에서 완벽한 테스트를 거친 안정적인 절차를 담고 있습니다. Docker와 Coolify 환경에서 Ghost 블로그를 운영 중인 분들에게 실질적인 도움이 될 것입니다.
0. 시작 전 필수 확인: 사전 준비물
원활한 마이그레이션을 위해 작업을 시작하기 전 아래 항목들이 준비되었는지 반드시 확인하세요.
- ✅ 서버 SSH 접속 정보: Coolify 서버에 SSH로 접속할 수 있는 계정과 비밀번호 또는 키
- ✅ 기존 Ghost 블로그 정보: 현재 운영 중인 Ghost 블로그의
docker-compose.yml파일 내용 - ✅ 백업 파일 저장 공간: SQL 및 콘텐츠 백업 파일을 저장할 충분한 디스크 공간
- ✅ SMTP 메일 서버 정보: YAML 파일에 입력할 Gmail 앱 비밀번호 등 SMTP 인증 정보
1. 마이그레이션의 핵심: 왜 DB 폴더를 분리해야 할까?
이번 마이그레이션에서 가장 중요한 결정은 기존 데이터베이스 폴더를 재사용하지 않고, 완전히 새로운 경로에 DB를 구성한 것입니다. 이는 안정적인 마이그레이션을 위한 필수적인 조치입니다.
| 이유 | 상세 설명 |
|---|---|
| 버전 호환성 에러 방지 | MySQL 8.0은 5.7 등 이전 버전의 데이터 디렉토리를 그대로 사용할 경우, 스토리지 엔진 차이로 인해 Invalid database directory 에러를 발생시키며 실행되지 않을 수 있습니다. |
| 명확한 권한 관리 | Ghost 콘텐츠(이미지, 테마)와 DB 데이터 파일은 서로 다른 사용자 권한(UID/GID)을 요구합니다. 두 데이터를 한 폴더에 혼합하면 권한 충돌의 원인이 될 수 있습니다. |
| 깨끗한 시작 (Clean Slate) | /root/volumes/userdata/ghost/mysqldb8 와 같이 완전히 새로운 폴더를 지정하고 비운 상태에서 배포하면, MySQL 8.0이 필요한 시스템 파일을 오류 없이 생성합니다. |
결론적으로, 새로운 DB 폴더를 생성하고 백업된 SQL 파일을 임포트하는 것이 가장 안전하고 확실한 복구 방법입니다.
2. 최적화된 인프라 설계: Docker Compose (YAML)
안정적인 운영을 위해 SMTP 메일 설정을 포함한 최종 docker-compose.yml 파일입니다. Ghost는 관리자 로그인, 비밀번호 찾기 등 핵심 기능에 메일 서버를 사용하므로, 메일 설정이 누락되면 예기치 않은 오류가 발생할 수 있습니다.
version: '3.9'
services:
ghost-db:
image: 'mysql:8.0'
container_name: Ghost-DB
hostname: ghost-db
command: '--default-authentication-plugin=mysql_native_password'
security_opt:
- 'no-new-privileges:true'
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
environment:
- PUID=1000
- PGID=1000
- TZ=Asia/Seoul
- MYSQL_ROOT_PASSWORD=supersuperpassword
- MYSQL_DATABASE=ghost
- MYSQL_USER=ghostuser
- MYSQL_PASSWORD=ghostpass
volumes:
- '/root/volumes/userdata/ghost/mysqldb8:/var/lib/mysql:rw' # 새로운 DB 폴더 경로
restart: always
ghost:
image: 'ghost:latest'
container_name: Ghost
healthcheck:
test: "timeout 10s bash -c ':> /dev/tcp/127.0.0.1/2368' || exit 1"
interval: 10s
timeout: 5s
retries: 3
start_period: 90s
hostname: ghost
security_opt:
- 'no-new-privileges:true'
user: '1000:1000'
environment:
database__client: mysql
database__connection__host: ghost-db
database__connection__user: ghostuser
database__connection__password: ghostpass
database__connection__database: ghost
mail__transport: SMTP
mail__options__service: SMTP
mail__from: your-email@gmail.com
mail__options__host: smtp.gmail.com
mail__options__port: 587
mail__options__auth__user: your-email@gmail.com
mail__options__auth__pass: your-app-password
NODE_ENV: production
volumes:
- '/root/volumes/userdata/ghost:/var/lib/ghost/content:rw'
- '/root/volumes/userdata/static:/static:ro'
restart: always
depends_on:
ghost-db:
condition: service_healthy
3. 단계별 마이그레이션 프로세스
Coolify의 SSH 기능이나 웹 터미널을 사용하여 아래 절차를 순서대로 진행합니다.
Step 1. 기존 데이터 백업
1.1. 컨테이너 ID 확인
먼저 현재 실행 중인 DB 컨테이너의 ID를 확인합니다.
docker ps -f "name=ghost-db"
# 결과 예시
CONTAINER ID IMAGE COMMAND ... NAMES
497c9a50fa45 mysql "docker-entrypoint.s…" ... ghost-db-i8wwog44kocwsoocss4g8kc0
1.2. 데이터베이스(SQL) 백업
mysqldump 명령어를 사용하여 데이터베이스를 SQL 파일로 백업합니다. 아래 명령어는 컨테이너 내부에서 mysqldump를 실행하고, 그 결과를 호스트 서버의 특정 경로에 파일로 즉시 저장합니다.
⚠️ 주의:-u(사용자),-p(비밀번호)는 YAML 파일에 설정된 값과 일치해야 합니다.
docker exec 497c9a50fa45 /usr/bin/mysqldump -u ghostuser -p'ghostpass' \
--no-tablespaces \
--set-gtid-purged=OFF \
--single-transaction \
ghost > /root/volumes/userdata/ghost_db_backup_$(date +%F).sql
1.3. Ghost 콘텐츠 폴더 백업
만일의 사태에 대비하여 Ghost의 콘텐츠 폴더(images, themes 등)도 압축하여 백업해 둡니다.
# 예시: /root/volumes/userdata/ghost 폴더를 백업
zip -r /root/volumes/userdata/ghost_content_backup.zip /root/volumes/userdata/ghost
Step 2. 신규 환경 배포
- 서비스 정지: Coolify 대시보드에서 기존 Ghost 서비스를 정지합니다.
- YAML 수정: 위에서 제공된 최신
docker-compose.yml내용으로 업데이트합니다. 가장 중요한 변경 사항은ghost-db서비스의volumes경로를 새로운 폴더로 지정하는 것입니다. - Deploy (배포): 수정된 설정으로 서비스를 다시 배포합니다. MySQL 8.0 컨테이너가 생성되고, 지정된 새 볼륨에 초기 파일들이 생성됩니다.
- 로그 확인: 배포 후 에러 로그가 없는지 꼼꼼히 확인합니다.
Step 3. 데이터베이스 복원
3.1. 새로운 컨테이너 ID 확인
새로 생성된 MySQL 8.0 컨테이너의 ID를 확인합니다.
docker ps -f "name=ghost-db"
3.2. SQL 파일 임포트
docker exec와 리다이렉션(<)을 사용하여 백업해 둔 SQL 파일을 새로운 DB 컨테이너로 임포트합니다.
# 예시: 792624d011c8이 새로운 컨테이너 ID라고 가정
docker exec -i 792624d011c8 /usr/bin/mysql -u ghostuser -p'ghostpass' ghost < /root/volumes/userdata/ghost_db_backup_2026-02-23.sql
3.3. 서비스 재시작
데이터 복원이 완료되면, 안정적인 적용을 위해 Coolify에서 Ghost 서비스를 재시작(Restart)합니다. 이제 블로그에 접속하여 모든 콘텐츠가 정상적으로 표시되는지 확인합니다.
4. Pro-Tip: Docker 컨테이너와 호스트 간 파일 복사 완벽 정리
컨테이너 환경에서 파일을 다룰 때 자주 헷갈리는 세 가지 방법을 상황에 맞게 정리했습니다.
| 방법 | 명령어 형식 / 예시 | 특징 및 용도 |
|---|---|---|
1. docker cp |
docker cp [컨테이너]:[내부경로] [호스트경로] |
가장 표준적이고 직관적인 방법. 컨테이너가 실행/중지 상태와 무관하게 파일을 내보내거나 가져올 때 사용합니다. |
2. 리다이렉션 (>/<) |
docker exec [컨테이너] [명령] > [호스트파일] |
백업/복원 시 가장 효율적. 컨테이너 내부에 중간 파일을 만들지 않고, 명령어의 표준 출력/입력을 호스트 파일과 직접 연결합니다. |
| 3. 볼륨 마운트 | volumes: - [호스트경로]:[컨테이너경로] |
실시간 동기화. YAML에 미리 정의된 폴더는 컨테이너와 호스트 간에 파일이 자동으로 공유됩니다. 별도의 복사 명령이 필요 없습니다. |
"이미 컨테이너 내부에 들어와 있다면?"
만약docker exec -it [컨테이너] bash로 컨테이너 쉘에 진입한 상태라면,exit명령어로 먼저 빠져나온 후 1번docker cp명령어를 사용해야 합니다. 컨테이너 내부에서는 호스트의 파일 시스템에 직접 접근할 수 없기 때문입니다.
5. 문제 해결 (Troubleshooting)
마이그레이션 중 발생할 수 있는 일반적인 오류와 해결 방법입니다.
- 오류:
Permission denied가 발생하며 백업/복원이 실패하는 경우- 원인: 호스트 서버의 폴더에 대한 Docker 또는 현재 사용자의 쓰기/읽기 권한이 없는 경우입니다.
- 해결:
sudo chmod -R 777 /백업/폴더/경로와 같이 임시로 권한을 열어주거나,sudo chown -R $(whoami):$(whoami) /백업/폴더/경로명령어로 폴더 소유자를 현재 사용자로 변경한 후 다시 시도해 보세요. 작업 완료 후에는 보안을 위해 권한을 원래대로 복구하는 것이 좋습니다.
- 오류: DB 임포트 후 한글이 깨져 보이는 경우
- 원인: MySQL 8.0의 기본 캐릭터셋(Character Set)과 콜레이션(Collation) 설정이 백업 파일과 다른 경우 발생할 수 있습니다.
- 해결:
docker-compose.yml의ghost-db서비스command항목에--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci옵션을 추가하여 DB 서버의 기본 인코딩을 명시적으로 지정한 후 재배포하고 임포트를 다시 시도합니다.
6. 결론: 마이그레이션을 통해 얻은 성과
이번 마이그레이션을 통해 다음과 같은 중요한 성과를 달성했습니다.
- 보안 강화: Ghost 6.19.0 이상 버전으로 업데이트하여 치명적인 DB 탈취 취약점을 완벽하게 해결했습니다.
- 환경 현대화: MySQL 8.0으로 업그레이드하여 "Unsupported Database" 경고를 제거하고, 최신 기술 스택을 유지하여 기술 부채를 청산했습니다.
- 안정성 확보: 데이터 경로 분리 및 체계적인 백업/복원 절차를 통해 안정적인 서버 운영 기반을 마련했습니다.
7. 자주 묻는 질문 (FAQ)
Q1: 왜 기존 DB 폴더를 그대로 사용하면 안 되나요?
A1: MySQL 5.7과 8.0은 데이터 저장 방식과 시스템 파일 구조가 다릅니다. 기존 폴더를 그대로 사용하면 버전 비호환성으로 인해 DB 서비스가 시작되지 않는 Invalid database directory 오류가 발생할 확률이 매우 높습니다.
Q2: DB 백업 시 mysqldump 명령어의 옵션들은 어떤 의미인가요?
A2: --no-tablespaces는 테이블스페이스 관련 정보를 제외하여 호환성을 높이고, --set-gtid-purged=OFF는 복제 환경에서 발생할 수 있는 GTID 관련 오류를 방지합니다. --single-transaction은 백업 중 데이터 일관성을 보장하는 중요한 옵션입니다.
Q3: docker cp와 리다이렉션(>)을 이용한 백업의 가장 큰 차이점은 무엇인가요?
A3: docker cp는 컨테이너 내부에 이미 존재하는 파일을 호스트로 복사하는 방식입니다. 반면, 리다이렉션을 사용하면 mysqldump의 결과(백업 데이터)가 컨테이너 내부에 파일로 저장되지 않고 곧바로 호스트의 파일로 만들어져 디스크 공간을 절약하고 절차가 간소화됩니다.