하나의 서버에서 여러 개의 WordPress 사이트를 운영하면서 SSL 인증서까지 자동으로 관리하고 싶으신가요? Docker Compose와 Let’s Encrypt를 활용하면 이 모든 것을 간단하게 해결할 수 있습니다.
이번 포스트에서는 Nginx 리버스 프록시 + 멀티 WordPress + Certbot SSL 자동 갱신을 Docker Compose로 구축하는 방법을 상세히 알아보겠습니다.
🎯 목표
- 하나의 서버에서 여러 WordPress 사이트 운영
- 각 사이트마다 독립적인 SSL 인증서 적용
- Let’s Encrypt를 통한 SSL 인증서 자동 갱신
- Docker Compose를 활용한 간편한 관리
📁 프로젝트 구조
먼저 우리가 구축할 프로젝트의 디렉터리 구조를 살펴보겠습니다:
wordpress-multi/
├── docker-compose.yml
├── init-letsencrypt-multi.sh
├── nginx/
│ ├── nginx.conf
│ └── conf.d/
│ ├── blog.conf # blog.mydomain.com
│ └── shop.conf # shop.mydomain.com
└── certbot/
├── conf/ # SSL 인증서 저장소
└── www/ # ACME 챌린지용
WordPress는 컨테이너 내에서 모든 기능을 처리하므로 별도의 파일 디렉터리가 필요하지 않습니다. 매우 깔끔한 구조죠!
🐳 Docker Compose 설정
docker-compose.yml
version: '3.8'
services:
nginx:
image: nginx:alpine
container_name: nginx-proxy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
depends_on:
- certbot
- blog-wordpress
- shop-wordpress
networks:
- web
certbot:
image: certbot/certbot
container_name: certbot
restart: unless-stopped
volumes:
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
networks:
- web
# 블로그 WordPress (blog.mydomain.com)
blog-wordpress:
image: wordpress:latest
container_name: blog-wordpress
restart: unless-stopped
environment:
WORDPRESS_DB_HOST: blog-db
WORDPRESS_DB_USER: blog_user
WORDPRESS_DB_PASSWORD: blog_secure_password
WORDPRESS_DB_NAME: blog_db
WORDPRESS_CONFIG_EXTRA: |
define('WP_HOME', 'https://blog.mydomain.com');
define('WP_SITEURL', 'https://blog.mydomain.com');
define('FORCE_SSL_ADMIN', true);
volumes:
- blog_wordpress_data:/var/www/html
depends_on:
- blog-db
networks:
- web
# 블로그 데이터베이스
blog-db:
image: mysql:8.0
container_name: blog-db
restart: unless-stopped
environment:
MYSQL_DATABASE: blog_db
MYSQL_USER: blog_user
MYSQL_PASSWORD: blog_secure_password
MYSQL_ROOT_PASSWORD: blog_root_password
volumes:
- blog_db_data:/var/lib/mysql
networks:
- web
# 쇼핑몰 WordPress (shop.mydomain.com)
shop-wordpress:
image: wordpress:latest
container_name: shop-wordpress
restart: unless-stopped
environment:
WORDPRESS_DB_HOST: shop-db
WORDPRESS_DB_USER: shop_user
WORDPRESS_DB_PASSWORD: shop_secure_password
WORDPRESS_DB_NAME: shop_db
WORDPRESS_CONFIG_EXTRA: |
define('WP_HOME', 'https://shop.mydomain.com');
define('WP_SITEURL', 'https://shop.mydomain.com');
define('FORCE_SSL_ADMIN', true);
volumes:
- shop_wordpress_data:/var/www/html
depends_on:
- shop-db
networks:
- web
# 쇼핑몰 데이터베이스
shop-db:
image: mysql:8.0
container_name: shop-db
restart: unless-stopped
environment:
MYSQL_DATABASE: shop_db
MYSQL_USER: shop_user
MYSQL_PASSWORD: shop_secure_password
MYSQL_ROOT_PASSWORD: shop_root_password
volumes:
- shop_db_data:/var/lib/mysql
networks:
- web
networks:
web:
driver: bridge
volumes:
blog_wordpress_data:
blog_db_data:
shop_wordpress_data:
shop_db_data:
🌐 Nginx 설정
nginx/nginx.conf
기본 Nginx 설정 파일입니다:
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# SSL Settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Gzip Settings
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private must-revalidate auth;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/x-javascript
application/xml+rss
application/javascript
application/json;
include /etc/nginx/conf.d/*.conf;
}
nginx/conf.d/blog.conf
블로그 사이트용 설정:
# HTTP 서버 - HTTPS로 리다이렉트
server {
listen 80;
server_name blog.mydomain.com;
# Let's Encrypt ACME 챌린지
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# HTTPS로 리다이렉트
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS 서버 - WordPress Blog
server {
listen 443 ssl http2;
server_name blog.mydomain.com;
# SSL 인증서 설정
ssl_certificate /etc/letsencrypt/live/blog.mydomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.mydomain.com/privkey.pem;
# SSL 보안 설정
ssl_session_timeout 1d;
ssl_session_cache shared:MozTLS:10m;
ssl_session_tickets off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
# WordPress 특화 설정
client_max_body_size 100M;
# WordPress 프록시 설정
location / {
proxy_pass http://blog-wordpress:80;
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;
proxy_set_header X-Forwarded-Host $host;
# WordPress 특화 헤더
proxy_redirect off;
proxy_buffering off;
}
# Let's Encrypt ACME 챌린지
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
}
nginx/conf.d/shop.conf
쇼핑몰 사이트용 설정:
# HTTP 서버 - HTTPS로 리다이렉트
server {
listen 80;
server_name shop.mydomain.com;
# Let's Encrypt ACME 챌린지
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# HTTPS로 리다이렉트
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS 서버 - WordPress Shop
server {
listen 443 ssl http2;
server_name shop.mydomain.com;
# SSL 인증서 설정
ssl_certificate /etc/letsencrypt/live/shop.mydomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/shop.mydomain.com/privkey.pem;
# SSL 보안 설정
ssl_session_timeout 1d;
ssl_session_cache shared:MozTLS:10m;
ssl_session_tickets off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
# WordPress 특화 설정
client_max_body_size 100M;
# WordPress 프록시 설정
location / {
proxy_pass http://shop-wordpress:80;
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;
proxy_set_header X-Forwarded-Host $host;
# WordPress 특화 헤더
proxy_redirect off;
proxy_buffering off;
}
# Let's Encrypt ACME 챌린지
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
}
🔐 SSL 인증서 자동 발급 스크립트
1. shell 스크립트 버전 : init-letsencrypt-multi.sh
#!/bin/bash
# 멀티 도메인 설정
declare -A domains_config=(
["blog.mydomain.com"]="blog.mydomain.com"
["shop.mydomain.com"]="shop.mydomain.com"
)
rsa_key_size=4096
data_path="./certbot"
email="admin@mydomain.com" # 실제 이메일로 변경
staging=0 # 테스트: 1, 운영: 0
# 색상 정의
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
echo -e "${GREEN}Let's Encrypt 멀티 도메인 인증서 초기화 시작${NC}"
# 기존 데이터 확인
if [ -d "$data_path" ]; then
read -p "기존 데이터가 발견되었습니다. 계속하시겠습니까? (y/N) " decision
if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then
exit
fi
fi
# TLS 매개변수 다운로드
if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then
echo -e "${YELLOW}### TLS 매개변수 다운로드 중...${NC}"
mkdir -p "$data_path/conf"
curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf"
curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem"
echo
fi
# 각 도메인별 더미 인증서 생성
for domain in "${!domains_config[@]}"; do
echo -e "${YELLOW}### 더미 인증서 생성 중: ${domain}${NC}"
path="/etc/letsencrypt/live/$domain"
mkdir -p "$data_path/conf/live/$domain"
docker-compose run --rm --entrypoint "\
openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1\
-keyout '$path/privkey.pem' \
-out '$path/fullchain.pem' \
-subj '/CN=localhost'" certbot
echo
done
# Nginx 시작
echo -e "${YELLOW}### Nginx 시작 중...${NC}"
docker-compose up --force-recreate -d nginx
# 각 도메인별 실제 인증서 발급
for domain in "${!domains_config[@]}"; do
echo -e "${BLUE}### 처리 중인 도메인: ${domain}${NC}"
# 기존 더미 인증서 삭제
echo -e "${YELLOW}### 더미 인증서 삭제 중: ${domain}${NC}"
docker-compose run --rm --entrypoint "\
rm -Rf /etc/letsencrypt/live/$domain && \
rm -Rf /etc/letsencrypt/archive/$domain && \
rm -Rf /etc/letsencrypt/renewal/$domain.conf" certbot
# 실제 인증서 요청
echo -e "${YELLOW}### 실제 인증서 요청 중: ${domain}${NC}"
case "$email" in
"") email_arg="--register-unsafely-without-email" ;;
*) email_arg="--email $email" ;;
esac
if [ $staging != "0" ]; then staging_arg="--staging"; fi
docker-compose run --rm --entrypoint "\
certbot certonly --webroot -w /var/www/certbot \
$staging_arg \
$email_arg \
-d $domain \
--rsa-key-size $rsa_key_size \
--agree-tos \
--force-renewal" certbot
if [ $? -eq 0 ]; then
echo -e "${GREEN}### ${domain} 인증서 발급 성공!${NC}"
else
echo -e "${RED}### ${domain} 인증서 발급 실패!${NC}"
fi
done
# Nginx 재시작
echo -e "${YELLOW}### Nginx 재시작 중...${NC}"
docker-compose exec nginx nginx -s reload
echo -e "${GREEN}### 완료! 모든 도메인의 인증서가 처리되었습니다.${NC}"
echo -e "${GREEN}### 인증서는 12시간마다 자동으로 갱신됩니다.${NC}"
2. Powershell 스크립트 버전 : init-letsencrypt-multi.ps1
# PowerShell 스크립트 - Let's Encrypt 멀티 도메인 인증서 설정
# 실행 전 PowerShell을 관리자 권한으로 실행하세요
# 멀티 도메인 설정 - 실제 값으로 변경하세요
$domains_config = @{
"blog.mydomain.com" = "blog.mydomain.com"
"shop.mydomain.com" = "shop.mydomain.com"
# 필요한 도메인을 추가하세요
# "new.mydomain.com" = "new.mydomain.com"
}
$rsa_key_size = 4096
$data_path = ".\certbot"
$email = "admin@mydomain.com" # 실제 이메일로 변경
$staging = 0 # 테스트할 때는 1로 설정, 실제 운영에서는 0
# 색상 함수 정의
function Write-ColorOutput($ForegroundColor) {
$fc = $host.UI.RawUI.ForegroundColor
$host.UI.RawUI.ForegroundColor = $ForegroundColor
if ($args) {
Write-Output $args
}
$host.UI.RawUI.ForegroundColor = $fc
}
function Write-Red($text) { Write-ColorOutput Red $text }
function Write-Green($text) { Write-ColorOutput Green $text }
function Write-Yellow($text) { Write-ColorOutput Yellow $text }
function Write-Blue($text) { Write-ColorOutput Blue $text }
Write-Green "Let's Encrypt 멀티 도메인 인증서 초기화 스크립트"
# Docker 및 Docker Compose 확인
try {
$dockerVersion = docker --version
Write-Green "Docker 확인됨: $dockerVersion"
} catch {
Write-Red "Docker가 설치되어 있지 않거나 실행되지 않았습니다."
Write-Red "Docker Desktop을 설치하고 실행한 후 다시 시도하세요."
exit 1
}
try {
$composeVersion = docker-compose --version
Write-Green "Docker Compose 확인됨: $composeVersion"
} catch {
Write-Red "Docker Compose가 설치되어 있지 않습니다."
Write-Red "Docker Desktop과 함께 설치되는 Docker Compose를 확인하세요."
exit 1
}
# 관리자 권한 확인
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
$isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
Write-Yellow "주의: 관리자 권한으로 실행하지 않았습니다."
Write-Yellow "Docker 명령어 실행에 문제가 있을 수 있습니다."
}
# 기존 데이터 확인
if (Test-Path $data_path) {
$decision = Read-Host "기존 데이터가 발견되었습니다. 계속하시겠습니까? (y/N)"
if ($decision -ne "Y" -and $decision -ne "y") {
exit
}
}
# TLS 매개변수 다운로드
$sslConfigPath = Join-Path $data_path "conf\options-ssl-nginx.conf"
$dhParamsPath = Join-Path $data_path "conf\ssl-dhparams.pem"
if (-not (Test-Path $sslConfigPath) -or -not (Test-Path $dhParamsPath)) {
Write-Yellow "### TLS 매개변수 다운로드 중..."
$confDir = Join-Path $data_path "conf"
if (-not (Test-Path $confDir)) {
New-Item -Path $confDir -ItemType Directory -Force | Out-Null
}
try {
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf" -OutFile $sslConfigPath
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem" -OutFile $dhParamsPath
Write-Green "TLS 매개변수 다운로드 완료"
} catch {
Write-Red "TLS 매개변수 다운로드 실패: $($_.Exception.Message)"
exit 1
}
}
# 각 도메인별 더미 인증서 생성
foreach ($domain in $domains_config.Keys) {
Write-Yellow "### 더미 인증서 생성 중: $domain"
$livePath = Join-Path $data_path "conf\live\$domain"
if (-not (Test-Path $livePath)) {
New-Item -Path $livePath -ItemType Directory -Force | Out-Null
}
$dockerCmd = "docker-compose run --rm --entrypoint `"openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1 -keyout '/etc/letsencrypt/live/$domain/privkey.pem' -out '/etc/letsencrypt/live/$domain/fullchain.pem' -subj '/CN=localhost'`" certbot"
try {
Invoke-Expression $dockerCmd
Write-Green "더미 인증서 생성 완료: $domain"
} catch {
Write-Red "더미 인증서 생성 실패: $domain - $($_.Exception.Message)"
}
}
# Nginx 시작
Write-Yellow "### Nginx 시작 중..."
try {
Invoke-Expression "docker-compose up --force-recreate -d nginx"
Write-Green "Nginx 시작 완료"
Start-Sleep -Seconds 5 # Nginx가 완전히 시작될 때까지 대기
} catch {
Write-Red "Nginx 시작 실패: $($_.Exception.Message)"
exit 1
}
# 각 도메인별 실제 인증서 발급
foreach ($domain in $domains_config.Keys) {
Write-Blue "### 처리 중인 도메인: $domain"
# 기존 더미 인증서 삭제
Write-Yellow "### 더미 인증서 삭제 중: $domain"
$removeCmd = "docker-compose run --rm --entrypoint `"rm -Rf /etc/letsencrypt/live/$domain && rm -Rf /etc/letsencrypt/archive/$domain && rm -Rf /etc/letsencrypt/renewal/$domain.conf`" certbot"
try {
Invoke-Expression $removeCmd
} catch {
Write-Yellow "더미 인증서 삭제 중 오류 (정상적인 경우도 있음): $($_.Exception.Message)"
}
# 실제 인증서 요청
Write-Yellow "### 실제 인증서 요청 중: $domain"
# 이메일 매개변수 구성
if ([string]::IsNullOrEmpty($email)) {
$email_arg = "--register-unsafely-without-email"
} else {
$email_arg = "--email $email"
}
# 스테이징 매개변수 구성
$staging_arg = ""
if ($staging -ne 0) {
$staging_arg = "--staging"
}
# 인증서 발급 명령어 구성
$certbotCmd = "docker-compose run --rm --entrypoint `"certbot certonly --webroot -w /var/www/certbot $staging_arg $email_arg -d $domain --rsa-key-size $rsa_key_size --agree-tos --force-renewal`" certbot"
try {
Invoke-Expression $certbotCmd
if ($LASTEXITCODE -eq 0) {
Write-Green "### $domain 인증서 발급 성공!"
} else {
Write-Red "### $domain 인증서 발급 실패!"
Write-Red "### DNS 설정과 도메인 접근성을 확인하세요."
}
} catch {
Write-Red "### $domain 인증서 발급 중 오류: $($_.Exception.Message)"
Write-Red "### DNS 설정과 도메인 접근성을 확인하세요."
}
Write-Output ""
}
# Nginx 재시작
Write-Yellow "### Nginx 재시작 중..."
try {
Invoke-Expression "docker-compose exec nginx nginx -s reload"
Write-Green "Nginx 재시작 완료"
} catch {
Write-Yellow "Nginx 재시작 중 오류: $($_.Exception.Message)"
Write-Yellow "수동으로 컨테이너를 재시작해야 할 수 있습니다."
}
Write-Green "### 완료! 모든 도메인의 인증서가 처리되었습니다."
Write-Green "### 인증서는 12시간마다 자동으로 갱신됩니다."
# 인증서 상태 확인
Write-Blue "### 인증서 상태 확인:"
try {
Invoke-Expression "docker-compose exec certbot certbot certificates"
} catch {
Write-Yellow "인증서 상태 확인 중 오류: $($_.Exception.Message)"
}
Write-Green "스크립트 실행 완료!"
Read-Host "아무 키나 눌러서 종료하세요"
2. 배치 파일 버전 : init-letsencrypt-multi.bat
@echo off
chcp 65001 >nul
setlocal enabledelayedexpansion
REM Let's Encrypt 멀티 도메인 인증서 초기화 스크립트
REM Bash 원본을 Windows 배치 파일로 변환
REM ===============================
REM 멀티 도메인 설정 - 실제 값으로 변경하세요
REM ===============================
REM 도메인 목록 (공백으로 구분)
set "domains=blog.mydomain.com shop.mydomain.com"
REM 필요한 도메인을 추가하세요
REM set "domains=blog.mydomain.com shop.mydomain.com new.mydomain.com"
set "rsa_key_size=4096"
set "data_path=.\certbot"
set "email=admin@mydomain.com"
set "staging=0"
REM 색상 정의 (Windows 10 이상)
set "RED=[91m"
set "GREEN=[92m"
set "YELLOW=[93m"
set "BLUE=[94m"
set "NC=[0m"
echo %GREEN%Let's Encrypt 멀티 도메인 인증서 초기화 스크립트%NC%
REM 관리자 권한 확인 (루트 권한 확인과 동일)
net session >nul 2>&1
if not errorlevel 1 (
echo %YELLOW%주의: 관리자 권한으로 실행하고 있습니다.%NC%
)
REM 기존 데이터 확인
if exist "%data_path%" (
set /p "decision=기존 데이터가 발견되었습니다. 계속하시겠습니까? (y/N) "
if /i not "!decision!"=="y" (
exit
)
)
REM TLS 매개변수 다운로드
if not exist "%data_path%\conf\options-ssl-nginx.conf" (
echo %YELLOW%### TLS 매개변수 다운로드 중...%NC%
if not exist "%data_path%\conf" mkdir "%data_path%\conf"
powershell -Command "Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf' -OutFile '%data_path%\conf\options-ssl-nginx.conf'"
powershell -Command "Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem' -OutFile '%data_path%\conf\ssl-dhparams.pem'"
echo.
)
REM 각 도메인별 더미 인증서 생성
for %%d in (%domains%) do (
echo %YELLOW%### 더미 인증서 생성 중: %%d%NC%
set "path=/etc/letsencrypt/live/%%d"
if not exist "%data_path%\conf\live\%%d" mkdir "%data_path%\conf\live\%%d"
docker-compose run --rm --entrypoint "openssl req -x509 -nodes -newkey rsa:%rsa_key_size% -days 1 -keyout '!path!/privkey.pem' -out '!path!/fullchain.pem' -subj '/CN=localhost'" certbot
echo.
)
REM Nginx 시작
echo %YELLOW%### Nginx 시작 중...%NC%
docker-compose up --force-recreate -d nginx
echo.
REM 각 도메인별 실제 인증서 발급
for %%d in (%domains%) do (
echo %BLUE%### 처리 중인 도메인: %%d%NC%
REM 기존 더미 인증서 삭제
echo %YELLOW%### 더미 인증서 삭제 중: %%d%NC%
docker-compose run --rm --entrypoint "rm -Rf /etc/letsencrypt/live/%%d && rm -Rf /etc/letsencrypt/archive/%%d && rm -Rf /etc/letsencrypt/renewal/%%d.conf" certbot
echo.
REM 실제 인증서 요청
echo %YELLOW%### 실제 인증서 요청 중: %%d%NC%
REM 이메일 매개변수 구성
if "%email%"=="" (
set "email_arg=--register-unsafely-without-email"
) else (
set "email_arg=--email %email%"
)
REM 스테이징 매개변수 구성
set "staging_arg="
if "%staging%" neq "0" set "staging_arg=--staging"
REM 인증서 발급
docker-compose run --rm --entrypoint "certbot certonly --webroot -w /var/www/certbot !staging_arg! !email_arg! -d %%d --rsa-key-size %rsa_key_size% --agree-tos --force-renewal" certbot
if errorlevel 1 (
echo %RED%### %%d 인증서 발급 실패!%NC%
echo %RED%### DNS 설정과 도메인 접근성을 확인하세요.%NC%
) else (
echo %GREEN%### %%d 인증서 발급 성공!%NC%
)
echo.
)
REM Nginx 재시작
echo %YELLOW%### Nginx 재시작 중...%NC%
docker-compose exec nginx nginx -s reload
echo %GREEN%### 완료! 모든 도메인의 인증서가 처리되었습니다.%NC%
echo %GREEN%### 인증서는 12시간마다 자동으로 갱신됩니다.%NC%
REM 인증서 상태 확인
echo %BLUE%### 인증서 상태 확인:%NC%
docker-compose exec certbot certbot certificates
pause
🚀 설치 및 실행 가이드
1. 사전 준비
DNS 설정
blog.mydomain.com A YOUR_SERVER_IP
shop.mydomain.com A YOUR_SERVER_IP
서버 환경
- Docker 및 Docker Compose 설치
- 80, 443 포트 개방
- 방화벽 설정 확인
2. 설정 파일 수정
도메인 변경
docker-compose.yml: WordPress 환경변수의 도메인 수정nginx/conf.d/*.conf:server_name변경init-letsencrypt-multi.sh: 도메인 배열 및 이메일 수정
보안 설정
- 데이터베이스 비밀번호 변경
- WordPress 시크릿 키 설정 (선택사항)
3. 인증서 발급 및 실행
# 1. 실행 권한 부여
chmod +x init-letsencrypt-multi.sh
# 2. 테스트 모드로 먼저 실행 (staging=1)
./init-letsencrypt-multi.sh
# 3. 성공 확인 후 운영 모드로 실행 (staging=0)
./init-letsencrypt-multi.sh
# 4. 모든 서비스 시작
docker-compose up -d
# 5. 상태 확인
docker-compose ps
4. WordPress 초기 설정
각 도메인에 접속하여 WordPress 초기 설정을 진행합니다:
https://blog.mydomain.com– 블로그 설정https://shop.mydomain.com– 쇼핑몰 설정
🔄 자동 갱신 메커니즘
인증서 갱신 주기
- Certbot: 12시간마다 인증서 갱신 확인
- 실제 갱신: 만료 30일 전부터 갱신 시작
- Nginx 리로드: 6시간마다 설정 리로드
갱신 상태 확인
# 인증서 상태 확인
docker-compose exec certbot certbot certificates
# 수동 갱신 테스트
docker-compose exec certbot certbot renew --dry-run
# 로그 확인
docker-compose logs certbot
🛠️ 관리 및 유지보수
새 도메인 추가하기
- DNS 설정: 새 도메인의 A 레코드 추가
- Nginx 설정: 새
.conf파일 생성 - Docker Compose: 새 WordPress 서비스 추가
- 스크립트 수정:
domains_config에 도메인 추가 - 인증서 발급:
./init-letsencrypt-multi.sh재실행
유용한 명령어
# 전체 서비스 재시작
docker-compose restart
# 특정 서비스만 재시작
docker-compose restart blog-wordpress
# 로그 실시간 확인
docker-compose logs -f nginx
# 데이터베이스 접속
docker-compose exec blog-db mysql -u blog_user -p blog_db
# WordPress 파일 백업
docker-compose exec blog-wordpress tar -czf /tmp/wp-backup.tar.gz /var/www/html
백업 전략
데이터베이스 백업
# 블로그 DB 백업
docker-compose exec blog-db mysqldump -u blog_user -p blog_db > blog_backup.sql
# 쇼핑몰 DB 백업
docker-compose exec shop-db mysqldump -u shop_user -p shop_db > shop_backup.sql
파일 백업
# WordPress 파일 백업
docker run --rm -v wordpress-multi_blog_wordpress_data:/source -v $(pwd):/backup alpine tar -czf /backup/blog_files.tar.gz -C /source .
🔧 문제 해결
자주 발생하는 문제들
1. 인증서 발급 실패
# DNS 전파 확인
nslookup blog.mydomain.com
# 포트 접근성 확인
telnet your-server-ip 80
# Let's Encrypt 제한 확인 (주간 50회 제한)
2. WordPress 연결 오류
# 컨테이너 상태 확인
docker-compose ps
# 네트워크 연결 확인
docker-compose exec nginx ping blog-wordpress
# WordPress 로그 확인
docker-compose logs blog-wordpress
3. SSL 인증서 갱신 실패
# Certbot 로그 확인
docker-compose logs certbot
# 수동 갱신 시도
docker-compose exec certbot certbot renew --force-renewal
🎉 마무리
이제 하나의 서버에서 여러 WordPress 사이트를 SSL과 함께 안전하게 운영할 수 있습니다!
이 구성의 장점:
- ✅ 간편한 관리: Docker Compose로 모든 서비스 통합 관리
- ✅ 자동 SSL: Let’s Encrypt 인증서 자동 갱신
- ✅ 확장성: 새 도메인 추가가 간단
- ✅ 독립성: 각 WordPress 사이트가 완전히 독립적
- ✅ 보안: 최신 SSL/TLS 설정 및 보안 헤더 적용
운영 중 궁금한 점이나 문제가 생기면 Docker 로그를 통해 디버깅하고, 정기적인 백업을 잊지 마세요!
참고 자료: