feat: Docker Compose setup untuk development & production
- docker/php/Dockerfile: PHP 8.4-FPM + GD + imagick (PECL) + semua extension Laravel - docker/php/php.ini: upload 20MB, memory 512MB, opcache, Asia/Kuala_Lumpur - docker/php/php-dev.ini: validate_timestamps=1, display_errors=On (dev) - docker/nginx/default.conf: gzip, security headers, static asset caching - docker/entrypoint.sh: tunggu MySQL → migrate → seed AdminSeeder → cache (prod) - docker-compose.yml: dev stack — port 8003, DB host 33060, queue worker - docker-compose.prod.yml: production overrides — storage volume, no DB port exposed - .env.docker: template env untuk Docker (DB_HOST=db) - .dockerignore: exclude node_modules, vendor, .env, logs fix: testGenerate try/catch kembalikan JSON error (bukan HTML 500) fix: loadPreview() semak r.ok, tunjuk error alert, loading spinner Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
48
.dockerignore
Normal file
48
.dockerignore
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# .dockerignore — fail yang TIDAK disertakan dalam Docker build context
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# Dependencies (akan dipasang semula dalam container)
|
||||||
|
node_modules
|
||||||
|
vendor
|
||||||
|
|
||||||
|
# Environment secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.docker
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Build output (akan dihasilkan semula)
|
||||||
|
public/hot
|
||||||
|
public/build
|
||||||
|
|
||||||
|
# Dev tools
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.code-workspace
|
||||||
|
.editorconfig
|
||||||
|
.phpunit.cache
|
||||||
|
phpunit.xml
|
||||||
|
|
||||||
|
# Docker files (tidak perlu dalam app container)
|
||||||
|
docker-compose*.yml
|
||||||
|
docker/
|
||||||
|
|
||||||
|
# Logs & cache
|
||||||
|
storage/logs/*
|
||||||
|
storage/framework/cache/*
|
||||||
|
storage/framework/sessions/*
|
||||||
|
storage/framework/views/*
|
||||||
|
bootstrap/cache/*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
tests/
|
||||||
65
.env.docker
Normal file
65
.env.docker
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# eCert MBIP — Contoh .env untuk Docker
|
||||||
|
# Salin ke .env dan ubah nilai yang perlu:
|
||||||
|
# cp .env.docker .env
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
APP_NAME="eCert MBIP"
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG=true
|
||||||
|
# PENTING: Tukar ke domain sebenar untuk production
|
||||||
|
# Contoh: APP_URL=https://ecert.mbip.gov.my
|
||||||
|
APP_URL=http://localhost:8003
|
||||||
|
|
||||||
|
APP_LOCALE=ms
|
||||||
|
APP_FALLBACK_LOCALE=ms
|
||||||
|
APP_FAKER_LOCALE=ms_MY
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
# Production: guna level "warning" atau "error"
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
# ── Database ──────────────────────────────────────────────────────────────────
|
||||||
|
DB_CONNECTION=mysql
|
||||||
|
DB_HOST=db # nama service dalam docker-compose.yml
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=ecert_mbip
|
||||||
|
DB_USERNAME=ecert
|
||||||
|
# Tukar password ini!
|
||||||
|
DB_PASSWORD=ecert_secret_2025
|
||||||
|
|
||||||
|
# ── Session ───────────────────────────────────────────────────────────────────
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=false
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
# ── Cache & Queue (guna database — tiada Redis diperlukan) ────────────────────
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
CACHE_STORE=database
|
||||||
|
|
||||||
|
# ── Mail ──────────────────────────────────────────────────────────────────────
|
||||||
|
# Dev: guna "log" untuk lihat email dalam storage/logs/laravel.log
|
||||||
|
# Production: tukar ke SMTP
|
||||||
|
MAIL_MAILER=log
|
||||||
|
MAIL_SCHEME=null
|
||||||
|
MAIL_HOST=smtp.mbip.gov.my
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_ENCRYPTION=tls
|
||||||
|
MAIL_FROM_ADDRESS="ecert@mbip.gov.my"
|
||||||
|
MAIL_FROM_NAME="eCert MBIP"
|
||||||
|
|
||||||
|
# ── Vite ──────────────────────────────────────────────────────────────────────
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
@@ -94,7 +94,7 @@ class CertificateTemplateController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGenerate(Request $request, Program $program, CertificateService $service): Response
|
public function testGenerate(Request $request, Program $program, CertificateService $service): \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
|
||||||
{
|
{
|
||||||
$template = $program->certificateTemplate;
|
$template = $program->certificateTemplate;
|
||||||
abort_if(! $template, 404);
|
abort_if(! $template, 404);
|
||||||
@@ -102,7 +102,11 @@ class CertificateTemplateController extends Controller
|
|||||||
$sampleName = $request->input('sample_name', 'NAMA PESERTA CONTOH');
|
$sampleName = $request->input('sample_name', 'NAMA PESERTA CONTOH');
|
||||||
$sampleNo = $request->input('sample_no', 'ECT/2025/0001');
|
$sampleNo = $request->input('sample_no', 'ECT/2025/0001');
|
||||||
|
|
||||||
|
try {
|
||||||
$imageData = $service->generatePreview($template, $sampleName, $sampleNo);
|
$imageData = $service->generatePreview($template, $sampleName, $sampleNo);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return response()->json(['error' => $e->getMessage()], 500);
|
||||||
|
}
|
||||||
|
|
||||||
return response($imageData, 200, [
|
return response($imageData, 200, [
|
||||||
'Content-Type' => 'image/jpeg',
|
'Content-Type' => 'image/jpeg',
|
||||||
|
|||||||
65
docker-compose.prod.yml
Normal file
65
docker-compose.prod.yml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
###############################################################################
|
||||||
|
# eCert MBIP — Docker Compose Production Overrides (Ubuntu Server)
|
||||||
|
#
|
||||||
|
# Penggunaan:
|
||||||
|
# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
|
||||||
|
#
|
||||||
|
# Perbezaan dari dev:
|
||||||
|
# • APP_ENV=production, APP_DEBUG=false
|
||||||
|
# • DB port TIDAK didedahkan ke host
|
||||||
|
# • Storage sijil/template disimpan dalam named volume (kekal semasa deploy)
|
||||||
|
# • Opcache validate_timestamps=0 (prestasi)
|
||||||
|
# • php-dev.ini tidak dimuat
|
||||||
|
###############################################################################
|
||||||
|
name: ecert
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
# ── PHP-FPM Application (production) ──────────────────────────────────────
|
||||||
|
app:
|
||||||
|
container_name: ecert_app
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
# Kod dari server (git pull)
|
||||||
|
- .:/var/www
|
||||||
|
# php.ini sahaja (tanpa php-dev.ini)
|
||||||
|
- ./docker/php/php.ini:/usr/local/etc/php/conf.d/99-ecert.ini:ro
|
||||||
|
# Storage kekal semasa redeploy
|
||||||
|
- storage_data:/var/www/storage
|
||||||
|
environment:
|
||||||
|
APP_ENV: production
|
||||||
|
APP_DEBUG: "false"
|
||||||
|
|
||||||
|
# ── Nginx (production) ─────────────────────────────────────────────────────
|
||||||
|
nginx:
|
||||||
|
container_name: ecert_nginx
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- .:/var/www:ro
|
||||||
|
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
# Mount storage supaya nginx boleh serve fail statik jika perlu
|
||||||
|
- storage_data:/var/www/storage:ro
|
||||||
|
|
||||||
|
# ── MySQL (production) ─────────────────────────────────────────────────────
|
||||||
|
db:
|
||||||
|
container_name: ecert_db
|
||||||
|
restart: always
|
||||||
|
ports: [] # Jangan dedahkan DB port ke luar dalam production
|
||||||
|
volumes:
|
||||||
|
- dbdata:/var/lib/mysql
|
||||||
|
|
||||||
|
# ── Queue Worker (production) ──────────────────────────────────────────────
|
||||||
|
queue:
|
||||||
|
container_name: ecert_queue
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- .:/var/www
|
||||||
|
- ./docker/php/php.ini:/usr/local/etc/php/conf.d/99-ecert.ini:ro
|
||||||
|
- storage_data:/var/www/storage
|
||||||
|
environment:
|
||||||
|
APP_ENV: production
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
volumes:
|
||||||
|
storage_data:
|
||||||
|
driver: local
|
||||||
123
docker-compose.yml
Normal file
123
docker-compose.yml
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
###############################################################################
|
||||||
|
# eCert MBIP — Docker Compose (Development — Windows 11 / Linux)
|
||||||
|
#
|
||||||
|
# Penggunaan:
|
||||||
|
# docker compose up -d --build
|
||||||
|
#
|
||||||
|
# Aplikasi: http://localhost:8003
|
||||||
|
# DB (host): localhost:33060 (untuk TablePlus / HeidiSQL)
|
||||||
|
###############################################################################
|
||||||
|
name: ecert
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
# ── PHP-FPM Application ────────────────────────────────────────────────────
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/php/Dockerfile
|
||||||
|
container_name: ecert_app
|
||||||
|
restart: unless-stopped
|
||||||
|
working_dir: /var/www
|
||||||
|
volumes:
|
||||||
|
- .:/var/www
|
||||||
|
- ./docker/php/php.ini:/usr/local/etc/php/conf.d/99-ecert.ini:ro
|
||||||
|
- ./docker/php/php-dev.ini:/usr/local/etc/php/conf.d/99-ecert-dev.ini:ro
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
APP_ENV: local
|
||||||
|
APP_DEBUG: "true"
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- ecert
|
||||||
|
|
||||||
|
# ── Nginx Web Server ───────────────────────────────────────────────────────
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.27-alpine
|
||||||
|
container_name: ecert_nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8003:80"
|
||||||
|
volumes:
|
||||||
|
- .:/var/www:ro
|
||||||
|
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
depends_on:
|
||||||
|
- app
|
||||||
|
networks:
|
||||||
|
- ecert
|
||||||
|
|
||||||
|
# ── MySQL 8.0 ──────────────────────────────────────────────────────────────
|
||||||
|
db:
|
||||||
|
image: mysql:8.0
|
||||||
|
container_name: ecert_db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MYSQL_DATABASE: ${DB_DATABASE:-ecert_mbip}
|
||||||
|
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-secret}
|
||||||
|
MYSQL_USER: ${DB_USERNAME:-ecert}
|
||||||
|
MYSQL_PASSWORD: ${DB_PASSWORD:-secret}
|
||||||
|
volumes:
|
||||||
|
- dbdata:/var/lib/mysql
|
||||||
|
ports:
|
||||||
|
- "33060:3306" # port host 33060 → elak konflik dengan MySQL tempatan (3306)
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- mysqladmin
|
||||||
|
- ping
|
||||||
|
- -h
|
||||||
|
- localhost
|
||||||
|
- -u
|
||||||
|
- root
|
||||||
|
- --password=${DB_PASSWORD:-secret}
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 15
|
||||||
|
start_period: 20s
|
||||||
|
networks:
|
||||||
|
- ecert
|
||||||
|
|
||||||
|
# ── Queue Worker ───────────────────────────────────────────────────────────
|
||||||
|
queue:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/php/Dockerfile
|
||||||
|
container_name: ecert_queue
|
||||||
|
restart: unless-stopped
|
||||||
|
working_dir: /var/www
|
||||||
|
volumes:
|
||||||
|
- .:/var/www
|
||||||
|
- ./docker/php/php.ini:/usr/local/etc/php/conf.d/99-ecert.ini:ro
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
APP_ENV: local
|
||||||
|
# Override entrypoint: langkau migrate/seed (app container dah buat)
|
||||||
|
entrypoint: []
|
||||||
|
command:
|
||||||
|
- php
|
||||||
|
- artisan
|
||||||
|
- queue:work
|
||||||
|
- --sleep=3
|
||||||
|
- --tries=3
|
||||||
|
- --max-time=3600
|
||||||
|
- --timeout=90
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
app:
|
||||||
|
condition: service_started
|
||||||
|
networks:
|
||||||
|
- ecert
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
networks:
|
||||||
|
ecert:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dbdata:
|
||||||
|
driver: local
|
||||||
82
docker/entrypoint.sh
Normal file
82
docker/entrypoint.sh
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# eCert MBIP — Container Entrypoint
|
||||||
|
# Jalankan sebelum php-fpm bermula:
|
||||||
|
# 1. Tunggu MySQL
|
||||||
|
# 2. Install Composer deps (dev sahaja)
|
||||||
|
# 3. Generate APP_KEY jika tiada
|
||||||
|
# 4. Migrate + seed AdminSeeder
|
||||||
|
# 5. Storage link
|
||||||
|
# 6. Cache (prod sahaja)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "╔══════════════════════════════════════╗"
|
||||||
|
echo "║ eCert MBIP — Container Start ║"
|
||||||
|
echo "╚══════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── 1. Tunggu MySQL bersedia ──────────────────────────────────────────────────
|
||||||
|
DB_HOST="${DB_HOST:-db}"
|
||||||
|
DB_PORT="${DB_PORT:-3306}"
|
||||||
|
DB_DATABASE="${DB_DATABASE:-ecert_mbip}"
|
||||||
|
DB_USERNAME="${DB_USERNAME:-root}"
|
||||||
|
DB_PASSWORD="${DB_PASSWORD:-secret}"
|
||||||
|
|
||||||
|
echo "⏳ Menunggu MySQL di ${DB_HOST}:${DB_PORT}..."
|
||||||
|
|
||||||
|
until mysqladmin ping \
|
||||||
|
-h "${DB_HOST}" \
|
||||||
|
-P "${DB_PORT}" \
|
||||||
|
-u "${DB_USERNAME}" \
|
||||||
|
--password="${DB_PASSWORD}" \
|
||||||
|
--silent 2>/dev/null; do
|
||||||
|
printf "."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo "✓ MySQL bersedia."
|
||||||
|
|
||||||
|
# ── 2. Pasang Composer dependencies (development sahaja) ─────────────────────
|
||||||
|
if [ "${APP_ENV}" != "production" ] && [ ! -d /var/www/vendor ]; then
|
||||||
|
echo "📦 Memasang Composer dependencies (dev)..."
|
||||||
|
composer install \
|
||||||
|
--no-interaction \
|
||||||
|
--no-progress \
|
||||||
|
--prefer-dist
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 3. Generate APP_KEY jika kosong ───────────────────────────────────────────
|
||||||
|
if [ -z "${APP_KEY}" ] || [ "${APP_KEY}" = "" ]; then
|
||||||
|
echo "🔑 Menjana APP_KEY..."
|
||||||
|
php artisan key:generate --force
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 4. Database migration ─────────────────────────────────────────────────────
|
||||||
|
echo "🗄 Menjalankan migration..."
|
||||||
|
php artisan migrate --force
|
||||||
|
|
||||||
|
# Seed admin account (idempotent — guna firstOrCreate)
|
||||||
|
echo "👤 Seeding AdminSeeder..."
|
||||||
|
php artisan db:seed --class=AdminSeeder --force
|
||||||
|
|
||||||
|
# ── 5. Storage symbolic link ──────────────────────────────────────────────────
|
||||||
|
php artisan storage:link 2>/dev/null || true
|
||||||
|
|
||||||
|
# ── 6. Cache (production sahaja) ──────────────────────────────────────────────
|
||||||
|
if [ "${APP_ENV}" = "production" ]; then
|
||||||
|
echo "⚡ Caching config, routes, views..."
|
||||||
|
php artisan config:cache
|
||||||
|
php artisan route:cache
|
||||||
|
php artisan view:cache
|
||||||
|
php artisan event:cache
|
||||||
|
# Opcache: matikan validate_timestamps untuk prestasi
|
||||||
|
# (sudah dikonfigur dalam php.ini prod)
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Aplikasi bersedia."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
78
docker/nginx/default.conf
Normal file
78
docker/nginx/default.conf
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# eCert MBIP — Nginx Server Block
|
||||||
|
# Document root: /var/www/public | PHP-FPM upstream: app:9000
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_comp_level 5;
|
||||||
|
gzip_min_length 256;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_types
|
||||||
|
text/plain text/css text/javascript application/javascript
|
||||||
|
application/json application/xml image/svg+xml font/woff2;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /var/www/public;
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
# Max upload (kena sama dengan php.ini: post_max_size)
|
||||||
|
client_max_body_size 25M;
|
||||||
|
|
||||||
|
charset utf-8;
|
||||||
|
|
||||||
|
# ── Security headers ──────────────────────────────────────────────────────
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
# ── Laravel routes ────────────────────────────────────────────────────────
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── PHP-FPM ───────────────────────────────────────────────────────────────
|
||||||
|
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;
|
||||||
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||||
|
|
||||||
|
fastcgi_read_timeout 120s;
|
||||||
|
fastcgi_connect_timeout 10s;
|
||||||
|
fastcgi_buffer_size 16k;
|
||||||
|
fastcgi_buffers 8 16k;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Static assets — cache 1 tahun ─────────────────────────────────────────
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|ico|svg|css|js|woff2?|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
access_log off;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Halang akses fail tersembunyi ─────────────────────────────────────────
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Halang akses terus ke fail sensitif ───────────────────────────────────
|
||||||
|
location ~* \.(env|log|htaccess|htpasswd|ini|sh|sql|bak)$ {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Logging ───────────────────────────────────────────────────────────────
|
||||||
|
access_log /var/log/nginx/ecert-access.log;
|
||||||
|
error_log /var/log/nginx/ecert-error.log warn;
|
||||||
|
}
|
||||||
70
docker/php/Dockerfile
Normal file
70
docker/php/Dockerfile
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
###############################################################################
|
||||||
|
# eCert MBIP — PHP-FPM Runtime Image
|
||||||
|
# PHP 8.4 + GD + imagick + semua extension Laravel
|
||||||
|
###############################################################################
|
||||||
|
FROM php:8.4-fpm
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="eCert MBIP" \
|
||||||
|
org.opencontainers.image.description="Sistem Pengurusan Sijil Digital MBIP"
|
||||||
|
|
||||||
|
# ── System libraries ──────────────────────────────────────────────────────────
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
zip \
|
||||||
|
unzip \
|
||||||
|
# zip extension
|
||||||
|
libzip-dev \
|
||||||
|
# GD: PNG, JPEG, WebP, FreeType
|
||||||
|
libpng-dev \
|
||||||
|
libjpeg62-turbo-dev \
|
||||||
|
libfreetype6-dev \
|
||||||
|
libwebp-dev \
|
||||||
|
# mbstring
|
||||||
|
libonig-dev \
|
||||||
|
# xml / intl
|
||||||
|
libxml2-dev \
|
||||||
|
libicu-dev \
|
||||||
|
# imagick
|
||||||
|
libmagickwand-dev \
|
||||||
|
# mysql client (wait-for-db in entrypoint)
|
||||||
|
default-mysql-client \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# ── GD (untuk Intervention Image — GD driver) ─────────────────────────────────
|
||||||
|
RUN docker-php-ext-configure gd \
|
||||||
|
--with-freetype \
|
||||||
|
--with-jpeg \
|
||||||
|
--with-webp \
|
||||||
|
&& docker-php-ext-install -j$(nproc) gd
|
||||||
|
|
||||||
|
# ── Core PHP extensions ───────────────────────────────────────────────────────
|
||||||
|
RUN docker-php-ext-install -j$(nproc) \
|
||||||
|
pdo_mysql \
|
||||||
|
mbstring \
|
||||||
|
exif \
|
||||||
|
pcntl \
|
||||||
|
bcmath \
|
||||||
|
zip \
|
||||||
|
intl \
|
||||||
|
opcache
|
||||||
|
|
||||||
|
# ── imagick (PECL) — tersedia sebagai driver alternatif ───────────────────────
|
||||||
|
RUN pecl install imagick \
|
||||||
|
&& docker-php-ext-enable imagick \
|
||||||
|
&& rm -rf /tmp/pear
|
||||||
|
|
||||||
|
# ── Composer ──────────────────────────────────────────────────────────────────
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
# ── Working directory ─────────────────────────────────────────────────────────
|
||||||
|
WORKDIR /var/www
|
||||||
|
|
||||||
|
# ── Entrypoint ────────────────────────────────────────────────────────────────
|
||||||
|
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
|
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 9000
|
||||||
|
|
||||||
|
ENTRYPOINT ["entrypoint.sh"]
|
||||||
|
CMD ["php-fpm"]
|
||||||
13
docker/php/php-dev.ini
Normal file
13
docker/php/php-dev.ini
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
; ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
; Development overrides — dilengkap di atas php.ini
|
||||||
|
; Hanya dimuat dalam docker-compose.yml (dev), tidak dalam prod
|
||||||
|
; ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
; Reload fail PHP tanpa restart container
|
||||||
|
opcache.validate_timestamps = 1
|
||||||
|
opcache.revalidate_freq = 0
|
||||||
|
|
||||||
|
; Tunjuk ralat dalam browser semasa dev
|
||||||
|
display_errors = On
|
||||||
|
display_startup_errors = On
|
||||||
|
error_reporting = E_ALL
|
||||||
44
docker/php/php.ini
Normal file
44
docker/php/php.ini
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
; ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
; eCert MBIP — PHP Runtime Configuration
|
||||||
|
; Letakkan dalam /usr/local/etc/php/conf.d/99-ecert.ini
|
||||||
|
; ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
; Timezone Malaysia
|
||||||
|
date.timezone = Asia/Kuala_Lumpur
|
||||||
|
|
||||||
|
; ── Upload (template sijil hingga 10MB + buffer) ──────────────────────────────
|
||||||
|
upload_max_filesize = 20M
|
||||||
|
post_max_size = 25M
|
||||||
|
max_file_uploads = 10
|
||||||
|
|
||||||
|
; ── Execution (image generation + queue workers) ──────────────────────────────
|
||||||
|
max_execution_time = 120
|
||||||
|
max_input_time = 120
|
||||||
|
|
||||||
|
; ── Memory (Intervention Image untuk sijil resolusi tinggi) ───────────────────
|
||||||
|
memory_limit = 512M
|
||||||
|
|
||||||
|
; ── Session ───────────────────────────────────────────────────────────────────
|
||||||
|
session.cookie_httponly = 1
|
||||||
|
session.cookie_samesite = Lax
|
||||||
|
session.gc_maxlifetime = 7200
|
||||||
|
|
||||||
|
; ── OPcache ───────────────────────────────────────────────────────────────────
|
||||||
|
opcache.enable = 1
|
||||||
|
opcache.memory_consumption = 192
|
||||||
|
opcache.interned_strings_buffer = 16
|
||||||
|
opcache.max_accelerated_files = 10000
|
||||||
|
; validate_timestamps: 1 untuk dev (auto-reload), 0 untuk prod (prestasi)
|
||||||
|
opcache.validate_timestamps = 1
|
||||||
|
opcache.revalidate_freq = 2
|
||||||
|
opcache.fast_shutdown = 1
|
||||||
|
|
||||||
|
; ── Error (override per environment via APP_ENV) ──────────────────────────────
|
||||||
|
display_errors = Off
|
||||||
|
display_startup_errors = Off
|
||||||
|
log_errors = On
|
||||||
|
error_log = /var/log/php_errors.log
|
||||||
|
|
||||||
|
; ── imagick ───────────────────────────────────────────────────────────────────
|
||||||
|
[imagick]
|
||||||
|
imagick.skip_version_check = 1
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
value="NAMA PESERTA CONTOH" placeholder="Nama untuk pratonton">
|
value="NAMA PESERTA CONTOH" placeholder="Nama untuk pratonton">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<button type="button" class="btn btn-sm btn-primary w-100" onclick="loadPreview()">
|
<button type="button" id="previewBtn" class="btn btn-sm btn-primary w-100" onclick="loadPreview()">
|
||||||
<i class="bi bi-eye me-1"></i> Pratonton
|
<i class="bi bi-eye me-1"></i> Pratonton
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -247,18 +247,34 @@ function toggleCertNo(cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadPreview() {
|
function loadPreview() {
|
||||||
const name = document.getElementById('sampleName').value || 'NAMA PESERTA CONTOH';
|
const name = document.getElementById('sampleName').value.trim() || 'NAMA PESERTA CONTOH';
|
||||||
const img = document.getElementById('templatePreview');
|
const img = document.getElementById('templatePreview');
|
||||||
|
const btn = document.getElementById('previewBtn');
|
||||||
const url = "{{ route('admin.programs.template.test', $program) }}";
|
const url = "{{ route('admin.programs.template.test', $program) }}";
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status"></span> Memuatkan...';
|
||||||
|
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append('_token', '{{ csrf_token() }}');
|
form.append('_token', '{{ csrf_token() }}');
|
||||||
form.append('sample_name', name);
|
form.append('sample_name', name);
|
||||||
|
|
||||||
fetch(url, { method: 'POST', body: form })
|
fetch(url, { method: 'POST', body: form })
|
||||||
.then(r => r.blob())
|
.then(r => {
|
||||||
|
if (!r.ok) return r.json().then(j => { throw new Error(j.error || 'Ralat pelayan (' + r.status + ')'); });
|
||||||
|
return r.blob();
|
||||||
|
})
|
||||||
.then(blob => {
|
.then(blob => {
|
||||||
|
const prevSrc = img.src;
|
||||||
img.src = URL.createObjectURL(blob);
|
img.src = URL.createObjectURL(blob);
|
||||||
|
if (prevSrc.startsWith('blob:')) URL.revokeObjectURL(prevSrc);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
alert('Gagal jana pratonton: ' + err.message);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="bi bi-eye me-1"></i> Pratonton';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user