Hampir sepuluh tahun terakhir semua website yang saya kelola selalu berjalan di atas web panel. Dimulai dari Webuzo, lalu pindah ke CentOS Web Panel, sempat singgah di aaPanel, dan akhirnya sekarang semuanya saya pindahkan ke Docker. Tulisan ini merangkum alasan dan cara migrasinya.

Manajemen pakai panel
Panel sangat membantu di awal. Membuat domain, database, email, dan sertifikat SSL cukup dengan beberapa klik. Untuk satu atau dua website, cara ini masih masuk akal.
Masalah mulai terasa ketika jumlah website bertambah dan kebutuhannya berbeda-beda. Ada aplikasi lama yang hanya jalan di PHP 7.4, ada WordPress yang butuh PHP 8.3, ada OJS dengan kebutuhan ekstensi sendiri. Semuanya berbagi satu web server, satu MySQL, dan satu sistem operasi. Satu situs yang terinfeksi malware berpotensi menular ke situs lain di server yang sama, seperti yang pernah saya ceritakan di mitigasi malware di web server.
Webuzo
Webuzo adalah panel pertama yang saya pakai serius. Ringan dan mudah, tetapi versi gratisnya hanya menyediakan satu user untuk mengelola semuanya. SSL juga masih harus dipasang manual setiap tiga bulan.
CentOS Web Panel
CWP menyelesaikan masalah di Webuzo: multiuser, ModSecurity, dan auto SSL dari Let’s Encrypt. Bertahun-tahun CWP menjadi andalan. Sayangnya CWP terikat dengan keluarga CentOS. Ketika CentOS 8 dihentikan lebih awal dan CentOS 7 berakhir masa dukungannya pada Juni 2024, saya harus mencari jalan keluar.
aaPanel
aaPanel sempat menjadi pengganti karena bisa berjalan di Ubuntu dan Debian. Tampilannya rapi dan instalasinya cepat. Tetapi pada dasarnya polanya sama: semua aplikasi tetap menumpuk di satu sistem. Setiap kali upgrade PHP atau MySQL, saya harus berdoa supaya tidak ada situs yang rusak.
Di titik ini saya sadar yang saya butuhkan bukan panel yang lebih baik, melainkan cara kerja yang berbeda.
Docker
Dengan Docker, setiap aplikasi berjalan di container sendiri lengkap dengan versi PHP, web server, dan databasenya. Satu situs bermasalah tidak menyeret situs lain. Konfigurasi juga tersimpan sebagai file teks yang bisa disimpan di Git, jadi membangun ulang server tidak lagi bergantung pada ingatan.

Polanya seperti diagram di atas. Satu reverse proxy berdiri di depan dan menerima semua trafik HTTPS, lalu meneruskannya ke container masing-masing situs. Database hanya bisa diakses dari network internal situsnya, tidak pernah dibuka ke internet.
1. Struktur direktori
Saya memakai satu direktori per situs di /opt/stack:
/opt/stack/
├── proxy/ # Caddy
├── situs-a/ # WordPress
│ ├── compose.yaml
│ ├── .env
│ ├── app/
│ └── db/
└── ojs/
2. Reverse proxy
Buat network bersama untuk proxy, lalu jalankan Caddy. Caddy otomatis mengurus sertifikat Let’s Encrypt, fitur yang dulu menjadi alasan saya memilih CWP.
docker network create proxy
Isi proxy/Caddyfile cukup sesederhana ini:
situs-a.ac.id {
reverse_proxy situs-a-web-1:80
}
jurnal.ac.id {
reverse_proxy ojs-app-1:80
}
3. Compose untuk setiap situs
Contoh situs-a/compose.yaml untuk WordPress:
services:
web:
image: nginx:1.27-alpine
volumes:
- ./app:/var/www/html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks: [internal, proxy]
restart: unless-stopped
php:
image: wordpress:php8.3-fpm
volumes:
- ./app:/var/www/html
env_file: .env
networks: [internal]
restart: unless-stopped
db:
image: mariadb:11.4
volumes:
- ./db:/var/lib/mysql
env_file: .env
networks: [internal]
restart: unless-stopped
networks:
internal:
proxy:
external: true
Perhatikan hanya container web yang tergabung ke network proxy. Container php dan db tidak bisa dijangkau dari luar.
Langkah migrasi dari panel
Proses migrasi setiap situs kurang lebih sama:
1. Turunkan TTL DNS beberapa hari sebelumnya, misalnya menjadi 300 detik, supaya perpindahan cepat terasa.
2. Dump database dan salin file dari server lama.
mysqldump --single-transaction -u user -p nama_db > situs-a.sql
rsync -avz user@server-lama:/home/user/public_html/ /opt/stack/situs-a/app/
3. Jalankan container, lalu import database.
cd /opt/stack/situs-a
docker compose up -d
docker compose exec -T db mariadb -u root -p"$DB_ROOT_PASSWORD" nama_db < situs-a.sql
4. Uji dengan mengarahkan domain ke IP server baru lewat file hosts di laptop sendiri. Jika semua normal, baru ubah record DNS.
5. Hapus file dump setelah selesai.
Langkah terakhir ini sering terlupa. Saya pernah menulis kenapa file .sql dan .zip sisa migrasi wajib dihapus.
Update jadi lebih tenang
Upgrade versi PHP sekarang cukup mengganti tag image di compose.yaml. Kalau ternyata bermasalah, kembalikan tag lama dan jalankan ulang. Tidak ada lagi drama upgrade satu server penuh.

docker compose pull
docker compose up -d
Yang perlu diperhatikan
Docker bukan tanpa kekurangan. Tidak ada lagi tombol klik-klik seperti di panel, jadi perlu terbiasa dengan terminal. Jika butuh tampilan visual, Portainer bisa membantu. Image lama juga menumpuk dan memakan disk, sehingga perlu sesekali dibersihkan dengan docker image prune.
Email adalah satu-satunya layanan yang tidak saya pindahkan ke Docker. Mengelola mail server sendiri terlalu merepotkan, jadi saya serahkan ke layanan pihak ketiga dan cukup mengatur record MX-nya.
Terakhir, container tidak menggantikan backup. Volume database tetap harus dicadangkan ke luar server, misalnya dengan immutable backup ke object storage.
Setelah semua situs pindah, saya justru merasa lebih dekat dengan server. Semua yang berjalan tertulis jelas di file konfigurasi, bukan tersembunyi di balik panel.
