Image produksi & ECR
Image produksi yang baik hanya berisi apa yang dibutuhkan saat runtime. Semua yang dipakai untuk membangunnya — Composer dev, Node, kode sumber aset — tinggal di stage yang dibuang.
Intisari
- Multi-stage: satu stage untuk dependensi PHP, satu untuk aset frontend, satu untuk runtime.
- Basis Debian, bukan Alpine — kedua runtime worker mode menyarankan itu.
php artisan optimizedijalankan saat build, bukan saat kontainer menyala.- Jalankan sebagai pengguna tanpa hak istimewa; filesystem read-only kecuali beberapa folder.
- Beri tag image dengan SHA commit, bukan
latest— supaya rollback bisa menunjuk versi persis.
Dockerfile
# syntax=docker/dockerfile:1
# ── Stage 1: dependensi PHP ────────────────────────────────────────────
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
--no-dev --no-scripts --no-autoloader \
--prefer-dist --no-interaction
COPY . .
RUN composer dump-autoload --optimize --classmap-authoritative --no-dev
# ── Stage 2: aset frontend ─────────────────────────────────────────────
FROM node:22-bookworm-slim AS aset
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY resources/ resources/
COPY vite.config.js ./
RUN npm run build
# ── Stage 3: runtime ───────────────────────────────────────────────────
FROM dunglas/frankenphp:php8.4-bookworm AS runtime
RUN install-php-extensions \
pdo_mysql redis intl opcache zip bcmath pcntl gd
COPY docker/php.ini /usr/local/etc/php/conf.d/99-app.ini
COPY docker/Caddyfile /etc/frankenphp/Caddyfile
WORKDIR /app
COPY --from=vendor /app /app
COPY --from=aset /app/public/build /app/public/build
# Cache dibangun SAAT BUILD — kontainer menyala tanpa pekerjaan tambahan
RUN php artisan config:clear \
&& php artisan route:cache \
&& php artisan view:cache \
&& php artisan event:cache
RUN chown -R www-data:www-data storage bootstrap/cache
USER www-data
ENV SERVER_NAME=":8000"
EXPOSE 8000
HEALTHCHECK --interval=15s --timeout=3s --start-period=20s \
CMD curl -fsS http://127.0.0.1:8000/up || exit 1
CMD ["php", "artisan", "octane:frankenphp", \
"--host=0.0.0.0", "--port=8000", "--workers=4", "--max-requests=500", \
"--log-level=info"]
Perhatikan apa yang tidak ikut ke image akhir. Composer, Node, node_modules,
folder resources/js, dan seluruh dependensi --dev tinggal di stage sebelumnya.
Hasilnya bukan cuma image yang lebih kecil dan lebih cepat ditarik saat autoscaling, tapi juga permukaan
serangan yang jauh lebih sempit — tidak ada Node di dalam kontainer produksimu.
Kenapa config:cache tidak dijalankan saat build
RUN php artisan route:cache view:cache event:cache # ← aman saat build
# config:cache TIDAK dijalankan di sini
Karena variabel environment belum ada saat build. Menjalankan config:cache di Dockerfile
akan membekukan nilai kosong ke dalam image. Rute, view, dan event tidak bergantung pada environment, jadi
aman di-cache saat build. Konfigurasi harus di-cache setelah environment disuntikkan —
yaitu di entrypoint, atau tidak sama sekali (Laravel tetap membaca config/ dengan cepat kalau
optimize:clear pernah dijalankan).
#!/bin/sh
# docker/entrypoint.sh — dijalankan setelah environment tersedia
set -e
php artisan config:cache
exec "$@"
Caddyfile untuk kontainer
{
frankenphp {
num_threads 8
max_threads 24
}
# Tanpa TLS: ALB yang menanganinya
auto_https off
admin off
servers {
trusted_proxies static 10.0.0.0/8
}
}
:8000 {
root /app/public
encode zstd gzip
@statis path /build/* /favicon.ico /robots.txt
file_server @statis
php_server {
try_files {path} index.php
root /app/public
resolve_root_symlink false
}
}
php.ini produksi
memory_limit = 256M
expose_php = Off
opcache.enable = 1
opcache.enable_cli = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
realpath_cache_size = 4096K
realpath_cache_ttl = 600
upload_max_filesize = 32M
post_max_size = 32M
Membangun dan mendorong ke ECR
SHA=$(git rev-parse --short HEAD)
REPO=123456789012.dkr.ecr.ap-southeast-1.amazonaws.com/portal
aws ecr get-login-password --region ap-southeast-1 \
| docker login --username AWS --password-stdin "$REPO"
docker buildx build \
--platform linux/amd64 \
--cache-from type=registry,ref=$REPO:cache \
--cache-to type=registry,ref=$REPO:cache,mode=max \
-t "$REPO:$SHA" \
-t "$REPO:latest" \
--push .
Tag dengan SHA commit, bukan hanya latest. Task definition harus menunjuk ke tag yang
spesifik. Kalau ia menunjuk latest, rollback jadi tidak berarti — latest hari ini
bukan latest kemarin, dan kamu kehilangan kemampuan menunjuk "versi yang tadi berjalan baik".
Untuk jaminan yang lebih kuat lagi, gunakan digest image.
Menyetel repositori ECR
# Pindai kerentanan otomatis setiap kali image didorong
aws ecr put-image-scanning-configuration \
--repository-name portal --image-scanning-configuration scanOnPush=true
# Cegah tag ditimpa — image yang sudah ada tidak bisa berubah isinya
aws ecr put-image-tag-mutability \
--repository-name portal --image-tag-mutability IMMUTABLE
Tambahkan juga lifecycle policy yang menyimpan sekitar 20 image terakhir dan menghapus sisanya. Tanpa itu, repositori ECR akan berisi ratusan image lama yang tetap ditagih penyimpanannya.
Membuat kontainer lebih aman
| Praktik | Cara |
|---|---|
| Tidak berjalan sebagai root | USER www-data |
| Filesystem read-only | readonlyRootFilesystem di task definition, plus volume tmpfs untuk storage/framework dan /tmp |
| Tanpa rahasia di dalam image | Semua lewat secrets di task definition |
| Pemindaian kerentanan | scanOnPush, plus Trivy di CI |
| Basis image diperbarui | Build ulang berkala, bukan hanya saat kode berubah |
Filesystem read-only butuh persiapan di Laravel. Aplikasi menulis ke
storage/framework/{cache,sessions,views} dan storage/logs. Solusinya: view
sudah di-cache saat build, session dan cache dipindahkan ke Redis, dan log diarahkan ke stderr.
Setelah itu satu volume tmpfs kecil untuk storage/framework sudah cukup — dan
kontainermu benar-benar tanpa keadaan.
Latihan: bangun image dengan Dockerfile di atas dan periksa ukurannya dengan
docker images. Lalu jalankan docker run --read-only --tmpfs /app/storage/framework …
dan cari tahu bagian mana yang masih gagal — itulah daftar tempat aplikasimu masih menulis ke disk.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.