SSD Nodes Learn Hosting plans →
Panduan Matt ConnorOleh Matt Connor

Dasar Docker Compose untuk VPS Ubuntu 24.04

Pelajari instalasi Docker Engine dan plugin Compose di Ubuntu 24.04, stack Miniflux-PostgreSQL, jebakan port UFW, serta cara mencadangkan volume.

Yang Anda bangun

Docker Compose adalah fondasi untuk hampir semua hal lain di situs ini. Nextcloud, Vaultwarden, n8n, Immich, Rocket.Chat, semua panduan tersebut diawali dengan "tulis file compose ini", dan halaman ini menjelaskan arti sebenarnya dari file tersebut. Anda akan menginstal Docker Engine dan plugin Compose dari repositori apt milik Docker sendiri pada Ubuntu 24.04, lalu menjalankan stack dua service yang nyata: Miniflux, pembaca RSS ringan, serta PostgreSQL. Pasangan ini mencakup semua pola yang digunakan aplikasi yang lebih besar: image yang versinya dikunci, database dengan healthcheck, named volume, secret dalam file .env, dan port yang hanya dipublikasikan ke localhost.

Instalasi ini memerlukan waktu lima menit. Bagian selanjutnya membahas masalah yang dapat muncul kemudian: docker group yang pada dasarnya setara dengan root, port yang dipublikasikan dan melewati aturan ufw, serta satu flag pada docker compose down yang menghapus database tanpa meminta konfirmasi.

Prasyarat: KVM VPS Ubuntu 24.04 yang masih baru, user dengan akses sudo, dan RAM minimal 1 gigabyte. Instalasi Docker yang sudah ada juga tidak menjadi masalah; bagian pertama menjelaskan hal-hal yang perlu dihapus.

Instal dari repositori Docker, bukan repositori Ubuntu

Ada dua pilihan keliru yang harus dihindari sebelum menjalankan perintah pertama. Paket docker.io bawaan Ubuntu dapat digunakan, tetapi tertinggal dari rilis Docker dan tidak menyediakan tata letak plugin yang diasumsikan oleh komponen lain. Perintah docker-compose dengan tanda hubung juga mengarahkan Anda ke jalur yang digunakan tutorial lama. Pada server lama, perintah tersebut biasanya adalah Compose v1 yang ditulis dalam Python. Docker telah menyatakan versi itu mencapai akhir masa pakainya. Itulah sebabnya tutorial tersebut gagal. Docker masih menerbitkan biner docker-compose mandiri yang dibuat dari kode Go terbaru, tetapi halaman instalasinya sendiri menyebut cara tersebut tidak direkomendasikan dan hanya didukung untuk kompatibilitas mundur. Saat ini, Compose adalah docker compose dengan spasi, berupa plugin CLI yang diinstal dari repositori yang sama dengan engine.

Jika salah satu komponen tersebut sudah ada di server, hapus terlebih dahulu, termasuk docker-compose-v2 dan docker-buildx, yaitu paket plugin bawaan Ubuntu, agar semuanya berasal dari satu repositori:

sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc

Package 'docker.io' is not installed, so not removed adalah keluaran normal pada VPS baru. Selanjutnya, tambahkan repositori Docker dan lakukan instalasi:

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Verifikasi ketiga lapisan tersebut:

docker --version
docker compose version
sudo docker run --rm hello-world

Dua perintah pertama menampilkan string versi. Docker Compose version v5.x.x mengonfirmasi bahwa Anda memiliki plugin, bukan biner v1 yang sudah tidak digunakan. Plugin tersebut langsung berubah dari v2 ke v5 pada Desember 2025. Versi 3 dan 4 dilewati untuk menghindari kebingungan dengan versi format file compose lama. Karena itu, panduan yang menampilkan v2.x.x di sini merujuk pada alat yang sama. Perintah hello-world harus berakhir dengan Hello from Docker!. Paket tersebut mengaktifkan service saat boot. systemctl is-enabled docker menampilkan enabled.

Grup docker adalah root, jadi buat keputusan dengan memahami risikonya

Saat ini setiap perintah docker memerlukan sudo karena socket daemon di /var/run/docker.sock dimiliki oleh root dan grup docker. Tanpa keanggotaan tersebut, Anda akan mendapatkan error Docker yang paling sering dicari di Google:

permission denied while trying to connect to the Docker daemon socket at
unix:///var/run/docker.sock

Perbaikan standar:

sudo usermod -aG docker $USER

Keanggotaan grup berlaku saat login, sehingga error tetap muncul di shell saat ini. Jalankan newgrp docker untuk sesi ini, atau keluar lalu login kembali. Setelah itu, id seharusnya mencantumkan docker dalam grup Anda.

Berikut bagian pentingnya, secara langsung: keanggotaan dalam grup docker memberikan hak root pada host. Bukan sekadar hak yang mirip root atau hak yang ditingkatkan, melainkan root. Siapa pun yang berada dalam grup tersebut dapat menjalankan docker run --rm -it -v /:/host alpine chroot /host dan menguasai seluruh filesystem tanpa memerlukan kata sandi. Grup ini tersedia untuk kemudahan, bukan untuk membatasi akses.

Mode rootless Docker adalah alternatif yang sebenarnya. Dalam mode ini, daemon berjalan sebagai pengguna Anda yang tidak memiliki hak istimewa. Namun, ada konsekuensinya: port di bawah 1024 memerlukan konfigurasi tambahan, jaringan berjalan melalui shim userspace dengan overhead yang dapat diukur, dan beberapa image tidak berfungsi dengan baik tanpa root yang sebenarnya. Pada VPS dengan satu administrator, yang satu-satunya login-nya sudah memiliki sudo, perubahan grup ini secara praktik tidak mengubah apa pun. Mode ini digunakan oleh semua panduan di sini, tetapi jangan pernah memberikan akses tersebut seolah-olah haknya lebih rendah daripada sudo.

Anatomi file compose

Berikan setiap stack direktori sendiri. Nama direktori menjadi nama project yang digunakan sebagai awalan untuk container, network, dan volume:

sudo mkdir -p /opt/miniflux && sudo chown $USER /opt/miniflux && cd /opt/miniflux

Buat compose.yml (nama modernnya; docker-compose.yml masih berfungsi). Jangan gunakan key lama version: karena sudah obsolete, dan Compose akan menampilkan peringatan jika menemukannya.

services:
  miniflux:
    image: miniflux/miniflux:2.2.9
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    environment:
      - DATABASE_URL=postgres://miniflux:${POSTGRES_PASSWORD}@db/miniflux?sslmode=disable
      - RUN_MIGRATIONS=1
      - CREATE_ADMIN=1
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=${ADMIN_PASSWORD}
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      - POSTGRES_USER=miniflux
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=miniflux
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "miniflux", "-d", "miniflux"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  db-data:

Setiap baris di atas merupakan sebuah keputusan. Pahami satu per satu.

Pin versi image, :latest ditambah pull adalah upgrade tanpa pengawasan

Gunakan postgres:16-alpine, bukan postgres:latest. Tag tidak bersifat tetap: :latest akan mengarah ulang ke image yang terakhir didorong maintainer setiap kali Anda melakukan pull. Jika digabungkan dengan kebiasaan upgrade rutin yang akan Anda pelajari, docker compose pull && docker compose up -d, maka :latest berarti lompatan major version terjadi setiap kali upstream merilisnya, bukan saat Anda memilihnya. Pada PostgreSQL, hal ini bukan sekadar kemungkinan: lompatan mendadak dari 16 ke 17 membuat container terus crash-loop pada data directory yang tidak kompatibel, karena upgrade major Postgres memerlukan dump dan restore, bukan restart.

Setidaknya pin major version (postgres:16-alpine mengikuti patch release 16.x), dan pin aplikasi ke release yang tepat seperti miniflux/miniflux:2.2.9. Periksa halaman releases project dan gunakan versi yang sedang berlaku saat Anda menulis file. Dengan demikian, upgrade menjadi edit satu baris yang Anda lakukan secara sengaja dan terlihat di git diff.

Publish ke 127.0.0.1 karena Docker melewati ufw

"127.0.0.1:8080:8080", alamat host, port host, port container. Sebagian besar tutorial menulis "8080:8080", yang merupakan bentuk singkat dari 0.0.0.0:8080:8080: listening pada setiap interface, termasuk interface publik.

Berikut jebakannya, dan hampir semua orang mengalaminya setidaknya sekali. Docker mem-publish port dengan menulis aturan DNAT yang mengubah tujuan paket menjadi IP internal container sebelum filtering dilakukan. Dengan demikian, paket melewati jalur FORWARD dan tidak pernah menyentuh INPUT, tempat aturan ufw Anda berada. sudo ufw deny 8080 melaporkan keberhasilan, ufw status menunjukkan port ditolak, tetapi service tetap menjawab seluruh Internet. Firewall Anda tidak rusak; trafik memang melewatinya sesuai desain. Mengapa Docker melewati ufw dan cara memfilter trafik container dengan benar menjelaskan mekanismenya dan solusi DOCKER-USER untuk port yang harus tetap publik.

Kebiasaan yang menghilangkan seluruh masalah ini adalah mengikat port yang dipublish ke 127.0.0.1, kecuali ada alasan khusus untuk tidak melakukannya. Gunakan reverse proxy di depannya untuk layanan yang harus menghadap Internet. Itulah yang dibangun oleh panduan reverse proxy Traefik sebagai langkah berikutnya setelah halaman ini: satu container yang memiliki port 80 dan 443 serta merutekan semua layanan lain berdasarkan hostname, dengan TLS. Jika Anda menggunakan setup Traefik v2 yang lebih lama, panduan migrasi Traefik v2 ke v3 membahas perubahan nama dan aturan.

Verifikasi binding setelah stack dijalankan: sudo ss -tlnp | grep 8080 harus menampilkan 127.0.0.1:8080, bukan 0.0.0.0:8080 atau *:8080.

Named volume vs bind mount

db-data:/var/lib/postgresql/data adalah named volume: Docker membuat dan mengelola direktori di bawah /var/lib/docker/volumes/, lalu me-mount-nya ke dalam container. Alternatifnya adalah bind mount, ./data:/var/lib/postgresql/data, yang memetakan path yang Anda pilih pada host.

Pembagian yang terbukti efektif dalam praktik adalah named volume untuk data yang hanya disentuh oleh container, terutama database, karena Docker menginisialisasi volume dengan ownership yang diharapkan image dan permission file dapat langsung berfungsi. Gunakan bind mount untuk file yang Anda akses dari host, seperti file konfigurasi yang diedit dengan text editor, media library yang di-rsync ke host, atau apa pun yang path-nya ingin Anda lihat dengan jelas. Masalah klasik bind mount adalah ownership: image menjalankan prosesnya dengan UID tetap (image Miniflux berjalan sebagai UID 65534, postgres:16-alpine menurunkan UID menjadi 70), sedangkan direktori pada host dimiliki UID 1000. Akibatnya, aplikasi gagal saat startup dan menampilkan permission denied di log. Named volume hampir menghilangkan jenis masalah ini, dengan konsekuensi data berada pada path yang dikelola Docker, seperti dijelaskan di bawah.

environment dan .env, jauhkan secret dari git

Compose mengisi ${POSTGRES_PASSWORD} dari file bernama .env yang berada di sebelah compose.yml, tetapi hanya jika shell Anda belum mendefinisikan variabel tersebut. Nilai yang diekspor di environment shell memiliki prioritas atas file .env. Karena itu, export POSTGRES_PASSWORD=... yang terlupa dari eksperimen sebelumnya dapat secara diam-diam menimpa isi file, dan docker compose config di bawah menunjukkan nilai yang digunakan. Buat file tersebut:

cat > .env <<'EOF'
POSTGRES_PASSWORD=change-me-to-something-long
ADMIN_PASSWORD=change-me-too
EOF
chmod 600 .env
echo ".env" >> .gitignore

Buat nilai sebenarnya dengan openssl rand -hex 24. Gunakan hex, bukan base64, dengan sengaja: password ini akan berada di dalam connection string DATABASE_URL, sedangkan karakter /, +, dan = yang dihasilkan base64 dapat merusak parsing URL. Kegagalan ini muncul sebagai error autentikasi, bukan syntax error, dan dapat menghabiskan waktu berjam-jam. Baris .gitignore harus ditambahkan sebelum commit pertama: compose file aman untuk dipublikasikan dan disimpan dalam version control, sedangkan file .env tidak boleh dipublikasikan. Secret yang pernah masuk ke riwayat git harus dianggap sebagai secret yang perlu dirotasi. Jika Anda menjalankan stack saat sebuah variabel tidak ada, Compose menampilkan peringatan dan melanjutkan dengan string kosong. Untuk password Postgres, hal ini menghasilkan deployment yang rusak:

WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string.

docker compose config menampilkan file yang telah diinterpolasi sepenuhnya. Ini adalah cara tercepat untuk memeriksa nilai yang benar-benar akan diterima container. Ingat bahwa output-nya mencakup secret Anda.

depends_on tidak menunggu apa pun kecuali Anda menambahkan healthcheck

depends_on: [db] biasa hanya mengatur urutan startup: Compose menjalankan Postgres terlebih dahulu, lalu aplikasi sesaat kemudian, sementara Postgres masih memerlukan beberapa detik sebelum menerima koneksi. Aplikasi mencoba mengakses database, gagal, lalu crash atau melakukan retry, bergantung pada kualitas implementasinya.

Versi yang andal adalah yang digunakan file di atas: service db mendefinisikan healthcheck (Postgres menyediakan pg_isready khusus untuk keperluan ini), dan aplikasi mendeklarasikan depends_on dengan condition: service_healthy. Compose menjalankan database, memeriksa statusnya setiap 10 detik, lalu hanya menjalankan Miniflux setelah pemeriksaan berhasil. Jika database tidak pernah menjadi healthy, misalnya karena password salah atau volume rusak, aplikasi tidak akan dijalankan dan Compose akan memberi tahu dependency yang gagal:

dependency failed to start: container miniflux-db-1 is unhealthy

Pesan tersebut mengarahkan Anda ke docker compose logs db, tempat error sebenarnya berada.

restart: unless-stopped

restart: unless-stopped pada kedua service berarti container akan berjalan kembali setelah crash dan setelah reboot VPS, tetapi tetap berhenti jika Anda sengaja menjalankan docker compose stop. Alternatif always akan menjalankan kembali container bahkan setelah penghentian manual, yang biasanya bukan hasil yang diinginkan. Tanpa restart policy, reboot akibat kernel update pada pukul 4 pagi dapat menghentikan service Anda tanpa terlihat sampai Anda menyadarinya.

Perintah harian

Semua kegiatan sehari-hari cukup menggunakan lima perintah yang dijalankan dari direktori project.

docker compose up -d        # create and start; idempotent, recreates only what changed
docker compose ps           # status, ports, and health of this project's containers
docker compose logs -f miniflux   # follow one service's logs; --tail 100 for recent history
docker compose pull && docker compose up -d   # upgrade to the pinned tags
docker compose down         # stop and remove containers and the network

up -d aman dijalankan berulang kali. Perintah ini membandingkan file dengan kondisi aktual dan hanya memproses ulang service yang konfigurasi atau image-nya berubah. Pasangan perintah upgrade mengambil apa pun yang kini ditunjuk oleh tag yang Anda pin: patch release di bawah postgres:16-alpine, dan tidak ada perubahan untuk pin yang persis sampai Anda mengeditnya. Itulah tujuannya. Image lama akan menumpuk setelah upgrade. Untuk membebaskan ruang disk, gunakan docker image prune -f.

Sekarang perintah yang bersifat destruktif, dan ini perlu ditegaskan: docker compose down aman karena container dan network dapat dibuat ulang, sedangkan data Anda berada di volume. docker compose down -v juga menghapus volume bernama. Itu berarti database Anda langsung hilang, tanpa prompt konfirmasi dan tanpa cara untuk membatalkannya. Flag -v digunakan untuk membongkar eksperimen. Pada stack yang menyimpan data nyata, perlakukan flag tersebut seperti rm -rf. Tidak ada tempat sampah di bawah /var/lib/docker/volumes/.

Untuk membuka shell satu kali di dalam container yang sedang berjalan: docker compose exec db psql -U miniflux membawa Anda ke database, sedangkan docker compose exec miniflux sh membuka shell pada app.

Lokasi sebenarnya data Anda

Named volume menggunakan awalan project. Karena itu, db-data dalam direktori bernama miniflux menjadi miniflux_db-data:

docker volume ls
docker volume inspect miniflux_db-data

Output inspect menyertakan baris yang penting:

"Mountpoint": "/var/lib/docker/volumes/miniflux_db-data/_data"

Direktori tersebut adalah database, dimiliki oleh root, berada pada sistem berkas host, dan tetap ada setelah down, upgrade, serta pembuatan ulang container. Direktori ini juga merupakan data yang harus dicadangkan oleh backup Anda.

Cadangkan volume bernama

Pola standar menggunakan container sementara yang memasang volume dalam mode hanya-baca di samping direktori host, lalu menjalankan tar untuk menyalin data:

docker run --rm \
  -v miniflux_db-data:/data:ro \
  -v "$PWD":/backup \
  alpine:3.22 tar czf /backup/miniflux-db-$(date +%F).tar.gz -C /data .

Tidak perlu melakukan instalasi dan tidak ada proses yang tetap berjalan. Pemulihan menggunakan pola kebalikan, yaitu tar xzf ke volume baru yang kosong dengan mount yang dibalik.

Ada satu hal yang perlu diperhatikan untuk database: menjalankan tar pada direktori data Postgres yang sedang berjalan dapat menangkap kondisi saat penulisan sedang berlangsung, sehingga database tidak dapat start dengan baik. Hentikan database selama beberapa detik yang diperlukan tar, yaitu docker compose stop, atau, lebih baik, buat logical dump yang secara bawaan konsisten:

docker compose exec -T db pg_dump -U miniflux miniflux | gzip > miniflux-$(date +%F).sql.gz

-T menonaktifkan pseudo-terminal yang secara default dialokasikan Compose. Menyalurkan output dump melalui TTY dapat merusaknya. Masukkan salah satu perintah ini ke cron dan salin hasilnya ke luar VPS. Backup pada disk yang sama dengan data yang dilindunginya hanyalah salinan, bukan backup. Panduan Nextcloud menyusun rutinitas terjadwal lengkap berdasarkan kedua pola ini.

Mode kegagalan dan string yang akan Anda lihat

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock, Anda belum tergabung dalam grup docker, atau sudah tergabung tetapi sesi saat ini dimulai sebelum perubahan tersebut. id menampilkan grup efektif Anda; newgrp docker memperbaiki shell saat ini, sedangkan logout lalu login kembali menerapkan semuanya.

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?, ini masalah yang berbeda: daemon itu sendiri sedang berhenti. sudo systemctl status docker dan sudo journalctl -u docker -n 50 menjelaskan penyebabnya. Pada VPS, penyebab yang umum adalah disk penuh. Jalankan df -h /var/lib/docker terlebih dahulu.

Bind for 127.0.0.1:8080 failed: port is already allocated, container lain sudah memublikasikan port host tersebut. docker ps menampilkan container yang menggunakannya; container lama dari percobaan docker run beberapa minggu lalu biasanya menjadi penyebabnya. Jika docker ps tidak menampilkan apa pun, proses non-Docker sedang menggunakan port tersebut: sudo ss -tlnp | grep 8080 menampilkan prosesnya.

yaml: line 14: did not find expected key, terdapat kesalahan indentasi pada atau tepat di atas baris yang disebutkan. File Compose menggunakan YAML: indentasi dua spasi, hanya spasi, dan satu karakter tab di mana pun akan menyebabkan kegagalan. docker compose config memvalidasi file tanpa menjalankan apa pun. Menjalankannya setelah setiap perubahan adalah kebiasaan yang mudah dilakukan.

Kejutan ufw tidak menampilkan error sama sekali. Inilah yang membuatnya berbahaya: deploy berhasil, ufw status terlihat benar, tetapi pemindaian port dari luar tetap menemukan database Anda. Baca kembali bagian port di atas, periksa setiap entri ports: untuk memastikan tidak ada awalan 127.0.0.1: yang hilang, lalu konfirmasi dari mesin yang berbeda dengan curl http://your-vps-ip:8080. Hasil yang Anda inginkan adalah connection refused.

Dari sini, panduan Traefik mengubah stack tunggal ini menjadi banyak aplikasi di balik satu entry point HTTPS, sedangkan layanan yang layak di-self-host pada 2026 adalah daftar aplikasi yang dapat Anda jalankan di baliknya. Setelah beberapa stack tersebut berjalan dan masing-masing memiliki formulir login sendiri, server SSO self-hosted seperti Authentik menggabungkannya kembali menjadi satu akun di balik proxy yang sama.

Game server seperti server Minecraft pada VPS adalah proyek Compose pertama yang mudah digunakan untuk berlatih. Jika Anda lebih suka belajar dengan sesuatu yang dibuka setiap hari, openGym, pelacak latihan self-hosted adalah stack kecil yang dikunci pada git tag, bukan image tag. Layanan ini memerlukan TLS di depannya sebelum Anda mendaftarkan passkey pertama. Foto biasanya menjadi hal pertama yang ingin dipindahkan kembali dari cloud milik orang lain, dan perbandingan PhotoPrism dan Immich membantu menentukan kebutuhan RAM minimum serta prosedur backup yang akan Anda gunakan sebelum menetapkan volume untuk salah satunya. Saat dua service mulai terasa tidak cukup, menyiapkan AFFiNE sebagai workspace bergaya Notion menerapkan pola yang sama pada empat container. Ini juga menjadi pengujian yang baik untuk memastikan pinned tag, healthcheck, dan named volume di atas sudah menjadi kebiasaan.

FAQ

Mengapa saya mendapatkan pesan "permission denied while trying to connect to the Docker daemon socket"?

User Anda belum berada dalam grup docker, atau ditambahkan setelah sesi saat ini dimulai. Keanggotaan grup hanya berlaku saat login. Jalankan sudo usermod -aG docker $USER, kemudian newgrp docker, atau logout lalu login kembali, dan konfirmasikan dengan id. Grup ini memberikan akses yang setara dengan root ke host. Karena itu, hanya tambahkan user yang juga akan Anda beri akses sudo.

Apakah docker compose down menghapus data saya?

docker compose down biasa tidak menghapus data. Perintah tersebut menghapus container dan network proyek. Named volume tetap ada, dan up -d berikutnya akan menggunakannya kembali. docker compose down -v adalah bentuk destruktif. Perintah tersebut menghapus named volume, termasuk database Anda, tanpa konfirmasi dan tanpa cara untuk membatalkannya. Jangan pernah menjalankan -v pada stack dengan data produksi kecuali Anda memiliki backup yang sudah diverifikasi.

Apa perbedaan antara docker-compose dan docker compose?

docker-compose (dengan tanda hubung) adalah cara lama untuk menjalankan Compose. Pada server lama, perintah ini biasanya adalah Compose v1, versi asli berbasis Python yang telah dinyatakan end of life oleh Docker. Docker juga masih menerbitkan binary docker-compose mandiri yang dibuat dari kode Go saat ini, tetapi menyatakan bahwa instalasi tersebut tidak direkomendasikan dan hanya didukung untuk kompatibilitas mundur. Karena itu, jangan gunakan instalasi tersebut pada server baru. docker compose (dengan spasi) adalah Compose saat ini, yaitu v2 dan sejak December 2025 diberi nomor v5. Compose ini merupakan plugin Go untuk Docker CLI, yang diinstal sebagai docker-compose-plugin dari repositori apt Docker. Perintah dan YAML hampir sepenuhnya kompatibel. Jadi, jika tutorial lama menyebut docker-compose up, ketik docker compose up.

Mengapa container Docker saya dapat diakses dari Internet meskipun ufw memblokir port tersebut?

Docker menerbitkan port menggunakan aturan DNAT di chain PREROUTING milik iptables. Paket yang telah ditulis ulang melewati path FORWARD melalui chain milik Docker sendiri. Paket tersebut tidak pernah mencapai chain INPUT tempat aturan ufw diterapkan. Karena itu, ufw deny 8080 tidak berpengaruh pada port container yang diterbitkan. Perbaiki masalah ini dari sumbernya. Terbitkan port ke 127.0.0.1:, lalu akses layanan melalui reverse proxy.

Sebaiknya saya menggunakan named volume atau bind mount?

Gunakan named volume untuk data yang hanya diakses oleh container, terutama database. Docker akan menetapkan ownership sesuai harapan image sehingga permission dapat berfungsi tanpa konfigurasi tambahan. Gunakan bind mount untuk file yang juga Anda kelola dari host, seperti konfigurasi yang Anda edit dan media yang Anda upload, atau file yang path-nya ingin Anda lihat dengan jelas. Jika container gagal saat startup dengan permission denied pada bind mount, hal pertama yang perlu diperiksa adalah ketidaksesuaian UID antara host dan container.