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), bukandocker-compose(pakai strip) yang sudah usang. Keyversion: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.
| Kondisi | Artinya |
|---|---|
service_started | Default. Tunggu container dependensi start saja. |
service_healthy | Tunggu sampai healthcheck dependensi berstatus healthy. |
service_completed_successfully | Tunggu 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:
| Mekanisme | Dipakai untuk | Contoh |
|---|---|---|
File .env di folder project | Interpolasi ${VAR} di dalam compose.yaml | image: mysql:${MYSQL_VERSION} |
env_file: / environment: di service | Variabel yang masuk ke dalam container | DB_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 data | Folder di laptop | Dikelola Docker |
| Cocok untuk | Source code (edit langsung terlihat) | Data database, vendor/, node_modules/ |
| Performa di macOS/Windows | Lebih 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
| Action | Kapan dipakai |
|---|---|
sync | Kode yang di-hot-reload oleh framework (React, Vite, Nodemon) |
sync+restart | File config yang butuh restart proses |
rebuild | package.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.1di 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, misal3307:3306. depends_ontanpaconditionβ app start sebelum DB siap. Tambahkan healthcheck +service_healthy.- File milik
rootsetelahcomposer installdi container β lihat bagian permission UID di catatan Docker Volume. docker compose down -vmenghapus data database. Biasakandownsaja.- 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
Dockerfiletidak 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.