운영 워드프레스를 건드리지 않고 로컬 Docker로 클론해 개발하기
운영 중인 워드프레스 사이트를 직접 열어 코드를 수정하며 작업해야 할 때가 있습니다. 하지만 운영 서버에 바로 손을 대는 건 언제나 불안한 일입니다. 한 줄 잘못 저장하면 실제 사용자가 보는 화면이 깨지고, 데이터베이스에 잘못된 변경 한 번이면 되돌리기 어려운 일이 벌어지기도 합니다.
그래서 이번에는 운영 서버에는 단 한 글자도 쓰지 않으면서, 똑같은 사이트를 로컬 Docker 위에 그대로 복제해 개발 환경을 만드는 방법을 정리해보려 합니다. 막상 해보니 단순한 파일 복사가 아니라, 도메인과 멀티사이트, PHP 버전, 문자 인코딩에서 생각보다 많은 함정이 숨어 있었습니다.
왜 굳이 로컬로 복제해야 했는가
워드프레스는 코드와 콘텐츠가 한 서버 안에 뒤섞여 있는 구조입니다. 테마와 플러그인은 파일로, 글과 설정은 데이터베이스로 존재합니다. 그래서 기능 하나를 손보려 해도 파일을 고치는 동시에 DB의 데이터를 들여다봐야 하는 경우가 많습니다.
운영 서버에서 이 작업을 그대로 하면, 작업하는 모든 순간이 곧 실제 서비스에 반영됩니다. 저장 버튼이 곧 배포 버튼인 셈입니다. 스테이징 환경이 따로 없는 소규모 사이트라면 더욱 그렇습니다. 결국 운영을 건드리지 않고도 운영과 똑같이 동작하는 환경이 손에 필요했고, 그 답이 로컬 Docker였습니다.
첫 번째 원칙 — 운영에는 "읽기"만 한다
가장 중요하게 정한 원칙은 단 하나였습니다. 운영 서버와 운영 DB에는 어떤 쓰기도 하지 않는다. 복제에 필요한 작업은 전부 읽기 동작뿐입니다. 데이터베이스는 덤프를 뜨고, 파일은 내려받기만 합니다.
데이터베이스 덤프는 --single-transaction 옵션을 쓰면 일관된 스냅샷을 읽어오면서도 테이블을 잠그지 않습니다. 즉 운영 서비스에 영향을 주지 않는 순수 읽기 동작입니다.
mysqldump -h <HOST> -u <USER> -p \
--single-transaction --no-tablespaces \
--default-character-set=utf8mb4 <DB_NAME> > dump.sql
여기서 한 가지 짚고 넘어갈 점이 있습니다. 많은 공유 호스팅은 외부에서 데이터베이스(3306 포트)로 직접 접속하는 것을 기본적으로 막아둡니다. "DB 접속 정보를 안다"는 것과 "그 정보로 외부에서 접속이 된다"는 건 전혀 다른 이야기였습니다. 포트가 열려 있는지부터 확인하는 것이 순서였습니다.
파일 복제에서도 함정이 있었습니다. 제가 다룬 서버는 SFTP는 되지만 셸 명령 실행은 막혀 있는 계정이었습니다. 이런 계정에서는 rsync가 동작하지 않습니다. rsync는 원격에서 자신의 프로세스를 실행해야 하는데, 셸이 막혀 있으니 거부당하기 때문입니다. 대신 SFTP 프로토콜만으로 병렬 미러링이 가능한 lftp를 사용했습니다.
lftp -u <USER>,<PASS> sftp://<HOST> -e "
set sftp:auto-confirm yes;
mirror --parallel=6 /웹루트경로 ./site; bye"
의외의 복병 — IDE의 자동 업로드
읽기만 하겠다는 원칙을 세워두고도, 가장 위험했던 건 엉뚱한 곳에 있었습니다. PhpStorm 같은 IDE의 Deployment 기능에는 자동 업로드(automatic upload) 옵션이 있습니다. 켜져 있으면 로컬 파일을 저장하는 순간 그 변경이 운영 서버로 그대로 올라갑니다.
로컬에서 개발하려고 설정 파일을 로컬용으로 고치는 순간, 그 변경이 운영의 설정을 덮어쓸 수도 있다는 뜻입니다. 그래서 가장 먼저 한 일이 자동 업로드를 꺼두는 것이었습니다. "운영에 쓰지 않는다"는 원칙은 명령어뿐 아니라 도구 설정에서도 지켜야 했습니다.
로컬 Docker로 같은 환경 재현하기
데이터와 파일을 확보했다면, 이제 그것을 돌릴 환경이 필요합니다. 워드프레스는 결국 웹서버 + PHP + MySQL/MariaDB의 조합입니다. Docker Compose로 이 세 가지를 묶었습니다.
여기서 핵심은 운영의 PHP 버전과 데이터베이스 종류를 맞추는 것입니다. 버전이 어긋나면 오래된 플러그인이 치명적 오류(fatal error)를 내며 사이트가 아예 뜨지 않을 수 있습니다.
services:
web:
image: wordpress:php7.3-apache # 운영 PHP 버전에 맞춤
ports:
- "80:80"
volumes:
- ./site:/var/www/html
db:
image: mariadb:10.11 # 운영 DB 종류에 맞춤 (MariaDB)
environment:
MARIADB_DATABASE: wp
MARIADB_USER: wp
MARIADB_PASSWORD: wp_pass
MARIADB_ROOT_PASSWORD: root_pass
volumes:
- db_data:/var/lib/mysql
volumes:
db_data:
그다음 내려받은 wp-config.php를 로컬용으로 바꿉니다. 데이터베이스 호스트를 Docker 서비스 이름(db)으로, 계정과 비밀번호를 로컬 컨테이너 값으로 교체하는 것입니다. 이때 운영의 인증 정보가 담긴 설정 파일은 절대 다시 운영으로 올라가지 않도록 주의해야 합니다.
도메인이라는 함정 — 특히 멀티사이트
컨테이너를 띄우고 http://localhost로 접속했더니, 화면이 곧장 운영 도메인으로 튕겨 나갔습니다. 워드프레스의 고질적인 함정입니다. DB의 wp_options 테이블에 사이트 주소(siteurl, home)가 운영 도메인으로 박혀 있기 때문입니다.
단순히 SQL로 값을 바꾸면 될 것 같지만, 그렇지 않습니다. 페이지 빌더나 슬라이더 플러그인은 설정을 직렬화(serialize)된 형태로 저장합니다. 직렬화 문자열은 길이 정보를 함께 담고 있어서, 단순 치환으로 문자열 길이가 바뀌면 데이터가 깨집니다. 그래서 WP-CLI의 search-replace를 써야 합니다. 직렬화된 데이터의 길이까지 자동으로 보정해주기 때문입니다.
wp search-replace 'https://운영도메인' 'http://localhost' \
--all-tables --skip-columns=guid
그런데 제가 다룬 사이트는 멀티사이트(multisite)였습니다. 여기서 일이 한 단계 더 복잡해졌습니다. 멀티사이트는 도메인을 wp_site, wp_blogs 테이블과 wp-config의 DOMAIN_CURRENT_SITE 상수에까지 박아둡니다. 이 값들이 WP-CLI가 부팅하기도 전에 참조되기 때문에, 먼저 원시 SQL로 도메인을 바꿔야 WP-CLI가 정상 동작했습니다.
UPDATE wp_site SET domain='localhost';
UPDATE wp_blogs SET domain='localhost';
또 한 가지, 멀티사이트는 표준 포트(80)를 좋아합니다. localhost:8080처럼 포트가 붙으면 도메인 매칭에서 예상치 못한 문제가 생기기 쉽습니다. 그래서 컨테이너를 80 포트로 띄워 포트 없는 localhost로 접속하도록 맞췄습니다.
띄우고 나서 하나씩 터진 것들
환경을 맞췄다고 끝이 아니었습니다. 막상 띄우자 작고 성가신 문제들이 하나씩 튀어나왔습니다. 정리하면 다음과 같습니다.
| 증상 | 원인 | 해결 |
|---|---|---|
| 일부 플러그인 fatal error | 로컬 PHP 버전이 운영보다 높음 | 운영과 동일한 PHP 버전 이미지로 맞춤 |
| 한글 깨짐(mojibake) | 덤프·임포트 charset 불일치 | 원본 charset 그대로 보존해 임포트 |
| Apache 500 오류 | .htaccess의 오래된 PHP 지시문 | obsolete 지시문 주석 처리 |
| 글 상세 URL 404 | 퍼머링크용 .htaccess 무시 | AllowOverride All 설정 |
특히 문자 인코딩은 신경 써야 했습니다. 덤프를 뜰 때와 가져올 때의 charset이 어긋나면 한글이 통째로 깨지고, 한 번 잘못 변환하면 복구가 까다롭습니다. 변환하려 들지 말고 원본 charset 그대로 떠서 그대로 넣는 것이 가장 안전했습니다.
결국 남은 것
돌이켜 생각해보면, 이 작업의 핵심은 화려한 기술이 아니라 하나의 원칙을 끝까지 지키는 것이었습니다. 운영은 읽기만 한다. 모든 위험한 작업은 로컬의 복제본에서만 한다.
운영 서버를 신성불가침의 영역으로 두고, 그 옆에 마음껏 부수고 다시 세울 수 있는 안전지대를 하나 만들어 두는 것. Docker가 해준 일은 결국 그 안전지대를 몇 줄의 설정으로 손쉽게 복제 가능하게 만든 것이었습니다. 컨테이너는 언제든 지우고 다시 띄울 수 있으니, 무엇을 시도하든 운영에는 흔적조차 남지 않습니다.
같은 고민을 하는 분이라면, 가장 먼저 자동 업로드부터 꺼두시길 권합니다. 읽기만 하겠다는 다짐은 명령어가 아니라 의외로 도구 설정에서 먼저 무너지기 쉬우니까요.
관련 글
SaaS 블로그 플랫폼을 Coolify로 배포하며 마주친 실전 문제들
글력(SaaS 블로그 플랫폼)을 Coolify에 처음 배포하면서 겪은 메모리 최적화, OG 이미지 문제, 봇 스캔 방어, Redis 연결, CORS까지. 실전에서 하나씩 해결한 과정을 정리했습니다.
개발도 결국은 상품을 만드는 일 — 기술 스택 선택이 납품과 유지보수를 결정한다
사이드 프로젝트의 모던 스택을 클라이언트에게 납품하려 하니, 기술 선택이 곧 유지보수 비용과 클라이언트 경험을 좌우한다는 걸 깨달았습니다. 개발자가 시장과 목적에 맞는 기술 스택을 선택해야 하는 이유를 정리했습니다.
Claude Code Skills로 Git Worktree 병렬 개발 환경 자동화하기
Claude Code Skills로 git worktree 기반 병렬 개발 환경을 자동화한 경험을 공유합니다. 환경변수 복사, 포트 격리, Docker DB 분리, E2E 테스트 대응, Slack 알림까지 포함된 워크플로우를 만들었습니다.