귀찮음을 모두 이겨내고 블로그 서버를 옮겼다

2026-05-31

블로그 서버 옮기는 자체는 어려운 일이 아니다. 사이즈가 많거나 아키텍처가 복잡하지도 않다. 그냥 귀찮음의 문제다. 기존에 쓰던 인스턴스에 분명 처음엔 블로그 혼자 있었는데, 어느샌가 기생한 다른 사이트가 늘었다. 딱히 트래픽을 유발하진 않는데, 어쨌거나 Nginx에서 분기 설정을 해야 했다. 기생 사이트와 로그도 분리하고 모든 걸 분리해놨지만, 괜히 스펙도 매우 낮은 서버인데 여러 서비스가 같이 있다는 게 신경 쓰였다.

워드프레스 서버 이사는 간단하다. 만약 처음에 설치한 구성 환경을 똑바로 이해하고 있다는 조건하에서는 정말 간단하다. 내 설치 환경은 앞서 글에서 말했던 도커 없이 설치한 조건이다.

Nginx나 PHP는 상관없이 새로 설치하면 되고, 워드프레스 전체 파일과 DB만 잘 떠서 페어를 맞춰서 옮긴다는 게 기본 원리다.

만약 주변에 스냅샷 무새들이 있다면, AWS에서 스냅샷 떠서 새 인스턴스에 올리면 된다고 할 것이다. 그들은 내 서버 리소스를 책임져주지 않는다. 스냅샷은 동급 또는 그 이상 스펙의 서버로만 옮길 수 있다. 또한 AWS를 떠날 수도 없다. 목적이 단순 스펙업이라면 매우 편하고 좋은 선택일 수 있다. 하지만, 단순 스펙업만을 목표로 서버를 이전하진 않는다. 스냅샷 무새는 어차피 내 지갑 사정도 전혀 고려하지 않는다. 즉, 스냅샷 무새는 나는 그냥 조용히 무시한다.

백업과 복원 스크립트

백업과 복원 스크립트를 간단히 만들어 두고, 필요할 때 이걸 수정하면서 썼다. 완벽하게 이걸로 커버할 수 있을 정도로 확실하게 정적인 상태로 운영할 수 있다면 베스트다. 그렇지 않다면 그냥 적당히 수정해서 써야한다.

백업

워드프레스 파일과 DB 용량에 따라 소요시간 편차는 매우 크다. 대충 전체 용량 기준으로 워드프레스 파일이 100GB, DB 덤프 파일이 1GB 정도일 때 30~40분 가량 걸렸다. 이 속도도 서버 스펙에 따라 많이 달라지겠지만, 소요시간은 적당히 예측하고 있어야 편하다.

#!/bin/bash
set -e

# 설정 매개 변수 지정
BACKUP_DIR="/backup"
WORDPRESS_DIR="/a36/wordpress"
DATE=$(date +%Y%m%d_%H%M%S)

# 압축 및 덤프할 파일명
WORDPRESS_FILE="wordpress_${DATE}.tar.gz"
WORDPRESS_DB="wordpress_DB_${DATE}.sql"

# DB 정보
DB_HOST="IP Address" # 단일 인스턴스의 경우 localhost
DB_NAME="a36_wp"
DB_USER="a36_root"
DB_PASSWORD="DB_password"

# 스크립트 시작
mkdir -p "$BACKUP_DIR"
logfile="${BACKUP_DIR}/backup.log"

log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$logfile"
}

log "=== 백업 프로세스를 시작합니다 ==="

# 1. 워드프레스 파일 압축
if tar -zcf "${BACKUP_DIR}/${WORDPRESS_FILE}" -C "$(dirname "$WORDPRESS_DIR")" "$(basename "$WORDPRESS_DIR")"; then
    log "웹 서비스 리소스 백업 완료: ${WORDPRESS_FILE}"
else
    log "[CRITICAL] 웹 리소스 압축 중단 오류"
    exit 1
fi

# 2. DB 백업
if mysqldump --single-transaction -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" > "${BACKUP_DIR}/${WORDPRESS_DB}"; then
    log "데이터베이스 덤프 백업 완료: ${WORDPRESS_DB}"
else
    log "[CRITICAL] 데이터베이스 덤프 연산 중단 오류"
    exit 1
fi

log "=== 모든 백업이 끝났습니다 ==="

주기적으로 백업을 돌리겠다는 마음을 먹었다면, 우선 용량부터 확실히 계산해야 한다. 로컬 호스트든 원격 호스트든, 가랑비에 옷 젖기 딱 좋다.

# 원격 백업 서버 정보 (로컬 호스트에 해도 됨)
REMOTE_HOST="backup_server_host"
REMOTE_USER="backup_server_user"
REMOTE_DIR="/backup"

# 안전 대역폭 제한을 걸고 원격 서버로 복사
if [ -n "$REMOTE_HOST" ] && [ "$REMOTE_HOST" != "backup_server_host" ]; then
    log "원격 서버로 복사 시작(대역폭 한도 지정)"
    scp -l 200000 "${BACKUP_DIR}/${WORDPRESS_FILE}" "${BACKUP_DIR}/${WORDPRESS_DB}" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/" || log "[WARNING] 원격 서버로 복사 실패"
fi

# 60일 경과 백업 파일 정리
log "60일 경과 노후 파일 정리 시작"
find "$BACKUP_DIR" -type f \( -name "wordpress_*" -o -name "wordpress_DB_*" \) -mtime +60 -exec rm -f {} \;

백업 주기를 정했다면, crontab에 걸어두면 된다.

# 실행 권한 부여 및 크론 주기 등록
sudo chmod 700 /opt/backup.sh

# crontab -e: 매일 새벽 3시 무단 구동 배치
0 3 * * * /opt/backup.sh > /dev/null 2>&1

주기적인 백업까진 고민하지말고, 맨 위에 있는 단순 압축 스크립트만 필요할 때 써서 파일질라로 파일 빼고 넣으면 된다. 주기적인 백업은 사실 AWS에서는 스냅샷에 맡기는 게 신경을 덜 쓰는 방법이다. 주기적인 백업이 왜 필요했는가를 먼저 고민해보기를 바란다.

복원

복원은 목적이 먼저 명확해야 한다. 단순 서버 이전인지, 동일한 조건인 테스트 서버를 만들기 위함인지에 따라 설정값이 바뀐다. 서버 이전인 경우에는 기존 서버는 결국 삭제할 것이고 도메인이나 워드프레스 설정값 자체가 다 그대로 이동하는 것이므로, 기존 서버와 최대한 똑같은 위치만 맞추면 된다. 단, 디렉터리를 다르게 구성한다면 이미지 로딩이 올바르게 되는지는 꼭 확인하길 바란다.

기존 운영 서버는 그대로 있는 상태로 사이트를 하나 더 올리는 테스트 서버 같은 개념이라면, 도메인이 바뀐다. 워드프레스 설정값이나 DB안에 있는 URL값을 모조리 치환해야 한다. 디렉터리 위치값도 달라진다면, 그것도 다 맞춰서 DB에서 값을 바꿔야 한다. 꼬이기 딱 좋은 데이터다.

서버 이전

서버 이전은 그냥 다 똑같으면 된다. Nginx, PHP, MariaDB는 미리 설치해서 설정을 마친 후에 그대로 남은 빈자리를 채운다고 보면 된다. 간단하게는 압축해제 명령어와 mysql 덤프 삽입 명령어만 알면 된다.

sudo tar -zxvf /backup$targzfile -C /.
sudo mysql -u account -ppassword $sqlname < /tmp/$sqlfile

이걸 엮고 엮고 엮어서, 복잡하게 만들면 한 없이 복잡하게 만들어서 자동화 스크립트를 만들 수 있다. 개인적으로 자동화 스크립트는 서버 이전에선 쓰지 않는다. 쓸데없이 디렉터리나 DB 이름만 난잡하게 길어져서, 운영할 때 디렉터리 조회만 귀찮아지기더라.

#!/bin/bash
set -e

# 설정 매개 변수 지정
RESTORE_SOURCE_DIR="/tmp"
WORDPRESS_TARGET_DIR="/a36/wordpress"
DB_TARGET_NAME="a36_wp"

DB_HOST="localhost"
DB_USER="a36_root"
DB_PASSWORD="DB_password"
DB_PREFIX="wp_"  # 데이터베이스 테이블 접두사 표준

# 최신 백업 파일 획득
sqlfile=$(ls -t ${RESTORE_SOURCE_DIR}/*.sql 2>/dev/null | head -n 1)
targzfile=$(ls -t ${RESTORE_SOURCE_DIR}/*.tar.gz 2>/dev/null | head -n 1)

if [ -z "$sqlfile" ] || [ -z "$targzfile" ]; then
    echo "[ERROR] 마이그레이션을 수행하기 위한 *.sql 및 *.tar.gz 소스 파일들을 ${RESTORE_SOURCE_DIR} 내부에서 확인하지 못했습니다."
    exit 1
fi

echo "마이그레이션 타겟 소스 획득:"
echo " - DB 덤프: $sqlfile"
echo " - 웹 리소스: $targzfile"

# 소스 압축 해제 및 권한 설정
echo "워드프레스 파일 압축 해제"
sudo mkdir -p "$WORDPRESS_TARGET_DIR"
sudo tar -zxf "$targzfile" -C "$(dirname "$WORDPRESS_TARGET_DIR")"

echo "웹 리소스 소유권 및 권한 안전 복원 (www-data 표준)"
sudo chown -R www-data:www-data "$WORDPRESS_TARGET_DIR"
sudo find "$WORDPRESS_TARGET_DIR" -type d -exec chmod 755 {} \;
sudo find "$WORDPRESS_TARGET_DIR" -type f -exec chmod 644 {} \;

# DB 데이터 복원
echo "운영 데이터베이스 무결성 복원"
sudo mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASSWORD" -e "CREATE DATABASE IF NOT EXISTS \`${DB_TARGET_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
sudo mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_TARGET_NAME" < "$sqlfile"

# wp-config.php 설정 파일 내의 새로운 DB 접속 매핑
echo "설정 환경 변경"
sudo sed -i "s/define(\s*'DB_NAME',\s*'.*'\s*);/define( 'DB_NAME', '${DB_TARGET_NAME}' );/g" "${WORDPRESS_TARGET_DIR}/wp-config.php"
sudo sed -i "s/define(\s*'DB_USER',\s*'.*'\s*);/define( 'DB_USER', '${DB_USER}' );/g" "${WORDPRESS_TARGET_DIR}/wp-config.php"
sudo sed -i "s/define(\s*'DB_PASSWORD',\s*'.*'\s*);/define( 'DB_PASSWORD', '${DB_PASSWORD}' );/g" "${WORDPRESS_TARGET_DIR}/wp-config.php"
sudo sed -i "s/define(\s*'DB_HOST',\s*'.*'\s*);/define( 'DB_HOST', '${DB_HOST}' );/g" "${WORDPRESS_TARGET_DIR}/wp-config.php"

echo "서버 데이터 복구 완료"
echo "워드프레스 경로: $WORDPRESS_TARGET_DIR"
echo "데이터베이스: $DB_TARGET_NAME"

테스트 서버 생성

앞서 말한 것처럼, 테스트 서버는 원본 사이트와 URL이 바뀐다는 특징을 꼭 기억해야 한다. 위의 스크립트에 아래 내용을 추가하면 된다.

# 도메인 정보
PROD_DOMAIN="원본 사이트 도메인"
STAGING_DOMAIN="테스트 사이트 도메인"

# 도메인 일괄 교체
echo "데이터베이스 도메인 일괄 안전 교체: ${PROD_DOMAIN} -> ${STAGING_DOMAIN}"
sudo mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_TARGET_NAME" <<EOF
UPDATE ${DB_PREFIX}options SET option_value = REPLACE(option_value, 'https://${PROD_DOMAIN}', 'https://${STAGING_DOMAIN}') WHERE option_name = 'home' OR option_name = 'siteurl';
UPDATE ${DB_PREFIX}options SET option_value = REPLACE(option_value, 'http://${PROD_DOMAIN}', 'http://${STAGING_DOMAIN}') WHERE option_name = 'home' OR option_name = 'siteurl';
UPDATE ${DB_PREFIX}posts SET post_content = REPLACE(post_content, 'https://${PROD_DOMAIN}', 'https://${STAGING_DOMAIN}');
UPDATE ${DB_PREFIX}posts SET post_content = REPLACE(post_content, 'http://${PROD_DOMAIN}', 'http://${STAGING_DOMAIN}');
UPDATE ${DB_PREFIX}posts SET guid = REPLACE(guid, 'https://${PROD_DOMAIN}', 'https://${STAGING_DOMAIN}');
UPDATE ${DB_PREFIX}posts SET guid = REPLACE(guid, 'http://${PROD_DOMAIN}', 'http://${STAGING_DOMAIN}');
UPDATE ${DB_PREFIX}postmeta SET meta_value = REPLACE(meta_value, 'https://${PROD_DOMAIN}', 'https://${STAGING_DOMAIN}');
UPDATE ${DB_PREFIX}postmeta SET meta_value = REPLACE(meta_value, 'http://${PROD_DOMAIN}', 'http://${STAGING_DOMAIN}');
EOF

# 테스트 서버에 맞는 설정으로 변경
echo " 테스트 서버 전용 모드"

# 검색 엔진 노출 차단
echo "검색 엔진 차단 설정 업데이트"
sudo mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_TARGET_NAME" <<EOF
UPDATE ${DB_PREFIX}options SET option_value = '0' WHERE option_name = 'blog_public';
EOF

# robots.txt 설정 변경
echo "robots.txt 검색 엔진 수집 거부 규칙 추가"
sudo tee "${WORDPRESS_TARGET_DIR}/robots.txt" > /dev/null <<EOF
User-agent: *
Disallow: /
EOF
sudo chown www-data:www-data "${WORDPRESS_TARGET_DIR}/robots.txt"
sudo chmod 644 "${WORDPRESS_TARGET_DIR}/robots.txt"

echo " 테스트 서버 세팅 완료"
echo " 워드프레스 경로: $WORDPRESS_TARGET_DIR"
echo " 데이터베이스: $DB_TARGET_NAME"
echo " 도메인: $STAGING_DOMAIN"

WP-CLI를 쓰는 환경이라면, 좀 더 직렬화된 방식으로 안정적인 변환을 할 수 있다. DB 규모 자체가 얼마 안 되는 소형 사이트는 직렬화 보완이 크게 필요치 않다. 수시로 데이터가 계속 업데이트되는 대규모 사이트일 땐, 직렬화 방식을 쓰는 것이 여러모로 안정적이므로 WP-CLI를 쓰거나, DB 업데이트를 단위별로 쪼개어 하는 것이 좋을 수 있다.

즉, 자동화 스크립트는 여러 번 테스트를 거쳐서 확실하다 싶을 때 써야 한다. 어설프게 썼다가는 설정값만 꼬여서 된통 제자리만 헛돌게 된다.

워드프레스 이관 플러그인은 왜 안 쓰는가?

쓰면 좋다. 편하고 자잘하게 신경쓰지 않아도 된다. 그런데 플러그인마다 특성이 다르고 특정 기능은 유료로만 동작한다.

플러그인은 결국 플러그인이 인지할 수 있는 권한 범위까지만 서버를 건드릴 수 있다. 만약, 내가 쓰는 커스텀 파일이나 특별한 플러그인이 그 권한 밖에 있다면, 워드프레스 이관 플러그인은 그걸 챙겨주지 못한다. 확실하지 않으면, 승부를 걸지마라는 말이 있지 않은가. 워드프레스 이관 플러그인 중에 확실한 걸 아직 경험해보지 못했다. 그렇다면, 손으로 해야 하지 않겠는가.