WordPress masih jadi content management system paling banyak dipakai di dunia, dan banyak Sysadmin/DevOps Engineer yang akhirnya berurusan dengan migrasi WordPress lama dari shared hosting ke infrastruktur berbasis container. Berbeda dengan studi kasus aplikasi custom seperti Laravel atau Django, WordPress punya karakteristik khas: source code inti jarang diubah, tapi folder wp-content yang berisi theme, plugin, dan media upload justru berubah terus-menerus, bahkan sering dari dalam dashboard admin itu sendiri. Karakteristik ini membuat strategi containerizing WordPress agak berbeda dari aplikasi yang seluruh kodenya dikelola lewat Git. Bab ini membahas studi kasus penuh: dari setup WordPress dengan database MySQL/MariaDB, mengelola persistent storage untuk media dan plugin, menambahkan reverse proxy dengan SSL/TLS termination, sampai strategi backup dan migrasi data WordPress antar environment.
32.1 Setup WordPress dengan MySQL/MariaDB
Docker menyediakan image resmi wordpress yang sudah membungkus Apache dan PHP dalam satu container, sehingga Developer maupun Sysadmin/DevOps Engineer tidak perlu menyusun Dockerfile dari nol untuk kasus penggunaan standar. Image ini butuh koneksi ke database MySQL atau MariaDB yang berjalan terpisah, mengikuti pola arsitektur multi-container yang sama seperti studi kasus aplikasi lain.
32.1.1 Struktur Compose Dasar
Buat file compose.yaml dengan dua service: db untuk database dan wordpress untuk aplikasinya.
services:
db:
image: mariadb:11.4
environment:
MARIADB_DATABASE: wordpress
MARIADB_USER: wordpress
MARIADB_PASSWORD: secret
MARIADB_ROOT_PASSWORD: rootsecret
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
start_period: 60s
retries: 3
wordpress:
image: wordpress:6.7-apache
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: secret
volumes:
- wp-content:/var/www/html/wp-content
depends_on:
db:
condition: service_healthy
volumes:
db-data:
wp-content:Image resmi mariadb menyediakan healthcheck script bawaan bernama healthcheck.sh yang dirancang khusus untuk memeriksa kesiapan MariaDB, opsi --innodb_initialized memastikan health check baru menandai container sehat setelah proses inisialisasi storage engine InnoDB benar-benar selesai, bukan cuma setelah proses mariadbd mulai berjalan. Opsi start_period: 60s memberi jeda waktu di awal sebelum kegagalan health check ikut dihitung sebagai percobaan gagal, penting karena proses inisialisasi database pada saat container pertama kali dibuat bisa memakan waktu lebih lama dibanding start berikutnya. Kombinasi depends_on dengan condition: service_healthy pada service wordpress memastikan WordPress baru mencoba konek setelah database siap menerima koneksi, mencegah error koneksi yang sering muncul kalau container database masih dalam proses inisialisasi awal.
Variabel WORDPRESS_DB_HOST memakai nama service db beserta port 3306, bukan localhost, karena Docker Compose menyediakan DNS resolution antar-service berdasarkan nama service tersebut di dalam network internal yang sama. Jebakan yang sering ditemui Developer yang baru pertama kali men-dockerize WordPress adalah tetap menuliskan localhost di sini, kebiasaan yang terbawa dari instalasi WordPress di shared hosting tempat database dan aplikasi berjalan di mesin yang sama.
32.1.2 Menjalankan dan Menyelesaikan Instalasi
Jalankan kedua service dalam mode detached.
docker compose up -dPantau proses startup lewat log, terutama untuk memastikan WordPress berhasil terhubung ke database.
docker compose logs -f wordpressBuka http://localhost:8080 lewat browser untuk melanjutkan proses instalasi lewat setup wizard WordPress, mulai dari pemilihan bahasa sampai pembuatan akun administrator. Proses ini sama persis dengan instalasi WordPress konvensional, karena container cuma menyediakan runtime-nya, sementara alur instalasi tetap ditangani oleh WordPress itu sendiri.
32.1.3 Verifikasi Koneksi Database
Kalau muncul pesan "Error establishing a database connection" di browser, periksa dulu apakah container db sudah berstatus healthy.
docker compose psKalau statusnya masih starting atau unhealthy, periksa log MariaDB untuk melihat detail errornya.
docker compose logs dbPenyebab lain yang sering ditemui di lapangan adalah nilai MARIADB_USER dan WORDPRESS_DB_USER yang tidak sama persis, misalnya beda kapitalisasi atau ada trailing space tak sengaja saat menyalin dari file environment lain. Pastikan juga volume db-data belum terisi data lama dari kredensial berbeda, karena skrip inisialisasi MariaDB cuma membuat user dan database baru saat volume-nya benar-benar kosong. Kalau perlu mengulang dari awal saat masih di tahap eksperimen, hapus volume tersebut lebih dulu.
docker compose down
docker volume rm docker_db-dataPerintah docker volume rm ini bersifat destruktif dan menghapus seluruh data database secara permanen, sehingga cuma aman dijalankan di environment development atau saat memang sengaja ingin memulai instalasi dari nol, bukan di production yang sudah menyimpan data nyata.
32.2 Persistent Storage untuk Media dan Plugin
WordPress menyimpan tiga jenis data yang perlu tetap ada meskipun container-nya di-rebuild atau dipindah ke host lain: data di database, file yang diunggah lewat media library, serta file theme dan plugin yang terinstal. Salah kelola bagian ini adalah penyebab paling umum hilangnya data WordPress saat proses containerizing, terutama data yang berubah lewat dashboard admin dan tidak pernah tersentuh oleh proses deployment berbasis Git.
32.2.1 Named Volume vs Bind Mount untuk wp-content
Konfigurasi pada bagian sebelumnya memakai satu named volume untuk seluruh folder wp-content, mencakup themes, plugins, dan uploads sekaligus. Pendekatan ini paling sederhana dan cocok kalau WordPress dikelola sepenuhnya lewat dashboard admin, termasuk instalasi plugin dan theme baru langsung dari sana.
Kalau theme dan plugin justru dikelola lewat kode yang di-commit ke Git, misalnya untuk custom theme yang dikembangkan sendiri, bind mount jadi pilihan yang lebih pas untuk folder tersebut, sementara folder uploads tetap memakai named volume karena isinya murni data runtime yang tidak relevan untuk disimpan di repository.
services:
wordpress:
image: wordpress:6.7-apache
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: secret
volumes:
- ./wp-content/themes:/var/www/html/wp-content/themes
- ./wp-content/plugins:/var/www/html/wp-content/plugins
- wp-uploads:/var/www/html/wp-content/uploads
depends_on:
db:
condition: service_healthy
volumes:
db-data:
wp-uploads:Kombinasi ini memisahkan secara tegas antara kode yang bisa diverifikasi lewat code review dan diberi version control, dengan data upload yang sifatnya murni operasional dan biasanya berukuran jauh lebih besar dibanding source code theme atau plugin. Perhatikan bahwa opsi ini menuntut disiplin: kalau ada plugin yang diinstal langsung lewat dashboard admin sementara folder plugins memakai bind mount ke host, perubahan itu langsung muncul di host dan wajib disinkronkan manual ke repository Git kalau memang ingin dipertahankan.
32.2.2 Permission dan Kepemilikan File
Image resmi wordpress menjalankan Apache sebagai user www-data, dan proses PHP di dalamnya butuh hak tulis ke folder wp-content untuk keperluan upload media, instalasi plugin, maupun proses auto-update. Kalau memakai bind mount seperti pada bagian sebelumnya, folder di host bisa saja punya kepemilikan berbeda dari www-data di dalam container, tergantung user ID yang membuat folder tersebut di host.
Periksa kepemilikan folder wp-content di dalam container.
docker compose exec wordpress ls -la /var/www/html/wp-contentKalau folder hasil bind mount muncul dengan kepemilikan user ID yang berbeda dari www-data dan WordPress gagal menulis file dengan error permission di log, sesuaikan kepemilikan folder tersebut langsung dari host supaya cocok dengan user ID www-data di dalam image (biasanya UID 33 pada base image Debian yang dipakai image resmi wordpress).
sudo chown -R 33:33 ./wp-contentDi lapangan, jebakan yang sering ditemui Sysadmin/DevOps Engineer adalah menjalankan container dengan user custom demi alasan keamanan, tapi lupa menyesuaikan kepemilikan folder bind mount, sehingga WordPress gagal total menulis file meskipun container berjalan tanpa error saat startup. Selalu verifikasi hak tulis dengan mencoba upload gambar kecil lewat media library setelah perubahan permission semacam ini.
32.2.3 Backup Volume Sebagai Bagian dari Storage Strategy
Karena wp-uploads dan db-data menyimpan satu-satunya salinan data yang tidak ada duplikatnya di tempat lain, kedua volume ini wajib masuk ke dalam strategi backup rutin, bukan cuma diandalkan sebagai penyimpanan sementara. Verifikasi ukuran data yang tersimpan di named volume lewat docker system df -v untuk memperkirakan kebutuhan kapasitas backup.
docker system df -vKolom SIZE pada output perintah ini menunjukkan total ukuran data yang tersimpan di tiap volume, angka yang berguna sebagai patokan kapasitas penyimpanan yang perlu disiapkan di sisi backup, baik itu disk terpisah, object storage, maupun server backup lain. Di lapangan, ukuran volume wp-uploads pada situs yang sudah lama berjalan biasanya jauh lebih besar dibanding dump database, terutama kalau media lama tidak pernah dibersihkan atau dikompres.
32.3 Reverse Proxy dan SSL/TLS Termination
Container WordPress dari image resmi cuma melayani HTTP biasa lewat port 80, sementara situs WordPress di production wajib diakses lewat HTTPS. Pola yang lazim dipakai adalah menaruh Nginx sebagai reverse proxy, yaitu server perantara yang menerima seluruh request dari luar lalu meneruskannya ke container WordPress di belakangnya. Nginx menangani terminasi SSL/TLS pada pola ini, sementara trafik ke WordPress tetap berupa HTTP biasa di dalam network internal Docker.
32.3.1 Nginx sebagai Reverse Proxy
Tambahkan service proxy yang meneruskan request ke service wordpress lewat nama service-nya, bukan lewat localhost, karena keduanya saling terhubung lewat network internal Compose.
server {
listen 443 ssl;
server_name blog.example.com;
ssl_certificate /etc/letsencrypt/live/blog.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.example.com/privkey.pem;
client_max_body_size 64m;
location / {
proxy_pass http://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;
}
}
server {
listen 80;
server_name blog.example.com;
return 301 https://$host$request_uri;
}Header X-Forwarded-Proto jadi bagian paling krusial dari konfigurasi ini. Image resmi wordpress sudah dilengkapi mekanisme bawaan yang membaca header tersebut untuk mendeteksi bahwa koneksi asli dari client sebenarnya memakai HTTPS, meskipun trafik yang diteruskan ke container WordPress sendiri berupa HTTP biasa. Tanpa header ini, WordPress salah mendeteksi protocol koneksi dan menghasilkan URL berawalan http:// di seluruh halaman, termasuk untuk asset seperti CSS dan JavaScript, yang ujungnya memicu peringatan mixed content di browser pengunjung. Direktif client_max_body_size 64m perlu disesuaikan dengan batas upload yang dikonfigurasi di php.ini WordPress, karena Nginx akan menolak request lebih dulu dengan error 413 Request Entity Too Large kalau batasnya lebih kecil dari yang diharapkan, sebelum request tersebut sempat sampai ke WordPress.
Definisikan service proxy di compose.yaml, dengan sertifikat SSL yang disimpan di host lewat bind mount read-only.
services:
proxy:
image: nginx:1.27-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./certs/etc:/etc/letsencrypt:ro
depends_on:
- wordpress
wordpress:
image: wordpress:6.7-apache
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: secret
WORDPRESS_CONFIG_EXTRA: |
define('FORCE_SSL_ADMIN', true);
volumes:
- wp-content:/var/www/html/wp-content
depends_on:
db:
condition: service_healthy
db:
image: mariadb:11.4
environment:
MARIADB_DATABASE: wordpress
MARIADB_USER: wordpress
MARIADB_PASSWORD: secret
MARIADB_ROOT_PASSWORD: rootsecret
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
start_period: 60s
retries: 3
volumes:
db-data:
wp-content:Perhatikan bahwa port 80 dan 443 cuma dipetakan pada service proxy. Service wordpress tetap mendengarkan di port 80 secara internal, tapi tidak diekspos langsung ke host, sehingga satu-satunya jalur masuk trafik dari luar wajib melalui Nginx. Variabel WORDPRESS_CONFIG_EXTRA dipakai untuk menyuntikkan konstanta PHP tambahan ke dalam wp-config.php yang di-generate otomatis oleh image ini, dan konstanta FORCE_SSL_ADMIN pada contoh di atas memaksa seluruh halaman login dan dashboard admin WordPress diakses lewat HTTPS.
32.3.2 Sertifikat Let's Encrypt dengan Certbot
Untuk sertifikat SSL gratis dan otomatis diperbarui, pola yang umum dipakai adalah menjalankan container Certbot terpisah yang berbagi volume sertifikat dengan Nginx. Jalankan Certbot dalam mode standalone lebih dulu untuk penerbitan sertifikat awal, sebelum Nginx aktif memakai port 443.
docker run --rm -p 80:80 \
-v "$(pwd)/certs/etc:/etc/letsencrypt" \
-v "$(pwd)/certs/lib:/var/lib/letsencrypt" \
certbot/certbot certonly --standalone \
-d blog.example.com \
--email [email protected] \
--agree-tos --no-eff-emailDirektori /etc/letsencrypt perlu dipetakan secara utuh, bukan cuma folder live-nya saja, karena Certbot menyimpan sertifikat asli di folder archive dan cuma menaruh symbolic link ke folder tersebut di dalam live. Kalau folder archive tidak ikut dipetakan ke host, symbolic link di live jadi rusak begitu container Certbot berhenti dan volume sementara di dalamnya hilang. Folder ./certs/etc hasil terbitan Certbot ini yang nanti dipetakan utuh ke /etc/letsencrypt di dalam container Nginx, sehingga path ssl_certificate dan ssl_certificate_key yang dipakai Nginx tetap konsisten dengan struktur folder asli Certbot. Sertifikat Let's Encrypt berlaku 90 hari, sehingga proses perpanjangan perlu dijadwalkan berkala, misalnya lewat cron job di host yang menjalankan certbot renew dengan volume yang sama seperti perintah penerbitan awal, lalu me-reload Nginx setelah sertifikat baru terbit, supaya situs tidak mendadak menampilkan peringatan sertifikat kedaluwarsa ke pengunjung.
32.3.3 Verifikasi HTTPS dan Troubleshooting Mixed Content
Verifikasi sertifikat SSL sudah terpasang dengan benar lewat curl.
curl -vI https://blog.example.com 2>&1 | grep -i "SSL certificate"Kalau situs sudah bisa diakses lewat HTTPS tapi sebagian asset masih dimuat lewat http://, penyebab paling umum adalah nilai siteurl dan home di database WordPress yang masih tersimpan sebagai http:// dari sebelum HTTPS diaktifkan. Periksa kedua nilai tersebut lewat WP-CLI.
docker compose exec wordpress wp option get siteurl --allow-root
docker compose exec wordpress wp option get home --allow-rootKalau hasilnya masih memakai http://, perbarui keduanya supaya konsisten memakai https://.
docker compose exec wordpress wp option update siteurl https://blog.example.com --allow-root
docker compose exec wordpress wp option update home https://blog.example.com --allow-rootImage resmi wordpress tidak menyertakan WP-CLI secara bawaan. WP-CLI adalah command-line interface resmi untuk mengelola WordPress lewat terminal, mencakup operasi seperti mengubah opsi, menjalankan migrasi database, sampai instalasi plugin, tanpa perlu membuka dashboard admin lewat browser. Perintah di atas cuma berjalan kalau WP-CLI sudah ditambahkan ke image lewat Dockerfile kustom, atau dijalankan lewat container WP-CLI terpisah yang berbagi volume wp-content dan network yang sama dengan service wordpress.
32.4 Backup dan Migrasi Data WordPress
Backup WordPress yang lengkap wajib mencakup dua komponen sekaligus: dump database yang menyimpan seluruh konten, pengaturan, dan metadata, serta arsip folder wp-content yang menyimpan media, theme, dan plugin. Kehilangan salah satunya membuat proses restore tidak bisa menghasilkan situs yang utuh kembali.
32.4.1 Backup Database dengan mysqldump
Jalankan mysqldump langsung di dalam container database untuk mengekspor seluruh isi database ke file SQL.
docker compose exec db sh -c 'exec mysqldump -u wordpress -psecret wordpress' > backup-db-$(date +%Y%m%d).sqlPerintah exec di dalam sh -c memastikan proses mysqldump menerima sinyal langsung dari docker compose exec, dan hasil output-nya dialihkan ke file di host lewat redirection biasa. Untuk database berukuran besar, tambahkan opsi --single-transaction supaya proses dump berjalan dalam satu transaksi konsisten tanpa mengunci tabel secara penuh, opsi yang direkomendasikan dokumentasi resmi MySQL khusus untuk tabel dengan storage engine InnoDB seperti yang dipakai WordPress secara default.
docker compose exec db sh -c 'exec mysqldump --single-transaction -u wordpress -psecret wordpress' > backup-db-$(date +%Y%m%d).sql32.4.2 Backup Folder wp-content
Arsipkan isi named volume wp-content lewat container sementara yang me-mount volume tersebut sebagai read-only, lalu menjalankan tar untuk membungkusnya jadi satu file.
docker run --rm \
-v docker_wp-content:/wp-content:ro \
-v "$(pwd)":/backup \
alpine tar czf /backup/backup-wp-content-$(date +%Y%m%d).tar.gz -C /wp-content .Pendekatan ini tidak menyentuh isi volume sama sekali (dipasang read-only) dan tidak butuh mengubah konfigurasi service wordpress yang sedang berjalan, sehingga proses backup bisa dijalankan kapan saja tanpa mengganggu situs yang sedang diakses pengunjung.
32.4.3 Restore ke Environment Baru
Untuk migrasi ke server atau environment baru, siapkan dulu service db dan wordpress lewat compose.yaml yang sama seperti sumbernya, tapi jangan langsung dijalankan sampai data lama selesai dipulihkan.
Salin arsip wp-content ke dalam named volume yang baru.
docker run --rm \
-v docker_wp-content:/wp-content \
-v "$(pwd)":/backup \
alpine sh -c "tar xzf /backup/backup-wp-content-20260101.tar.gz -C /wp-content"Jalankan service db lebih dulu, tunggu sampai statusnya healthy, lalu impor dump SQL ke dalamnya.
docker compose up -d db
docker compose exec -T db sh -c 'exec mysql -u wordpress -psecret wordpress' < backup-db-20260101.sqlOpsi -T pada docker compose exec menonaktifkan alokasi pseudo-TTY, langkah yang wajib dipakai saat mengalirkan isi file lewat standard input seperti pada perintah impor ini, karena TTY interaktif bisa mengganggu proses streaming data biner atau teks panjang ke dalam container.
32.4.4 Migrasi Domain dengan wp search-replace
WordPress menyimpan URL situs secara hardcoded di banyak tempat dalam database, termasuk di dalam konten post dan pengaturan tertentu, bukan cuma di opsi siteurl dan home saja. Kalau migrasi disertai perubahan domain, misalnya dari staging.example.com ke blog.example.com, dump database saja tidak cukup karena seluruh referensi URL lama masih tertanam di dalamnya.
Perintah wp search-replace dari WP-CLI dirancang khusus untuk kasus ini, mengganti seluruh kemunculan string lama dengan string baru di seluruh tabel database secara aman.
docker compose exec wordpress wp search-replace \
'https://staging.example.com' 'https://blog.example.com' \
--skip-columns=guid --allow-rootOpsi --skip-columns=guid sengaja dipakai untuk melewati kolom guid, sesuai rekomendasi dokumentasi resmi WP-CLI, karena nilai GUID (globally unique identifier) pada setiap post seharusnya bersifat permanen sejak pertama kali dibuat dan tidak boleh berubah meskipun domain situsnya berpindah. Sebelum benar-benar menjalankan perubahan, lakukan dulu dry run untuk melihat berapa banyak baris yang akan terdampak tanpa benar-benar menulis perubahan ke database.
docker compose exec wordpress wp search-replace \
'https://staging.example.com' 'https://blog.example.com' \
--skip-columns=guid --dry-run --allow-rootPerintah wp search-replace mengubah data secara langsung dan permanen begitu dijalankan tanpa --dry-run, sehingga backup database yang sudah dibuat sebelumnya wajib dipastikan valid dan bisa di-restore, sebagai jaring pengaman kalau proses replace ternyata menghasilkan efek samping tak terduga, misalnya pada konten yang secara tidak sengaja menyertakan string domain lama dalam konteks yang berbeda dari URL.
32.4.5 Verifikasi Hasil Migrasi
Setelah seluruh langkah selesai, jalankan service wordpress dan periksa apakah situs tampil dengan benar, termasuk memastikan seluruh media dari folder uploads lama ikut termuat.
docker compose up -d wordpress
docker compose logs -f wordpressPeriksa juga tabel wp_options untuk memastikan tidak ada nilai serialized yang rusak akibat perubahan panjang string lewat metode search-replace yang keliru (misalnya lewat query SQL manual, bukan lewat wp search-replace). WordPress menyimpan sebagian data dalam format serialized PHP array yang menyertakan panjang karakter setiap string di dalamnya, sehingga penggantian teks yang tidak memperhitungkan format ini berisiko membuat data tersebut gagal dibaca sama sekali oleh WordPress. WP-CLI menangani kasus ini secara otomatis, salah satu alasan utama kenapa wp search-replace jadi metode yang direkomendasikan dibanding query SQL manual untuk keperluan migrasi domain.

