SSD Nodes Learn
Panduan Matt ConnorOleh Matt Connor · Dikemas kini 2026-07-24

Cara guna Docker Compose di VPS Ubuntu

Panduan pasang Docker Engine dan Compose v2 pada Ubuntu 24.04. Pelajari cara bina fail compose.yml serta cara elak masalah port ufw dan pemadaman data.

Apa yang anda bina

Docker Compose adalah asas bagi hampir semua perkara lain di laman ini. Nextcloud, Vaultwarden, n8n, Immich, Rocket.Chat — setiap panduan tersebut bermula dengan arahan "tulis fail compose ini", dan halaman ini menjelaskan maksud sebenar fail tersebut. Anda akan memasang Docker Engine dan plugin Compose v2 daripada repositori apt Docker pada Ubuntu 24.04, kemudian membina stack dua perkhidmatan yang sebenar — Miniflux, sebuah pembaca RSS kecil, bersama PostgreSQL — kerana pasangan ini menggunakan setiap corak yang digunakan oleh aplikasi lebih besar: imej yang ditetapkan (pinned images), pangkalan data dengan semakan kesihatan (healthcheck), volume bernama, rahsia dalam fail .env, dan port yang hanya diterbitkan ke localhost.

Pemasangan mengambil masa lima minit. Bahagian selebihnya dalam panduan ini merangkumi perkara yang akan menyukarkan anda kemudian: kumpulan docker yang mempunyai hak root, port yang diterbitkan melangkaui peraturan ufw anda, dan satu flag pada docker compose down yang memadam pangkalan data anda tanpa sebarang permintaan pengesahan.

Prasyarat: VPS KVM Ubuntu 24.04 yang baharu, pengguna dengan hak sudo, dan sekurang-kurangnya 1 GB RAM. Pemasangan Docker sedia ada juga dibenarkan — bahagian pertama merangkumi perkara yang perlu dibuang.

Pasang dari repo Docker, bukan Ubuntu

Terdapat dua kesilapan yang perlu dielakkan sebelum menjalankan arahan pertama. Pakej docker.io milik Ubuntu berfungsi, tetapi versinya ketinggalan berbanding Docker dan tidak mempunyai susun atur plugin yang diperlukan. Binari docker-compose yang berdiri sendiri — yang mempunyai tanda sempang — adalah Compose v1: berasaskan Python, telah tamat tempoh sokongan sejak 2023, dan merupakan punca tutorial lama gagal. Compose sekarang adalah docker compose dengan ruang, iaitu plugin CLI, yang dipasang dari repositori yang sama dengan engine.

Jika mana-mana komponen ini sudah ada pada sistem, padamkan dahulu — termasuk docker-compose-v2, iaitu pembungkusan plugin milik Ubuntu, supaya semua komponen datang dari satu repositori:

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

Package 'docker.io' is not installed, so not removed adalah output biasa pada VPS baharu. Kemudian tambah repositori Docker dan pasang:

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

Sahkan ketiga-tiga lapisan:

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

Dua yang pertama memaparkan string versi — Docker Compose version v2.x.x mengesahkan anda mempunyai plugin, bukan binari v1 yang sudah tidak disokong. Arahan hello-world harus berakhir dengan Hello from Docker!. Pakej ini membolehkan servis diaktifkan semasa boot; systemctl is-enabled docker memaparkan enabled.

Kumpulan docker adalah root — buat keputusan dengan teliti

Pada masa ini, setiap arahan docker memerlukan sudo, kerana soket daemon pada /var/run/docker.sock dimiliki oleh root dan kumpulan docker. Tanpa keahlian kumpulan ini, anda akan mendapat ralat Docker yang paling kerap dicari:

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

Penyelesaian standard:

sudo usermod -aG docker $USER

Keahlian kumpulan hanya terpakai semasa log masuk, jadi ralat tersebut akan berterusan dalam shell semasa anda. Jalankan newgrp docker untuk sesi ini, atau log keluar dan log masuk semula; id sepatutnya menyenaraikan docker dalam kumpulan anda.

Bahagian yang jujur, dinyatakan secara terus terang: keahlian kumpulan docker adalah root pada hos. Bukan "seperti root", bukan "berkuasa tinggi" — tetapi root. Sesiapa dalam kumpulan tersebut boleh menjalankan docker run --rm -it -v /:/host alpine chroot /host dan memiliki keseluruhan sistem fail tanpa memerlukan kata laluan. Kumpulan ini wujud untuk kemudahan, bukan untuk kawalan keselamatan (containment).

Mod rootless Docker adalah alternatif sebenar — daemon itu sendiri berjalan sebagai pengguna tanpa keistimewaan anda. Ia mempunyai kos: port di bawah 1024 memerlukan tetapan tambahan, rangkaian berjalan melalui shim userspace dengan beban (overhead) yang nyata, dan beberapa imej tidak berfungsi dengan betul tanpa root sebenar. Pada VPS dengan pentadbir tunggal di mana satu-satunya log masuk sudah mempunyai sudo, perubahan kumpulan ini tidak mengubah apa-apa secara praktikal, dan inilah yang dianggap oleh setiap panduan di sini — cuma jangan berikan keahlian ini seolah-olah ia kurang daripada sudo.

Anatomi fail compose

Berikan setiap stack satu direktori khas — nama direktori tersebut akan menjadi nama projek, yang menjadi awalan bagi container, rangkaian, dan volume:

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

Cipta compose.yml (nama moden; docker-compose.yml masih berfungsi). Abaikan kunci version: yang lama — ia sudah usang dan Compose akan memberi amaran jika ia dikesan.

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 adalah satu keputusan. Laksanakan satu demi satu.

Tetapkan versi imej — :latest ditambah dengan pull adalah naik taraf tanpa pengawasan

postgres:16-alpine, bukan postgres:latest. Tag tidak bersifat kekal: :latest akan merujuk semula kepada apa sahaja yang paling baru dimuat naik oleh penyedia setiap kali anda melakukan pull. Gabungkan ini dengan tabiat naik taraf rutin yang bakal anda pelajari — docker compose pull && docker compose up -d — dan :latest bermaksud lonjakan versi utama akan berlaku bila-bila masa pembekal menghantarnya, bukan apabila anda memilih. Bagi PostgreSQL, ini bukan sekadar teori: lonjakan mengejut dari 16 ke 17 akan menyebabkan container mengalami crash-loop kerana direktori data tidak serasi, kerana naik taraf major Postgres memerlukan dump dan restore, bukan sekadar restart.

Tetapkan sekurang-kurangnya versi utama (postgres:16-alpine mengikut rilis patch 16.x), dan tetapkan aplikasi kepada rilis tepat seperti miniflux/miniflux:2.2.9 — semak halaman rilis projek dan gunakan apa sahaja yang terkini semasa anda menulis fail tersebut. Naik taraf kemudiannya menjadi suntingan satu baris yang anda lakukan dengan sengaja, yang boleh dilihat dalam git diff.

Terbitkan ke 127.0.0.1, kerana Docker memintas ufw

"127.0.0.1:8080:8080" — alamat hos, port hos, port container. Kebanyakan tutorial menulis "8080:8080", yang merupakan singkatan bagi 0.0.0.0:8080:8080: mendengar pada setiap interface, termasuk interface awam.

Inilah perangkapnya, dan ia akan menimpa hampir semua orang sekali. Docker menerbitkan port dengan menulis peraturan DNAT yang menulis semula destinasi paket ke IP dalaman container sebelum penapisan, jadi paket tersebut mengambil laluan FORWARD dan tidak pernah menyentuh INPUT, di mana peraturan ufw anda berada. sudo ufw deny 8080 melaporkan kejayaan, ufw status menunjukkan port ditolak, tetapi servis masih menjawab seluruh internet. Firewall anda tidak rosak; ia dipintas secara reka bentuk. Mengapa Docker memintas ufw, dan cara menapis trafik container yang sebenar menerangkan mekanisme tersebut dan penyelesaian DOCKER-USER untuk port yang mesti kekal awam.

Tabiat yang menghapuskan keseluruhan masalah ini: ikat (bind) port yang diterbitkan ke 127.0.0.1 kecuali jika anda mempunyai sebab khusus untuk tidak melakukannya, dan letakkan reverse proxy di hadapan untuk apa sahaja yang perlu berhadapan dengan dunia luar. Itulah yang dibina oleh panduan reverse proxy Traefik sebagai langkah seterusnya selepas halaman ini — satu container yang memiliki port 80 dan 443 dan menghala ke semua perkara lain melalui hostname, dengan TLS. (Beralih dari tetapan Traefik v2 yang lama? panduan migrasi Traefik v2 ke v3 merangkumi penamaan semula dan perubahan peraturan.)

Sahkan ikatan (bind) selepas memulakan stack: sudo ss -tlnp | grep 8080 sepatutnya menunjukkan 127.0.0.1:8080, bukan 0.0.0.0:8080 atau *:8080.

Named volumes vs bind mounts

db-data:/var/lib/postgresql/data adalah named volume: Docker mencipta dan menguruskan direktori di bawah /var/lib/docker/volumes/ dan memasukinya (mount) ke dalam container. Alternatifnya ialah bind mount, ./data:/var/lib/postgresql/data, yang memetakan laluan yang anda pilih pada hos.

Pembahagian yang berkesan dalam amalan: named volumes hanya untuk container yang menyentuh data — pangkalan data terutamanya, kerana Docker memulakan volume dengan pemilikan yang dijangkakan oleh imej dan kebenaran fail berfungsi dengan betul. Bind mounts untuk fail yang anda sentuh dari hos — fail konfigurasi yang anda edit dengan editor teks, perpustakaan media yang anda rsync ke dalamnya, apa sahaja yang anda mahu laluannya jelas. Kegagalan bind-mount yang klasik ialah pemilikan: container berjalan sebagai UID 999, direktori hos anda dimiliki oleh UID 1000, dan aplikasi gagal semasa permulaan dengan permission denied dalam lognya. Named volumes menjadikan jenis pepijat ini hampir hilang, dengan kos data disimpan pada laluan yang diuruskan oleh Docker — dibincangkan di bawah.

environment dan .env — simpan rahsia jauh daripada git

${POSTGRES_PASSWORD} tidak dibaca dari shell anda; Compose mengambil nilainya daripada fail bernama .env yang terletak bersebelahan dengan compose.yml. Ciptanya:

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

Jana nilai sebenar dengan openssl rand -hex 24. Hex, bukan base64, dengan sengaja: kata laluan ini masuk ke dalam string sambungan DATABASE_URL, dan karakter /, +, dan = yang dihasilkan oleh base64 akan merosakkan parsing URL — kegagalan yang muncul sebagai ralat pengesahan, bukan ralat sintaks, dan membuang masa satu petang. Baris .gitignore diletakkan sebelum commit pertama: fail compose selamat untuk diterbitkan dan diuruskan versinya, fail .env tidak akan selamat, dan rahsia yang telah menyentuh sejarah git adalah rahsia yang perlu anda tukar (rotate). Jika anda memulakan stack dengan pemboleh ubah yang hilang, Compose akan memberi amaran dengan jelas dan meneruskan dengan string kosong — yang bagi kata laluan Postgres bermaksud deployment yang gagal:

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

docker compose config mencetak fail yang telah diisi sepenuhnya — cara terpantas untuk menyemak apa yang sebenarnya akan diterima oleh container; ingat outputnya mengandungi rahsia anda.

depends_on tidak menunggu apa-apa — kecuali jika anda menambah healthcheck

depends_on: [db] kosong hanya mengawal urutan permulaan: Compose melancarkan Postgres dahulu dan aplikasi sejurus kemudian, semasa Postgres masih beberapa saat sebelum menerima sambungan. Aplikasi cuba menghubungi pangkalan data, gagal, dan terhenti (crash) atau mencuba semula bergantung pada kualiti kodnya.

Versi yang boleh dipercayai adalah apa yang digunakan oleh fail di atas: servis db mentakrifkan healthcheck (Postgres menyediakan pg_isready untuk tujuan ini), dan aplikasi mengisytiharkan depends_on dengan condition: service_healthy. Compose memulakan pangkalan data, menyemak status setiap 10 saat, dan hanya memulakan Miniflux setelah semakan lulus. Jika pangkalan data tidak pernah sihat — kata laluan salah, volume rosak — aplikasi tidak akan bermula dan Compose akan memberitahu anda kebergantungan mana yang gagal:

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

Mesej itu menunjukkan docker compose logs db, di mana ralat sebenar berada.

restart: unless-stopped

restart: unless-stopped pada kedua-dua servis bermaksud container akan kembali berfungsi selepas crash dan selepas reboot VPS, tetapi kekal terhenti jika anda sengaja menjalankan docker compose stop. Alternatif always akan menghidupkan semula container walaupun selepas dihentikan secara manual — jarang sekali ini yang anda mahukan. Tanpa polisi restart, reboot kemas kini kernel pada jam 4 pagi akan menghentikan servis anda secara senyap sehingga anda menyedarinya.

Perintah harian

Semua tugasan harian melibatkan lima perintah, dijalankan dari direktori projek.

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 selamat untuk dijalankan berulang kali — ia membandingkan fail dengan keadaan sebenar dan hanya mengemas kini perkhidmatan yang mempunyai perubahan konfigurasi atau imej. Pasangan naik taraf akan mengambil apa sahaja yang ditunjuk oleh tag yang ditetapkan: rilis tampalan di bawah postgres:16-alpine, tiada perubahan untuk pin tepat sehingga anda mengeditnya — itulah tujuannya. Imej lama akan terkumpul selepas naik taraf; gunakan docker image prune -f untuk mengosongkan ruang cakera.

Sekarang untuk perintah pemusnah, perlu diingat: docker compose down adalah selamat — kontena dan rangkaian boleh dibuang, dan data anda berada dalam volume. docker compose down -v turut memadam volume bernama. Itu adalah pangkalan data anda, hilang serta-merta, tanpa permintaan pengesahan dan tanpa fungsi undo. Flag -v wujud untuk menghentikan eksperimen; pada stack yang menyimpan data sebenar, layan ia seperti anda melayan rm -rf. Tiada tong sampah di bawah /var/lib/docker/volumes/.

Untuk shell sekali guna di dalam kontena yang sedang berjalan: docker compose exec db psql -U miniflux membawa anda terus ke pangkalan data, dan docker compose exec miniflux sh memberikan anda shell dalam aplikasi.

Di mana data anda sebenarnya disimpan

Volume bernama akan menerima awalan projek, jadi db-data dalam direktori bernama miniflux akan menjadi miniflux_db-data:

docker volume ls
docker volume inspect miniflux_db-data

Output inspect mengandungi baris yang penting:

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

Direktori tersebut adalah pangkalan data — dimiliki oleh root, berada pada sistem fail hos, dan ia kekal selepas down, naik taraf, dan bina semula kontena. Ia juga merupakan perkara tepat yang mesti dirakam oleh sandaran anda.

Sandarkan (Back up) volume bernama

Corak standard adalah menggunakan kontena sementara yang memuatkan (mount) volume secara read-only bersebelahan dengan direktori hos, kemudian melakukan proses tar:

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 .

Tiada pemasangan diperlukan, tiada proses yang terus berjalan, dan pemulihan adalah imej cermin — tar xzf ke dalam volume kosong baharu dengan tetapan mount yang sama tetapi terbalik.

Satu amaran untuk pangkalan data: melakukan tar pada direktori data Postgres yang sedang berjalan boleh menangkap keadaan semasa penulisan (mid-write state) yang akan menyebabkan ia gagal bermula dengan bersih. Sama ada gunakan docker compose stop untuk tempoh masa tar dijalankan, atau — lebih baik — lakukan dump logikal, yang secara semula jadi adalah konsisten:

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

-T melumpuhkan pseudo-terminal yang diperuntukkan oleh Compose secara lalai — menghantar output dump melalui TTY boleh merosakkan data tersebut. Letakkan salah satu arahan ini dalam cron dan salin hasilnya keluar dari VPS; sandaran pada cakera yang sama dengan data yang dilindungi hanyalah salinan, bukan sandaran (backup). Panduan Nextcloud membina rutin berjadual lengkap berdasarkan kedua-dua corak ini.

Mod kegagalan, bersama rentetan teks yang akan anda lihat

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock — anda belum berada dalam kumpulan docker, atau anda sudah berada di dalamnya tetapi sesi anda bermula sebelum perubahan itu dibuat. id menunjukkan kumpulan berkesan anda; newgrp docker membaiki shell semasa, log keluar dan masuk semula akan membaiki kesemuanya.

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? — masalah berbeza: daemon itu sendiri terhenti. sudo systemctl status docker dan sudo journalctl -u docker -n 50 menyatakan puncanya. Pada VPS, punca biasa ialah cakera penuh — semak df -h /var/lib/docker terlebih dahulu.

Bind for 127.0.0.1:8080 failed: port is already allocated — kontena lain telah menerbitkan port hos tersebut. docker ps menunjukkan kontena tersebut; kontena lama daripada docker run beberapa minggu lalu biasanya menjadi punca. Jika docker ps bersih, proses bukan Docker memegang port tersebut: sudo ss -tlnp | grep 8080 menamakan proses itu.

yaml: line 14: did not find expected key — ralat inden pada atau tepat di atas baris yang dinamakan. Fail Compose adalah YAML: inden dua ruang, gunakan ruang sahaja, dan penggunaan aksara tab di mana-mana akan menyebabkan ralat kritikal. docker compose config mengesahkan fail tanpa memulakan apa-apa, dan menjalankannya selepas setiap suntingan adalah amalan yang baik.

Kejutan ufw tidak memaparkan sebarang ralat, itulah yang menjadikannya berbahaya: deployment berjaya, ufw status kelihatan betul, tetapi imbasan port dari luar tetap menemui pangkalan data anda. Baca semula bahagian port di atas, semak setiap entri ports: untuk memastikan tiada awalan 127.0.0.1: yang tertinggal, dan sahkan dari mesin berbeza menggunakan curl http://your-vps-ip:8080 — jawapan "connection refused" adalah apa yang anda mahukan.

Dari sini, panduan Traefik menukarkan satu stack ini kepada banyak aplikasi di belakang satu titik masuk HTTPS, dan apa yang berbaloi untuk self-hosting pada 2026 adalah senarai semak untuk dijalankan melaluinya.

Pelayan permainan seperti pelayan Minecraft pada VPS adalah projek Compose pertama yang sesuai untuk latihan.

FAQ

Why do I get "permission denied while trying to connect to the Docker daemon socket"?

Your user is not in the docker group, or was added after the current session began — membership only applies at login. Run sudo usermod -aG docker $USER, then newgrp docker or log out and back in, and confirm with id. The group grants root-equivalent access to the host, so only add users you would give sudo.

Does docker compose down delete my data?

Plain docker compose down does not — it removes containers and the project network; named volumes survive and the next up -d reattaches them. docker compose down -v is the destructive form: it deletes the named volumes, meaning your database, with no confirmation and no undo. Never run -v on a stack with real data unless you hold a verified backup.

What is the difference between docker-compose and docker compose?

docker-compose (hyphen) is Compose v1, a standalone Python binary that reached end of life in 2023 and should not be installed on new servers. docker compose (space) is Compose v2, a Go plugin for the Docker CLI, installed as docker-compose-plugin from Docker's apt repository. Commands and YAML are almost fully compatible, so when an old tutorial says docker-compose up, type docker compose up.

Why can I reach my Docker container from the internet even though ufw blocks the port?

Because Docker publishes ports with DNAT rules in iptables' PREROUTING chain, and the rewritten packets travel the FORWARD path through Docker's own chains — they never hit the INPUT chain where ufw's rules apply. ufw deny 8080 therefore does nothing to a published container port. Fix it at the source: publish to 127.0.0.1: and expose services through a reverse proxy instead.

Should I use a named volume or a bind mount?

Named volumes for data only the container touches — databases especially, since Docker sets the ownership the image expects and permissions just work. Bind mounts for files you also handle from the host: configs you edit, media you upload, anything whose path you want obvious. If a container fails at startup with permission denied on a bind mount, host-vs-container UID mismatch is the first thing to check.