September 27, 2026

Docker Compose untuk Dev Environment.

Menyusun environment development lengkap (app Laravel/Node + MySQL + Redis + Mailpit) dengan Docker Compose: healthcheck, depends_on, env file, profiles, bind mount vs volume, dan hot reload.

Catatan ini membahas cara menyusun environment development yang bisa dijalankan dengan satu perintah: docker compose up. Contohnya aplikasi Laravel (atau Node) + MySQL + Redis + Mailpit. Tujuannya sederhana: siapa pun yang clone repo cukup punya Docker, tanpa harus install PHP, MySQL, atau Redis di laptop masing-masing.

Catatan versi: sekarang kita memakai Compose v2 (docker compose, pakai spasi), bukan docker-compose (pakai strip) yang sudah usang. Key version: di bagian atas file juga tidak diperlukan lagi β€” Compose akan mengabaikannya dan menampilkan warning.


1. Struktur Project

my-app/
β”œβ”€β”€ compose.yaml          # nama default yang dicari Compose (docker-compose.yml juga masih dibaca)
β”œβ”€β”€ compose.override.yaml # opsional, otomatis di-merge saat `docker compose up`
β”œβ”€β”€ .env                  # dipakai Compose untuk interpolasi ${VAR}
β”œβ”€β”€ docker/
β”‚   └── php/Dockerfile
└── src/ ...              # source code aplikasi

Compose otomatis membaca compose.yaml lalu compose.override.yaml (jika ada). Pola umum: compose.yaml berisi konfigurasi dasar, compose.override.yaml berisi hal khusus development (bind mount, port debug).


2. Contoh compose.yaml Lengkap

name: my-app   # nama project (prefix container/network/volume)

services:
  app:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    env_file:
      - path: .env
        required: true
      - path: .env.local
        required: false        # tidak error kalau file tidak ada
    volumes:
      - ./:/var/www/html        # bind mount: kode di laptop = kode di container
      - vendor:/var/www/html/vendor  # vendor disimpan di named volume
    ports:
      - "8000:8000"
    command: php artisan serve --host=0.0.0.0 --port=8000
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy

  mysql:
    image: mysql:8.4
    environment:
      MYSQL_DATABASE: ${DB_DATABASE:-app}
      MYSQL_USER: ${DB_USERNAME:-app}
      MYSQL_PASSWORD: ${DB_PASSWORD:-secret}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-root}
    volumes:
      - mysql-data:/var/lib/mysql
    ports:
      - "3307:3306"   # 3307 di host supaya tidak bentrok dengan MySQL lokal
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-u", "root", "-p${DB_ROOT_PASSWORD:-root}"]
      interval: 5s
      timeout: 3s
      retries: 20
      start_period: 20s

  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10

  mailpit:
    image: axllent/mailpit
    ports:
      - "8025:8025"   # web UI
      - "1025:1025"   # SMTP
    profiles: ["mail"]

  queue:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    command: php artisan queue:work --tries=3
    volumes:
      - ./:/var/www/html
      - vendor:/var/www/html/vendor
    depends_on:
      app:
        condition: service_started
    profiles: ["worker"]

volumes:
  mysql-data:
  redis-data:
  vendor:

Lalu di .env Laravel, host database bukan 127.0.0.1, melainkan nama service:

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
REDIS_HOST=redis
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
QUEUE_CONNECTION=redis

Kenapa DB_PORT=3306 dan bukan 3307? Karena app berbicara lewat network internal Compose, bukan lewat port yang dipublish ke host. Detailnya ada di catatan Docker Network .


3. Healthcheck & depends_on

depends_on versi pendek hanya menjamin urutan start, bukan β€œsudah siap”. MySQL butuh beberapa detik sebelum menerima koneksi, sehingga migrasi yang jalan terlalu cepat akan gagal dengan Connection refused.

KondisiArtinya
service_startedDefault. Tunggu container dependensi start saja.
service_healthyTunggu sampai healthcheck dependensi berstatus healthy.
service_completed_successfullyTunggu container dependensi selesai dengan exit code 0 (cocok untuk job migrasi/seed).

Contoh job migrasi sekali jalan:

  migrate:
    build: .
    command: php artisan migrate --force
    depends_on:
      mysql:
        condition: service_healthy

  app:
    depends_on:
      migrate:
        condition: service_completed_successfully

Cek status health:

docker compose ps
# kolom STATUS akan menampilkan (healthy) / (unhealthy) / (health: starting)

4. Env File: Dua Hal yang Sering Tertukar

Ada dua mekanisme env yang berbeda:

MekanismeDipakai untukContoh
File .env di folder projectInterpolasi ${VAR} di dalam compose.yamlimage: mysql:${MYSQL_VERSION}
env_file: / environment: di serviceVariabel yang masuk ke dalam containerDB_HOST=mysql terbaca oleh Laravel

Kebetulan Laravel juga memakai .env, jadi file yang sama sering dipakai untuk keduanya β€” tidak masalah, asal paham bedanya. Cek hasil akhir konfigurasi (setelah interpolasi dan merge) dengan:

docker compose config

5. Profiles: Service Opsional

Service yang punya profiles tidak ikut jalan secara default. Cocok untuk tool yang tidak selalu dibutuhkan (Mailpit, worker queue , phpMyAdmin).

docker compose up -d                          # app, mysql, redis saja
docker compose --profile mail up -d           # + mailpit
docker compose --profile mail --profile worker up -d
COMPOSE_PROFILES=mail,worker docker compose up -d   # via env var

6. Bind Mount vs Named Volume untuk Dev

Bind mount (./:/var/www/html)Named volume (mysql-data:/var/lib/mysql)
Lokasi dataFolder di laptopDikelola Docker
Cocok untukSource code (edit langsung terlihat)Data database, vendor/, node_modules/
Performa di macOS/WindowsLebih lambat (sinkronisasi file host ↔ VM)Cepat (native di VM Docker)

Trik yang sering dipakai: bind mount seluruh project, lalu β€œtimpa” folder dependensi berat dengan named volume (vendor:/var/www/html/vendor atau node_modules:/app/node_modules). Hasilnya kode tetap live, tapi ribuan file dependensi tidak disinkronkan. Pembahasan lengkap ada di catatan Docker Volume .


7. Hot Reload

Opsi A: Bind mount + dev server

Untuk Laravel, php artisan serve langsung membaca file terbaru. Untuk Vite/Node, jalankan dev server dengan host 0.0.0.0:

  node:
    image: node:22-alpine
    working_dir: /app
    command: sh -c "npm install && npm run dev -- --host 0.0.0.0"
    volumes:
      - ./:/app
      - node_modules:/app/node_modules
    ports:
      - "5173:5173"

Jika file watcher tidak mendeteksi perubahan (sering terjadi di Windows/WSL atau macOS dengan bind mount), aktifkan polling di vite.config:

server: {
  host: '0.0.0.0',
  hmr: { host: 'localhost' },
  watch: { usePolling: true },
}

Opsi B: Compose Watch (develop.watch)

Compose punya fitur watch bawaan yang menyalin file yang berubah ke container (atau rebuild), tanpa bind mount:

  web:
    build: .
    command: npm run dev
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
          ignore:
            - node_modules/
        - action: sync+restart
          path: ./config
          target: /app/config
        - action: rebuild
          path: package.json
docker compose up --watch    # jalan + watch
docker compose watch         # watch terpisah agar log tidak campur
ActionKapan dipakai
syncKode yang di-hot-reload oleh framework (React, Vite, Nodemon)
sync+restartFile config yang butuh restart proses
rebuildpackage.json/composer.json berubah β†’ image perlu dibangun ulang

8. Perintah Harian

docker compose up -d                  # jalankan di background
docker compose logs -f app            # ikuti log service app
docker compose exec app php artisan migrate
docker compose exec app bash          # masuk shell
docker compose run --rm app composer install   # container sekali pakai
docker compose restart queue
docker compose down                   # stop + hapus container & network (volume aman)
docker compose down -v                # HATI-HATI: ikut menghapus volume (data DB hilang)
docker compose build --no-cache app   # rebuild image tanpa cache

9. Pitfalls

  • DB_HOST=127.0.0.1 di dalam container β†’ mengarah ke container itu sendiri, bukan MySQL. Pakai nama service (mysql).
  • Port bentrok (port is already allocated) β†’ MySQL/Redis lokal masih jalan. Ganti sisi host, misal 3307:3306.
  • depends_on tanpa condition β†’ app start sebelum DB siap. Tambahkan healthcheck + service_healthy.
  • File milik root setelah composer install di container β†’ lihat bagian permission UID di catatan Docker Volume.
  • docker compose down -v menghapus data database. Biasakan down saja.
  • Lambat di macOS β†’ jangan bind mount vendor/ & node_modules/; pakai named volume atau Compose Watch. Kalau CPU Docker tinggi, lihat Mengatasi Docker CPU 90% .
  • Perubahan Dockerfile tidak terlihat β†’ docker compose up -d --build.

Setup seperti ini (app + MySQL + Redis di Compose) juga saya pakai di Proxmox Management Server dan Chat App Multiapps .

Referensi: Docker Compose docs , Compose file reference , Compose Watch .

Hey! I’m Fanny, the software engineer tending to this digital garden. You can read more about me, or subscribe by email.

Comments