SSD Nodes Learn 🎉 VPS mulai $5.50/bln
Panduan Matt ConnorOleh Matt Connor · Diperbarui 2026-08-13

Database di Docker atau Host: Mana yang Tepat?

Menjalankan PostgreSQL, MySQL, MongoDB, atau Redis di Docker aman untuk produksi. Pahami risiko volume, upgrade versi mayor, backup, dan batas memori.

Apakah database sebaiknya berjalan di Docker atau pada host?

Jalankan database di Docker. Untuk satu stack aplikasi pada satu VPS, PostgreSQL, MySQL, MongoDB, atau Redis dalam container merupakan pilihan produksi yang wajar. Perdebatan tentang hal ini biasanya berfokus pada argumen yang keliru. Container adalah proses Linux dengan namespace dan cgroup di sekitarnya, bukan mesin virtual. Jadi, tidak ada hypervisor antara database dan disk. Dengan bind mount atau local named volume, operasi baca dan tulis berlangsung pada filesystem host, yaitu filesystem yang sama dengan yang digunakan oleh instalasi paket.

Biaya sebenarnya terletak pada aspek operasional. Empat hal menentukan apakah pengaturan ini berjalan baik atau berubah menjadi masalah yang lambat dan sulit ditangani: lokasi data, pemilik direktori tersebut, proses upgrade major version, dan apakah Anda pernah memulihkan backup. Jika semua hal itu benar, container hanya menjadi detail implementasi. Jika tidak, container akan menjadi hal yang Anda salahkan.

Keputusan ini berlaku untuk setiap database server. Contoh di bawah menggunakan PostgreSQL, MySQL, MongoDB, dan Redis. Perbedaan khusus setiap produk akan dijelaskan jika relevan.

Perubahan yang sebenarnya terjadi pada container

Bukan path penyimpanannya, selama Anda melakukan mount. Kernel, page cache, dan filesystem yang digunakan tetap sama.

Ada satu jebakan performa yang nyata, yaitu saat Anda tidak melakukan mount apa pun. Tanpa volume, direktori data ditempatkan pada writable layer milik container, yaitu overlay filesystem yang ditumpuk di atas image. Penulisan data di sana lebih lambat, dan seluruh layer tersebut dihapus saat container dihapus. Inilah penyebab munculnya kasus “database saya kosong pagi ini”.

Hal-hal yang benar-benar berubah:

  • Siklus hidupnya. docker compose down menghapus container. Semua data yang tidak berada dalam volume ikut terhapus.
  • Versinya. Tag image menentukan versi. Tidak ada apt upgrade di dalam container database yang dapat bertahan setelah docker compose pull berikutnya.
  • Penggunaan memori. Batas cgroup adalah batas keras yang diberlakukan oleh kernel, dan database tidak mengetahui bahwa batas tersebut ada.
  • Penggunanya. Proses berjalan sebagai user id numerik di dalam container, yang mungkin tidak memiliki file apa pun pada host Anda.

Lokasi data menentukan semuanya

Ada dua pilihan yang baik dan satu kesalahan umum.

  • Named volume: pgdata:/var/lib/postgresql/data. Docker membuat direktori di /var/lib/docker/volumes/<project>_pgdata/_data, lalu entrypoint image menetapkan ownership pada proses pertama. Ini adalah pilihan default.
  • Bind mount: /srv/appname/pg:/var/lib/postgresql/data. Anda memilih path-nya, sehingga Anda bertanggung jawab atas masalah permission.
  • Tanpa mount sama sekali. Lihat penjelasan di atas. Data berada di dalam container.

Pembahasan lengkap tentang komprominya merupakan topik tersendiri, dan bind mount dibandingkan named volume membahasnya. Untuk database, versi singkatnya adalah: gunakan named volume kecuali Anda memiliki alasan khusus untuk mengetahui path pada host. Jika Anda menggunakan bind mount, letakkan di lokasi yang stabil seperti /srv/appname/pg, bukan di dalam direktori project yang dapat dijangkau oleh git clean.

Ada satu batasan penting: jangan menempatkan direktori data database pada NFS (network file system) atau network mount apa pun yang perilaku locking dan fsync-nya belum Anda uji. Database mengasumsikan bahwa fsync yang berhasil berarti byte telah tersimpan pada penyimpanan stabil. Jika asumsi tersebut tidak benar, korupsi dapat terjadi dan baru terlihat beberapa minggu kemudian.

Tetapkan nama volume sebelum volume hilang

Compose memberi nama volume <project>_<volume>, dan nama project secara default mengikuti nama direktori. Jadi, identitas volume bergantung pada nama direktori, yang dapat diubah orang tanpa menyadarinya.

Pindahkan /srv/app ke /srv/app-old, atau ubah nama key pgdata di file compose, lalu docker compose up -d berikutnya akan membuat volume kosong yang benar-benar baru. Postgres menginisialisasi cluster baru ke dalamnya. Container tetap sehat, aplikasi berjalan, dan semua tabel hilang. Volume lama masih tersimpan di disk dengan nama lama. Ini kabar baiknya.

docker volume ls
docker volume inspect app_pgdata

Tetapkan nama tersebut secara eksplisit agar hal ini tidak terjadi. Tetapkan nama project dan nama volume secara eksplisit:

name: myapp

services:
  db:
    image: postgres:17
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
    name: myapp_pgdata

Jika volume yang tidak sesuai sudah menyimpan data Anda, salin data tersebut saat database dihentikan:

docker compose stop db
docker run --rm -v app_pgdata:/from -v myapp_pgdata:/to alpine sh -c 'cp -a /from/. /to/'
docker compose start db

Jika Anda menyalinnya saat database masih berjalan, hasilnya adalah salinan file yang tidak konsisten karena file tersebut sedang ditulis. Hentikan database terlebih dahulu.

Siapa yang memiliki direktori data

Image resmi Postgres, MySQL, dan MongoDB menjalankan server sebagai user tanpa hak istimewa, biasanya dengan id 999. Saat container dimulai sebagai root, entrypoint mengubah kepemilikan direktori data menjadi milik user tersebut, lalu menurunkan hak istimewa. Karena itu, bind mount kosong biasanya langsung berfungsi pada percobaan pertama.

Konfigurasi tersebut gagal saat Anda menetapkan user: dalam file compose, karena entrypoint tidak lagi memiliki hak istimewa untuk memperbaiki apa pun. Postgres menyatakannya secara langsung:

initdb: error: could not change permissions of directory "/var/lib/postgresql/data": Operation not permitted

Direktori data yang sudah ada dengan mode yang salah menghasilkan pesan berbeda. Pesan ini perlu dikenali karena perbaikannya adalah chmod, bukan chown:

FATAL:  data directory "/var/lib/postgresql/data" has invalid permissions
DETAIL:  Permissions should be u=rwx (0700) or u=rwx,g=rx (0750).

MongoDB pada bind mount yang dimiliki root gagal saat mengakses file lock:

Unable to create/open the lock file: /data/db/mongod.lock (Permission denied). Ensure the user executing mongod is the owner of the lock file and has the appropriate permissions.

Perbaikannya adalah menjalankan chown pada direktori host menggunakan id numerik, bukan nama:

sudo chown -R 999:999 /srv/appname/pg
sudo chmod 700 /srv/appname/pg
ls -ldn /srv/appname/pg

ls -ldn menampilkan angka, bukan nama, dan hasilnya seharusnya menunjukkan 999 999. Account bernama postgres di host Anda dan account bernama postgres di dalam image tidak saling terkait: kernel membandingkan angka, sedangkan nama dicari secara terpisah pada masing-masing sisi. cara PUID dan PGID memetakan user host ke dalam container menjelaskan pemetaan tersebut dengan benar. Pada rootless Docker atau pemetaan namespace user, angkanya dapat berubah lagi. Karena itu, baca id dari container yang sedang berjalan, bukan dengan mengasumsikan nilainya 999.

Named volume membuat seluruh masalah pada bagian ini tidak terjadi saat pertama kali dijalankan, karena Docker membuat direktori kosong dan entrypoint menetapkan kepemilikan atas direktori tersebut.

Upgrade: peningkatan paket dibandingkan perubahan tag image

Pada host, apt upgrade memindahkan Anda ke versi minor berikutnya. Distribusi Anda tidak akan menaikkan versi mayor database secara otomatis. Saat Anda memilih untuk melakukannya, kedua set binary dapat diinstal secara bersamaan. Inilah yang dibutuhkan pg_upgrade.

Dalam container, tag menunjukkan versi. Jadi, upgrade cukup dilakukan dengan mengubah satu baris. Karena itu, upgrade minor menjadi sederhana, sedangkan upgrade mayor memerlukan prosedur.

Ubah postgres:16 menjadi postgres:17, jalankan docker compose up -d, dan container langsung berhenti:

PostgreSQL Database directory appears to contain a database; Skipping initialization
FATAL:  database files are incompatible with server
DETAIL:  The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17.

Tidak ada data yang rusak. Binary baru menolak membaca tata letak katalog pada disk yang lama karena tata letak tersebut berubah antarversi mayor. Kembalikan tag ke postgres:16, lalu container dapat berjalan kembali. Rollback ini adalah satu-satunya keunggulan upgrade yang benar-benar diberikan oleh container.

Prosedur yang didukung adalah dump dan restore. PostgreSQL lebih menyarankan dump dibuat oleh client yang lebih baru. Jadi, jalankan perintah tersebut dari image baru terhadap server lama yang masih berjalan pada jaringan compose:

docker run --rm --network myapp_default -e PGPASSWORD="$POSTGRES_PASSWORD" \
  postgres:17 pg_dumpall -h db -U postgres > /srv/backups/all.sql
ls -lh /srv/backups/all.sql
tail -n 2 /srv/backups/all.sql

File tersebut setidaknya harus berukuran puluhan kilobyte dan diakhiri baris yang berisi PostgreSQL database cluster dump complete. File yang hanya berukuran beberapa ratus byte berarti dump gagal. Jika Anda melanjutkan, volume akan dihapus tanpa alasan. Lakukan pemeriksaan berikutnya hanya setelah itu:

docker compose down
docker volume rm myapp_pgdata
# edit the compose file: image: postgres:17
docker compose up -d db
docker compose exec -T db psql -U postgres -f /dev/stdin < /srv/backups/all.sql

Engine lainnya memiliki prosedur berbeda:

  • MySQL 8 memperbarui data dictionary-nya sendiri saat startup. Jadi, perubahan tag minor biasanya hanya memerlukan restart. Baca catatan rilis sebelum berpindah antarseri rilis, dan tetap buat dump terlebih dahulu.
  • MariaDB mengharuskan mariadb-upgrade dijalankan setelah server aktif pada versi baru.
  • MongoDB harus di-upgrade satu versi mayor setiap kali. Setelah setiap langkah, tetapkan feature compatibility version sebelum melanjutkan. Melewati satu versi menyebabkan mongod menolak untuk start dan mencatat baris UPGRADE PROBLEM yang menyebutkan featureCompatibilityVersion. Mulai MongoDB 7.0, perintah tersebut memerlukan flag konfirmasi eksplisit: db.adminCommand({ setFeatureCompatibilityVersion: "8.0", confirm: true }).
  • Redis dapat memuat file snapshot versi lama, tetapi tidak dapat memuat file snapshot versi yang lebih baru. Karena itu, upgrade cukup dilakukan dengan restart, sedangkan downgrade dapat gagal memuat data.

Aturan umumnya: container memudahkan downgrade, tetapi tidak membuat upgrade menjadi lebih mudah.

Mengapa container database saya keluar dengan kode 137?

Karena kernel menghentikannya dengan OOM killer akibat kehabisan memori. Kode 137 adalah 128 ditambah sinyal 9.

docker compose ps
docker inspect myapp-db-1 | grep -i oomkilled
journalctl -k | tail -n 20

docker compose ps menampilkan Exited (137), baris inspect berbunyi "OOMKilled": true, dan log kernel memuat entri yang sesuai:

Memory cgroup out of memory: Killed process 4711 (postgres) total-vm:2170416kB

Berikut mekanismenya. Hal ini sering mengejutkan pengguna. PostgreSQL dan MySQL menentukan ukuran buffer berdasarkan total memori yang dilaporkan host. Batas cgroup tidak mengubah angka tersebut. Pada host dengan memori 16 GB dan batas 2 GB, database merencanakan penggunaan seolah-olah memiliki 16 GB. cgroup kemudian menghentikannya jauh sebelum host benar-benar mengalami tekanan memori. Jadi, batas memori saja tidak cukup. Anda juga harus memberi tahu database jumlah memori yang tersedia:

  • PostgreSQL: tetapkan shared_buffers, dan perhatikan work_mem. work_mem dialokasikan untuk setiap operasi pengurutan pada setiap koneksi. Jadi, nilai besar dikalikan lima puluh koneksi biasanya menyebabkan container berhenti saat menerima beban, bukan saat startup.
  • MySQL dan MariaDB: tetapkan innodb_buffer_pool_size, yang secara default bernilai 128M. Biarkan innodb_dedicated_server nonaktif dalam container karena fungsinya adalah menentukan ukuran berdasarkan memori mesin yang terdeteksi.
  • MongoDB: tetapkan ukuran cache WiredTiger secara eksplisit. Jangan biarkan MongoDB memperkirakannya berdasarkan memori host.
  • Redis: maxmemory secara default tidak memiliki batas. Redis akan terus menambah penggunaan memori hingga cgroup menghentikannya. Tetapkan maxmemory dengan nilai yang cukup di bawah batas container, lalu pilih maxmemory-policy.

Postgres juga melaporkan kejadian ini dari sisinya. Berikut pasangan baris yang akan Anda temukan dalam log:

LOG:  server process (PID 123) was terminated by signal 9: Killed
LOG:  terminating any other active sessions due to crash of another server process

Penghentian satu backend memaksa semua backend lain untuk memulai ulang karena shared memory mungkin berada dalam kondisi tidak konsisten. Bagi aplikasi Anda, hal ini menyebabkan lonjakan koneksi, bukan kejadian yang tidak berdampak. menetapkan batas memori di Docker Compose menjelaskan sintaksis serta perbedaan antara bentuk mem_limit dan deploy.resources.

Semua ini tidak hilang pada host. Dampaknya hanya berpindah. Tanpa cgroup, database bersaing dengan semua proses lain pada mesin. OOM killer pada host memilih korban berdasarkan skor, dan korbannya dapat berupa sshd. Batas yang menghentikan database secara terprediksi lebih mudah dioperasikan daripada OOM pada host yang dapat membuat Anda kehilangan akses.

Cadangan: lakukan dump di dalam, buat cadangan di luar

Jangan membuat cadangan database yang sedang berjalan dengan menyalin direktori datanya. Salinan tingkat berkas yang dibuat saat server sedang menulis merupakan salinan yang tidak utuh. Masalahnya baru diketahui saat pemulihan.

Ada dua metode yang benar: lakukan dump menggunakan alat bawaan database saat database berjalan, lalu buat cadangan dump tersebut; atau hentikan container dan salin volume dalam kondisi tidak aktif.

docker compose exec -T db pg_dump -U postgres -Fc appdb > /srv/backups/appdb.dump
docker compose exec -T db mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --single-transaction --all-databases > /srv/backups/mysql.sql
docker compose exec -T db mongodump --archive --gzip --db appdb > /srv/backups/appdb.archive.gz
docker compose exec -T redis redis-cli BGSAVE

-T sangat penting. Tanpanya, docker compose exec dapat menghubungkan terminal ke perintah tersebut, dan lapisan terminal menambahkan carriage return ke aliran output. Dump teks kemudian dipulihkan dengan error yang tidak wajar, sedangkan dump biner menjadi rusak. Kegagalan ini tidak terlihat saat pencadangan, tetapi baru muncul satu bulan kemudian.

--single-transaction memberi mysqldump snapshot konsisten untuk tabel InnoDB tanpa mengunci seluruh server.

Perintah tersebut masing-masing menulis satu file. Perintah itu bukan sistem pencadangan: tidak ada retensi, salinan di luar server, atau verifikasi. Serahkan direktori dump kepada alat yang menangani ketiganya. Itulah kegunaan pencadangan restic dari VPS. Buat cadangan /srv/backups, bukan /var/lib/docker/volumes.

Selanjutnya, jalankan pemulihan karena cadangan yang belum pernah dipulihkan bukanlah cadangan:

docker compose exec -T db createdb -U postgres restore_test
docker compose exec -T db pg_restore -U postgres -d restore_test < /srv/backups/appdb.dump
docker compose exec -T db psql -U postgres -d restore_test -c '\dt'

\dt seharusnya menampilkan tabel aplikasi Anda. Hasil kosong atau Did not find any relations. berarti dump tersebut tidak seperti yang Anda kira. Hapus restore_test setelah selesai.

Perintah yang menghapus semuanya

docker compose down -v.

down biasa menghapus container dan network. -v juga menghapus setiap named volume yang dideklarasikan dalam file compose tersebut, serta setiap anonymous volume yang terpasang pada container-container itu. Tidak ada konfirmasi dan tidak ada cara untuk membatalkannya. Ini adalah cara paling umum yang menyebabkan database self-hosted hancur. Biasanya hal ini terjadi saat melakukan troubleshooting terhadap masalah yang tidak terkait, karena sebuah jawaban di forum menyarankan perintah tersebut.

Empat hal berikut mengurangi dampak kerusakan:

  • Deklarasikan volume database sebagai external: true. Compose tidak akan menghapus volume yang bukan miliknya, sehingga -v tidak dapat menjangkaunya. Buat volume tersebut sekali dengan docker volume create myapp_pgdata.
  • Gunakan docker compose stop dan docker compose start untuk restart rutin. perbandingan down dan stop di Compose menjelaskan hal yang dihapus oleh masing-masing perintah.
  • Simpan dump pada host path di luar semua volume yang dikelola compose.
  • Jangan pernah menempelkan -v dari jawaban troubleshooting ke stack yang menyimpan data penting.

Jangan publikasikan port database

Baris ini menempatkan database Anda di Internet publik:

    ports:
      - "5432:5432"

Baris tersebut mengikat port ke semua antarmuka. Docker memublikasikan port dengan mengubah tujuan paket sebelum aturan input firewall Anda memprosesnya. Aturan ufw berada di rantai input, sehingga ufw deny 5432 sama sekali tidak berpengaruh. mengapa port yang dipublikasikan Docker melewati ufw menjelaskan penelusuran rantai tersebut.

Aplikasi dalam project compose yang sama dapat mengakses database menggunakan nama service pada jaringan compose, sehingga tidak memerlukan port yang dipublikasikan. Hapus blok tersebut. Jika Anda ingin client pada host mengaksesnya, ikat port hanya ke loopback:

    ports:
      - "127.0.0.1:5432:5432"

Periksa port yang benar-benar sedang listening:

sudo ss -ltnp | grep 5432

127.0.0.1:5432 adalah konfigurasi yang Anda inginkan. 0.0.0.0:5432 berarti siapa pun dapat mencoba kata sandi Anda.

Jalankan di mana

Satu aplikasi pada satu VPS. Gunakan container. Pakai named volume dengan nama yang ditetapkan, tanpa port yang dipublikasikan, memory limit yang sesuai dengan pengaturan database, serta dump setiap malam ke path pada host yang dikumpulkan oleh restic. Mulai dari instalasi Docker yang bersih pada VPS dan simpan stack dalam satu file compose yang Anda commit. Keuntungannya nyata: versi database menjadi baris yang dapat ditinjau di git.

Host yang menjalankan beberapa service. Gunakan container, satu database untuk setiap aplikasi, bukan satu server bersama untuk semuanya. Server bersama mengikat semua aplikasi pada satu jadwal upgrade, dan satu query yang tidak terkendali dapat menyebabkan outage untuk semua aplikasi. Berikan setiap container memory limit sendiri agar query yang bermasalah hanya berdampak pada aplikasi yang menjalankannya. Beberapa instance Postgres kecil membutuhkan sedikit lebih banyak disk, tetapi jauh lebih sedikit koordinasi.

Database adalah produknya. Jalankan database pada host dari repository paket vendor, atau gunakan managed service berbayar. pg_upgrade memerlukan kedua versi mayor binary terpasang secara bersamaan. Hal ini dapat disediakan oleh paket, tetapi tidak oleh image dengan satu versi. Replication dan point in time recovery dengan pengarsipan WAL (write ahead log) juga lebih mudah jika database mengelola mesin dan disk-nya sendiri. Pilih cara yang sederhana dan stabil untuk sistem yang akan mengirimkan page kepada Anda pada 03:00.

Aplikasinya kecil. Pertimbangkan untuk tidak menjalankan server database sama sekali. Aplikasi web single-writer pada satu VPS sering kali lebih sesuai menggunakan SQLite di production pada VPS, karena backup-nya hanya satu file dan jalur upgrade-nya berupa versi library.

FAQ

Apakah aman menjalankan database produksi di Docker?

Ya, untuk stack aplikasi pada satu server. Container adalah proses Linux dengan namespace dan cgroup di sekitarnya. Setelah volume dipasang, database menulis ke filesystem host yang sama seperti jika database diinstal dari package. Risikonya bersifat operasional, bukan terkait kecepatan: volume yang namanya tidak dipatok, bind mount yang dimiliki user id yang salah, restore yang belum pernah diuji, dan docker compose down -v. Atasi keempat hal tersebut dan container dapat digunakan. Gunakan instalasi pada host jika database adalah beban kerja utama dan Anda memerlukan pg_upgrade, replikasi, atau pemulihan point-in-time.

Sebaiknya menggunakan bind mount atau named volume untuk data database?

Gunakan named volume, kecuali Anda memiliki alasan khusus untuk mengetahui path pada host. Docker membuat directory tersebut, lalu entrypoint image menetapkan ownership saat start pertama sehingga masalah permission tidak muncul. Patok volume dengan name: eksplisit atau tandai sebagai external: true. Jika tidak, penggantian nama directory project secara diam-diam akan menghasilkan volume kosong baru dan database kosong. Bind mount dapat digunakan jika Anda menjalankan chown pada directory host agar dimiliki numeric user id yang digunakan image. Untuk image resmi Postgres, MySQL, dan MongoDB, nilainya adalah 999. Verifikasi dengan ls -ldn, karena ls -l menampilkan nama host untuk angka tersebut dan nama itu tidak bermakna di dalam container.

Apa yang dihapus oleh docker compose down -v?

Perintah tersebut menghapus container dan network seperti down biasa. Selain itu, -v menghapus setiap named volume yang dideklarasikan dalam file compose tersebut, serta setiap anonymous volume yang terpasang pada container tersebut. Database juga ikut terhapus. Tidak ada prompt konfirmasi dan tidak ada pemulihan. Volume yang ditandai external: true tidak dihapus. Inilah alasan utama untuk menandai volume database sebagai external. Untuk restart rutin, gunakan docker compose stop dan docker compose start.

Bagaimana cara meng-upgrade PostgreSQL ke versi major baru di Docker?

Lakukan dump dan restore. Mengubah postgres:16 menjadi postgres:17 lalu melakukan restart akan menghasilkan FATAL: database files are incompatible with server dengan baris DETAIL yang mencantumkan kedua versi. Binary baru tidak dapat membaca layout catalog lama. Tidak ada data yang rusak. Kembalikan tag lama dan container akan dapat start. Buat pg_dumpall menggunakan client versi baru terhadap container lama yang masih berjalan. Pastikan file tersebut diakhiri dengan PostgreSQL database cluster dump complete. Setelah itu, jalankan tag baru pada volume kosong dan muat dump tersebut. Upgrade minor dalam versi major yang sama hanya memerlukan pull dan restart.

Mengapa container database saya keluar dengan code 137?

137 adalah 128 ditambah signal 9. Artinya, suatu proses menghentikannya secara paksa. Jalankan docker inspect <container> | grep -i oomkilled. Nilai true berarti container mencapai memory limit cgroup-nya. Penyebab yang umum adalah PostgreSQL dan MySQL membaca total memory dari host dan tidak melihat limit container. Akibatnya, keduanya merencanakan penggunaan 16 GB saat berjalan dalam limit 2 GB. Tetapkan shared_buffers dan work_mem, atau innodb_buffer_pool_size, agar sesuai dengan limit yang diberikan kepada container. Periksa journalctl -k untuk menemukan baris Memory cgroup out of memory yang sesuai dan memastikan proses yang dihentikan oleh kernel.