Membungkus aplikasi CodeIgniter dalam container Docker dengan Nginx dan PHP-FPM, lalu membangun pipeline GitHub Actions yang mengetes dan men-deploy otomatis ke server VPS.

Di episode 16 kita deploy manual ke VPS — rsync, composer install, migrate. Bekerja, tapi rapuh: tiap deploy butuh langkah manual, versi tidak terdokumentasi, dan jika ada 3 server, semua diulang 3 kali. Di episode 23 ini kita profesionalkan prosesnya dengan Docker untuk mengemas aplikasi, dan GitHub Actions untuk otomasi build, test, dan deploy.
Mengapa penting? Karena container dan CI/CD adalah standar industri. Dengan Docker, "jalan di laptop saya" hilang selamanya — image yang sama berjalan di mana pun. Dengan CI/CD, tiap push ke git otomatis dites dan di-deploy, sehingga regresi tertangkap sebelum produksi.
Container dibangun dari image PHP resmi. Untuk CodeIgniter, kita perlu PHP-FPM + ekstensi yang dibutuhkan (intl, mbstring, pdo_mysql):
FROM composer:2 AS composer
WORKDIR /tmp/app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-progress --no-interaction --prefer-dist --ignore-platform-reqs
FROM php:8.4-fpm-alpine
RUN docker-php-ext-install intl pdo_mysql opcache && docker-php-ext-enable intl
COPY --from=composer /tmp/app/vendor /var/www/html/vendor
COPY . /var/www/html
RUN chown -R www-data:www-data /var/www/html/writable
WORKDIR /var/www/html
EXPOSE 9000Poin penting:
docker-php-ext-install menambah ekstensi PHP di image resmi.chown memberikan writable/ ke user www-data (pengguna PHP-FPM).Aplikasi nyata butuh lebih dari satu layanan: app (PHP-FPM), web (Nginx), database (MySQL), dan cache (Redis):
services:
app:
build: .
container_name: ci4_app
env_file: .env
depends_on:
- db
- redis
volumes:
- ./writable:/var/www/html/writable
web:
image: nginx:1.27-alpine
container_name: ci4_web
ports:
- "80:80"
volumes:
- ./:/var/www/html
- ./docker/nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- app
db:
image: mysql:8.4
container_name: ci4_db
environment:
MYSQL_DATABASE: ci_blog
MYSQL_USER: ci_user
MYSQL_PASSWORD: ci_password
MYSQL_ROOT_PASSWORD: root_password
volumes:
- db_data:/var/lib/mysql
redis:
image: redis:7-alpine
container_name: ci4_redis
volumes:
db_data:Volume penting:
./writable:/var/www/html/writable — file log/session/upload tetap ada saat container diganti.db_data — data MySQL tidak hilang saat container di-recreate.server {
listen 80;
server_name _;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass app:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~ /\. {
deny all;
}
}fastcgi_pass app:9000 menunjuk ke service app di network Docker compose. root tetap ke public/ dan file dot di-deny — prinsip yang sama seperti episode 20.
Warning
Jangan lupa set app.baseURL di .env sesuai host yang melayani — jika diakses via http://localhost, set app.baseURL = 'http://localhost/'. baseURL yang salah membuat link dan redirect patah, dan ini penyebab paling umum "aplikasi jalan di Docker tapi link-nya rusak".
Buat workflow yang mengetes setiap push:
name: CI
on:
push:
branches: [main, staging]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.4"
extensions: intl, mbstring, pdo_mysql
coverage: xdebug
- name: Install dependencies
run: composer install --no-progress --prefer-dist
- name: Setup test database
run: |
sudo systemctl start mysql
mysql -uroot -proot -e "CREATE DATABASE ci_blog_test;"
- name: Run tests
run: php spark test
env:
database.tests.default.hostname: 127.0.0.1
database.tests.default.database: ci_blog_test
database.tests.default.username: root
database.tests.default.password: root
- name: Build Docker image
run: docker build -t ci4-app .Setiap push dijalankan: install dependency, siapkan database test, jalankan seluruh test PHPUnit, dan pastikan image Docker berhasil dibangun. Ini pipeline terus-menerus yang menjaga main selalu hijau.
Setelah test hijau, deploy otomatis. Pendekatan paling sederhana untuk VPS: SSH + rsync setelah test sukses.
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy ke VPS via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /var/www/ci4
git pull origin main
composer install --no-dev --optimize-autoloader --no-interaction
php spark migrate --all
php spark cache:clear
sudo systemctl reload php8.4-fpm
sudo systemctl reload nginxPoin penting di workflow ini:
VPS_HOST, VPS_USER, VPS_SSH_KEY) disimpan di GitHub → Settings → Secrets — bukan di kode.main.Tip
Untuk produksi dengan banyak server, pendekatan SSH manual akan kesulitan. Ganti dengan strategi yang lebih maju: build image ke registry (GHCR), lalu server menarik image baru (docker compose pull && docker compose up -d). Konsepnya sama, eksekusinya lebih bersih.
chown writable.root ke public/, fastcgi_pass app:9000, deny file dot.php spark test.Inti yang harus dibawa pulang:
Di episode 24 selanjutnya kita akan membahas scaling & optimisasi — Opcache, strategi cache, indexing database, session Redis, serta load testing untuk membuktikan tuning kalian. Dari satu server menuju banyak server!