Containerization menggunakan Docker dan Docker Compose telah menjadi standar industri yang sangat populer untuk menyatukan lingkungan pengembangan perangkat lunak dengan lingkungan rilis. Namun, menjalankan Docker di komputer lokal (development) sangat berbeda dengan menjalankannya di server cloud publik (production).
Banyak tim pengembang membawa berkas konfigurasi lokal langsung ke server live: menjalankan aplikasi sebagai user root, memaparkan port basis data ke internet publik, serta membiarkan ukuran image membengkak hingga gigabytes. Panduan ini menyajikan langkah menyiapkan Docker Compose untuk lingkungan produksi yang lebih aman, ramping, dan stabil, lengkap dengan Dockerfile dan compose.yaml yang dapat Anda sesuaikan.
- Hindari menjalankan aplikasi sebagai root di dalam kontainer. Gunakan user non-root agar dampak celah aplikasi lebih terbatas.
- Multi-stage build memisahkan build tools dari image produksi sehingga image lebih kecil dan permukaan serangan berkurang.
- Jangan mempublikasikan port database atau Redis. Port yang dipublikasikan Docker melewati aturan firewall UFW, jadi gunakan jaringan internal dan bind ke 127.0.0.1 bila perlu.
- Tetapkan batas memori dan CPU agar satu kontainer yang bocor memori tidak menghabiskan sumber daya host.
- Healthcheck hanya menandai status. Docker Compose di satu host tidak me-restart kontainer yang unhealthy secara otomatis.
- Kelola rahasia lewat secrets, pin versi image, dan pindai kerentanan. Compose di satu host bukan solusi high availability.
1. Keamanan Utama: Menjalankan Kontainer Tanpa Hak Akses Root
Secara default, proses di dalam kontainer Docker berjalan dengan UID 0 (root). Root di dalam kontainer tidak otomatis sama dengan root di host karena kontainer diisolasi lewat namespace, tetapi bila aplikasi Anda memiliki celah (misalnya pengunggahan file atau eksekusi kode jarak jauh) dan kontainer salah dikonfigurasi, dampaknya akan jauh lebih besar. Menjalankan proses sebagai user non-root menambah satu lapis pertahanan.
Definisikan pengguna non-root pada Dockerfile produksi Anda (contoh lengkapnya ada di bagian berikutnya).
Hindari Root, Privileged, dan docker.sock di Produksi
2. Multi-Stage Build: Image Ringan dengan Permukaan Serangan Lebih Kecil
Di tahap pengembangan, Anda membutuhkan compiler, paket TypeScript, dev-dependencies, dan berbagai CLI tool. Di produksi, semua berkas tersebut hanya menjadi beban ukuran dan memperluas permukaan serangan (attack surface).
Teknik Multi-Stage Build memungkinkan Anda melakukan kompilasi di stage sementara (builder), lalu hanya menyalin artefak jadi ke stage runtime yang minimalis (misalnya berbasis Alpine). Besarnya penyusutan ukuran bergantung pada aplikasi Anda, jadi ukur sendiri dengan docker images.
Build Terpisah dari Runtime
Compiler dan dev-dependencies hanya ada di stage builder. Image akhir hanya membawa artefak jadi, sehingga lebih kecil, lebih cepat di-pull, dan memuat lebih sedikit paket yang dapat dieksploitasi.
1# ---- Stage 1: builder (berisi compiler dan dev-dependencies) ----2# Gunakan versi LTS yang masih didukung, dan pin tag atau digest-nya3FROM node:22-alpine AS builder4WORKDIR /app5 6COPY package*.json ./7RUN npm ci8 9COPY . .10RUN npm run build && npm prune --omit=dev11 12# ---- Stage 2: runtime (minimal, tanpa build tools) ----13FROM node:22-alpine AS runtime14ENV NODE_ENV=production15WORKDIR /app16 17# Salin hanya artefak yang dibutuhkan, dimiliki oleh user non-root bawaan image18COPY --from=builder --chown=node:node /app/dist ./dist19COPY --from=builder --chown=node:node /app/node_modules ./node_modules20COPY --from=builder --chown=node:node /app/package.json ./package.json21COPY --chown=node:node healthcheck.js ./healthcheck.js22 23# Jalankan sebagai user non-root24USER node25EXPOSE 300026CMD ["node", "dist/server.js"]
Tambahkan juga berkas .dockerignore (misalnya mengecualikan node_modules, .git, dan berkas .env) agar rahasia dan berkas yang tidak perlu tidak ikut tersalin ke dalam image.
3. Isolasi Jaringan: Jangan Publikasikan Port Database
Salah satu kesalahan paling berbahaya adalah mencantumkan ports: - "5432:5432" pada service database di docker-compose.yml. Secara default, port yang dipublikasikan dapat diakses dari semua alamat eksternal, sehingga database Anda terbuka ke internet.
Ada jebakan yang sering terlewat: aturan firewall UFW tidak melindungi port yang dipublikasikan Docker. Docker mengelola aturan iptables-nya sendiri, dan lalu lintas ke port yang dipublikasikan melewati jalur yang tidak diperiksa oleh UFW. Server dengan ufw deny aktif tetap bisa terbuka. Cara yang aman:
Jangan publikasikan port yang tidak perlu. Database dan Redis cukup dijangkau oleh kontainer aplikasi lewat jaringan Docker.
Bind ke localhost bila Anda memang perlu mengaksesnya dari host:
127.0.0.1:5432:5432. Aplikasi yang berada di balik reverse proxy juga cukup di-bind ke127.0.0.1.Gunakan jaringan internal (
internal: true) untuk service data agar tidak punya jalur ke internet. Perhatikan bahwaexpose:hanya dokumentasi port, bukan kontrol akses.Bila harus memakai firewall pada port yang dipublikasikan, pasang aturan pada rantai
DOCKER-USER, bukan pada aturan UFW biasa.
Verifikasi dari luar, jangan hanya mengandalkan konfigurasi:
1# Di server: lihat port yang benar-benar dipublikasikan Docker2docker ps --format "table {{.Names}}\t{{.Ports}}"3 4# Dari komputer lain: pastikan port database dan Redis tertutup dari luar5nmap -p 5432,6379 IP_SERVER_ANDA
4. Contoh compose.yaml Produksi Lengkap
Berikut contoh yang menggabungkan isolasi jaringan, healthcheck, batas sumber daya, rotasi log, dan pengerasan kontainer. Nama image, versi, dan angka limit hanyalah contoh. Sesuaikan dengan aplikasi Anda.
1services:2 app:3 image: ghcr.io/arkatek/web-app:1.4.2 # contoh: pin ke tag versi, bukan latest4 restart: unless-stopped5 ports:6 - "127.0.0.1:3000:3000" # hanya localhost, reverse proxy di host yang melayani publik7 env_file:8 - .env.production # jangan di-commit. DATABASE_URL harus memakai password yang sama dengan secret db9 depends_on:10 db:11 condition: service_healthy12 redis:13 condition: service_healthy14 networks:15 - edge16 - backend17 healthcheck:18 test: ["CMD", "node", "healthcheck.js"]19 interval: 30s20 timeout: 5s21 retries: 322 start_period: 20s23 read_only: true24 tmpfs:25 - /tmp26 cap_drop:27 - ALL28 security_opt:29 - no-new-privileges:true30 deploy:31 resources:32 limits:33 cpus: "1.0"34 memory: 512M35 logging:36 driver: json-file37 options:38 max-size: "50m"39 max-file: "3"40 41 db:42 image: postgres:16-alpine43 restart: unless-stopped44 environment:45 POSTGRES_DB: app46 POSTGRES_USER: app47 POSTGRES_PASSWORD_FILE: /run/secrets/db_password48 secrets:49 - db_password50 volumes:51 - db_data:/var/lib/postgresql/data52 networks:53 - backend54 healthcheck:55 test: ["CMD-SHELL", "pg_isready -U app -d app"]56 interval: 10s57 timeout: 5s58 retries: 559 deploy:60 resources:61 limits:62 memory: 1G63 logging:64 driver: json-file65 options:66 max-size: "50m"67 max-file: "3"68 69 redis:70 image: redis:7-alpine71 restart: unless-stopped72 command: ["redis-server", "--appendonly", "yes"]73 volumes:74 - redis_data:/data75 networks:76 - backend77 healthcheck:78 test: ["CMD", "redis-cli", "ping"]79 interval: 10s80 timeout: 3s81 retries: 582 deploy:83 resources:84 limits:85 memory: 256M86 logging:87 driver: json-file88 options:89 max-size: "50m"90 max-file: "3"91 92networks:93 edge: {}94 backend:95 internal: true # tanpa jalur ke internet, hanya untuk service data96 97volumes:98 db_data:99 redis_data:100 101secrets:102 db_password:103 file: ./secrets/db_password.txt
Jaringan: hanya
appyang berada di jaringanedgedan dapat diakses dari host.dbdanredishanya ada di jaringanbackendyang internal.Batas sumber daya:
deploy.resources.limitsmencegah satu kontainer menghabiskan memori host. Pada Docker Compose v2, nilainya dihormati olehdocker compose up. Cek hasilnya dengandocker statsdan tentukan angkanya dari pemakaian nyata.Pengerasan kontainer:
read_only,cap_drop: ALL, danno-new-privilegesmengurangi apa yang bisa dilakukan proses yang disusupi. Tidak semua image kompatibel dengan opsi ini (aplikasi yang menulis ke disk membutuhkan volume atau tmpfs), jadi uji dulu.Urutan start:
depends_ondengancondition: service_healthymenundaappsampai database dan Redis lolos healthcheck, hal yang tidak dijamin olehdepends_onbiasa.
5. Secrets dan Higiene Image
Jangan menaruh rahasia di image atau repositori. Gunakan file secrets (seperti contoh di atas) atau secrets manager, simpan berkas dengan izin akses terbatas, dan jangan commit
.envproduksi.Pin versi image: gunakan tag versi spesifik (atau digest) alih-alih
latestagar deployment dapat direproduksi.Pindai kerentanan image secara rutin dengan alat seperti Trivy atau Docker Scout, dan bangun ulang image saat base image mendapat perbaikan keamanan.
Gunakan registry privat untuk image aplikasi Anda.
6. Healthcheck dan Restart: Apa yang Sebenarnya Terjadi
Healthcheck adalah perintah yang dijalankan Docker secara berkala di dalam kontainer, lalu hasilnya disimpan sebagai status starting, healthy, atau unhealthy. Penting dipahami: Docker Engine di satu host tidak me-restart kontainer yang unhealthy. Kebijakan restart: unless-stopped hanya bereaksi ketika proses utama keluar. Kontainer yang hang tetapi masih berjalan akan tetap berstatus unhealthy tanpa tindakan apa pun.
Healthcheck Tidak Me-restart Kontainer
Berikut skrip healthcheck minimal yang dipanggil oleh konfigurasi di atas. Sesuaikan path /health dengan aplikasi Anda:
1const http = require("node:http");2 3const request = http.get("http://127.0.0.1:3000/health", (res) => {4 process.exit(res.statusCode === 200 ? 0 : 1);5});6 7request.on("error", () => process.exit(1));8request.setTimeout(3000, () => {9 request.destroy();10 process.exit(1);11});
7. Manajemen Log
Dengan driver json-file bawaan, file log kontainer tumbuh tanpa batas dan dapat memenuhi disk server hingga host crash. Batasi ukuran log per kontainer, seperti pada contoh di atas dengan max-size: "50m" dan max-file: "3", atau atur default di level daemon Docker. Untuk produksi yang lebih serius, pertimbangkan mengirim log ke sistem terpusat.
Docker Compose di Satu Host vs Kubernetes (K8s) Cluster
- Sangat hemat sumber daya server karena tidak ada overhead control plane kluster
- Proses pemeliharaan dan backup volume relatif sederhana
- Setup cepat untuk satu server bisnis dengan beban menengah
- Compose mengelola satu host: tidak ada failover otomatis, rolling update bawaan, maupun autoscaling
- Kubernetes memberi orkestrasi penuh, tetapi kurva belajarnya curam dan biaya operasionalnya lebih besar
- Pilihan menengah: Docker Swarm atau layanan container terkelola dari penyedia cloud
8. Operasional di Luar Docker
Backup: volume database perlu dibackup secara terjadwal (misalnya dump berkala yang disimpan di luar server) dan pemulihannya diuji.
Keamanan host: perbarui OS secara rutin, nonaktifkan login SSH dengan password dan gunakan SSH key, dan batasi port publik hanya untuk yang diperlukan (misalnya 80 dan 443).
Reverse proxy dan TLS: layani trafik publik lewat reverse proxy (Nginx, Caddy, atau Traefik) dengan sertifikat TLS.
Pemantauan: pantau penggunaan CPU, memori, disk, dan status health, dan pasang alert.
Menghubungkan dengan Arsitektur Aplikasi Anda
Terapkan optimasi indeks dan connection pooling pada kontainer database sesuai panduan Strategi Optimasi Database Skala Besar. Pola kontainer ini juga cocok untuk menjalankan Redis dan worker antrean seperti di Otomatisasi Alur Kerja dengan Webhooks dan AI Agents, untuk men-deploy API seperti di Tutorial RESTful API Skalabel Laravel & PostgreSQL, dan untuk menimbang kapan satu host cukup berdasarkan Panduan Arsitektur Web Modern: Monolith vs Microservices.
Ingin Mengamankan & Memodernisasi Infrastruktur Cloud Anda?
Diskusikan audit keamanan server, setup Docker produksi, dan otomatisasi deployment bersama tim engineer Arkatek.
Pertanyaan Umum Seputar Docker Compose untuk Produksi
Apakah root di dalam kontainer sama dengan root di host?
Tidak otomatis, karena kontainer diisolasi lewat namespace. Namun root di dalam kontainer memperbesar dampak bila ada celah aplikasi atau konfigurasi yang salah (misalnya privileged atau docker.sock). Menjalankan proses sebagai non-root menambah lapisan pertahanan.
Mengapa port database tetap terbuka padahal UFW aktif?
Docker mengelola aturan iptables sendiri, dan lalu lintas ke port yang dipublikasikan tidak melewati aturan UFW. Jangan publikasikan port database, atau bind ke 127.0.0.1, dan verifikasi dari luar dengan alat seperti nmap.
Apakah healthcheck me-restart kontainer yang bermasalah?
Tidak untuk Docker Compose di satu host. Healthcheck hanya menandai status. Untuk pemulihan otomatis butuh Swarm atau Kubernetes, sidecar seperti autoheal, atau alert agar ada yang menindaklanjuti.
Apakah Docker Compose cukup untuk produksi?
Untuk satu server dengan beban menengah dan toleransi downtime yang jelas, cukup. Bila Anda membutuhkan high availability, rolling update, atau autoscaling, pertimbangkan Swarm, Kubernetes, atau layanan container terkelola.
Bagaimana menyimpan password database dengan aman?
Gunakan file secrets atau secrets manager, batasi izin akses berkasnya, jangan commit file .env produksi, dan rotasi kredensial secara berkala.
Kesimpulan
Docker Compose adalah alat yang andal dan hemat biaya untuk satu host produksi bila dikonfigurasi dengan benar. Dengan multi-stage build, proses non-root, jaringan database yang internal, batas sumber daya, pengelolaan rahasia, serta pemahaman yang jelas tentang apa yang dilakukan (dan tidak dilakukan) healthcheck, infrastruktur aplikasi web Anda akan jauh lebih aman dan mudah dipelihara. Ingat bahwa Compose di satu host tidak menggantikan kebutuhan akan backup, pemantauan, dan rencana pemulihan.
Referensi: Packet filtering and firewalls (Docker Docs), Docker Compose (Docker Docs), dan Building best practices (Docker Docs).