Menjalankan php artisan migrate di laptop hampir bebas risiko — database lokal tinggal di-refresh dengan migrate:fresh. Di production, tabel yang sama berisi pesanan pelanggan dan riwayat transaksi nyata, sehingga satu migration yang salah bisa menghapus kolom yang masih dipakai kode lama. Chapter ini membahas perintah migrasi produksi yang benar, strategi urutan migrasi dalam alur deploy, dan pola perubahan skema yang aman untuk data hidup — semuanya diuji nyata dengan environment production.
Kenapa –force Dibutuhkan #
Ketika APP_ENV=production, Laravel memaksa perintah yang mengubah data meminta konfirmasi eksplisit. Kita coba langsung dengan override environment dari shell dan menjawab no pada prompt:
APP_ENV=production php artisan migrateOutput yang Dihasilkan:
APPLICATION IN PRODUCTION.
WARN Command cancelled. Perintah ditolak begitu kita menjawab tidak — dan hasil yang sama terjadi dengan --no-interaction karena penolakan adalah default aman. Script deployment berjalan tanpa manusia untuk menjawab prompt, sehingga niat eksekusi harus dinyatakan eksplisit:
APP_ENV=production php artisan migrate --forceOutput yang Dihasilkan:
INFO Running migrations.
2026_10_13_221830_add_biografi_to_users_table .................. 1.82ms DONEBackup Sebelum Migrasi #
Aturan pertama migrasi produksi: jangan pernah migrasi tanpa jalan pulang. Backup harus dibuat sebelum perintah migrate --force berjalan. Pada SQLite, operasi VACUUM INTO membuat salinan konsisten dari database tanpa mengganggu koneksi berjalan:
<?php
$pdo = new PDO('sqlite:database/database.sqlite');
$ok = $pdo->exec("VACUUM INTO 'database/backup-pre-migrasi.sqlite'");
echo $ok === false ? 'GAGAL' : 'OK', PHP_EOL;Output yang Dihasilkan:
OKls -la database/Output yang Dihasilkan:
-rw-rw-r-- 1 z z 122880 Oct 13 22:17 database.sqlite
-rw-rw-r-- 1 z z 122880 Oct 13 22:18 backup-pre-migrasi.sqliteUntuk MySQL di server sungguhan, padanannya adalah mysqldump — contoh tampilan perintahnya:
mysqldump --single-transaction --routines \
-u tokobudi_app -p tokobudi_prod > backup-$(date +%F).sqlOpsi --single-transaction mengambil snapshot konsisten tanpa mengunci tabel, dan berkas .sql hasilnya bisa dipulihkan dengan mysql tokobudi_prod < backup-....sql. Uji pemulihan backup setidaknya sekali — backup yang belum pernah direstore hanyalah harapan, bukan rencana.
Waktu Migrasi dalam Alur Deploy #
Ada dua strategi kapan migrasi dijalankan relatif terhadap pergantian kode. Migrasi sebelum kode baru aktif cocok untuk perubahan aditif (kolom baru ditambahkan dulu; kode lama tidak mengenalinya, kode baru langsung siap). Migrasi sesudah kode aktif diperlukan saat kolom lama dihapus: kode baru harus berhenti membacanya lebih dulu. Kebanyakan rilis sehat memakai strategi aditif sehingga migrasi bisa berjalan di awal alur deploy tanpa downtime.
Pola Migrasi Aman: Perubahan Aditif #
Pola paling aman untuk kolom baru adalah empat langkah yang tidak pernah mengubah data lama secara destruktif. Kita praktekkan dengan menambahkan kolom biografi ke tabel users:
- Tambahkan kolom nullable lewat migration.
- Deploy kode yang mulai menulis dan membaca kolom tersebut.
- Backfill nilai untuk baris lama secara bertahap.
- Baru pertimbangkan menjadikannya wajib (mis. kolom
NOT NULLterpisah).
Langkah pertama adalah migration aditif berikut:
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->text('biografi')->nullable();
});
}Karena nullable, baris yang sudah ada tidak perlu diberi nilai — migration selesai dalam milidetik. Setelah migrate --force, struktur tabel terbukti berubah:
php -r '$p=new PDO("sqlite:database/database.sqlite");
foreach($p->query("PRAGMA table_info(users)") as $c){echo $c["name"],"\n";}'Output yang Dihasilkan:
id
name
email
email_verified_at
password
remember_token
created_at
updated_at
biografiKolom biografi berada di baris terakhir, dan status migrasinya tercatat dengan nomor batch baru:
php artisan migrate:statusOutput yang Dihasilkan:
Migration name .............................................. Batch / Status
0001_01_01_000000_create_users_table ............................... [1] Ran
0001_01_01_000001_create_cache_table ............................... [1] Ran
0001_01_01_000002_create_jobs_table ................................ [1] Ran
2026_10_13_153049_create_personal_access_tokens_table .............. [1] Ran
2026_10_13_221830_add_biografi_to_users_table ...................... [2] Ran Backfill di langkah ketiga dikerjakan per-cabang agar tidak mengunci tabel besar sekaligus: User::whereNull('biografi')->chunkById(500, fn ($users) => $users->each->update([...])).
Rencana Rollback #
Setelah migrasi berjalan, migrate:rollback mengembalikan skema dengan memanggil method down():
APP_ENV=production php artisan migrate:rollback --forceOutput yang Dihasilkan:
2026_10_13_221830_add_biografi_to_users_table .................. 4.46ms DONEKolom biografi lenyap dari PRAGMA table_info(users) — namun begitu juga datanya. Karena down() yang destruktif hanya menyelamatkan skema, pemulihan data sejati tetap bergantung pada backup yang dibuat sebelum migrasi. Untuk tabel berukuran besar (puluhan juta baris), migrasi berat seperti penambahan index sebaiknya dipecah atau dijadwalkan di jam sepi karena ALTER TABLE bisa berjalan menit-ke-jam. Kebiasaan ini melengkapi dasar konsep migration
dan rollback lokal
yang sudah kita kuasai, kini dengan taruhannya yang sebenarnya.
Skema database aman, tapi aplikasi masih menyimpan satu area yang setiap hari ditulis berulang kali oleh server itu sendiri: direktori storage/. Kesalahan izin di area inilah penyebab klasik halaman putih 500 setelah rilis pertama — kita urai di Storage Permissions
.