Docker dari nol, biar “extract → compose up” beneran masuk akal
Bukan teori abstrak. Ini peta visual kenapa Laravel + Nuxt + Postgres bisa hidup cuma pakai Docker, plus hal yang sering bikin bingung: localhost, port mapping, volume, network, entrypoint, dan bedanya dev vs prod.
Isi
up --buildstep engine Docker
8. Port, localhost, CORStabrakan VPS & remap
9. Data & lifecycleapa yang hilang / aman
10. Command hariancheat sheet
11. Pitfall klasikchecklist debug
12. Next levelmount, multi-stage, registry…
13. Diagram animasipacket flow host → FE/BE/DB
14. docker run ≈ composemanual multi-container setara
15. Quiz 8 soalcek apakah konsep nempel
1. Masalah yang diselesaikan Docker
Tanpa Docker, tiap mesin harus disetup manual. Versi PHP beda, extension hilang, Postgres conflict, Node 18 vs 20 — “di laptop gua jalan”.
Host-dependent
Install PHP 8.3 + pgsql, Composer, Node 20, Postgres 16, bikin DB/user, .env, migrate, seed, jalanin 2 server. Ulangi di tiap laptop/VPS.
App bawa runtime-nya
Host cuma butuh Docker engine. PHP/Node/Postgres hidup di container. Setup app tertulis di Dockerfile + compose + entrypoint.
2. Enam konsep wajib hafal
Image
Cetakan read-only. Contoh: postgres:16-alpine, image hasil build backend lo.
Container
Image yang lagi running. Satu image → banyak container terisolasi.
Dockerfile
Resep bikin image: base OS, install deps, copy source, CMD start.
Volume
Disk persistent. DB butuh ini biar data nggak ilang saat container recreate.
Network
LAN virtual. Service name jadi hostname DNS internal (db, backend).
Port map
host:container. Tanpa ini browser di host nggak bisa masuk ke port container.
docker compose = orkestrator: “jalankan N container bareng, env ini, port itu, depend on yang ini dulu”.3. Analogi cepat
| Dunia nyata | Docker |
|---|---|
| Resep masakan | Dockerfile |
| Makanan beku pabrik | Image |
| Piring di meja lo | Container |
| Dapur / restoran | Host (VPS/laptop) |
| Kulkas bersama | Volume |
| Intercom antar ruangan | Docker network |
| Nomor meja restoran | Port mapping |
| Menu set (nasi+lauk+minum) | docker-compose.yml |
4. Dockerfile dibedah (claim-approval)
Backend — php:8.3-cli-alpine
# base runtime PHP, bukan PHP host lo FROM php:8.3-cli-alpine # extension + lib Postgres RUN apk add ... && docker-php-ext-install pdo pdo_pgsql bcmath # curi binary Composer dari image lain COPY --from=composer:2 /usr/bin/composer /usr/bin/composer WORKDIR /var/www/html COPY . . RUN composer install ... # deps PHP dibakar ke image saat BUILD EXPOSE 8000 ENTRYPOINT ["./docker-entrypoint.sh"] # jalan tiap container START
Frontend — node:20-alpine
FROM node:20-alpine WORKDIR /app COPY package.json ./ RUN npm install # layer cache: ganti source ≠ reinstall deps COPY . . ENV HOST=0.0.0.0 PORT=3000 # WAJIB 0.0.0.0 biar port-map host masuk CMD ["npm", "run", "dev"]
RUN = build time
Dieksekusi saat docker build. Hasilnya layer image. Contoh: composer install, npm install.
CMD/ENTRYPOINT = run time
Dieksekusi saat container start. Contoh: entrypoint migrate+serve, npm run dev.
Postgres — tanpa Dockerfile project
Cuma image: postgres:16-alpine. Image resmi sudah: start server, baca env POSTGRES_*, auto-create DB/user saat volume kosong first boot.
5. Entrypoint = kenapa migrate & seed auto
backend/docker-entrypoint.sh jalan setiap backend start:
- Kalau belum ada
.env→ copy dari.env.example - Kalau
APP_KEYkosong →php artisan key:generate - Loop tunggu Postgres benar-benar accept PDO (bukan cuma TCP open)
php artisan migrate --force --seed→ table + demo users/claimsphp artisan serve --host=0.0.0.0 --port=8000
6. Compose: 3 service = 1 app
HOST (VPS) │ port map │ :3010 ──────────────► frontend :3000 (Nuxt) │ :8000 ──────────────► backend :8000 (Laravel) │ :5437 ──────────────► db :5432 (Postgres) // opsional dari host │ └─ docker network: claim-approval-system_default backend ──DB_HOST=db:5432──► db frontend tidak ke DB browser-lah yang nembak API di host:8000 volume db_data → /var/lib/postgresql/data (data tahan recreate)
Service ringkas
| Service | Dari mana | Port host (VPS kita) | Catatan |
|---|---|---|---|
| db | postgres:16-alpine | 5437→5432 | healthcheck pg_isready |
| backend | build: ./backend | 8000→8000 | depends_on: db healthy |
| frontend | build: ./frontend | 3010→3000 | NUXT_PUBLIC_API_BASE=http://localhost:8000/api |
Tabel “localhost mana?”
| Siapa → ke mana | Host | Port |
|---|---|---|
| Backend container → Postgres container | db | 5432 |
| Browser di host → API | 127.0.0.1 | 8000 |
| Browser di host → UI | 127.0.0.1 | 3010 |
| Tools host → Postgres publish | 127.0.0.1 | 5437 |
localhost di dalam backend container | = container backend sendiri, bukan VPS / db | |
http://localhost:8000/api bukan http://backend:8000? Karena request API dilakuin browser user, bukan proses Node di container FE. Hostname backend cuma resolve di Docker network.7. Alur persis: docker compose up --build
- Parse
docker-compose.yml→ 3 service, volume, network. - Build service yang punya
build:(backend, frontend). Layer cache dipakai kalau bisa.dbcuma pull image. - Create network project + volume
db_data. - Start db → healthcheck lolos.
- Start backend → entrypoint: key, wait DB, migrate, seed, serve.
- Start frontend →
npm run devlisten 0.0.0.0:3000. - Publish ports ke host → lo bisa curl/browser.
docker compose up --build cukup. Di VPS penuh service: sesuaikan yaml (remap port + CORS) dulu.8. Port mapping & kenapa VPS perlu edit yaml
Format: "HOST:CONTAINER". Kiri = lubang di mesin lo. Kanan = port proses di dalam container.
# default README (laptop bersih) "5432:5432" # host 5432 → postgres container "8000:8000" "3000:3000" # VPS kita (yang kepake: 5432, 3000, 3001-3003) "5437:5432" # host 5437 → postgres tetap 5432 INTERNAL "8000:8000" "3010:3000" # browser buka :3010
Yang berubah saat remap
Cuma cara host/browser mengakses. Wiring internal backend → db:5432 tetap sama.
Yang ikut diubah di VPS
CORS_ALLOWED_ORIGINS tambah http://localhost:3010 biar browser di port FE baru diizinkan hit API.
9. Data: apa hilang, apa aman
| Data | Lokasi | Hilang kalau |
|---|---|---|
| Source di image | image layers | rebuild image (tanpa bind mount) |
| Isi Postgres | volume db_data | docker compose down -v |
| File di container tanpa volume | container FS | container dihapus/recreate |
| Env compose | runtime config | edit yaml + recreate |
docker compose down # stop + hapus container; VOLUME TETAP docker compose down -v # + hapus volume → DB ILANG
10. Command harian
Project (compose)
cd claim-approval-system docker compose ps docker compose up --build -d docker compose logs -f backend docker compose exec backend sh docker compose exec db psql -U postgres -d approval_system docker compose down
Engine level
docker ps
docker images
docker volume ls
docker network ls
docker logs <container>
docker system prune # hati-hati
docker = engine. docker compose = project multi-service (baca yaml di folder sekarang).
11. Pitfall klasik + checklist debug
1. localhost salah ruang
Di container, localhost = dirinya sendiri. Backend harus DB_HOST=db, bukan localhost.
2. Port host full
Error bind address already in use. Remap kiri di yaml. Cek: ss -ltn.
3. FE listen 127.0.0.1 saja
Port map gagal diam-diam. Set HOST=0.0.0.0.
4. API base pakai hostname Docker di browser
http://backend:8000 gagal di browser user. Pakai URL yang browser bisa resolve.
5. depends_on ≠ app ready
Cuma urutan start/healthy. Bootstrap app (migrate) tetap di entrypoint/health app.
6. Volume dihapus tanpa sadar
down -v wipe DB. Seed bisa balik, data manual hilang.
7. Cache build basi
Ubah deps tapi layer lama kepakai. docker compose build --no-cache.
8. CORS / throttle kelihatan “rusak”
FE beda origin → CORS. Login 5/menit → 429 by design (claim-approval).
Checklist “kok nggak jalan”
docker compose ps— container up?docker compose logs backend— error migrate/key/DB?- Salah localhost / service name?
- Port host free + mapping bener?
- DNS
dbcuma di network project itu - Volume kosong first boot vs data stale
- CORS /
NUXT_PUBLIC_API_BASEdari sudut browser
Bonus: layer cache kenapa build kadang 2 menit / 10 detik
COPY package.json ./ RUN npm install # mahal — cache selama package.json sama COPY . . # sering berubah — taruh di bawah
Urutan Dockerfile = strategi cache. Ubah 1 file source nggak harus reinstall 800 package.
Dev style deliverable vs production
Dev-friendly
npm run dev, php artisan serve, password postgres, APP_DEBUG=true, DB port dipublish.
Hardened
Multi-stage build, nginx/Caddy + php-fpm, secret manager, jangan publish DB ke 0.0.0.0, healthcheck app, restart policy, resource limit.
Konsep Docker sama; yang beda Dockerfile/CMD/security posture.
12. Next level (belum dibahas di atas, tapi perlu tau)
| Topik | Buat apa |
|---|---|
Bind mount ./src:/app | Dev: edit di host, kelihatan di container tanpa rebuild |
| Multi-stage build | Image prod kecil: build di stage 1, copy artifact ke stage 2 |
| .dockerignore | Jangan kirim node_modules/.git ke build context |
| Healthcheck app | Orchestrator tau service beneran ready, bukan cuma process hidup |
| restart: unless-stopped | Auto nyala setelah reboot host |
| Resource limits | Cegah 1 container makan habis RAM VPS |
| Registry (Hub/GHCR) | Push image, pull di server lain tanpa build di prod |
| Compose profiles | Service opsional: worker, mailpit, debug tools |
| Non-root user | Hardening security image |
| Buildx / multi-arch | ARM Mac vs AMD64 VPS image cocok |
| Docker vs VM | Share kernel host; lebih ringan; di Mac lewat Linux VM (Desktop/OrbStack) |
| Swarm / Kubernetes | Multi-host orchestration — belakangan, app kecil belum perlu |
Ringkas 10 poin biar nempel
- Docker bungkus app + dependency jadi image portable.
- Container = image yang lagi running.
- Dockerfile = resep image (
RUNbuild-time,CMDrun-time). - Compose = banyak container sebagai 1 project.
- Service name (
db) = hostname internal. localhostdi container ≠ localhost di VPS.- Port map
host:containercuma untuk akses dari luar. - Volume = data yang nggak ikut mati pas container ilang.
- Entrypoint backend yang bikin migrate/seed otomatis.
compose up --build= build + wiring + start sesuai yaml.
Contoh nyata yang kita jalanin
claim-approval-system @ VPS
Path: /home/ubuntu/projects/claim-approval-system/claim-approval-system/
UI local: http://127.0.0.1:3010 · API: http://127.0.0.1:8000/api · health: /up
Demo: [email protected] / [email protected] / [email protected] / [email protected] — password di README deliverable.
Smoke yang lolos: login 4 role, claim draft→submitted→reviewed→approved, admin list, FE 200.
13. Diagram animasi: request lo ke mana?
Bayangin lo buka UI, login, create claim. Packet nggak “nyampe DB dari browser”. Alurnya: browser → FE (HTML/JS) + browser → API → Postgres internal.
- 0 · Host publish ports3010→FE:3000 · 8000→BE:8000 · 5437→DB:5432 (opsional tools)
- 1 · Browser buka UIGET http://127.0.0.1:3010 → Nuxt container
- 2 · Browser hit APIPOST /api/login ke :8000 — JS di browser, bukan FE server
- 3 · Backend → db:5432hostname service
dbdi Docker network - 4 · Volume tahan dataPostgres tulis ke volume
db_data - 5 · Response baliktoken Sanctum ke browser; FE cuma render
http://backend:8000, animasi step 2 putus di browser user — hostname backend cuma hidup di network Docker.14. docker run setara compose (biar keliatan “magic”-nya)
Compose bukan engine terpisah. Dia cuma file deklaratif yang nge-generate rangkaian docker network/volume/build/run.
Setara kasar claim-approval
# 1. network + volume docker network create claimnet docker volume create db_data # 2. database docker run -d --name db --network claimnet \ -e POSTGRES_DB=approval_system \ -e POSTGRES_USER=postgres \ -e POSTGRES_PASSWORD=postgres \ -v db_data:/var/lib/postgresql/data \ -p 5437:5432 \ postgres:16-alpine # 3. build images docker build -t claim-be ./backend docker build -t claim-fe ./frontend # 4. backend (nunggu DB healthy dulu — manual) docker run -d --name backend --network claimnet \ -e DB_HOST=db -e DB_PORT=5432 \ -e DB_DATABASE=approval_system \ -e DB_USERNAME=postgres -e DB_PASSWORD=postgres \ -e CORS_ALLOWED_ORIGINS=http://localhost:3010 \ -p 8000:8000 claim-be # 5. frontend docker run -d --name frontend --network claimnet \ -e NUXT_PUBLIC_API_BASE=http://localhost:8000/api \ -p 3010:3000 claim-fe
Yang kita pakai
docker compose up --build -d # plus gratis: # - project namespace (prefix nama) # - depends_on + healthcheck # - recreate konsisten # - logs/ps/exec per service # - down / down -v
Tiap baris di yaml map ke flag docker run:
| compose | docker run |
|---|---|
| image: | image name arg |
| build: | docker build + run image |
| ports: | -p host:container |
| environment: | -e KEY=val |
| volumes: | -v name:path |
| depends_on: | urutan start manual |
| networks (implicit) | --network |
| healthcheck: | --health-* flags |
Inspect mental model
# setelah compose up
docker compose ps
docker network inspect claim-approval-system_default | head
docker volume inspect claim-approval-system_db_data | head
docker inspect claim-approval-system-backend-1 --format '{{json .HostConfig.PortBindings}}'
docker inspect claim-approval-system-backend-1 --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
15. Quiz — 8 soal, cek konsep nempel
Klik jawaban. Langsung ada penjelasan. Nggak disimpan ke server mana pun — pure browser.