Kita bahas bebrapa hal penting dan jangan dilewatkan setelah mengerjakan projek freelancing menggunakan framework laravel 12 sehingga hasilnya bisa lebih baik dan klien makin happy dengan reputasi yang kita berikan.
Bagian 1: Kenapa Post-Development Penting untuk Freelancer Laravel
Gue sering banget nemuin freelancer yang mentalnya kayak gini: "Code udah jalan di local, tinggal deploy ke server, selesai dehβinvoice kirim!"
Terus pas udah sebulan, client komplain:
- "Mas, websitenya kok sering down ya?"
- "Kenapa checkout payment suka gagal?"
- "Kok server saya kena hack?"
Familiar nggak? π
Nah, ini masalahnya: kebanyakan freelancer nganggep development itu selesai pas code udah jalan. Padahal, ada gap besar antara "code yang jalan di laptop" dengan "aplikasi production-ready yang bisa dipakai ribuan user tanpa masalah".
Horror Story: Belajar dari Kegagalan
Gue pernah ngalamin sendiri. Tahun pertama jadi freelancer, gue bikin e-commerce untuk client property management. Budget lumayan, timelinenya OK, code review dari client juga satisfied. Deployment? Ya tinggal FTP upload, setup database, jalan deh.
Minggu pertama launching: server down 3 kali.
Why? Queue worker nggak jalan, jadi email confirmasi numpuk sampai memory habis. Payment webhook dari Midtrans? Kadang masuk, kadang nggakβkarena nggak ada proper logging. SSL? Pakai HTTP aja dulu lah, "nanti diperbaiki".
Result: Client kehilangan trust, 2 bulan kemudian mereka hire developer lain buat "fix" aplikasi gue. Lesson learned the hard way.
Apa Itu Post-Development?
Post-development adalah semua aktivitas yang dilakukan SETELAH code selesai dibuat, tapi SEBELUM aplikasi beneran production-ready. Ini bukan "nice to have"βini wajib hukumnya kalau lo mau dianggap sebagai professional developer.
Analogi gampangnya: lo bangun rumah, pondasi udah kokoh, dinding udah berdiri, atap udah terpasang. Tapi apakah rumah itu siap ditempatin? Belum! Lo masih perlu:
- Instalasi listrik yang aman (security)
- Sistem air bersih yang reliable (infrastructure)
- Pagar dan kunci pintu (firewall & access control)
- Asuransi rumah (backup & monitoring)
Code yang udah "jalan" itu kayak rumah yang udah berdiri. Tapi production-ready? Itu rumah yang bisa lo tinggalin tanpa khawatir bocor, kebakaran, atau kemasukan maling.
7 Hal yang Akan Kita Bahas
Di artikel ini, gue bakal breakdown 7 hal critical yang HARUS lo lakuin setelah selesai coding. Ini bukan teori doangβini step-by-step guide dengan code yang bisa langsung lo copy-paste.
POST-DEVELOPMENT WORKFLOW:
Code Complete
β
βΌ
ββββββββββββββββββββββββ
β 1. Docker Deploy β β Containerize app, konsisten di semua env
ββββββββββββ¬ββββββββββββ
βΌ
ββββββββββββββββββββββββ
β 2. Nginx + SSL β β Reverse proxy, HTTPS, caching
ββββββββββββ¬ββββββββββββ
βΌ
ββββββββββββββββββββββββ
β 3. Cloudflare WAF β β DDoS protection, CDN, security rules
ββββββββββββ¬ββββββββββββ
βΌ
ββββββββββββββββββββββββ
β 4. Queue Setup β β Background jobs yang reliable
ββββββββββββ¬ββββββββββββ
βΌ
ββββββββββββββββββββββββ
β 5. Webhook Test β β Payment integration yang aman
ββββββββββββ¬ββββββββββββ
βΌ
ββββββββββββββββββββββββ
β 6. Stress Test β β Pastikan bisa handle traffic
ββββββββββββ¬ββββββββββββ
βΌ
ββββββββββββββββββββββββ
β 7. Security Hardeningβ β Fail2ban, firewall, SSH hardening
ββββββββββββ¬ββββββββββββ
βΌ
Production Ready β
Mari kita breakdown satu per satu.
1. Deploy dengan Docker
Kenapa Docker? Karena "works on my machine" bukan excuse yang profesional. Docker ngasih lo environment yang konsisten, entah itu di laptop, staging, atau production server. No more "kok di server error, padahal di local jalan?"
2. Nginx Reverse Proxy + SSL
User nggak peduli code lo seberapa bagus kalau akses website mereka nggak secure (HTTP aja). SSL certificate (HTTPS) itu bukan opsionalβini mandatory di 2024. Bonus: Google ranking lo naik kalau pakai HTTPS.
3. Cloudflare WAF (Web Application Firewall)
Begitu website lo online, dalam hitungan jam bakal mulai ada bot yang nyoba exploit. SQL injection, XSS, brute force loginβsemua serangan ini bisa lo block pakai Cloudflare WAF tanpa perlu ngutak-atik code.
4. Queue Worker dengan Supervisor
Email, notification push, image processingβsemua ini nggak boleh dilakukan synchronous di request cycle. Queue worker ngejamin background jobs lo jalan reliable, dan kalau crash bisa auto-restart.
5. Webhook Payment yang Aman
Midtrans, Xendit, Stripeβsemua payment gateway kirim webhook (server-to-server notification) ke aplikasi lo. Kalau lo nggak handle ini dengan bener (signature verification, idempotency check), bisa-bisa ada user bayar tapi ordernya nggak masuk, atau worse: ada orang yang manipulasi webhook palsu.
6. Stress Testing dengan K6
Lo ngga tau aplikasi lo bisa handle berapa user sampai lo test. 100 concurrent users? 500? 1000? K6 ngasih lo gambaran jelas: di load berapa aplikasi lo mulai lambat atau crash. Fix-nya sebelum user real yang ngerasain.
7. Server Security (Fail2ban, UFW, SSH Hardening)
Server production = target empuk buat hacker. Kalau lo nggak setup firewall, disable root login, atau install fail2ban, dalam seminggu pasti ada yang nyoba brute force SSH lo. Gue ngga lebayβcek log lo sendiri nanti.
Kenapa Ini Penting untuk Freelancer?
Trust is everything dalam dunia freelance. Client yang puas bukan cuma karena code-nya bagus, tapi karena mereka tenang. Mereka nggak perlu mikir "wah websiteku down lagi nggak ya?" atau "wah kena hack nggak ya?".
Benefit langsung buat lo:
- β Client satisfaction tinggi β repeat order, referral
- β Bisa charge lebih mahal β karena lo deliver production-ready, bukan sekedar "jalan"
- β Portfolio yang kuat β "All my projects are deployed with Docker, SSL, monitoring, and tested under load"
- β Less stress β nggak ada midnight call "Mas website saya down!"
Post-Development Checklist (Printable)
Sebelum lo declare projek "selesai", pastikan checklist ini semua tercentang:
POST-DEVELOPMENT CHECKLIST:
DEPLOYMENT:
β‘ Code di-containerize dengan Docker
β‘ Docker Compose configuration tested
β‘ Environment variables secured (.env not in repo)
β‘ Database migration berhasil di production
β‘ Static assets served properly
SECURITY:
β‘ SSL certificate installed (HTTPS)
β‘ Cloudflare WAF aktif
β‘ Firewall (UFW) configured
β‘ Fail2ban installed dan running
β‘ SSH hardened (no root, key-based auth)
β‘ Sensitive routes protected (admin, API)
RELIABILITY:
β‘ Queue worker running with Supervisor/Docker
β‘ Failed job handling configured
β‘ Database backup automated
β‘ Storage/file backup automated
β‘ Error logging configured (Sentry, Bugsnag, atau custom)
PERFORMANCE:
β‘ Stress test dilakukan (K6 atau alternative)
β‘ Response time < 500ms untuk 95% request
β‘ Static caching enabled (Nginx, Cloudflare)
β‘ Database queries optimized (no N+1)
β‘ Opcode caching enabled (OPcache)
INTEGRATIONS:
β‘ Payment webhook tested (all scenarios)
β‘ Email delivery verified (SMTP, transactional email)
β‘ Third-party API rate limits diperhatikan
β‘ OAuth/SSO tested (jika ada)
MONITORING:
β‘ Uptime monitoring setup (UptimeRobot, Pingdom)
β‘ Application monitoring (Laravel Telescope/Horizon optional)
β‘ Server monitoring (disk space, memory, CPU)
β‘ Log rotation configured
HANDOVER:
β‘ Documentation lengkap (README, deployment guide)
β‘ Credentials documented dan di-share securely
β‘ Client training dilakukan (jika perlu)
β‘ Warranty period dikomunikasikan
β‘ Follow-up schedule agreed
Print checklist ini, tempel di meja kerja lo. Every. Single. Project.
Mindset Shift: From "Code Works" to "Production Ready"
Yang perlu lo pahami: coding itu cuma 50% dari job lo sebagai web developer. 50% sisanya adalah memastikan code itu bisa jalan di dunia nyataβdengan user real, traffic real, dan serangan real.
Developer junior mikir: "Pokoknya jalan dulu, nanti optimasi." Developer professional mikir: "Deploy dulu dengan proper infrastructure, baru iterasi feature."
Bedanya tipis, tapi impact-nya huge.
Di bagian-bagian selanjutnya, kita akan breakdown satu per satuβdari deploy Docker sampai handover dokumentasi. Semua dengan code yang bisa langsung lo pakai.
Ready? Let's dive in.
Bagian 2: Deploy ke VPS dengan Docker - Infrastructure as Code
Oke, code udah kelar, test lokal udah pass, sekarang waktunya deploy ke server production. Pertanyaan pertama yang muncul: kenapa harus Docker? Kenapa nggak upload manual via FTP atau deploy dengan Shared Hosting?
Jawaban singkatnya: Docker = consistency + scalability + reproducibility.
Kenapa Docker untuk Production?
Masalah deploy tradisional:
DEVELOPER: "Di laptop gue jalan kok!"
CLIENT: "Tapi di server gue error..."
PENYEBABNYA:
βββ PHP version beda (local 8.3, server 8.1)
βββ Extension nggak lengkap (GD, Imagick missing)
βββ Permission issues (storage folder nggak bisa write)
βββ Database version beda (MySQL 8 vs MySQL 5.7)
βββ ENV configuration salah (.env production beda)
Solusi dengan Docker:
Docker ngasih lo containerβsebuah isolated environment yang bawa semua dependencies. PHP version, extensions, libraries, semuanya ter-bundle. Kalau jalan di laptop lo, dijamin jalan di server manapun yang support Docker.
Benefits konkrit:
- β No more "works on my machine" syndrome
- β Easy rollback kalau deployment gagal (tinggal switch image version)
- β Horizontal scaling jadi gampang (tinggal spin up container baru)
- β Development = Production environment (parity)
- β Isolated services (app, database, redis masing-masing container)
Minimum VPS Specs untuk Laravel Production
Sebelum mulai, lo perlu VPS dengan specs minimum:
STARTER (untuk traffic kecil-menengah):
βββ RAM: 2GB
βββ CPU: 1 vCPU (2 cores lebih baik)
βββ Storage: 25GB SSD
βββ Bandwidth: 1-2TB/month
βββ OS: Ubuntu 22.04 LTS atau 24.04 LTS
RECOMMENDED (untuk traffic menengah):
βββ RAM: 4GB
βββ CPU: 2 vCPU
βββ Storage: 50GB SSD
βββ Bandwidth: 2-3TB/month
βββ OS: Ubuntu 24.04 LTS
Provider yang gue recommend:
- DigitalOcean - UI paling user-friendly, dokumentasi lengkap
- Vultr - Harga kompetitif, performance solid
- Linode (Akamai) - Support bagus, uptime tinggi
- IDCloudHost - Provider Indonesia, payment pakai Rupiah
Harga VPS specs starter (2GB RAM) biasanya sekitar $12-15/month atau ~Rp 180-225rb/bulan. Jangan pakai shared hosting untuk Laravel productionβtrust me, lo bakal nyesel.
Setup Awal VPS
Setelah VPS ready, lo perlu install Docker dan Docker Compose:
# Update system
sudo apt update && sudo apt upgrade -y
# Install dependencies
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
# Add Docker GPG key
curl -fsSL <https://download.docker.com/linux/ubuntu/gpg> | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Add Docker repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] <https://download.docker.com/linux/ubuntu> $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io
# Install Docker Compose
sudo curl -L "<https://github.com/docker/compose/releases/latest/download/docker-compose-$>(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# Add user to docker group (biar nggak perlu sudo)
sudo usermod -aG docker $USER
newgrp docker
# Verify installation
docker --version
docker-compose --version
Output yang diharapkan:
Docker version 26.0.0, build 1234567
Docker Compose version v2.24.5
Dockerfile untuk Laravel 12
Sekarang kita buat Dockerfile untuk Laravel app. File ini define gimana container lo di-build.
File: Dockerfile
# Base image - PHP 8.3 dengan FPM (FastCGI Process Manager)
FROM php:8.3-fpm-alpine
# Metadata
LABEL maintainer="[email protected]"
LABEL version="1.0"
LABEL description="Laravel 12 Production Container"
# Install system dependencies
RUN apk add --no-cache \\
nginx \\
supervisor \\
libpng-dev \\
libzip-dev \\
libjpeg-turbo-dev \\
freetype-dev \\
zip \\
unzip \\
git \\
curl \\
oniguruma-dev
# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \\
&& docker-php-ext-install \\
pdo_mysql \\
bcmath \\
gd \\
zip \\
opcache \\
exif \\
pcntl
# Install Redis extension
RUN pecl install redis \\
&& docker-php-ext-enable redis
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
# Copy composer files first (untuk caching layer)
COPY composer.json composer.lock ./
# Install PHP dependencies
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
# Copy application code
COPY . .
# Generate optimized autoloader
RUN composer dump-autoload --optimize
# Set permissions
RUN chown -R www-data:www-data \\
/var/www/html/storage \\
/var/www/html/bootstrap/cache
# PHP-FPM configuration optimizations
RUN echo "opcache.enable=1" >> /usr/local/etc/php/conf.d/opcache.ini \\
&& echo "opcache.memory_consumption=256" >> /usr/local/etc/php/conf.d/opcache.ini \\
&& echo "opcache.interned_strings_buffer=16" >> /usr/local/etc/php/conf.d/opcache.ini \\
&& echo "opcache.max_accelerated_files=10000" >> /usr/local/etc/php/conf.d/opcache.ini \\
&& echo "opcache.validate_timestamps=0" >> /usr/local/etc/php/conf.d/opcache.ini
# Expose port 9000 untuk PHP-FPM
EXPOSE 9000
# Start PHP-FPM
CMD ["php-fpm"]
Penjelasan penting:
- Alpine Linux - Base image yang ringan (~5MB), cocok untuk production
- PHP Extensions - Install semua extension yang Laravel butuhin (PDO, bcmath, GD untuk image processing, dll)
- OPcache - Caching bytecode PHP, bikin aplikasi lo 3-5x lebih cepat
- Multi-stage caching - Copy composer.json dulu sebelum copy app code, jadi kalau code berubah, dependency nggak perlu re-download
Docker Compose Configuration
Docker Compose ngatur multiple services (app, database, redis, nginx) dalam satu orchestration. Ini file yang bakal lo pakai paling sering.
File: docker-compose.yml
version: '3.8'
services:
# Laravel Application
app:
build:
context: .
dockerfile: Dockerfile
container_name: laravel_app
restart: unless-stopped
working_dir: /var/www/html
volumes:
- ./:/var/www/html
- ./storage:/var/www/html/storage
environment:
- APP_ENV=${APP_ENV}
- APP_DEBUG=${APP_DEBUG}
- DB_HOST=mysql
- REDIS_HOST=redis
networks:
- laravel_network
depends_on:
- mysql
- redis
# MySQL Database
mysql:
image: mysql:8.0
container_name: laravel_mysql
restart: unless-stopped
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
networks:
- laravel_network
command: --default-authentication-plugin=mysql_native_password --max_connections=200
# Redis Cache & Queue
redis:
image: redis:alpine
container_name: laravel_redis
restart: unless-stopped
volumes:
- redis_data:/data
networks:
- laravel_network
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
# Nginx Web Server
nginx:
image: nginx:alpine
container_name: laravel_nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./:/var/www/html
- ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
- ./docker/nginx/ssl:/etc/nginx/ssl
networks:
- laravel_network
depends_on:
- app
networks:
laravel_network:
driver: bridge
volumes:
mysql_data:
driver: local
redis_data:
driver: local
Breakdown:
- app service - Laravel application container (PHP-FPM)
- mysql service - Database dengan persistent volume (data nggak ilang pas restart)
- redis service - Cache & queue backend, dengan memory limit 256MB
- nginx service - Web server, proxy requests ke PHP-FPM
- networks - Semua service dalam satu network biar bisa communicate
- volumes - Data persistence untuk database & redis
Environment Configuration
Jangan lupa setup .env untuk production:
# Copy example
cp .env.example .env
# Edit dengan nano/vim
nano .env
.env Production (contoh):
APP_NAME="Client Project Name"
APP_ENV=production
APP_KEY=base64:GENERATED_KEY_HERE
APP_DEBUG=false
APP_URL=https://example.com
LOG_CHANNEL=stack
LOG_LEVEL=error
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel_prod
DB_USERNAME=laravel_user
DB_PASSWORD=secure_password_here
BROADCAST_DRIVER=log
CACHE_DRIVER=redis
FILESYSTEM_DISK=local
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
SESSION_LIFETIME=120
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=your_username
MAIL_PASSWORD=your_password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="[email protected]"
MAIL_FROM_NAME="${APP_NAME}"
β οΈ Security Notes:
- Jangan commit
.envke git - Add.envke.gitignore - APP_DEBUG=false di production (never show errors ke user)
- Generate APP_KEY dengan
php artisan key:generate - Password DB harus strong (min 16 characters, alphanumeric + symbol)
Deploy ke VPS - Step by Step
Sekarang waktunya deploy ke server production:
1. Clone repository ke VPS:
# SSH ke VPS
ssh user@your-vps-ip
# Clone repo (via HTTPS atau SSH)
git clone <https://github.com/username/project-name.git>
cd project-name
# Atau, upload via rsync (dari local):
rsync -avz --exclude 'node_modules' --exclude '.git' \\
/path/to/local/project/ user@vps-ip:/var/www/project-name/
2. Setup environment:
# Copy .env
cp .env.example .env
# Edit .env dengan credentials production
nano .env
# Generate app key
docker-compose run --rm app php artisan key:generate
3. Build dan start containers:
# Build images
docker-compose build
# Start services in detached mode
docker-compose up -d
# Verify services running
docker-compose ps
Output yang diharapkan:
NAME COMMAND STATUS PORTS
laravel_app "docker-php-entrypoiβ¦" Up 10 seconds 9000/tcp
laravel_mysql "docker-entrypoint.sβ¦" Up 10 seconds 0.0.0.0:3306->3306/tcp
laravel_nginx "/docker-entrypoint.β¦" Up 10 seconds 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
laravel_redis "docker-entrypoint.sβ¦" Up 10 seconds 6379/tcp
4. Run migrations dan setup Laravel:
# Run database migrations
docker-compose exec app php artisan migrate --force
# Cache configurations
docker-compose exec app php artisan config:cache
docker-compose exec app php artisan route:cache
docker-compose exec app php artisan view:cache
# Link storage (kalau belum)
docker-compose exec app php artisan storage:link
# Optional: Seed data
docker-compose exec app php artisan db:seed
5. Set permissions:
# Fix storage permissions
docker-compose exec app chown -R www-data:www-data /var/www/html/storage
docker-compose exec app chown -R www-data:www-data /var/www/html/bootstrap/cache
6. Test aplikasi:
# Check logs
docker-compose logs -f app
# Test database connection
docker-compose exec app php artisan tinker
>>> DB::connection()->getPdo();
# Access via browser
curl <http://your-vps-ip>
Troubleshooting Common Issues
Issue 1: Port 80 already in use
# Check apa yang pakai port 80
sudo netstat -tlnp | grep :80
# Kalau ada Apache/Nginx native, stop dulu
sudo systemctl stop apache2
sudo systemctl stop nginx
Issue 2: Permission denied errors
# Fix storage permissions
docker-compose exec app chmod -R 775 storage
docker-compose exec app chmod -R 775 bootstrap/cache
Issue 3: Database connection refused
# Check MySQL container running
docker-compose ps mysql
# Check MySQL logs
docker-compose logs mysql
# Verify .env DB credentials match docker-compose.yml
Issue 4: Out of memory
# Check container memory usage
docker stats
# Increase VPS RAM atau optimize config
# Edit docker-compose.yml, add memory limits:
services:
app:
mem_limit: 512m
mysql:
mem_limit: 1g
Docker Commands Cheat Sheet
Commands yang sering lo pakai:
# Start containers
docker-compose up -d
# Stop containers
docker-compose down
# Restart specific service
docker-compose restart app
# View logs
docker-compose logs -f app
# Execute command in container
docker-compose exec app php artisan migrate
# Rebuild containers (after Dockerfile changes)
docker-compose up -d --build
# Remove all containers, networks, volumes
docker-compose down -v
# SSH into container
docker-compose exec app sh
# Check container resource usage
docker stats
# Pull latest images
docker-compose pull
Deployment Workflow Summary
LOCAL DEVELOPMENT:
βββ Code changes
βββ Git commit & push
βββ Ready to deploy
VPS DEPLOYMENT:
βββ 1. SSH ke VPS
βββ 2. Git pull latest changes
βββ 3. docker-compose build (if Dockerfile changed)
βββ 4. docker-compose up -d
βββ 5. docker-compose exec app php artisan migrate --force
βββ 6. docker-compose exec app php artisan config:cache
βββ 7. Test di browser
ROLLBACK (jika ada masalah):
βββ 1. Git revert/checkout previous commit
βββ 2. docker-compose down
βββ 3. docker-compose up -d
βββ 4. Restore database backup (jika perlu)
Next Steps
Deploy dengan Docker itu baru langkah pertama. Sekarang aplikasi lo udah jalan di VPS, tapi masih ada beberapa hal yang kurang:
- β Belum ada SSL (masih HTTP, nggak secure)
- β Belum ada reverse proxy optimization
- β Belum ada caching strategy
- β Belum ada protection layer (firewall, rate limiting)
Di bagian selanjutnya, kita akan setup Nginx sebagai reverse proxy dan install SSL certificate dengan Let's Encryptβbiar website lo bisa diakses via HTTPS dan dapat trust badge dari browser.
Stay tuned!
Bagian 3: Setup Nginx Reverse Proxy dan SSL - Secure Your App
Oke, aplikasi lo udah jalan di Docker. Sekarang kalau user akses http://your-vps-ip, mereka bisa lihat homepage Laravel lo. Tapi ada beberapa masalah:
- Akses via IP, bukan domain - Nggak profesional, susah diinget
- HTTP, bukan HTTPS - Browser nunjukin "Not Secure", user ngga percaya
- No caching - Setiap request hit PHP-FPM, padahal static files bisa di-cache
- No compression - Bandwidth boros, loading lambat
Solusinya: Nginx sebagai reverse proxy + SSL certificate dari Let's Encrypt.
Apa Itu Reverse Proxy?
Reverse proxy itu kayak "receptionist" di depan aplikasi lo:
USER REQUEST:
<https://example.com/products>
β
βΌ
ββββββββββββββββ
β NGINX β β Reverse Proxy
β (Port 80/443)β - Handle SSL termination
ββββββββ¬ββββββββ - Serve static files
β - Gzip compression
β - Rate limiting
βΌ
ββββββββββββββββ
β PHP-FPM β β Laravel App
β (Port 9000) β - Process dynamic requests only
ββββββββββββββββ
Benefits:
- β SSL offloading - Nginx handle HTTPS, PHP-FPM nggak perlu mikir encryption
- β Static file serving - CSS, JS, images langsung di-serve Nginx (faster)
- β Compression - Gzip/Brotli untuk reduce bandwidth
- β Caching - Static content di-cache, reduce load ke PHP
- β Security headers - X-Frame-Options, CSP, dll
Nginx Configuration untuk Laravel
Kita buat Nginx config yang optimized untuk Laravel production.
File: docker/nginx/nginx.conf
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/html/public;
index index.php index.html;
charset utf-8;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# Disable server tokens (hide Nginx version)
server_tokens off;
# Gzip Compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/rss+xml
application/atom+xml
image/svg+xml;
# Main location
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP-FPM Configuration
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;
# Security: Hide PHP version
fastcgi_hide_header X-Powered-By;
# Timeouts
fastcgi_read_timeout 300;
fastcgi_send_timeout 300;
}
# Deny access to hidden files
location ~ /\\.(?!well-known).* {
deny all;
access_log off;
log_not_found off;
}
# Deny access to sensitive files
location ~* (\\.env|\\.git|\\.gitignore|composer\\.json|composer\\.lock|package\\.json|package-lock\\.json|\\.htaccess) {
deny all;
return 404;
}
# Cache static assets
location ~* \\.(jpg|jpeg|png|gif|ico|svg|webp|css|js|woff|woff2|ttf|eot|otf)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
# Enable CORS for fonts (jika load dari CDN)
location ~* \\.(woff|woff2|ttf|eot|otf)$ {
add_header Access-Control-Allow-Origin "*";
}
}
# Cache HTML files
location ~* \\.html$ {
expires 1h;
add_header Cache-Control "public, must-revalidate";
}
# Disable cache for dynamic content
location ~* \\.(php|json)$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# Optimize buffer sizes
client_max_body_size 100M;
client_body_buffer_size 128k;
client_header_buffer_size 1k;
large_client_header_buffers 4 16k;
}
Penjelasan konfigurasi:
Security Headers:
X-Frame-Options: SAMEORIGIN- Prevent clickjacking attacksX-Content-Type-Options: nosniff- Prevent MIME type sniffingX-XSS-Protection- Enable browser XSS protection (legacy, tapi tetep bagus)Referrer-Policy- Control referrer informationPermissions-Policy- Control browser features (geolocation, camera, dll)
Gzip Compression:
- Compress text-based files (HTML, CSS, JS, JSON, SVG)
gzip_comp_level 6- Balance antara compression vs CPU usagegzip_min_length 1024- Only compress files > 1KB (small files nggak worth it)
Static File Caching:
- Images, fonts, CSS, JS: cache 1 year (
expires 1y) - HTML: cache 1 hour (
expires 1h) - PHP, JSON: no cache (always fresh data)
File Upload Limits:
client_max_body_size 100M- Allow upload sampai 100MB (adjust sesuai kebutuhan)
Point Domain ke VPS
Sebelum install SSL, lo perlu point domain ke VPS IP address:
Di DNS Management (Cloudflare, Namecheap, GoDaddy, dll):
DNS RECORDS:
Type Name Content TTL Proxy
βββββββββββββββββββββββββββββββββββββββββββββ
A @ 123.456.789.10 Auto π DNS Only
A www 123.456.789.10 Auto π DNS Only
β οΈ Important: Kalau pakai Cloudflare, set proxy ke DNS Only dulu. Setelah SSL installed, baru aktifkan proxy.
Verify DNS propagation:
# Check dari terminal
dig example.com +short
dig www.example.com +short
# Atau pakai online tool
# <https://dnschecker.org>
Tunggu sampai DNS propagation selesai (biasanya 5-30 menit, max 24 jam).
Install SSL Certificate dengan Let's Encrypt
Let's Encrypt ngasih SSL certificate gratis dan trusted by all browsers. Install via Certbot:
1. Install Certbot:
# Install Certbot
sudo apt install certbot python3-certbot-nginx -y
# Verify installation
certbot --version
2. Stop Nginx container sementara:
# Certbot perlu akses port 80 untuk verification
docker-compose stop nginx
3. Generate SSL certificate:
# Generate cert untuk domain dan www subdomain
sudo certbot certonly --standalone \\
-d example.com \\
-d www.example.com \\
--email [email protected] \\
--agree-tos \\
--no-eff-email
# Output:
# Congratulations! Your certificate and chain have been saved at:
# /etc/letsencrypt/live/example.com/fullchain.pem
# Your key file has been saved at:
# /etc/letsencrypt/live/example.com/privkey.pem
4. Copy SSL files ke Docker volume:
# Create SSL directory
mkdir -p docker/nginx/ssl
# Copy certificates
sudo cp /etc/letsencrypt/live/example.com/fullchain.pem docker/nginx/ssl/
sudo cp /etc/letsencrypt/live/example.com/privkey.pem docker/nginx/ssl/
# Set permissions
sudo chmod 644 docker/nginx/ssl/fullchain.pem
sudo chmod 600 docker/nginx/ssl/privkey.pem
Nginx Configuration dengan SSL
Sekarang update Nginx config untuk support HTTPS:
File: docker/nginx/nginx.conf (updated)
# HTTP Server - Redirect to HTTPS
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
# Redirect all HTTP requests to HTTPS
return 301 https://$server_name$request_uri;
}
# HTTPS Server
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
root /var/www/html/public;
index index.php index.html;
charset utf-8;
# SSL Configuration
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
# SSL Protocols and Ciphers (Modern configuration)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# SSL Session Cache (improve performance)
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;
# OCSP Stapling (faster SSL handshake)
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/fullchain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# HSTS (HTTP Strict Transport Security)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# Disable server tokens
server_tokens off;
# Gzip Compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/rss+xml
application/atom+xml
image/svg+xml;
# Main location
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP-FPM Configuration
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_param HTTPS on;
fastcgi_hide_header X-Powered-By;
fastcgi_read_timeout 300;
fastcgi_send_timeout 300;
}
# Deny access to hidden files
location ~ /\\.(?!well-known).* {
deny all;
access_log off;
log_not_found off;
}
# Deny access to sensitive files
location ~* (\\.env|\\.git|\\.gitignore|composer\\.json|composer\\.lock|package\\.json|package-lock\\.json|\\.htaccess) {
deny all;
return 404;
}
# Cache static assets
location ~* \\.(jpg|jpeg|png|gif|ico|svg|webp|css|js|woff|woff2|ttf|eot|otf)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Cache HTML files
location ~* \\.html$ {
expires 1h;
add_header Cache-Control "public, must-revalidate";
}
# Disable cache for dynamic content
location ~* \\.(php|json)$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# Optimize buffer sizes
client_max_body_size 100M;
client_body_buffer_size 128k;
client_header_buffer_size 1k;
large_client_header_buffers 4 16k;
}
Update docker-compose.yml untuk mount SSL:
nginx:
image: nginx:alpine
container_name: laravel_nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./:/var/www/html
- ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
- ./docker/nginx/ssl:/etc/nginx/ssl # Mount SSL certificates
networks:
- laravel_network
depends_on:
- app
Restart Nginx:
# Restart Nginx container
docker-compose up -d nginx
# Check logs
docker-compose logs nginx
# Test SSL
curl -I <https://example.com>
SSL Certificate Auto-Renewal
Let's Encrypt certificates expire setiap 90 hari. Certbot automatically setup renewal via systemd timer:
# Check renewal timer
sudo systemctl list-timers | grep certbot
# Test renewal (dry-run)
sudo certbot renew --dry-run
# Manual renewal (if needed)
sudo certbot renew
# Copy renewed certs to Docker
sudo cp /etc/letsencrypt/live/example.com/fullchain.pem docker/nginx/ssl/
sudo cp /etc/letsencrypt/live/example.com/privkey.pem docker/nginx/ssl/
docker-compose restart nginx
Automate SSL renewal dengan cron:
# Edit crontab
crontab -e
# Add renewal script (runs daily at 3 AM)
0 3 * * * certbot renew --quiet --post-hook "cp /etc/letsencrypt/live/example.com/*.pem /var/www/project/docker/nginx/ssl/ && docker-compose -f /var/www/project/docker-compose.yml restart nginx"
Verify SSL Installation
1. Test SSL grade:
Go to https://www.ssllabs.com/ssltest/ dan masukkan domain lo. Target: A+ rating.
2. Check di browser:
- Visit
https://example.com - Look for padlock icon π
- No "Not Secure" warning
- Certificate valid for 90 days
3. Test redirect HTTP β HTTPS:
curl -I <http://example.com>
# Should return: 301 Moved Permanently
# Location: <https://example.com>
Laravel Configuration untuk HTTPS
Update Laravel config untuk recognize HTTPS:
File: app/Http/Middleware/TrustProxies.php (Laravel 10 atau sebelumnya)
<?php
namespace App\\Http\\Middleware;
use Illuminate\\Http\\Middleware\\TrustProxies as Middleware;
use Illuminate\\Http\\Request;
class TrustProxies extends Middleware
{
protected $proxies = '*';
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}
File: bootstrap/app.php (Laravel 11+)
<?php
use Illuminate\\Foundation\\Application;
use Illuminate\\Foundation\\Configuration\\Exceptions;
use Illuminate\\Foundation\\Configuration\\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(at: '*');
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
Clear cache:
docker-compose exec app php artisan config:clear
docker-compose exec app php artisan route:clear
docker-compose exec app php artisan view:clear
Troubleshooting SSL Issues
Issue 1: "Connection not secure" warning
# Check certificate files exist
ls -la docker/nginx/ssl/
# Check Nginx config syntax
docker-compose exec nginx nginx -t
# Check Nginx error logs
docker-compose logs nginx | grep -i error
Issue 2: Mixed content warnings
Kalau ada assets yang load via HTTP di page HTTPS:
// Force HTTPS in Laravel (AppServiceProvider)
public function boot()
{
if ($this->app->environment('production')) {
\\URL::forceScheme('https');
}
}
Issue 3: SSL certificate expired
# Check expiration date
openssl x509 -in docker/nginx/ssl/fullchain.pem -text -noout | grep "Not After"
# Renew certificate
sudo certbot renew --force-renewal
Performance Optimization Checklist
Dengan Nginx + SSL, aplikasi lo sekarang udah:
CHECKLIST OPTIMIZATION:
β
HTTPS enabled (secure connection)
β
HTTP/2 enabled (faster page load)
β
Gzip compression (reduce bandwidth 70-80%)
β
Static file caching (reduce server load)
β
Security headers (protect from attacks)
β
SSL session cache (faster handshake)
β
OCSP stapling (faster cert validation)
β
HSTS enabled (force HTTPS)
Next: Cloudflare WAF
Sekarang aplikasi lo udah secure dengan HTTPS, tapi masih vulnerable terhadap:
- DDoS attacks
- SQL injection attempts
- XSS attacks
- Brute force login
- Bot traffic
Di bagian selanjutnya, kita akan aktifkan Cloudflare WAF (Web Application Firewall) untuk add protection layer di depan Nginx lo.
Let's go!
Bagian 4: Aktifkan Cloudflare WAF - Protection Layer untuk Production
Aplikasi lo sekarang udah jalan di HTTPS dengan SSL certificate. Tapi begitu website lo online, dalam hitungan jam (bukan hari), lo bakal mulai lihat traffic aneh di logs:
`SUSPICIOUS ACCESS LOGS:
2024-02-15 03:24:11 - 185.220.101.x - "GET /admin' OR '1'='1" 2024-02-15 03:24:15 - 185.220.101.x - "GET /wp-admin.php" 2024-02-15 03:24:19 - 185.220.101.x - "POST /login" (100 requests in 10 seconds) 2024-02-15 03:25:03 - 45.142.212.x - "GET /?id=<script>alert(1)</script>"`
Itu semua bot yang nyoba exploit vulnerability. SQL injection, XSS, brute forceβsemuanya automated. Dan ini baru traffic "normal" di internet. Belum kalau ada yang beneran target aplikasi lo.
Solusinya? Cloudflare Web Application Firewall (WAF).
Kenapa Cloudflare?
Cloudflare itu kayak security guard + CDN + DNS manager sekaligus. Benefits yang lo dapet:
1. DDoS Protection Server lo cuma sanggup handle 1,000 requests/second. DDoS attack bisa nge-flood 100,000 requests/second. Cloudflare handle di layer merekaβtraffic jahat nggak sampai ke server lo.
2. Web Application Firewall (WAF) Managed rules yang block common attacks (SQL injection, XSS, LFI, RFI) secara otomatis. Updated regularly setiap ada vulnerability baru.
3. CDN (Content Delivery Network) Static assets (images, CSS, JS) di-cache di 300+ data centers worldwide. User di Jakarta access dari Jakarta edge server, user di London access dari London edge serverβfaster load time.
4. Bot Protection Block bad bots (scraper, spam bot, hack attempt), allow good bots (Google crawler, Facebook crawler).
5. Analytics Dashboard yang kasih insight: berapa traffic, dari mana, attack apa yang di-block, dll.
Dan yang paling penting: Tier free udah sangat cukup untuk most Laravel projects. Lo nggak perlu bayar sepeser pun untuk basic protection.
Setup Cloudflare - Step by Step
1. Daftar dan Add Site
- Go to https://dash.cloudflare.com/sign-up
- Sign up dengan email
- Klik "Add a Site"
- Masukkan domain lo (example.com)
- Pilih plan Free (USD 0/month)
2. DNS Records Migration
Cloudflare bakal scan existing DNS records dari domain registrar lo. Verify bahwa records yang penting udah ter-detect:
`DNS RECORDS (Example):
Type Name Content TTL Proxy Status ββββββββββββββββββββββββββββββββββββββββββββββββββββββ A @ 123.45.67.89 Auto π DNS Only (default) A www 123.45.67.89 Auto π DNS Only CNAME mail mail.provider Auto π DNS Only MX @ mail.provider Auto π DNS Only`
Klik Continue.
3. Update Nameservers
Cloudflare bakal kasih 2 nameservers. Lo perlu update ini di domain registrar (Namecheap, GoDaddy, Niagahoster, dll):
`NAMESERVERS (Example):
Remove:
Add:
- alice.ns.cloudflare.com
- bob.ns.cloudflare.com`
4. Wait for Propagation
Cloudflare check nameservers setiap 15 menit. Biasanya aktif dalam 5-30 menit. Lo bakal dapet email "Your site is now active on Cloudflare".
Cloudflare Recommended Settings
Setelah site aktif, kita optimize settings:
SSL/TLS Settings
Navigate ke SSL/TLS tab:
`SSL/TLS CONFIGURATION:
βββ Overview β βββ Encryption Mode: Full (Strict) β β βββ Edge Certificates β βββ Always Use HTTPS: ON β β βββ HTTP Strict Transport Security (HSTS): Enable β βββ Minimum TLS Version: TLS 1.2 β β βββ Opportunistic Encryption: ON β βββ Automatic HTTPS Rewrites: ON β β βββ Origin Server βββ Origin CA Certificate: (optional, tapi recommended)`
Important: Encryption Mode harus Full (Strict), bukan Flexible. Flexible = CloudFlare to Origin pakai HTTP (nggak secure).
Security Settings
Navigate ke Security > Settings:
`SECURITY SETTINGS:
βββ Security Level: Medium β β (High = more challenges, Medium = balanced) β βββ Challenge Passage: 30 minutes β (Duration setelah solve challenge) β βββ Browser Integrity Check: ON β β (Block browser yg nggak punya User Agent) β βββ Privacy Pass Support: ON`
WAF (Web Application Firewall)
Navigate ke Security > WAF:
`WAF MANAGED RULES:
βββ Cloudflare Managed Ruleset: ON β β (Block OWASP top 10 vulnerabilities) β βββ Cloudflare OWASP Core Ruleset: ON β β (Additional OWASP protection) β βββ Cloudflare Exposed Credentials Check: ON (Block login dengan leaked passwords)`
Semua ini included di free plan. Tinggal klik toggle ON.
Firewall Rules (Custom)
Sekarang buat custom rules untuk protect endpoint critical:
Rule 1: Rate Limit Login Page
`Navigate: Security > WAF > Custom Rules > Create Rule
Rule Name: Rate Limit Login Expression: (http.request.uri.path eq "/login") and (http.request.method eq "POST")
Action: Challenge Rate Limiting:
- Requests: 5
- Period: 60 seconds
- Counting method: IP Address`
Ini limit POST ke /login maksimal 5 requests per menit per IP. Lebih dari itu = challenge (CAPTCHA).
Rule 2: Block Countries (Optional)
Kalau aplikasi lo cuma untuk Indonesia, lo bisa block countries yang suspicious:
`Rule Name: Block High-Risk Countries Expression: (ip.geoip.country in {"CN" "RU" "VN"})
Action: Block`
β οΈ Note: Hati-hati pakai rule ini. Pastikan nggak block user legitimate.
Rule 3: Protect Admin Panel
`Rule Name: Challenge Admin Access Expression: (http.request.uri.path contains "/admin")
Action: Managed Challenge`
User yang akses /admin/* harus solve challenge dulu (verify they're human).
Speed Settings
Navigate ke Speed > Optimization:
`OPTIMIZATION SETTINGS:
βββ Auto Minify β βββ JavaScript: ON β β βββ CSS: ON β β βββ HTML: ON β β βββ Brotli: ON β β (Better compression than Gzip) β βββ Early Hints: ON β (Faster page load via HTTP 103) β βββ Rocket Loader: OFF β οΈ (Bisa break JavaScript - test dulu sebelum enable)`
Caching Settings
Navigate ke Caching > Configuration:
`CACHING:
βββ Caching Level: Standard β (Cache static content) β βββ Browser Cache TTL: 4 hours β (Expire time di browser user) β βββ Always Online: ON (Show cached version kalau origin down)`
Page Rules untuk Granular Control:
Cloudflare Free plan kasih 3 page rules gratis. Gunakan dengan bijak:
`PAGE RULE 1: Bypass API URL Pattern: example.com/api/* Settings:
- Cache Level: Bypass
- Security Level: High
PAGE RULE 2: Cache Static Files URL Pattern: example.com/*.{jpg,jpeg,png,gif,css,js} Settings:
- Cache Level: Cache Everything
- Edge Cache TTL: 1 month
PAGE RULE 3: High Security Admin URL Pattern: example.com/admin/* Settings:
- Security Level: High
- Browser Integrity Check: ON`
Laravel Configuration untuk Cloudflare
Sekarang aplikasi lo di-proxy oleh Cloudflare. Ada beberapa adjustment di Laravel:
1. Trust Cloudflare IPs
Cloudflare request ke server lo datang dari Cloudflare IPs, bukan user real IP. Laravel perlu tau ini.
File: bootstrap/app.php (Laravel 11)
php
>withMiddleware(function (Middleware $middleware) { $middleware>trustProxies(at: '*'); // Atau specify Cloudflare IPs specifically: // $middleware->trustProxies(at: [ // '173.245.48.0/20', // '103.21.244.0/22', // '103.22.200.0/22', // '103.31.4.0/22', // // ... more Cloudflare IPs // ]);})
2. Get Real IP dari Cloudflare Headers
Cloudflare pass user real IP via CF-Connecting-IP header:
php
`// app/Http/Middleware/TrustCloudflareIp.php (optional)
public function handle($request, Closure $next) { if ($cfIp = $request->header('CF-Connecting-IP')) { $request->server->set('REMOTE_ADDR', $cfIp); }
return $next($request);
}`
3. Rate Limiting (Laravel + Cloudflare)
Meskipun Cloudflare udah handle rate limiting, Laravel tetep perlu rate limiter untuk API:
php
// routes/api.php Route::middleware(['throttle:60,1'])->group(function () { Route::get('/properties', [PropertyController::class, 'index']); Route::get('/properties/{id}', [PropertyController::class, 'show']); });
Enable Cloudflare Proxy
Setelah semua setting OK, waktunya enable proxy:
Navigate: DNS > Records
Toggle Proxy Status dari π DNS Only ke π§ Proxied:
Type Name Content Proxy Status ββββββββββββββββββββββββββββββββββββββββββββββββ A @ 123.45.67.89 π§ Proxied β
A www 123.45.67.89 π§ Proxied β
Test:
bash
`# Check IP resolution (should return Cloudflare IP, not your VPS IP) dig example.com +short
Output: 104.21.x.x (Cloudflare IP)
Test website
curl -I https://example.com
Should see: cf-ray, cf-cache-status headers`
Monitoring Cloudflare Analytics
Navigate ke Analytics & Logs > Traffic:
Lo bisa lihat:
- Total requests per hari/minggu/bulan
- Bandwidth saved (berapa data yang di-serve dari cache)
- Threats blocked (berapa attack yang di-block WAF)
- Countries (dari mana traffic datang)
- Status codes (200, 404, 500, dll)
Ini dashboard yang gue check setiap minggu untuk monitor health aplikasi client.
Troubleshooting Cloudflare Issues
Issue 1: Error 521 (Web server is down)
Server lo down atau Nginx nggak respond:
bash
`# Check Nginx running docker-compose ps nginx
Check Nginx logs
docker-compose logs nginx`
Issue 2: Redirect loop
SSL/TLS mode salah configured:
Fix: Set SSL/TLS mode to "Full (Strict)" NOT "Flexible"
Issue 3: Assets nggak load (mixed content)
Beberapa assets load via HTTP:
php
// Force HTTPS di Laravel \\URL::forceScheme('https');
Issue 4: CloudFlare cache stale
Purge cache manually:
Navigate: Caching > Configuration > Purge Everything
Cloudflare sekarang jadi shield pertama aplikasi lo. DDoS, bot attacks, SQL injectionβsemua di-handle sebelum sampai ke server. Next: setup queue worker untuk background jobs.
Bagian 5: Setup Queue Worker dengan Supervisor - Background Jobs yang Reliable
Aplikasi Laravel production wajib pakai queue untuk:
- βοΈ Sending emails (welcome email, order confirmation, notifications)
- π³ Processing payments (update database, generate invoice)
- πΌοΈ Image processing (resize, thumbnail generation, watermark)
- π Generating reports (export PDF, Excel)
- π Push notifications
- π·οΈ Web scraping atau API calls ke third-party
Kenapa nggak langsung process di request cycle?
Coba imagine: User klik "Register". Aplikasi lo perlu:
- Save user ke database (50ms)
- Send welcome email via SMTP (2 detik)
- Send notification ke Slack (500ms)
- Generate avatar image (300ms)
Total: ~3 detik buat satu registration request. User nunggu 3 detik buat lihat "Registration successful". Itu pengalaman yang buruk.
Dengan queue:
- Save user ke database (50ms)
- Dispatch jobs ke queue (10ms)
- Return response (10ms)
Total response time: 70ms. Background worker handle sisanya. User happy, server happy.
Queue Configuration: Redis vs Database
Laravel support multiple queue drivers. Dua yang paling umum:
Database Queue:
- β Simple setup (no extra service)
- β Good untuk low-traffic apps
- β Slower (database polling overhead)
- β Nggak scale untuk high-traffic
Redis Queue:
- β Fast (in-memory, bisa handle ribuan jobs/second)
- β Scale dengan baik
- β Support priorities, delayed jobs
- β Need Redis service (tapi lo udah install di Docker)
Recommendation: Always use Redis untuk production.
Configure Redis Queue
File: config/queue.php
php
`'default' => env('QUEUE_CONNECTION', 'redis'),
'connections' => [ 'redis' => [ 'driver' => 'redis', 'connection' => 'default', 'queue' => env('REDIS_QUEUE', 'default'), 'retry_after' => 90, 'block_for' => null, 'after_commit' => false, ], ],`
File: .env
env
QUEUE_CONNECTION=redis REDIS_HOST=redis REDIS_PASSWORD=null REDIS_PORT=6379 REDIS_QUEUE=default
Create Queue Workers dengan Docker
Ada dua approach: Supervisor (di host) atau Docker service (recommended).
Update docker-compose.yml:
yaml
`# Queue Worker - Default queue-default: build: . container_name: laravel_queue_default restart: unless-stopped command: php artisan queue:work redis --queue=default --sleep=3 --tries=3 --max-time=3600 volumes: - ./:/var/www/html networks: - laravel_network depends_on: - redis - mysql environment: - APP_ENV=${APP_ENV} - DB_HOST=mysql - REDIS_HOST=redis
Queue Worker - Emails (High Priority)
queue-emails: build: . container_name: laravel_queue_emails restart: unless-stopped command: php artisan queue:work redis --queue=emails --sleep=3 --tries=3 --max-time=3600 volumes: - ./:/var/www/html networks: - laravel_network depends_on: - redis - mysql
Queue Worker - Payments (Critical)
queue-payments: build: . container_name: laravel_queue_payments restart: unless-stopped command: php artisan queue:work redis --queue=payments --sleep=3 --tries=2 --max-time=1800 volumes: - ./:/var/www/html networks: - laravel_network depends_on: - redis - mysql`
Penjelasan options:
`QUEUE WORKER OPTIONS:
--queue=default β Queue name --sleep=3 β Sleep 3 seconds kalau queue kosong --tries=3 β Retry max 3 times kalau gagal --max-time=3600 β Restart worker setelah 1 hour --timeout=60 β Job timeout (default 60s) --memory=128 β Memory limit (MB) --max-jobs=1000 β Process max 1000 jobs then restart`
Start queue workers:
bash
`docker-compose up -d queue-default queue-emails queue-payments
Verify running
docker-compose ps | grep queue
Check logs
docker-compose logs -f queue-default`
Dispatching Jobs
Create Job:
bash
docker-compose exec app php artisan make:job SendWelcomeEmail
File: app/Jobs/SendWelcomeEmail.php
php
`<?php
namespace App\Jobs;
use App\Models\User; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Mail;
class SendWelcomeEmail implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public User $user
) {}
public function handle(): void
{
Mail::to($this->user->email)->send(
new \\App\\Mail\\WelcomeEmail($this->user)
);
}
// Retry strategy
public $tries = 3;
public $backoff = [10, 30, 60]; // Wait 10s, 30s, 60s between retries
public $timeout = 120;
// Failed handler
public function failed(\\Throwable $exception): void
{
// Log error, send alert, etc
\\Log::error('Failed to send welcome email', [
'user_id' => $this->user->id,
'error' => $exception->getMessage(),
]);
}
}`
Dispatch Job:
php
`// Di Controller atau Service use App\Jobs\SendWelcomeEmail;
// Dispatch immediately SendWelcomeEmail::dispatch($user);
// Dispatch to specific queue SendWelcomeEmail::dispatch($user)->onQueue('emails');
// Delay dispatch SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5));
// Chain multiple jobs SendWelcomeEmail::dispatch($user) ->chain([ new GenerateUserAvatar($user), new SendSlackNotification($user), ]);`
Failed Jobs Handling
Jobs bisa fail karena berbagai alasan (network error, API down, database lock). Laravel store failed jobs untuk retry nanti.
Migration untuk failed_jobs table:
bash
docker-compose exec app php artisan queue:failed-table docker-compose exec app php artisan migrate
Commands:
bash
`# List failed jobs docker-compose exec app php artisan queue:failed
Retry specific failed job
docker-compose exec app php artisan queue:retry {id}
Retry all failed jobs
docker-compose exec app php artisan queue:retry all
Delete failed job
docker-compose exec app php artisan queue:forget {id}
Flush all failed jobs
docker-compose exec app php artisan queue:flush`
Monitor Queue dengan Laravel Horizon (Optional)
Horizon = beautiful dashboard untuk monitor queues, failed jobs, throughput, dll.
bash
composer require laravel/horizon php artisan horizon:install
File: config/horizon.php (adjust workers):
php
'environments' => [ 'production' => [ 'supervisor-1' => [ 'connection' => 'redis', 'queue' => ['default'], 'balance' => 'auto', 'processes' => 3, 'tries' => 3, ], 'supervisor-2' => [ 'connection' => 'redis', 'queue' => ['emails'], 'balance' => 'auto', 'processes' => 2, 'tries' => 3, ], ], ],
Update docker-compose.yml (replace queue workers with Horizon):
yaml
horizon: build: . container_name: laravel_horizon restart: unless-stopped command: php artisan horizon volumes: - ./:/var/www/html networks: - laravel_network depends_on: - redis - mysql
Access Horizon dashboard:
β οΈ Don't forget protect Horizon di production:
php
`// app/Providers/HorizonServiceProvider.php
protected function gate() { Gate::define('viewHorizon', function ($user) { return in_array($user->email, [ '[email protected]', ]); }); }`
Queue Worker Best Practices
`QUEUE CHECKLIST:
β Always use Redis (not database) untuk production β Separate queues untuk different priorities (default, emails, payments) β Set reasonable retry limits (3-5 tries max) β Implement failed() method untuk error handling β Use delays untuk rate-limited APIs β Monitor failed jobs regularly β Restart workers after deployment (php artisan queue:restart) β Set max-time untuk prevent memory leaks`
Queue worker sekarang udah reliable dan scalable. Net: konfigurasi webhook payment yang aman!
Bagian 6: Konfigurasi Webhook Payment yang Aman - Handle Payment Notification dengan Benar
Payment gateway kayak Midtrans, Xendit, atau Stripe nggak langsung update database lo setelah user bayar. Mereka kirim webhookβserver-to-server notificationβke aplikasi lo untuk bilang "Hei, user A udah bayar, update status ordernya!"
Masalahnya: kebanyakan developer handle webhook dengan sembarangan.
Horror Story: Webhook yang Salah Handle
Gue pernah audit e-commerce client yang komplain "Ada customer bayar tapi ordernya nggak masuk". Setelah gue check:
// CODE MEREKA (WRONG):
Route::post('/webhook/midtrans', function (Request $request) {
$order = Order::where('order_id', $request->order_id)->first();
$order->status = 'paid';
$order->save();
return response()->json(['status' => 'ok']);
});
Apa masalahnya?
β No signature verification - Siapa aja bisa hit endpoint ini dengan fake data
β No idempotency check - Kalau webhook dikirim 2x, order jadi "paid" 2x
β Synchronous processing - Kalau ada heavy task (kirim email, update inventory), webhook timeout
β No logging - Nggak ada audit trail kalau ada masalah
Result: Ada yang bisa fake payment, ada yang double-charged, ada yang ordernya pending terus.
Webhook Security Checklist
WEBHOOK MUST-HAVE:
β
Signature verification (ALWAYS)
β
Idempotency check (prevent duplicate processing)
β
CSRF token excluded (webhook = server-to-server)
β
Rate limiting (prevent abuse)
β
Comprehensive logging (audit trail)
β
Quick response < 5 seconds (queue heavy tasks)
β
HTTPS only (no plain HTTP)
β
IP whitelist (optional, tapi recommended)
Webhook Controller - The Right Way
File: app/Http/Controllers/WebhookController.php
<?php
namespace App\\Http\\Controllers;
use App\\Jobs\\ProcessPaymentWebhook;
use App\\Models\\PaymentLog;
use Illuminate\\Http\\Request;
use Illuminate\\Support\\Facades\\Log;
class WebhookController extends Controller
{
public function handleMidtrans(Request $request)
{
// 1. Log incoming request (ALWAYS log)
Log::channel('webhook')->info('Midtrans webhook received', [
'payload' => $request->all(),
'ip' => $request->ip(),
'timestamp' => now()->toIso8601String(),
]);
// 2. Verify signature
if (!$this->verifyMidtransSignature($request->all())) {
Log::channel('webhook')->warning('Invalid signature', [
'order_id' => $request->order_id,
'ip' => $request->ip(),
]);
return response()->json(['error' => 'Invalid signature'], 403);
}
// 3. Extract data
$orderId = $request->order_id;
$transactionStatus = $request->transaction_status;
$fraudStatus = $request->fraud_status ?? 'accept';
// 4. Check idempotency (prevent duplicate processing)
if ($this->isDuplicateWebhook($orderId, $transactionStatus)) {
Log::channel('webhook')->info('Duplicate webhook ignored', [
'order_id' => $orderId,
'status' => $transactionStatus,
]);
return response()->json(['status' => 'duplicate']);
}
// 5. Quick response - dispatch to queue
ProcessPaymentWebhook::dispatch([
'order_id' => $orderId,
'transaction_status' => $transactionStatus,
'fraud_status' => $fraudStatus,
'payment_type' => $request->payment_type,
'gross_amount' => $request->gross_amount,
'transaction_time' => $request->transaction_time,
'raw_payload' => $request->all(),
])->onQueue('payments');
return response()->json(['status' => 'ok']);
}
private function verifyMidtransSignature(array $data): bool
{
$serverKey = config('services.midtrans.server_key');
$expectedSignature = hash('sha512',
$data['order_id'] .
$data['status_code'] .
$data['gross_amount'] .
$serverKey
);
$receivedSignature = $data['signature_key'] ?? '';
return hash_equals($expectedSignature, $receivedSignature);
}
private function isDuplicateWebhook(string $orderId, string $status): bool
{
return PaymentLog::where('order_id', $orderId)
->where('transaction_status', $status)
->where('processed', true)
->exists();
}
}
Process Webhook Job
File: app/Jobs/ProcessPaymentWebhook.php
<?php
namespace App\\Jobs;
use App\\Models\\Order;
use App\\Models\\PaymentLog;
use App\\Notifications\\PaymentSuccessNotification;
use Illuminate\\Bus\\Queueable;
use Illuminate\\Contracts\\Queue\\ShouldQueue;
use Illuminate\\Foundation\\Bus\\Dispatchable;
class ProcessPaymentWebhook implements ShouldQueue
{
use Dispatchable, Queueable;
public function __construct(
public array $webhookData
) {}
public function handle(): void
{
$orderId = $this->webhookData['order_id'];
$status = $this->webhookData['transaction_status'];
// Find order
$order = Order::where('order_id', $orderId)->firstOrFail();
// Create payment log
PaymentLog::create([
'order_id' => $orderId,
'transaction_status' => $status,
'fraud_status' => $this->webhookData['fraud_status'],
'payment_type' => $this->webhookData['payment_type'],
'gross_amount' => $this->webhookData['gross_amount'],
'raw_payload' => json_encode($this->webhookData['raw_payload']),
'processed' => true,
'processed_at' => now(),
]);
// Update order based on status
match($status) {
'capture', 'settlement' => $this->handleSuccess($order),
'pending' => $this->handlePending($order),
'deny', 'cancel', 'expire' => $this->handleFailed($order),
default => null,
};
}
private function handleSuccess(Order $order): void
{
$order->update([
'payment_status' => 'paid',
'paid_at' => now(),
]);
// Send notification
$order->user->notify(new PaymentSuccessNotification($order));
}
private function handlePending(Order $order): void
{
$order->update(['payment_status' => 'pending']);
}
private function handleFailed(Order $order): void
{
$order->update([
'payment_status' => 'failed',
'failed_at' => now(),
]);
}
}
Route Configuration
File: routes/web.php
// Exclude CSRF untuk webhook
Route::post('/webhook/midtrans', [WebhookController::class, 'handleMidtrans'])
->withoutMiddleware(['web']);
Atau di bootstrap/app.php (Laravel 11):
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'webhook/*',
]);
})
Testing Webhook Locally dengan Ngrok
Payment gateway nggak bisa hit localhost. Pakai Ngrok untuk expose local server:
# Install ngrok
brew install ngrok # MacOS
# Or download from ngrok.com
# Start Laravel
php artisan serve
# Expose port 8000
ngrok http 8000
# Output:
# Forwarding: <https://abc123.ngrok.io> -> <http://localhost:8000>
Set webhook URL di Midtrans dashboard: https://abc123.ngrok.io/webhook/midtrans
Webhook Logging
File: config/logging.php
'channels' => [
'webhook' => [
'driver' => 'daily',
'path' => storage_path('logs/webhook.log'),
'level' => 'info',
'days' => 30,
],
],
Sekarang semua webhook activity ter-log dengan detail. Essential untuk debugging payment issues.
Bagian 7: Stress Testing dengan K6 - Pastikan Aplikasi Bisa Handle Load
Lo nggak tau aplikasi lo bisa handle berapa user sampai lo test.
"Kayaknya udah fast kok" β Fast under load
"Database query udah di-optimize" β Bisa handle 1000 concurrent users
K6 adalah modern load testing tool dari Grafana Labs. JavaScript-based, easy to use, dan hasil metrics-nya detail banget.
Install K6
# MacOS
brew install k6
# Linux (Ubuntu/Debian)
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] <https://dl.k6.io/deb> stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
# Windows
choco install k6
# Verify
k6 version
Basic Load Test
File: tests/load/basic-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 20 }, // Ramp up to 20 users
{ duration: '1m', target: 20 }, // Stay at 20 users
{ duration: '30s', target: 50 }, // Spike to 50 users
{ duration: '1m', target: 50 }, // Stay at 50
{ duration: '30s', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% requests < 500ms
http_req_failed: ['rate<0.01'], // Error rate < 1%
},
};
const BASE_URL = '<https://example.com>';
export default function () {
// Test homepage
const homeRes = http.get(BASE_URL);
check(homeRes, {
'homepage status 200': (r) => r.status === 200,
'homepage load < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
// Test API
const apiRes = http.get(`${BASE_URL}/api/properties`);
check(apiRes, {
'api status 200': (r) => r.status === 200,
'api response time < 300ms': (r) => r.timings.duration < 300,
});
sleep(1);
}
Run test:
k6 run tests/load/basic-test.js
Output example:
β homepage status 200
β homepage load < 500ms
β api status 200
β api response time < 300ms
checks.........................: 100.00% β 4000 β 0
data_received..................: 52 MB 173 kB/s
data_sent......................: 320 kB 1.1 kB/s
http_req_duration..............: avg=245ms min=89ms med=198ms max=1.2s p(95)=456ms
http_req_failed................: 0.00% β 0 β 2000
http_reqs......................: 2000 6.6/s
vus............................: 50 min=0 max=50
E-commerce Flow Test
File: tests/load/booking-flow.js
import http from 'k6/http';
import { check, group, sleep } from 'k6';
export const options = {
scenarios: {
booking_flow: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 10 },
{ duration: '5m', target: 10 },
{ duration: '2m', target: 0 },
],
},
},
thresholds: {
'http_req_duration{scenario:booking_flow}': ['p(95)<1000'],
'group_duration{group:::Booking Flow}': ['p(95)<5000'],
},
};
const BASE_URL = '<https://example.com>';
export default function () {
group('Booking Flow', function () {
// 1. Browse properties
let listRes = http.get(`${BASE_URL}/properties`);
check(listRes, {
'property list loaded': (r) => r.status === 200,
});
sleep(2);
// 2. View property detail
let detailRes = http.get(`${BASE_URL}/properties/villa-sunset`);
check(detailRes, {
'property detail loaded': (r) => r.status === 200,
'has booking form': (r) => r.body.includes('check-in'),
});
sleep(3);
// 3. Check availability (API call)
let availRes = http.post(
`${BASE_URL}/api/check-availability`,
JSON.stringify({
property_id: 1,
check_in: '2024-03-01',
check_out: '2024-03-03',
}),
{ headers: { 'Content-Type': 'application/json' } }
);
check(availRes, {
'availability checked': (r) => r.status === 200,
'response is JSON': (r) => r.json('available') !== undefined,
});
sleep(1);
});
}
Interpret Results
TARGET METRICS:
Response Time (http_req_duration p95):
βββ < 500ms β
Excellent
βββ 500-1000ms β οΈ Acceptable (optimize if possible)
βββ > 1000ms β Needs optimization
Error Rate (http_req_failed):
βββ < 1% β
Good
βββ 1-5% β οΈ Investigate
βββ > 5% β Critical issue
Throughput (http_reqs per second):
βββ Depends on expected traffic
Server Resources (check during test):
βββ CPU < 80% β
βββ Memory < 80% β
βββ Disk I/O not maxed β
Kalau metrics jelek, optimize:
- Database query (N+1 problem?)
- Add caching (Redis, Cloudflare)
- Optimize images (lazy load, CDN)
- Scale server (vertical: more RAM/CPU, horizontal: load balancer)
Bagian 8: Amankan Server - Fail2ban, UFW, SSH Hardening
Server production = target empuk untuk attackers. Dalam 1 jam setelah server online, lo bakal lihat ratusan brute force attempts di logs.
Security bukan optionalβini mandatory.
1. UFW (Uncomplicated Firewall)
Block semua ports kecuali yang dibutuhkan:
# Install (biasanya udah installed di Ubuntu)
sudo apt install ufw
# Default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH (PENTING: allow dulu sebelum enable!)
sudo ufw allow 22/tcp
# Allow HTTP & HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Enable firewall
sudo ufw enable
# Check status
sudo ufw status verbose
# Output:
# Status: active
# To Action From
# -- ------ ----
# 22/tcp ALLOW Anywhere
# 80/tcp ALLOW Anywhere
# 443/tcp ALLOW Anywhere
2. Fail2ban - Auto-Block Brute Force
Install:
sudo apt install fail2ban -y
Config:
# Copy default config
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
# Edit
sudo nano /etc/fail2ban/jail.local
File: /etc/fail2ban/jail.local
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
banaction = ufw
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 24h
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 3
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 10
Custom filter untuk Laravel login:
sudo nano /etc/fail2ban/filter.d/laravel-auth.conf
[Definition]
failregex = ^.*"POST.*\\/login.*" (401|422).*<HOST>.*$
ignoreregex =
Add jail untuk Laravel:
[laravel-auth]
enabled = true
filter = laravel-auth
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 5
bantime = 1h
Restart Fail2ban:
sudo systemctl restart fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
3. SSH Hardening
Edit SSH config:
sudo nano /etc/ssh/sshd_config
Critical changes:
# Disable root login
PermitRootLogin no
# Use key-based auth only
PasswordAuthentication no
PubkeyAuthentication yes
# Limit users
AllowUsers deployer
# Change default port (optional, reduce noise)
Port 2222
# Timeout settings
ClientAliveInterval 300
ClientAliveCountMax 2
# Disable empty passwords
PermitEmptyPasswords no
# Disable X11 forwarding
X11Forwarding no
Setup SSH key:
# Local machine
ssh-keygen -t ed25519 -C "[email protected]"
# Copy to server
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server
# Test login
ssh user@server
# Restart SSH
sudo systemctl restart sshd
β οΈ PENTING: Test SSH login dengan key SEBELUM disable password auth!
Security Checklist
SERVER SECURITY CHECKLIST:
β
UFW enabled, only necessary ports open
β
Fail2ban installed dan configured
β
SSH key-based auth only (no password)
β
Root login disabled
β
Non-standard SSH port (optional)
β
Auto security updates enabled
β
Unused services disabled
β
Server timezone set correctly
β
Log rotation configured
β
Monitoring setup
Bagian 9: Monitoring, Backup, dan Handover ke Client - Professional Finish
Aplikasi udah production-ready. Sekarang maintenance dan serah terima yang proper.
1. Uptime Monitoring (Free)
UptimeRobot - gratis 50 monitors:
SETUP UPTIMEROBOT:
1. Sign up: uptimerobot.com
2. Add Monitor:
- Type: HTTP(s)
- URL: <https://example.com>
- Interval: 5 minutes
3. Add Health Check:
- URL: <https://example.com/health>
- Interval: 5 minutes
4. Alert Contacts:
- Email
- Telegram/Slack (optional)
Laravel Health Endpoint:
// routes/web.php
Route::get('/health', function () {
try {
DB::connection()->getPdo();
Redis::ping();
return response()->json([
'status' => 'healthy',
'database' => 'connected',
'redis' => 'connected',
'timestamp' => now()->toIso8601String(),
]);
} catch (\\Exception $e) {
return response()->json([
'status' => 'unhealthy',
'error' => $e->getMessage(),
], 503);
}
});
2. Automated Backup
Backup script:
#!/bin/bash
# File: /scripts/backup.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups"
DB_CONTAINER="laravel_mysql"
DB_NAME="laravel"
DB_PASSWORD="your_password"
mkdir -p $BACKUP_DIR
# Database backup
docker exec $DB_CONTAINER mysqldump -u root -p$DB_PASSWORD $DB_NAME | gzip > $BACKUP_DIR/db_$DATE.sql.gz
# Storage backup
tar -czf $BACKUP_DIR/storage_$DATE.tar.gz /var/www/html/storage/app
# Keep only last 7 days
find $BACKUP_DIR -type f -mtime +7 -delete
echo "Backup completed: $DATE"
Cron job (daily at 2 AM):
crontab -e
# Add:
0 2 * * * /scripts/backup.sh >> /var/log/backup.log 2>&1
3. Handover Documentation
# PROJECT HANDOVER - [Project Name]
## Access Credentials
### Server
- IP: 123.45.67.89
- SSH User: deployer
- SSH Port: 22
- SSH Key: [attached separately]
### Application
- URL: <https://example.com>
- Admin: <https://example.com/admin>
- Email: [email protected]
- Password: [provided separately]
### Database
- Host: localhost (via SSH tunnel)
- Database: laravel_prod
- Username: laravel_user
- Password: [provided separately]
### Services
- Cloudflare: dash.cloudflare.com (invited to team)
- Midtrans: dashboard.midtrans.com
- UptimeRobot: uptimerobot.com
---
## Important Commands
### Application
```bash
# Restart containers
cd /var/www/project
docker-compose restart
# View logs
docker-compose logs -f app
# Run migration
docker-compose exec app php artisan migrate
# Clear cache
docker-compose exec app php artisan cache:clear
Backup
- Location: /backups/
- Schedule: Daily at 2:00 AM
- Retention: 7 days
SSL Certificate
- Provider: Let's Encrypt
- Auto-renewal: Yes (via cron)
- Expires: [date]
Maintenance Schedule
Daily (Automated)
- Database backup
- Log rotation
Weekly
- Check disk space:
df -h - Review error logs
- Verify backup integrity
Monthly
- Security updates:
sudo apt update && sudo apt upgrade - SSL certificate check
- Performance review
Emergency Contacts
- Developer: [email protected] / +62xxx
- Hosting Support: [contact]
Conclusion: From Developer to Professional
7 hal yang kita bahas bukan "extra work"βini adalah standar minimum untuk production deployment:
- β Docker - Consistent deployment
- β Nginx + SSL - Secure connection
- β Cloudflare WAF - Protection layer
- β Queue Worker - Reliable background jobs
- β Webhook Security - Safe payment handling
- β Stress Testing - Verified performance
- β Server Security - Protected infrastructure
Yang membedakan developer biasa dengan professional developer adalah: developer biasa kirim code yang "jalan", professional kirim sistem yang reliable, secure, dan maintainable.
Client repeat order bukan karena code lo fancy, tapi karena mereka tenang. Website nggak pernah down, payment lancar, security terjaga.
That's the difference.
Download Checklist PDF: [Link ke BuildWithAngga]
Need help? Join BuildWithAngga untuk tutorial lebih lengkap dan community support.
Share pengalaman lo di kolom komentar - pernah ngalamin deployment nightmare? Let's discuss! π