Server/Infra

Docker Compose untuk Development Environment - Multi-Container Setup yang Rapi

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.

Kenapa Docker Compose untuk Development?

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.

Struktur Project yang Clean

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.

Membuat docker-compose.yml dari Nol

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:

  • Port mapping: MySQL pakai 3307 di host, bukan 3306. Ini menghindari konflik kalau kamu sudah install MySQL di local machine.
  • Healthcheck: MySQL punya healthcheck. Service app menunggu MySQL benar-benar ready sebelum start. Ini mencegah error "connection refused" saat pertama kali build.
  • Named volumes: mysql-data dan redis-data persist di luar container. Data tidak hilang saat docker compose down.
  • Environment variables: Pakai ${VARIABLE:-default} supaya bisa di-override dari file .env.

Dockerfile untuk PHP

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.

Konfigurasi Nginx

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.

Environment Variables

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.

Perintah Sehari-hari

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

Tips Production-Ready Development

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.

Troubleshooting Umum

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.

Kesimpulan

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.


You may also like


0 Comments


Leave a Reply

Comments with links or spam keywords will be rejected.
Scroll to Top