7 Hal Penting yang Harus Dilakukan Setelah Mengerjakan Projek Freelance dengan Laravel 12

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 .env ke git - Add .env ke .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:

  1. Akses via IP, bukan domain - Nggak profesional, susah diinget
  2. HTTP, bukan HTTPS - Browser nunjukin "Not Secure", user ngga percaya
  3. No caching - Setiap request hit PHP-FPM, padahal static files bisa di-cache
  4. 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 attacks
  • X-Content-Type-Options: nosniff - Prevent MIME type sniffing
  • X-XSS-Protection - Enable browser XSS protection (legacy, tapi tetep bagus)
  • Referrer-Policy - Control referrer information
  • Permissions-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 usage
  • gzip_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

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:

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:

  1. Save user ke database (50ms)
  2. Send welcome email via SMTP (2 detik)
  3. Send notification ke Slack (500ms)
  4. 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:

  1. Save user ke database (50ms)
  2. Dispatch jobs ke queue (10ms)
  3. 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:

https://example.com/horizon

⚠️ 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


Conclusion: From Developer to Professional

7 hal yang kita bahas bukan "extra work"β€”ini adalah standar minimum untuk production deployment:

  1. βœ… Docker - Consistent deployment
  2. βœ… Nginx + SSL - Secure connection
  3. βœ… Cloudflare WAF - Protection layer
  4. βœ… Queue Worker - Reliable background jobs
  5. βœ… Webhook Security - Safe payment handling
  6. βœ… Stress Testing - Verified performance
  7. βœ… 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! πŸš€