docker.ryanprayoga.dev
self-contained HTML contoh nyata: claim-approval zero CDN

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.

image ≠ container compose = multi-service volume = data tahan network = DNS internal

Isi

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”.

Tanpa Docker

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.

Dengan Docker

App bawa runtime-nya

Host cuma butuh Docker engine. PHP/Node/Postgres hidup di container. Setup app tertulis di Dockerfile + compose + entrypoint.

Inti: Docker bukan “magic framework”. Dia cuma membungkus runtime + wiring biar reproducible.

2. Enam konsep wajib hafal

1

Image

Cetakan read-only. Contoh: postgres:16-alpine, image hasil build backend lo.

2

Container

Image yang lagi running. Satu image → banyak container terisolasi.

3

Dockerfile

Resep bikin image: base OS, install deps, copy source, CMD start.

4

Volume

Disk persistent. DB butuh ini biar data nggak ilang saat container recreate.

5

Network

LAN virtual. Service name jadi hostname DNS internal (db, backend).

6

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 nyataDocker
Resep masakanDockerfile
Makanan beku pabrikImage
Piring di meja loContainer
Dapur / restoranHost (VPS/laptop)
Kulkas bersamaVolume
Intercom antar ruanganDocker network
Nomor meja restoranPort 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:

  1. Kalau belum ada .env → copy dari .env.example
  2. Kalau APP_KEY kosong → php artisan key:generate
  3. Loop tunggu Postgres benar-benar accept PDO (bukan cuma TCP open)
  4. php artisan migrate --force --seed → table + demo users/claims
  5. php artisan serve --host=0.0.0.0 --port=8000
Tanpa entrypoint ini, container cuma punya PHP + source. Lo masih migrate manual. “Auto sampe DB” = compose wiring + entrypoint bootstrap, bukan sihir Laravel.

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

ServiceDari manaPort host (VPS kita)Catatan
dbpostgres:16-alpine5437→5432healthcheck pg_isready
backendbuild: ./backend8000→8000depends_on: db healthy
frontendbuild: ./frontend3010→3000NUXT_PUBLIC_API_BASE=http://localhost:8000/api

Tabel “localhost mana?”

Siapa → ke manaHostPort
Backend container → Postgres containerdb5432
Browser di host → API127.0.0.18000
Browser di host → UI127.0.0.13010
Tools host → Postgres publish127.0.0.15437
localhost di dalam backend container= container backend sendiri, bukan VPS / db
Kenapa FE pakai 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

  1. Parse docker-compose.yml → 3 service, volume, network.
  2. Build service yang punya build: (backend, frontend). Layer cache dipakai kalau bisa. db cuma pull image.
  3. Create network project + volume db_data.
  4. Start db → healthcheck lolos.
  5. Start backend → entrypoint: key, wait DB, migrate, seed, serve.
  6. Start frontendnpm run dev listen 0.0.0.0:3000.
  7. Publish ports ke host → lo bisa curl/browser.
Di laptop kosong port 3000/8000/5432 free: extract + 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

DataLokasiHilang kalau
Source di imageimage layersrebuild image (tanpa bind mount)
Isi Postgresvolume db_datadocker compose down -v
File di container tanpa volumecontainer FScontainer dihapus/recreate
Env composeruntime configedit 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”

  1. docker compose ps — container up?
  2. docker compose logs backend — error migrate/key/DB?
  3. Salah localhost / service name?
  4. Port host free + mapping bener?
  5. DNS db cuma di network project itu
  6. Volume kosong first boot vs data stale
  7. CORS / NUXT_PUBLIC_API_BASE dari 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

Claim-approval (demo)

Dev-friendly

npm run dev, php artisan serve, password postgres, APP_DEBUG=true, DB port dipublish.

Production tipikal

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)

TopikBuat apa
Bind mount ./src:/appDev: edit di host, kelihatan di container tanpa rebuild
Multi-stage buildImage prod kecil: build di stage 1, copy artifact ke stage 2
.dockerignoreJangan kirim node_modules/.git ke build context
Healthcheck appOrchestrator tau service beneran ready, bukan cuma process hidup
restart: unless-stoppedAuto nyala setelah reboot host
Resource limitsCegah 1 container makan habis RAM VPS
Registry (Hub/GHCR)Push image, pull di server lain tanpa build di prod
Compose profilesService opsional: worker, mailpit, debug tools
Non-root userHardening security image
Buildx / multi-archARM Mac vs AMD64 VPS image cocok
Docker vs VMShare kernel host; lebih ringan; di Mac lewat Linux VM (Desktop/OrbStack)
Swarm / KubernetesMulti-host orchestration — belakangan, app kecil belum perlu

Ringkas 10 poin biar nempel

  1. Docker bungkus app + dependency jadi image portable.
  2. Container = image yang lagi running.
  3. Dockerfile = resep image (RUN build-time, CMD run-time).
  4. Compose = banyak container sebagai 1 project.
  5. Service name (db) = hostname internal.
  6. localhost di container ≠ localhost di VPS.
  7. Port map host:container cuma untuk akses dari luar.
  8. Volume = data yang nggak ikut mati pas container ilang.
  9. Entrypoint backend yang bikin migrate/seed otomatis.
  10. 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.

canvas 2D · zero CDN claim-approval topology
host / browser container docker network volume packet

step 0/6 — idle

  1. 0 · Host publish ports3010→FE:3000 · 8000→BE:8000 · 5437→DB:5432 (opsional tools)
  2. 1 · Browser buka UIGET http://127.0.0.1:3010 → Nuxt container
  3. 2 · Browser hit APIPOST /api/login ke :8000 — JS di browser, bukan FE server
  4. 3 · Backend → db:5432hostname service db di Docker network
  5. 4 · Volume tahan dataPostgres tulis ke volume db_data
  6. 5 · Response baliktoken Sanctum ke browser; FE cuma render
Kalau API base diisi 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.

Manual · ribet

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
Compose · 1 command

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:

composedocker 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
Kalau lo ngerti tabel mapping ini, compose nggak misterius lagi — cuma YAML di atas API Docker yang sama.

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.

score 0/0
isi ↑