SSD Nodes Learn
Mga Gabay Matt ConnorNi Matt Connor · Na-update 2026-07-24

Docker Compose guide para sa Ubuntu 24.04

Matutong mag-install ng Docker Engine at Compose v2 sa Ubuntu 24.04. Iwasan ang ufw port-publishing errors at matutunan ang tamang pag-backup ng volumes.

Ang iyong bubuuin

Ang Docker Compose ang pundasyon ng halos lahat ng bagay sa site na ito. Nextcloud, Vaultwarden, n8n, Immich, Rocket.Chat — bawat guide na ito ay nagsisimula sa "write this compose file", at ang page na ito ang nagpapaliwanag kung ano ang ibig sabihin ng file na iyon. Mag-i-install ka ng Docker Engine at ng Compose v2 plugin mula sa official apt repository ng Docker sa Ubuntu 24.04. Pagkatapos, magtatayo ka ng isang two-service stack — ang Miniflux, isang maliit na RSS reader, at ang PostgreSQL — dahil ang pares na ito ay gumagamit ng lahat ng pattern na ginagamit ng mas malalaking apps: pinned images, database na may healthcheck, named volume, secrets sa isang .env file, at port na naka-publish lamang sa localhost.

Ang installation ay tatagal ng limang minuto. Ang natitirang bahagi ng guide na ito ay tungkol sa mga isyung magdudulot ng problema sa hinaharap: ang docker group na root din sa ibang pangalan, ang mga published ports na lumalampas sa iyong ufw rules, at ang isang flag sa docker compose down na nagbubura ng iyong database nang walang confirmation prompt.

Prerequisites: isang bagong Ubuntu 24.04 KVM VPS, isang user na may sudo access, at 1 GB na RAM o higit pa. Maaari ring gumamit ng existing na Docker install — tatalakayin sa unang section kung ano ang dapat tanggalin.

Mag-install mula sa repo ng Docker, hindi sa Ubuntu

May dalawang maling paraan na dapat iwasan bago ang unang command. Gumagana ang docker.io package ng Ubuntu, pero mas luma ito kaysa sa Docker releases at hindi sumusunod sa plugin layout na kailangan ng system. Ang standalone na docker-compose binary — ang may hyphen — ay Compose v1: Python-based ito at end-of-life na simula 2023, kaya ito ang dahilan kung bakit nag-eerror ang mga lumang tutorial. Ang Compose ngayon ay docker compose (may space), isang CLI plugin, na ini-install mula sa parehong repository ng engine.

Kung mayroon nang kahit alin sa mga ito sa system, i-uninstall muna ang mga ito — kasama ang docker-compose-v2 (ang packaging ng Ubuntu para sa plugin), para lahat ay magmula sa iisang repository:

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 ang normal na output sa isang bagong VPS. Pagkatapos, i-add ang repository ng Docker at mag-install:

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

I-verify ang tatlong layers:

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

Naglalabas ang unang dalawa ng version strings — kinukumpirma ng Docker Compose version v2.x.x na ang plugin ang gamit mo, hindi ang patay na v1 binary. Ang hello-world run ay dapat magtapos sa Hello from Docker!. Ang package ay nag-e-enable ng service sa boot; ang systemctl is-enabled docker ay nagpi-print ng enabled.

Ang docker group ay root — mag-ingat sa pagpili

Sa ngayon, kailangan ng bawat docker command ng sudo, dahil ang daemon socket sa /var/run/docker.sock ay pagmamay-ari ng root at ng docker group. Kung wala kang membership, makukuha mo ang pinaka-karaniwang Docker error:

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

Ang standard na solusyon:

sudo usermod -aG docker $USER

Nag-a-apply ang group membership sa oras ng login, kaya mananatili ang error sa kasalukuyang shell. Patakbuhin ang newgrp docker para sa session na ito, o mag-log out at mag-log in muli; dapat nang i-list ng id ang docker sa iyong mga grupo.

Ngayon ang tapat na bahagi: ang membership sa docker group ay root sa host. Hindi ito "root-ish" o "elevated" — ito ay root. Ang sinumang nasa grupong iyon ay maaaring magpatakbo ng docker run --rm -it -v /:/host alpine chroot /host at kontrolin ang buong filesystem nang walang password. Ang grupong ito ay para sa convenience, hindi para sa containment.

Ang rootless mode ng Docker ang tunay na alternatibo — ang daemon mismo ay tatakbo bilang iyong unprivileged user. May mga kapalit ito: kailangan ng extra setup para sa mga port na mababa sa 1024, ang networking ay dadaan sa isang userspace shim na may overhead, at may ilang images na hindi gagana nang maayos nang walang real root. Sa isang single-admin VPS kung saan ang tanging login ay may sudo na, walang pagbabago sa praktikal na aspeto, at ito ang assumption ng lahat ng guide dito — huwag lang itong ibigay na parang hindi ito kasing-lakas ng sudo.

Anatomy of a compose file

Bigyan ang bawat stack ng sariling directory — ang pangalan ng directory ang magiging project name, na magsisilbing prefix para sa mga container, network, at volume:

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

Gumamit ng compose.yml (ang modernong pangalan; gumagana pa rin ang docker-compose.yml). Huwag nang gamitin ang lumang version: key — obsolete na ito at magbibigay ng warning ang Compose kapag nakita ito.

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:

Ang bawat linya sa itaas ay isang desisyon. Gawin ang mga ito nang paisa-isa.

Pin image versions — ang :latest plus isang pull ay isang unattended upgrade

postgres:16-alpine, hindi postgres:latest. Ang tag ay hindi frozen: ang :latest ay laging nagre-resolve sa pinakabagong push ng maintainer sa tuwing mag-pu-pull ka. Kapag pinagsama ito sa routine upgrade habit na matututunan mo — docker compose pull && docker compose up -d — ang :latest ay nangangahulugang ang major-version jumps ay mangyayari kung kailan sila mag-release sa upstream, hindi kung kailan mo gusto. Sa PostgreSQL, hindi ito teorya lang: ang biglaang pagtalon mula 16 patungong 17 ay magreresulta sa container crash-loop dahil sa incompatible na data directory, dahil ang major upgrades ng Postgres ay nangangailangan ng dump at restore, hindi lang restart.

I-pin ang kahit ang major version (ang postgres:16-alpine ay sumusunod sa 16.x patch releases), at i-pin ang mga application sa eksaktong release gaya ng miniflux/miniflux:2.2.9 — i-check ang releases page ng project at gamitin ang kung ano ang current kapag isinusulat ang file. Ang upgrade ay magiging isang one-line edit na ginawa mo nang sinasadya, na makikita sa git diff.

Publish to 127.0.0.1, dahil nilalagpasan ng Docker ang ufw

"127.0.0.1:8080:8080" — host address, host port, container port. Karamihan sa mga tutorial ay gumagamit ng "8080:8080", na shorthand para sa 0.0.0.0:8080:8080: nakikinig sa lahat ng interface, kasama na ang public interface.

Narito ang bitag na nakakaapekto sa halos lahat: Nag-pu-publish ang Docker ng port sa pamamagitan ng pagsulat ng DNAT rule na nagre-rewrite ng destination ng packet sa internal IP ng container bago ang filtering. Dahil dito, ang packet ay dadaan sa FORWARD path at hindi kailanman tatama sa INPUT, kung saan nakalagay ang iyong mga ufw rules. Magpapakita ang sudo ufw deny 8080 ng success, magpapakita ang ufw status na denied ang port, pero ang service ay accessible pa rin sa buong internet. Hindi sira ang iyong firewall; sadyang nilalagpasan ito. Bakit nilalagpasan ng Docker ang ufw, at paano i-filter ang container traffic nang tama ang nagpapaliwanag sa mekanismo at ang DOCKER-USER fix para sa mga port na dapat manatiling public.

Ang habit na magpapawala sa problema: i-bind ang mga published ports sa 127.0.0.1 maliban na lang kung may espesipikong dahilan, at gumamit ng reverse proxy sa harap para sa anumang dapat harapin ang mundo. Iyan ang gagawin ng Traefik reverse proxy guide bilang susunod na hakbang pagkatapos ng page na ito — isang container na may hawak sa ports 80 at 443 at nagro-route sa lahat ng iba pa gamit ang hostname at TLS. (Galing sa lumang Traefik v2 setup? Ang Traefik v2 to v3 migration guide ang sumasaklaw sa mga renames at pagbabago sa rules.)

I-verify ang bind pagkatapos i-start ang stack: dapat ipakita ng sudo ss -tlnp | grep 8080 ang 127.0.0.1:8080, hindi 0.0.0.0:8080 o *:8080.

Named volumes vs bind mounts

Ang db-data:/var/lib/postgresql/data ay isang named volume: Gagawa at magmamanage ang Docker ng directory sa ilalim ng /var/lib/docker/volumes/ at i-mo-mount ito sa loob ng container. Ang alternatibo ay bind mount, ang ./data:/var/lib/postgresql/data, na nagma-map ng path na pinili mo sa host.

Ang tamang paggamit: named volumes para sa mga data na container lang ang humahawak — lalo na ang mga database, dahil i-ini-initialize ng Docker ang volume gamit ang ownership na inaasahan ng image at gumagana ang file permissions. Bind mounts para sa mga files na hinahawakan mo mula sa host — mga config files na ine-edit mo gamit ang text editor, isang media library na rsync mo sa loob, o anumang path na gusto mong madaling makita. Ang klasikong failure ng bind-mount ay ownership: ang container ay tumatakbo bilang UID 999, ang host directory mo ay owned ng UID 1000, at mag-eerror ang app sa startup na may permission denied sa logs nito. Ang mga named volumes ay nagpapawala sa ganitong klase ng bug, kapalit ng pag-imbak ng data sa isang Docker-managed path — tatalakayin sa ibaba.

environment at .env — panatilihing malayo sa git ang mga secrets

Ang ${POSTGRES_PASSWORD} ay hindi binabasa mula sa iyong shell; i-i-interpolate ito ng Compose mula sa file na may pangalang .env na katabi ng compose.yml. Gumawa nito:

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

Gumamit ng totoong values gamit ang openssl rand -hex 24. Hex, hindi base64, nang sinasadya: ang password na ito ay mapupunta sa loob ng DATABASE_URL connection string, at ang mga karakter na /, +, at = na ginagawa ng base64 ay sisira sa URL parsing — isang error na lalabas bilang authentication error, hindi syntax error, at magsasayang ng oras. Ang linya ng .gitignore ay dapat ilagay bago ang unang commit: ligtas nang i-publish at i-version ang compose file, pero ang .env file ay hindi kailanman dapat i-commit, at ang secret na nakapasok na sa git history ay dapat nang i-rotate. Kung i-start ang stack na may kulang na variable, magbibigay ng malakas na warning ang Compose at magpapatuloy gamit ang empty string — na para sa Postgres password ay nangangahulugang broken deployment:

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

Ipinapakita ng docker compose config ang file na may kumpletong interpolation — ang pinakamabilis na paraan para i-check kung ano ang aktwal na matatanggap ng mga container; tandaan na ang output nito ay kasama ang iyong mga secrets.

depends_on ay hindi naghihintay — maliban kung magdagdag ka ng healthcheck

Ang simpleng depends_on: [db] ay kumokontrol lamang sa order ng pag-start: I-la-launch ng Compose ang Postgres muna at ang app pagkatapos ng ilang sandali, habang ang Postgres ay ilang segundo pa lang bago tumanggap ng connections. Tatama ang app sa database, mag-eerror, at mag-crash o mag-re-retry depende sa pagkakasulat nito.

Ang mas maaasahang bersyon ay ang ginamit sa file sa itaas: ang db service ay nagtatakda ng healthcheck (ang Postgres ay may kasamang pg_isready para sa eksaktong ito), at ang app ay nagdedeklara ng depends_on gamit ang condition: service_healthy. I-sta-start ng Compose ang database, i-po-poll ang check bawat 10 segundo, at i-sta-start lang ang Miniflux kapag pasado na ang check. Kung hindi magiging healthy ang database — maling password, corrupt na volume — hindi mag-sta-start ang app at sasabihin ng Compose kung aling dependency ang nabigo:

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

Ang mensaheng iyon ang magtuturo sa iyo sa docker compose logs db, kung saan naroon ang totoong error.

restart: unless-stopped

Ang restart: unless-stopped sa parehong services ay nangangahulugang babalik ang mga container pagkatapos ng crash at pagkatapos ng VPS reboot, pero mananatiling down kung sadyang pinatakbo ang docker compose stop. Ang alternatibong always ay muling bubuhayin ang mga container kahit pagkatapos ng manual stop — na madalas ay hindi ang iyong intensyon. Kung walang restart policy, ang isang kernel-update reboot sa madaling araw ay tahimik na magpapatigil sa iyong mga service hangga't hindi mo ito napapansin.

Ang mga daily verbs

Ang lahat ng daily tasks ay binubuo ng limang commands na dapat patakbuhin mula sa project directory.

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

Ligtas patakbuhin nang paulit-ulit ang up -d — ikinukumpara nito ang file sa kasalukuyang state at binabago lamang ang mga services na may nagbago sa config o image. Ang upgrade pair ay kinukuha ang anumang nakaturo ang iyong mga pinned tags: patch releases sa ilalim ng postgres:16-alpine, at walang babaguhin sa exact pin hangga't hindi mo ito ini-edit — iyon ang layunin nito. Nag-iipon ang mga lumang images pagkatapos ng upgrades; gamitin ang docker image prune -f para magbawas ng disk space.

Para naman sa destructive command: ligtas ang docker compose down — ang mga container at network ay disposable, at ang iyong data ay nasa volume. Binubura ng docker compose down -v pati ang mga named volumes. Ibig sabihin, ang iyong database ay mawawala agad, nang walang confirmation prompt at hindi na maibabalik. Ang -v flag ay para sa pag-te-tear down ng mga experiment; sa isang stack na may totoong data, ituring ito gaya ng rm -rf. Walang trash can sa ilalim ng /var/lib/docker/volumes/.

Para sa one-off shell sa loob ng tumatakbong container: ang docker compose exec db psql -U miniflux ay magdadala sa iyo sa database, at ang docker compose exec miniflux sh ay magbibigay ng shell sa app.

Kung nasaan ang iyong data

Ang mga named volume ay nakakakuha ng project prefix, kaya ang db-data sa isang directory na tinatawag na miniflux ay nagiging miniflux_db-data:

docker volume ls
docker volume inspect miniflux_db-data

Kasama sa inspect output ang mahalagang linya:

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

Ang directory na iyon ang database — root-owned, nasa host filesystem, at mananatili kahit pagkatapos ng down, upgrades, at container rebuilds. Ito rin ang eksaktong dapat i-capture ng iyong mga backup.

I-back up ang isang named volume

Ang standard na pattern ay ang paggamit ng isang throwaway container. I-mount ang volume bilang read-only sa tabi ng isang host directory, at i-tar ang laman nito:

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 .

Walang kailangang i-install at walang prosesong tatakbo. Ang restore ay mirror image nito — i-extract ang tar xzf sa isang bagong empty volume gamit ang kabaligtad na mounts.

May babala para sa mga database: ang pag-tar sa isang running na Postgres data directory ay maaaring makakuha ng mid-write state. Maaari itong magdulot ng error sa pag-start. Gamitin ang docker compose stop habang tumatakbo ang tar, o mas mabuti, gumamit ng logical dump dahil consistent ito by construction:

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

Ididisable ng -T ang pseudo-terminal na default na ini-allocate ng Compose — maaaring ma-corrupt ang dump output kapag ipinasa ito sa isang TTY. Ilagay ang isa sa mga ito sa cron at i-copy ang resulta palabas ng VPS; ang backup na nasa parehong disk ng data na pinoprotektahan nito ay kopya lamang, hindi tunay na backup. Ang Nextcloud guide ay gumagamit ng dalawang pattern na ito para sa isang full scheduled routine.

Failure modes, kasama ang mga string na makikita mo

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock — hindi ka pa kasama sa docker group, o kasama ka na pero ang session mo ay bago pa ito. Ipinapakita ng id ang iyong effective groups; inaayos ng newgrp docker ang kasalukuyang shell. Ang pag-logout at pag-login muli ang mag-aayos sa lahat ng ito.

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? — ibang problema: ang daemon mismo ay down. Ipinapaliwanag ng sudo systemctl status docker at sudo journalctl -u docker -n 50 ang dahilan. Sa VPS, ang karaniwang sanhi ay puno ang disk — tingnan muna ang df -h /var/lib/docker.

Bind for 127.0.0.1:8080 failed: port is already allocated — may ibang container na gumagamit na ng host port na ito. Ipinapakita ng docker ps kung alin; ang sanhi ay karaniwang isang lumang container mula sa isang experimental docker run noong nakaraang mga linggo. Kung malinis ang docker ps, may non-Docker process na gumagamit ng port: tinutukoy ito ng sudo ss -tlnp | grep 8080.

yaml: line 14: did not find expected key — may indentation error sa o sa itaas ng linya na tinukoy. Ang Compose files ay YAML: dapat ay two-space indentation, spaces lang ang gamitin, at ang anumang tab character ay magdudulot ng error. Bineberipika ng docker compose config ang file nang hindi nagpapatakbo ng kahit ano; mainam na patakbuhin ito pagkatapos ng bawat edit.

The ufw surprise ay hindi naglalabas ng kahit anong error, kaya ito ay mapanganib: gagana ang deploy, mukhang tama ang ufw status, pero makakahanap pa rin ang port scan mula sa labas sa iyong database. Basahin muli ang ports section sa itaas, suriin ang bawat ports: entry para sa kulang na 127.0.0.1: prefix, at kumpirmahin gamit ang ibang machine gamit ang curl http://your-vps-ip:8080 — dapat ang sagot ay connection refused.

Mula rito, gagawing maraming apps sa likod ng isang HTTPS entry point ng Traefik guide ang single stack na ito, at ang what's worth self-hosting in 2026 ang listahan ng mga dapat patakbuhin gamit ito.

Ang isang game server gaya ng a Minecraft server on a VPS ay isang magandang unang Compose project para sa pagsasanay.

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.