Pernah ngalamin situasi kayak gini: baru pindah laptop, setup project butuh setengah hari. Install MySQL, Redis, Elasticsearch, belum lagi konfigurasi env-nya. Temen baru masuk tim? Seharian cuma buat bikin project jalan di local. Kalau kamu pernah ngerasain ini, Docker Compose jawabannya.
Saya dulu skeptis sama Docker. Mikirnya, "ngapain tambah layer lagi, mending install langsung." Tapi setelah nyoba bikin setup multi-container buat project CI4 + MySQL + Redis + Nginx, pandangan saya berubah total. Satu command, semua jalan. New joiner tinggal docker compose up, langsung ngoding. No drama.
Docker tanpa Compose masih bisa dipakai, tapi kamu harus jalankan setiap container manual dengan docker run yang panjang-panjang. Compose memungkinkan kamu definisikan semua service dalam satu file YAML. Satu file, satu command, semua service running.
Bayangkan project kamu butuh: PHP-FPM 8.2, Nginx, MySQL 8, Redis, dan Mailpit (buat testing email). Tanpa Compose, kamu perlu 5 terminal window. Dengan Compose, cukup:
docker compose up -d
Selesai. Semua service jalan dengan konfigurasi yang sudah didefinisikan.
Sebelum masuk ke kode, pastikan struktur project kamu rapi. Ini pattern yang saya pakai di hampir semua project:
project-root/
docker-compose.yml
.env
docker/
nginx/
default.conf
php/
Dockerfile
mysql/
init.sql
app/
... (source code)
public/
... (public files)
Folder docker/ terpisah dari source code. Ini penting biar Docker config tidak nyampur dengan application code. Kalau kamu pakai Git, folder ini masuk version control juga karena konfigurasi Docker seharusnya konsisten di semua environment.
Ini contoh setup yang saya pakai buat project CodeIgniter 4. Tapi prinsipnya sama untuk Laravel, Symfony, atau framework PHP lainnya:
services:
# PHP-FPM Application Server
app:
build:
context: .
dockerfile: docker/php/Dockerfile
container_name: myapp-php
restart: unless-stopped
working_dir: /var/www/html
volumes:
- .:/var/www/html
- ./docker/php/php.ini:/usr/local/etc/php/conf.d/custom.ini
networks:
- app-network
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
# Nginx Web Server
nginx:
image: nginx:1.25-alpine
container_name: myapp-nginx
restart: unless-stopped
ports:
- "8080:80"
volumes:
- .:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
networks:
- app-network
depends_on:
- app
# MySQL Database
mysql:
image: mysql:8.0
container_name: myapp-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-rootpass}
MYSQL_DATABASE: ${DB_DATABASE:-myapp}
MYSQL_USER: ${DB_USER:-appuser}
MYSQL_PASSWORD: ${DB_PASSWORD:-apppass}
ports:
- "3307:3306"
volumes:
- mysql-data:/var/lib/mysql
- ./docker/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
# Redis Cache
redis:
image: redis:7-alpine
container_name: myapp-redis
restart: unless-stopped
ports:
- "6380:6379"
volumes:
- redis-data:/data
networks:
- app-network
# Mailpit (Email Testing)
mailpit:
image: axllent/mailpit
container_name: myapp-mail
restart: unless-stopped
ports:
- "8025:8025" # Web UI
- "1025:1025" # SMTP
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
mysql-data:
redis-data:
Beberapa hal penting di sini:
3307 di host, bukan 3306. Ini menghindari konflik kalau kamu sudah install MySQL di local machine.app menunggu MySQL benar-benar ready sebelum start. Ini mencegah error "connection refused" saat pertama kali build.mysql-data dan redis-data persist di luar container. Data tidak hilang saat docker compose down.${VARIABLE:-default} supaya bisa di-override dari file .env.Buat file docker/php/Dockerfile:
FROM php:8.2-fpm-alpine
# Install system dependencies
RUN apk add --no-cache \
libzip-dev \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
oniguruma-dev \
icu-dev \
linux-headers
# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
pdo_mysql \
mbstring \
zip \
gd \
intl \
opcache \
bcmath
# Install Redis extension via PECL
RUN apk add --no-cache $PHPIZE_DEPS \
&& pecl install redis \
&& docker-php-ext-enable redis
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# PHP configuration
RUN echo "upload_max_filesize = 64M" >> /usr/local/etc/php/conf.d/custom.ini \
&& echo "post_max_size = 64M" >> /usr/local/etc/php/conf.d/custom.ini \
&& echo "memory_limit = 256M" >> /usr/local/etc/php/conf.d/custom.ini \
&& echo "max_execution_time = 60" >> /usr/local/etc/php/conf.d/custom.ini
WORKDIR /var/www/html
EXPOSE 9000
CMD ["php-fpm"]
Kenapa Alpine? Image-nya kecil. php:8.2-fpm biasa sekitar 450MB, Alpine cuma 150MB. Untuk development, ini menghemat disk space dan waktu pull.
Buat file docker/nginx/default.conf:
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php index.html;
# CI4 routing
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP-FPM
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_read_timeout 300;
}
# Block sensitive files
location ~ /\.(env|git) {
deny all;
}
# Static file caching
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2)$ {
expires 7d;
add_header Cache-Control "public, immutable";
}
}
Perhatikan fastcgi_pass app:9000. Di Docker network, kamu bisa pakai nama service sebagai hostname. Nginx akan resolve app ke IP container PHP-FPM. Tidak perlu tahu IP-nya, Docker handle DNS-nya.
Buat file .env di root project:
# Database
DB_ROOT_PASSWORD=rootpass
DB_DATABASE=myapp
DB_USER=appuser
DB_PASSWORD=apppass
# Application
APP_ENV=development
APP_DEBUG=true
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
# Mail
MAIL_HOST=mailpit
MAIL_PORT=1025
Penting: Tambahkan .env ke .gitignore. File ini berisi credentials. Untuk team, buat .env.example dengan value kosong sebagai template.
Ini command yang paling sering kamu pakai:
# Start semua service (background)
docker compose up -d
# Lihat log semua service
docker compose logs -f
# Lihat log satu service
docker compose logs -f app
# Masuk ke container PHP (shell)
docker compose exec app sh
# Jalankan artisan/migration/specific command
docker compose exec app php spark migrate
# Install dependency
docker compose exec app composer install
# Stop semua service
docker compose down
# Stop dan hapus volumes (fresh start)
docker compose down -v
# Rebuild image setelah ubah Dockerfile
docker compose build --no-cache
docker compose up -d
1. Pisahkan Compose files per environment.
Punya docker-compose.yml untuk base config, lalu docker-compose.override.yml untuk development-specific config (volume mounts, port exposure). Override file ini otomatis di-merge saat kamu jalankan docker compose up.
# docker-compose.override.yml (development only)
services:
app:
volumes:
- .:/var/www/html # Live reload
environment:
- XDEBUG_MODE=develop
nginx:
ports:
- "8080:80" # Expose ke host
2. Pakai named volume untuk database.
Jangan bind-mount folder database ke host. Named volume lebih cepat dan tidak bermasalah dengan permission. Kalau mau backup, pakai docker compose exec mysql mysqldump.
3. Healthcheck untuk dependency.
Tanpa healthcheck, container bisa start sebelum dependency ready. MySQL butuh 10-30 detik untuk inisialisasi pertama kali. Dengan healthcheck, Compose akan tunggu sampai MySQL benar-benar siap.
4. Jangan pakai latest tag di production.
Di development, mysql:8.0 atau redis:7-alpine aman. Tapi di production, pin ke versi spesifik seperti mysql:8.0.36. Ini mencegah surprise saat rebuild.
Port sudah dipakai:
# Cari process yang pakai port
lsof -i :3306
# Ganti port mapping di docker-compose.yml
ports:
- "3307:3306" # host:container
Permission denied pada volume:
# Alpine PHP-FPM pakai user www-data (UID 82)
# Pastikan file permission benar
docker compose exec app chown -R www-data:www-data /var/www/html/writable
Container restart terus-menerus:
# Cek log untuk error
docker compose logs app
# Kemungkinan: PHP syntax error, missing extension, atau config error
MySQL connection refused:
Pastikan di .env atau config database, host-nya pakai nama service Docker (mysql), bukan localhost atau 127.0.0.1. Di dalam Docker network, setiap service punya hostname sesuai nama servicenya.
Docker Compose bukan lagi "nice to have" untuk development tim. Ini kebutuhan. Satu file YAML menggantikan berjam-jam setup manual. New joiner langsung produktif di hari pertama. Environment konsisten di semua machine. Tidak ada lagi "works on my laptop" excuse.
Mulai dari setup sederhana PHP, MySQL, Redis. Tambah service lain (Elasticsearch, RabbitMQ, MinIO) sesuai kebutuhan. Kuncinya: mulai kecil, iterasi. Jangan langsung bikin 15-service compose file yang bikin pusing sendiri.
Kamu sudah pakai Docker Compose di project sehari-hari? Atau masih setup manual? Share pengalaman di kolom komentar siapa tahu ada tips yang bisa kita diskusikan bareng.