맥미니+Ubuntu 환경에서 Docker로 Ghost 블로그 설치하기
언제나 즐거은 삽질! 구형 맥미니 2014+Ubuntu 24.04 LTS에 Ghost 블로그를 Docker로 설치하는 방법 (HTTPS + MySQL 분리 네트워크 구성)
지난 포스트에서 맥OS에 도커로 ghost를 설치하는 방법을 소개했는데 그 방법으로 잘되지 않았다. ^^;; 사실 몇날 몇일을 고생했다. 개발자가 아니다 보니 나의 지식이 협소함에 다시금 놀라움을 금치 못했다. 몇 일을 삽질하고 드디어 성공해서 하염없이 기쁘기는 하다.
Ghost 블로그를 HTTPS 보안, Gmail SMTP, DB 분리, 접근 제어까지 적용해서 안정적으로 배포하기 위해 Docker Compose를 사용했습니다. 특히 nginx-proxy + acme-companion 조합을 통해 SSL 인증서를 자동으로 관리하도록 구성했습니다.
디렉토리 구조
/var/www/blog/
├── docker-compose.yml
├── .env
└── data/
├── ghost/
└── mysql/- OS : Ubuntu 24.04.2 + Docker 28.3.2
1. 전체 구조
version: "3.9"
services:
nginx-proxy:
acme-companion:
mysql:
ghost:
networks:
proxy-tier:
ghost-internal:nginx-proxy: reverse proxy로서 HTTPS 처리 및 도메인 기반 라우팅을 담당acme-companion: Let's Encrypt 인증서를 자동 발급 및 갱신mysql: Ghost의 DB, 내부 통신 전용ghost: Ghost 블로그 서비스networks: 프록시용 네트워크와 내부 DB 통신용 네트워크를 분리
2. nginx-proxy 설정
nginx-proxy:
image: jwilder/nginx-proxy:alpine
container_name: nginx-proxy
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./certs:/etc/nginx/certs:ro
- ./vhost.d:/etc/nginx/vhost.d
- ./html:/usr/share/nginx/html
- ./data/ghost:/var/www/html
- ./nginx.conf:/etc/nginx/conf.d/custom.conf:ro
environment:
- TZ=${TZ}
networks:
- proxy-tierjwilder/nginx-proxy는 컨테이너 자동 탐지 기반으로 리버스 프록시를 설정해줍니다.nginx.conf는 보안 강화 설정을 위한 커스텀 설정입니다.- 인증서 경로와 정적 리소스를 위한 볼륨도 정의되어 있습니다.
- 도커 소켓을
ro로 마운트하여 보안도 고려했습니다.
3. acme-companion 설정
acme-companion:
image: nginxproxy/acme-companion
container_name: acme-companion
depends_on:
- nginx-proxy
restart: always
environment:
- NGINX_PROXY_CONTAINER=nginx-proxy
- DEFAULT_EMAIL=${GMAIL_USER}
- TZ=${TZ}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./certs:/etc/nginx/certs
- ./vhost.d:/etc/nginx/vhost.d
- ./html:/usr/share/nginx/html
networks:
- proxy-tierNGINX_PROXY_CONTAINER환경변수는 꼭 필요합니다. 그래야 nginx-proxy 컨테이너와 연동됩니다.- 인증서 발급 이메일은
.env파일에 정의한 Gmail 주소입니다. - nginx와 공유되는 디렉토리를 동일하게 마운트해야 인증서 연동이 가능합니다.
4. MySQL 설정 (내부 전용)
mysql:
image: mysql:8.0.42
container_name: ghost-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
TZ: ${TZ}
volumes:
- ./data/mysql:/var/lib/mysql
networks:
- ghost-internal- Ghost에서만 접근 가능한 내부 DB입니다.
ghost-internal네트워크로만 연결되어 외부 노출을 차단했습니다.- 비밀번호 및 DB 정보는
.env파일로 관리합니다.
5. Ghost 블로그 서비스 설정
yaml복사편집 ghost:
ghost:
image: ghost:5
container_name: ${BLOG_CONTAINER_NAME}
restart: always
depends_on:
- mysql
environment:
url: https://${DOMAIN}
database__client: mysql
database__connection__host: mysql
database__connection__user: ${MYSQL_USER}
database__connection__password: ${MYSQL_PASSWORD}
database__connection__database: ${MYSQL_DATABASE}
mail__transport: SMTP
mail__options__service: Gmail
mail__options__auth__user: ${GMAIL_USER}
mail__options__auth__pass: ${GMAIL_PASS}
mail__from: "'mango' <${GMAIL_USER}>"
TZ: ${TZ}
VIRTUAL_HOST: ${DOMAIN}
LETSENCRYPT_HOST: ${DOMAIN}
LETSENCRYPT_EMAIL: ${GMAIL_USER}
volumes:
- ./data/ghost:/var/lib/ghost/content
networks:
- proxy-tier
- ghost-internalurl: Ghost가 인식하는 자기 주소. SSL이 적용되었기에https://로 설정.- DB 연결 정보 및 SMTP 설정은 모두
.env에서 관리. VIRTUAL_HOST,LETSENCRYPT_HOST,LETSENCRYPT_EMAIL은 nginx-proxy + acme-companion이 인식하는 키입니다.- nginx에서
client_max_body_size설정도 따로 적용하여 파일 업로드 문제를 예방했습니다.
6. 네트워크 설정
networks:
proxy-tier:
ghost-internal:
internal: trueproxy-tier: 외부 요청을 받을 수 있는 네트워크ghost-internal: DB 전용 내부 네트워크 (internal: true로 외부 통신 차단)
✍️ 결론
이 docker-compose.yml은 다음을 만족합니다:
- 자동 HTTPS (Let's Encrypt)
- 블로그와 DB 네트워크 분리
- Gmail SMTP 메일 발송
- 커스텀 nginx 보안 설정 (
nginx.conf) - Ghost 관리 페이지 접속 제어 가능 (IP 제한 설정)
.env기반 설정으로 비밀번호 및 도메인 안전하게 관리
docker-compose.yml
전체 구성
version: "3.9"
services:
nginx-proxy:
networks:
- proxy-tier
image: jwilder/nginx-proxy:alpine
container_name: nginx-proxy
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./certs:/etc/nginx/certs:ro
- ./vhost.d:/etc/nginx/vhost.d
- ./html:/usr/share/nginx/html
- ./data/ghost:/var/www/html
- ./nginx.conf:/etc/nginx/conf.d/custom.conf:ro
environment:
- TZ=${TZ}
acme-companion:
networks:
- proxy-tier
image: nginxproxy/acme-companion
container_name: acme-companion
depends_on:
- nginx-proxy
restart: always
environment:
- NGINX_PROXY_CONTAINER=nginx-proxy # 👈 반드시 추가
- DEFAULT_EMAIL=${GMAIL_USER}
- TZ=${TZ}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./certs:/etc/nginx/certs
- ./vhost.d:/etc/nginx/vhost.d
- ./html:/usr/share/nginx/html
mysql:
networks:
- ghost-internal # 외부 노출 X
image: mysql:8.0.42
container_name: ghost-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
TZ: ${TZ}
volumes:
- ./data/mysql:/var/lib/mysql
ghost:
networks:
- proxy-tier # 외부 접속용
- ghost-internal # DB 내부 접속용
image: ghost:5
container_name: ${BLOG_CONTAINER_NAME}
restart: always
depends_on:
- mysql
environment:
url: https://${DOMAIN}
database__client: mysql
database__connection__host: mysql
database__connection__user: ${MYSQL_USER}
database__connection__password: ${MYSQL_PASSWORD}
database__connection__database: ${MYSQL_DATABASE}
mail__transport: SMTP
mail__options__service: Gmail
mail__options__auth__user: ${GMAIL_USER}
mail__options__auth__pass: ${GMAIL_PASS}
mail__from: "'mango' <${GMAIL_USER}>"
TZ: ${TZ}
VIRTUAL_HOST: ${DOMAIN}
LETSENCRYPT_HOST: ${DOMAIN}
LETSENCRYPT_EMAIL: ${GMAIL_USER}
volumes:
- ./data/ghost:/var/lib/ghost/content
networks:
proxy-tier:
ghost-internal:
internal: true추가 보안 설정
✅ 1. 권장 소유자: 현재 사용자 또는 Docker가 접근 가능한 사용자
sudo chown -R $USER:$USER /var/www/blog$USER는 현재 로그인한 사용자- Ghost와 MySQL이 파일을 읽고 쓰려면 소유자 권한이 있어야 함
✅ 2. 권장 퍼미션 (디렉토리와 파일)
📁 디렉토리: 755
find /var/www/blog -type d -exec chmod 755 {} \;- 의미: 소유자
rwx, 그룹/기타 사용자rx(읽기+실행)
📄 파일: 644
find /var/www/blog -type f -exec chmod 644 {} \;- 의미: 소유자
rw, 그룹/기타 사용자r(읽기만)
🔒 예외: .env 파일, SMTP 비밀번호 등
.env에는 비밀번호가 들어가므로 보안을 강화하려면:
chmod 600 /var/www/blog/.env- 오직 소유자만 읽고 쓸 수 있음