Menutup series dengan rekap perjalanan episode 0-26 yang dipetakan ke skenario dunia nyata, lalu praktik end-to-end mendeploy aplikasi full-stack (web, API, database, cache, Traefik dengan TLS, dan monitoring) serta peta jalan menuju Kubernetes, GitOps, dan sertifikasi Docker.

Selamat, kalian sudah sampai di episode 27 — episode terakhir dari series Belajar Docker. Perjalanan yang dimulai dari menanyakan "mengapa kita butuh container?" kini berakhir dengan kemampuan untuk mendeploy aplikasi lengkap ke production. Sebelum episode ini, di episode 26 kita menutup sisi keamanan lanjutan: secrets, Rootless Docker, dan supply chain security. Kini saatnya dua hal: merangkum seluruh perjalanan agar kalian melihat benang merah setiap episode, dan menerapkannya dalam satu proyek hands-on yang utuh.
Ini bukan episode teori. Kita akan membangun dan mendeploy aplikasi full-stack — web frontend, API backend, database, cache, reverse proxy dengan TLS otomatis, dan monitoring — menggunakan semua yang telah dipelajari: multi-stage builds, Compose production, healthchecks, resource limits, networks, volumes, hingga label Traefik. Jika kalian mengikuti setiap langkah, di akhir episode ini kalian akan memiliki stack yang bentuknya persis seperti yang dipakai di banyak startup: satu server, satu compose.prod.yaml, dan semuanya berjalan di atas Docker.
Sebelum menulis kode, mari lihat peta lengkap yang sudah kita tempuh. Setiap fase di series ini menjawab satu pertanyaan nyata di dunia kerja:
| Fase | Episode | Konsep kunci | Skenario dunia nyata |
|---|---|---|---|
| Fondasi | 0-2 | Setup, sejarah, arsitektur client-server | Mengapa "works on my machine" terjadi & cara Docker menyelesaikannya |
| Operasi dasar | 3-4 | docker run, lifecycle, docker stats | Menjalankan & mengoperasikan aplikasi di server |
| Image | 5-7 | Dockerfile, layer caching, multi-stage | Membangun image yang kecil, cepat, dan aman |
| Storage & Network | 8-9 | Volume, bind mount, network driver | Database yang datanya bertahan & container yang saling bicara |
| Compose | 10-11 | Multi-container, env, profiles, anchors | Mengelola seluruh aplikasi dari satu file deklaratif |
| Registry & Security | 12-14 | Push/pull, hardening, scanning, Cosign | Distribusi image & memastikan image layak production |
| Observability | 15 | Logging, healthcheck, Prometheus | Tahu kapan aplikasi sakit sebelum user mengeluh |
| Orchestration | 16-17 | Swarm, service, secrets, rolling update | Mengelola banyak host dan update tanpa downtime |
| Build & CI/CD | 18-19 | Buildx, BuildKit, GitHub Actions | Build, scan, dan publish image secara otomatis |
| Production | 20-23 | Traefik, dev workflow, studi kasus, image internals | Menghubungkan aplikasi ke internet dengan TLS otomatis |
| Operations | 24-26 | daemon.json, resource limits, security advanced | Menjaga host sehat dan rahasia tetap rahasia |
Perhatikan polanya: setiap episode sebelumnya adalah bahan yang sekarang kalian satukan. Di proyek ini, mari gunakan semuanya.
Proyek kita bernama "Catat": aplikasi catatan sederhana — web frontend (Next.js), API (Node.js), database PostgreSQL, cache Redis, reverse proxy Traefik, dan monitoring Prometheus + Grafana. Tujuan pembelajaran di sini bukan aplikasinya (itu sederhana), melainkan arsitektur dan seluruh detail deployment yang menyertainya.
catat/
├── compose.prod.yaml
├── .env.production
├── web/ # Frontend Next.js
│ ├── Dockerfile
│ └── next.config.mjs # output: 'standalone'
├── api/ # Backend Node.js
│ ├── Dockerfile
│ └── server.js
└── monitoring/
└── prometheus.ymlAturan yang kita pegang dari episode-episode sebelumnya: satu service = satu direktori dengan Dockerfile multi-stage, dan satu file compose sebagai satu-satunya sumber kebenaran untuk lingkungan production.
Frontend memakai pola multi-stage tiga lapis dari episode 7: deps (install dependency), builder (build production), runner (runtime ramping). Kunci agar Next.js bisa jalan di container ramping adalah output: 'standalone' di next.config.mjs — Next.js menghasilkan server minimal yang hanya berisi file yang dibutuhkan:
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ARG API_URL
ENV NEXT_PUBLIC_API_URL=$API_URL
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -g 1001 -S nodejs \
&& adduser -S nextjs -u 1001
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]Perhatikan tiga hal yang berasal dari episode 6 dan 13: USER nextjs (non-root), ARG API_URL yang disuntikkan saat build untuk menentukan URL API yang diakses browser, dan stage final yang tidak membawa source code — hanya hasil build dan node_modules yang benar-benar diperlukan.
API memakai pola yang lebih ringkas: satu stage dependency, satu stage runtime. Tidak perlu stage builder karena aplikasi Node.js polos berjalan langsung; kunci utamanya adalah hanya production dependencies yang disalin:
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --chown=node:node . .
RUN addgroup -g 1001 -S app \
&& adduser -S app -G app \
&& chown -R app:app /app
USER app
EXPOSE 4000
CMD ["node", "server.js"]npm ci --omit=dev adalah detail kecil yang dampaknya besar: image produksi tidak membawa tooling development — lebih kecil, lebih sedikit CVE, dan lebih cepat di-pull. Ini praktik yang kalian kenal dari episode 7 dan 26 (minimal base image).
Inilah file terpenting proyek ini. Ia merangkai semua service dengan prinsip dari episode 10, 11, 15, 20, dan 25: healthchecks untuk mengatur depends_on, resource limits agar tidak ada noisy neighbor, networks untuk isolasi, volumes untuk data persisten, dan Traefik labels untuk routing + TLS otomatis:
name: catat
networks:
web:
backend:
monitoring:
volumes:
postgres-data:
redis-data:
traefik-certs:
grafana-data:
prometheus-data:
services:
traefik:
image: traefik:v3.1
restart: unless-stopped
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.letsencrypt.acme.tlschallenge=true
- --certificatesresolvers.letsencrypt.acme.email=admin@example.com
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik-certs:/letsencrypt
networks:
- web
web:
build:
context: ./web
args:
API_URL: https://api.catat.example
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000"]
interval: 30s
timeout: 5s
retries: 3
labels:
- "traefik.enable=true"
- "traefik.http.routers.web.rule=Host(`catat.example`)"
- "traefik.http.routers.web.entrypoints=websecure"
- "traefik.http.routers.web.tls.certresolver=letsencrypt"
- "traefik.http.services.web.loadbalancer.server.port=3000"
networks:
- web
- backend
depends_on:
api:
condition: service_healthy
mem_limit: 512m
cpus: 1
api:
build: ./api
restart: unless-stopped
environment:
NODE_ENV: production
DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@postgres:5432/app
REDIS_URL: redis://redis:6379
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:4000/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3
start_period: 5s
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`api.catat.example`)"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.tls.certresolver=letsencrypt"
- "traefik.http.services.api.loadbalancer.server.port=4000"
networks:
- backend
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
mem_limit: 512m
cpus: 1
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: app
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
mem_limit: 1g
cpus: 2
redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"]
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- backend
mem_limit: 512m
prometheus:
image: prom/prometheus:v2.53.0
restart: unless-stopped
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.retention.time=15d
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- backend
- monitoring
mem_limit: 512m
grafana:
image: grafana/grafana:11.1.0
restart: unless-stopped
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
volumes:
- grafana-data:/var/lib/grafana
labels:
- "traefik.enable=true"
- "traefik.http.routers.grafana.rule=Host(`monitor.catat.example`)"
- "traefik.http.routers.grafana.entrypoints=websecure"
- "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
networks:
- web
- monitoring
mem_limit: 512mHal-hal yang perlu kalian cermati dalam file ini — semuanya hasil dari episode-episode sebelumnya:
depends_on dengan condition: service_healthy — API hanya start setelah Postgres dan Redis benar-benar siap, bukan sekadar container-nya hidup (episode 10).pg_isready, redis-cli ping, dan HTTP check untuk web/API. Inilah sumber kebenaran yang dipakai Traefik dan Compose (episode 15).mem_limit/cpus — database dapat porsi paling besar; cache dibatasi agar tidak boros; masing-masing sesuai pelajaran episode 25.web untuk Traefik + yang perlu publik, backend untuk komunikasi internal, monitoring untuk telemetri. Postgres dan Redis tidak terlihat dari internet sama sekali (episode 9).${POSTGRES_PASSWORD} dari .env.production — bukan di-hardcode ke file (episode 11 dan 26). File .env.production ini harus masuk .gitignore dan tidak boleh di-commit.Konfigurasi Traefik di proyek ini murni lewat labels — seperti yang kalian pelajari di episode 20. Provider Docker membaca label dari tiap container, sehingga routing, TLS, dan service discovery menjadi otomatis: tambah service baru → tambah label → Traefik langsung melayani tanpa restart. Empat label inti yang dipakai di semua service:
traefik.enable=true → service ini dipublikasikan
traefik.http.routers.<nama>.rule=Host(`domain`) → hostname yang dilayani
traefik.http.routers.<nama>.entrypoints=websecure → masuk lewat port 443
traefik.http.routers.<nama>.tls.certresolver=letsencrypt → minta sertifikat otomatis--certificatesresolvers.letsencrypt.acme.tlschallenge=true membuat Traefik otomatis meminta dan memperbarui sertifikat Let's Encrypt untuk setiap hostname yang dipublikasikan — tanpa intervensi manual dan tanpa biaya. Ini contoh nyata bagaimana Docker + Traefik mengotomatisasi apa yang dulu dilakukan manual selama berjam-jam.
Setelah semua file siap, berikut rangkaian deployment dari awal sampai verifikasi. Jalankan di server dengan Docker terpasang (materi episode 0) dan domain yang sudah diarahkan ke IP server:
cp .env.example .env.production
docker compose -f compose.prod.yaml config --quietUrutan ini sengaja: validasi sebelum deploy (config --quiet memastikan YAML valid), deploy seluruh stack, lalu verifikasi dari luar dengan curl — bukan hanya "container-nya hidup". Langkah 4 adalah praktik update yang aman: --no-deps membangun ulang hanya service web tanpa menyentuh database, sehingga downtime nyaris nol ketika kalian merilis versi baru.
NAME IMAGE STATUS
catat-api-1 catat-api Up 2 minutes (healthy)
catat-web-1 catat-web Up 2 minutes (healthy)
catat-postgres-1 postgres:16-alpine Up 2 minutes (healthy)
catat-redis-1 redis:7-alpine Up 2 minutes (healthy)
catat-traefik-1 traefik:v3.1 Up 2 minutes
catat-grafana-1 grafana/grafana:11.1.0 Up 2 minutes (healthy)
catat-prometheus-1 prom/prometheus:v2.53.0 Up 2 minutesStatus (healthy) di setiap service adalah bukti bahwa healthcheck bekerja — dan ini persis yang akan dicek Traefik sebelum mengirim trafik, dan yang akan kalian andalkan saat otomasi (episode 15).
Sebelum menyatakan "siap production", jalankan checklist berikut — rangkuman seluruh series:
USER non-root di semua image, cap-drop/no-new-privileges di container sensitif, rahasia tidak di commit dan tidak di env polos (episode 6, 13, 26).mem_limit/cpus di semua service — tidak ada container tanpa pagar (episode 25).restart: unless-stopped, volume untuk semua data persisten (episode 8, 10, 15).docker system df + prune terkendali di host (episode 24).Jika semua poin ini terpenuhi, aplikasi kalian sudah memakai pola yang sama dengan yang dipegang tim DevOps di industri.
Series ini berakhir, tapi perjalanan kalian baru mulai. Docker adalah fondasi dari ekosistem yang jauh lebih besar, dan dua tujuan besar menanti:
Belajar Kubernetes — jika Docker adalah "satu server, banyak container", Kubernetes adalah "banyak server, satu platform". Kalian akan belajar multi-host orchestration, auto-scaling, self-healing (container yang mati langsung diganti), dan konsep Pod, Deployment, serta Service yang menata container pada skala data center. Semua konsep Docker yang kalian kuasai — image, volume, network, resource limits, healthcheck — menjadi fondasi yang membuat belajar Kubernetes jauh lebih mudah.
GitOps (Argo CD / Flux) — paradigma di mana repositori git menjadi satu-satunya sumber kebenaran untuk state infrastruktur. Deploy dilakukan bukan dengan mengetik perintah, melainkan dengan pull request: perubahan di git otomatis disinkronkan ke cluster oleh operator seperti Argo CD atau Flux. Ini adalah evolusi dari compose.prod.yaml versi manual menuju deployment yang auditable dan bisa di-rollback dengan mudah.
Jangan melompat. Urutan yang tepat: kuasai Docker di single-host (selesai), lalu perbanyak dengan Swarm untuk memahami orkestrasi dasar (episode 16-17), lalu melompat ke Kubernetes.
Untuk memperdalam dan melegitimasi kemampuan kalian:
docs.docker.com) — referensi paling otoritatif untuk docker run, Compose spec, daemon.json, dan fitur-fitur baru.docker/awesome-compose hingga template CI di dokumentasi GitHub Actions; kode adalah guru terbaik setelah kalian paham konsepnya.Pada episode 27 ini kalian telah menutup series dengan dua hal: rekap — melihat bagaimana 27 episode membentuk satu alur dari setup, operasi, image, storage, network, compose, keamanan, observability, orkestrasi, CI/CD, hingga production; dan penerapan — mendeploy aplikasi full-stack "Catat" dengan frontend Next.js dan API Node.js (multi-stage Dockerfile), PostgreSQL, Redis, reverse proxy Traefik dengan TLS otomatis, Prometheus, serta Grafana, semuanya dari satu compose.prod.yaml yang lengkap dengan healthcheck, resource limits, networks, dan volumes.
Inti yang harus kalian bawa dari seluruh series ini:
Dua puluh tujuh episode, dari "It works on my machine" hingga "deployed with one command". Kalian tidak lagi sekadar memakai Docker — kalian memahami Docker. Sekarang saatnya membangun, memecahkan masalah, dan terus belajar. Selamat, dan sampai jumpa di series Belajar Kubernetes — di mana petualangan container kalian berlanjut ke skala yang lebih besar. Tetap semangat!