Pernah nggak sih, kamu sudah berhasil bikin satu container jalan dengan Docker, tapi begitu coba hubungkan beberapa service sekaligus — database, backend, frontend, redis — rasanya ribet banget? Harus buka banyak terminal, jalankan perintah docker run satu per satu, belum lagi soal networking antar container. Capek, kan?

Nah, di situlah Docker Compose hadir sebagai penyelamat. Dengan satu file YAML dan satu perintah, kamu bisa menjalankan seluruh stack aplikasi sekaligus. Kalau kamu seorang developer yang ingin workflow development lebih rapi dan efisien, artikel ini wajib kamu baca sampai habis.

Di tutorial ini, kita akan belajar Docker Compose dari nol secara praktis. Kita akan bikin contoh aplikasi multi-container yang terdiri dari Node.js, PostgreSQL, dan Redis — sebuah stack yang sangat umum dipakai di dunia nyata.


Apa Itu Docker Compose dan Kenapa Kamu Butuh?

Sebelum masuk ke kode, penting untuk paham dulu konsep dasarnya.

Docker Compose adalah tool yang memungkinkan kamu mendefinisikan dan menjalankan beberapa container Docker sekaligus menggunakan satu file konfigurasi bernama docker-compose.yml (atau compose.yml di versi terbaru). Bayangkan dia semacam “skrip orkestra” yang mengatur semua container supaya bisa bekerja bersama.

Tanpa Docker Compose, kamu harus menjalankan perintah-perintah seperti ini satu per satu:

docker network create mynetwork
docker run -d --name postgres --network mynetwork -e POSTGRES_PASSWORD=secret postgres:16
docker run -d --name redis --network mynetwork redis:7
docker run -d --name app --network mynetwork -p 3000:3000 my-node-app

Ribet? Banget. Apalagi kalau ada dependency, environment variable, volume mounting, dan sebagainya.

Dengan Docker Compose, semua itu cukup jadi satu file dan satu perintah:

docker compose up

Selesai. Semua service langsung berjalan bersamaan, saling terhubung lewat network yang sama, dengan konfigurasi yang sudah kamu tentukan.

Kapan Waktu yang Tepat Pakai Docker Compose?

  • Development lokal: Kamu butuh database, cache, message queue, dan app server berjalan bersamaan.
  • Testing/CI: Butuh environment yang reproducible untuk test.
  • Prototyping: Ingin cepat spin up full stack tanpa setup manual.
  • Small-scale deployment: Untuk staging atau bahkan production sederhana, Compose sudah sangat memadai.

Persiapan: Apa Saja yang Perlu Diinstal?

Sebelum mulai coding, pastikan environment kamu sudah siap. Ini dia yang kamu butuhkan:

1. Docker Engine

Pastikan Docker sudah terinstal di mesin kamu. Cek dengan:

docker --version

Kalau belum terinstal, kamu bisa ikuti panduan resmi dari docs.docker.com.

2. Docker Compose

Kabar baiknya, Docker Compose V2 sekarang sudah built-in langsung ke dalam Docker CLI. Jadi kalau kamu sudah install Docker Desktop (Windows/Mac) atau Docker Engine terbaru (Linux), Compose sudah tersedia.

Cek dengan:

docker compose version

Kalau muncul versinya, kamu siap. Perhatikan bahwa perintahnya sekarang docker compose (spasi), bukan docker-compose (strip) seperti versi lama. Versi lama masih bisa dipakai tapi sudah deprecated.

3. Text Editor

VS Code, Vim, Nano, atau editor apapun yang kamu suka. Yang penting nyaman buat nulis YAML.


Struktur Project: Contoh Aplikasi Node.js + PostgreSQL + Redis

Sekarang kita bikin project nyata. Stack yang kita pakai:

  • Node.js (Express) — backend API
  • PostgreSQL — database utama
  • Redis — caching layer

Buat folder project baru:

mkdir compose-demo && cd compose-demo

File index.js — Aplikasi Node.js

const express = require("express");
const { Pool } = require("pg");
const redis = require("redis");

const app = express();
const port = 3000;

// Koneksi PostgreSQL — pakai nama service "db" dari docker-compose
const pgPool = new Pool({
  host: "db",
  port: 5432,
  user: "postgres",
  password: "postgres",
  database: "appdb",
});

// Koneksi Redis — pakai nama service "redis" dari docker-compose
const redisClient = redis.createClient({
  url: "redis://redis:6379",
});

redisClient.on("error", (err) => console.error("Redis error:", err));

app.get("/", async (req, res) => {
  // Cek cache dulu
  const cached = await redisClient.get("hit_count");
  if (cached) {
    return res.json({ source: "cache", hit_count: parseInt(cached) });
  }

  // Kalau tidak ada di cache, query database
  const result = await pgPool.query(
    "SELECT hit_count FROM counters WHERE name = 'main'"
  );
  const hitCount = result.rows[0]?.hit_count || 0;

  // Simpan ke cache selama 60 detik
  await redisClient.setEx("hit_count", 60, hitCount.toString());

  res.json({ source: "database", hit_count: hitCount });
});

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

File package.json

{
  "name": "compose-demo",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "pg": "^8.12.0",
    "redis": "^4.6.10"
  }
}

File Dockerfile untuk App

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install --production

COPY . .

EXPOSE 3000
CMD ["npm", "start"]

File init.sql — Inisialisasi Database

CREATE TABLE IF NOT EXISTS counters (
  name VARCHAR(50) PRIMARY KEY,
  hit_count INTEGER DEFAULT 0
);

INSERT INTO counters (name, hit_count) VALUES ('main', 42)
ON CONFLICT (name) DO NOTHING;

Membuat docker-compose.yml: Inti dari Semuanya

Sekorang kita masuk ke bagian paling penting. Buat file docker-compose.yml di root project:

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    volumes:
      - .:/app
      - /app/node_modules
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: appdb
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redisdata:/data
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:

Wah, kayaknya banyak ya? Tenang, kita bedah satu per satu.

Penjelasan Per Bagian

services — Ini daftar semua container yang mau kita jalankan. Setiap service punya nama unik (app, db, redis) yang sekaligus jadi hostname di internal Docker network. Makanya di kode Node.js tadi kita bisa langsung pakai host: "db" dan url: "redis://redis:6379".

build — Memberitahu Compose untuk build image dari Dockerfile di direktori yang ditentukan. Dipakai di service app karena kita punya Dockerfile sendiri.

image — Kalau kamu tidak build sendiri tapi pakai image yang sudah ada di Docker Hub, cukup tulis nama imagenya di sini. Dipakai di db dan redis.

ports — Mapping port dari container ke host. Formatnya host:container. Jadi "3000:3000" artinya port 3000 di container dipetakan ke port 3000 di localhost.

environment — Set environment variables di dalam container. Untuk PostgreSQL, ini penting banget karena image postgres butuh POSTGRES_USER, POSTGRES_PASSWORD, dan POSTGRES_DB saat pertama kali diinisialisasi.

depends_on — Mengatur urutan startup. Service app baru akan dijalankan setelah db healthy dan redis sudah started. Ini mencegah error koneksi di aplikasi karena database belum siap.

volumes — Ada dua jenis di sini:

  • Named volume (pgdata:/var/lib/postgresql/data): Data database tetap tersimpan walaupun container dihapus. Penting untuk data persisten.
  • Bind mount (.:/app): Mount direktori lokal ke container. Cocok untuk development supaya perubahan kode langsung ter-reflect tanpa rebuild.

healthcheck — Cara kita mengecek apakah service benar-benar sudah siap. Untuk PostgreSQL, kita pakai pg_isready. Service baru dianggap “healthy” kalau healthcheck berhasil.

restart — Kebijakan restart otomatis. unless-stopped artinya container akan restart sendiri kalau crash, kecuali kamu yang menghentikannya secara manual.


Menjalankan dan Mengelola Multi-Container

First Run

Sekarang saatnya membuktikan semua bekerja. Di root project, jalankan:

docker compose up --build

Flag --build memaksa Compose untuk build image ulang. Penting dijalankan pertama kali atau setiap kali kamu ubah Dockerfile atau kode.

Kamu akan melihat log dari ketiga service bercampur di terminal. Compose menampilkan log dengan warna berbeda untuk setiap service supaya mudah dibedakan.

Buka terminal baru dan test:

curl http://localhost:3000

Kalau berhasil, kamu akan dapat response JSON:

{
  "source": "database",
  "hit_count": 42

Di request kedua dalam 60 detik, data akan diambil dari cache:

{
  "source": "cache",
  "hit_count": 42
}

Perintah-Perintah Penting

Berikut perintah Docker Compose yang wajib kamu hafal:

# Jalankan di background (detached mode)
docker compose up -d

# Lihat status semua container
docker compose ps

# Lihat log real-time
docker compose logs -f

# Lihat log service tertentu
docker compose logs -f app

# Hentikan semua container (data tetap aman di volume)
docker compose down

# Hentikan DAN hapus semua volume (hati-hati: data hilang!)
docker compose down -v

# Jalankan perintah di dalam container
docker compose exec db psql -U postgres -d appdb

# Rebuild satu service saja
docker compose up -d --build app

# Scale satu service jadi banyak instance
docker compose up -d --scale app=3

Networking di Docker Compose

Salah satu fitur paling powerful dari Docker Compose adalah networking otomatis. Secara default, Compose membuat network baru bernama <nama-folder>_default dan menghubungkan semua service ke network tersebut.

Artinya, setiap service bisa saling “melihat” satu sama lain menggunakan nama service sebagai hostname. Inilah kenapa di kode Node.js kita cukup tulis host: "db" — Docker DNS akan me-resolve nama itu ke IP container PostgreSQL yang benar.

Custom Network

Kalau kamu butuh isolasi lebih lanjut, kamu bisa define network sendiri:

services:
  app:
    build: .
    networks:
      - frontend
      - backend

  db:
    image: postgres:16-alpine
    networks:
      - backend

  redis:
    image: redis:7-alpine
    networks:
      - backend

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    networks:
      - frontend

networks:
  frontend:
  backend:

Dengan konfigurasi ini, nginx dan app bisa saling berkomunikasi lewat network frontend, sementara app, db, dan redis berkomunikasi lewat backend. Tapi nginx tidak bisa langsung mengakses db atau redis. Ini bagus untuk keamanan!


Tips Lanjutan: Environment Variables dan Override

Menggunakan .env File

Daripada hardcode password di docker-compose.yml, lebih baik pakai file .env:

# .env
POSTGRES_USER=postgres
POSTGRES_PASSWORD=mysecretpassword
POSTGRES_DB=appdb
APP_PORT=3000

Lalu referensikan di docker-compose.yml:

services:
  app:
    build: .
    ports:
      - "${APP_PORT}:3000"

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}

Jangan lupa tambahkan .env ke .gitintore supaya password tidak ter-commit ke repository:

.env
node_modules

Override untuk Berbagai Environment

Kamu bisa bikin file override untuk membedakan konfigurasi antara development dan production:

docker-compose.override.yml (otomatis ter-load):

services:
  app:
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development

docker-compose.prod.yml (di-load manual):

services:
  app:
    volumes: []
    environment:
      - NODE_ENV=production
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: "0.5"
          memory: 512M

Jalankan untuk production:

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Best Practices yang Perlu Kamu Ingat

Setelah cukup lama pakai Docker Compose, berikut beberapa best practices yang menurut saya wajib diikuti:

1. Selalu pakai depends_on dengan healthcheck. Jangan cuma andalkan depends_on biasa tanpa condition. Container bisa “started” tapi belum benar-benar ready. Healthcheck memastikan service benar-benar siap menerima koneksi.

2. Pakai named volume untuk data persisten. Jangan simpan data penting (database, file upload) di dalam container. Container bersifat ephemeral — bisa dihapus kapan saja. Named volume memastikan data tetap aman.

3. Pisahkan konfigurasi berdasarkan environment. Jangan pakai satu docker-compose.yml untuk semua kebutuhan. Gunakan file override supaya konfigurasi development dan production tidak saling bentrok.

4. Jangan hardcode secrets. Pakai .env file atau Docker secrets untuk password, API key, dan konfigurasi sensitif lainnya.

5. Gunakan image versi spesifik. Hindari postgres:latest — pakai postgres:16-alpine supaya build lebih reproducible dan kamu tahu persis versi yang dipakai.

6. Manfaatkan restart policy. Set restart: unless-stopped untuk service penting supaya otomatis recover kalau crash.

7. Perhatikan .dockerignore. Buat file .dockerignore supaya build context tidak mengirim file yang tidak perlu:

n..oge.dinmetvd_modules

Masih Bingung? Ini FAQ yang Sering Ditanyakan

Q: Bedanya docker-compose.yml dan compose.yml yang mana yang benar?

Keduanya benar. Di Docker Compose V2, nama file default yang direkomendasikan adalah compose.yml (tanpa “docker-” di depan). Tapi docker-compose.yml masih didukung penuh dan bisa dipakai tanpa masalah. Kalau kamu bikin project baru, pakai compose.yml saja lebih ringkas.

Q: Bisa nggak pakai Docker Compose untuk production?

Bisa, tapi ada batasannya. Docker Compose sangat cocok untuk development, testing, dan deployment skala kecil. Untuk production skala besar dengan kebutuhan auto-scaling, rolling update, dan high availability, sebaiknya pertimbangkan Docker Swarm atau Kubernetes. Docker Compose tidak dirancang untuk orchestration di level tersebut.

Q: Kenapa container app saya langsung crash saat pertama kali dijalankan?

Penyebab paling umum adalah app mencoba koneksi ke database sebelum database siap. Solusinya: gunakan depends_on dengan condition: service_healthy dan tambahkan healthcheck di service database. Alternatif lain, implementasikan retry logic di kode aplikasi kamu. Banyak library database yang sudah mendukung auto-retry.

Q: Bagaimana cara melihat isi database PostgreSQL yang jalan di container?

Jalankan perintah ini:

docker compose exec db psql -U postgres -d appdb

Ini akan membuka shell psql langsung ke dalam container database. Kamu bisa jalankan SQL query seperti biasa. Ketik \q untuk keluar.

Q: Apakah volume akan terhapus saat docker compose down?

Tidak. Secara default, docker compose down hanya menghapus container dan network, tapi volume tetap tersimpan. Untuk menghapus volume juga, kamu harus menambahkan flag -v: docker compose down -v. Hati-hati, ini akan menghapus semua data di database!


Mulai Perjalanan Docker Compose Kamu!

Docker Compose benar-benar mengubah cara kita membangun dan menjalankan aplikasi multi-container. Yang dulunya butuh puluhan perintah dan banyak terminal, sekarang cukup satu file YAML dan satu perintah. Workflow development jadi jauh lebih rapi, cepat, dan yang paling penting — reproducible. Tim baru tinggal clone repo, jalankan docker compose up, dan langsung punya environment yang sama persis.

Tutorial ini baru permukaan dari apa yang bisa Docker Compose lakukan. Masih banyak fitur lain yang belum kita bahas — multi-stage build, Docker secrets, resource limits, dan masih banyak lagi. Tapi fondasi yang kamu pelajari di sini sudah cukup untuk membangun sebagian besar aplikasi modern.

Kalau kamu punya pertanyaan lebih lanjut, butuh bantuan setup Docker Compose untuk project-mu, atau ingin diskusi tentang arsitektur containerization yang tepat untuk kebutuhanmu, jangan ragu untuk menghubungi saya di [email protected]. Saya dengan senang hati akan membantu!


Pertanyaan Lain?

Kalau ada pertanyaan yang belum terjawab di artikel ini, cek bagian FAQ di atas atau langsung kirim email ke [email protected]. Saya berusaha membalas setiap email dalam 1×24 jam.

Selamat ngoding, dan semoga Docker Compose bikin hidupmu lebih mudah! 🐳