Pada bagian ini, fokus kita adalah membawa aplikasi Laravel berbasis Docker dari lingkungan lokal ke Virtual Private Server (VPS) agar bisa diakses publik melalui domain, dilindungi SSL, dan tetap mudah dioperasikan. Target akhirnya bukan sekadar aplikasi bisa berjalan, tetapi memiliki struktur deployment yang rapi, aman secara dasar, dan siap dikembangkan ke tahap hardening, monitoring, serta maintenance.
Artikel ini mengasumsikan Anda sudah memiliki project Laravel yang sudah dikontainerisasi. Jika sebelumnya Anda menjalankan aplikasi hanya di lokal, maka sekarang kita akan menyiapkan server Linux, user non-root, firewall, Docker, Docker Compose, Nginx sebagai reverse proxy, serta sertifikat SSL dari Let's Encrypt. Selain itu, kita juga akan membahas persistence data penting seperti database dan strategi update dengan minimal downtime.
1. Arsitektur deployment yang akan digunakan
Arsitektur sederhananya adalah sebagai berikut:
- Nginx di host VPS menerima request dari internet pada port 80 dan 443.
- Nginx meneruskan request ke container aplikasi Laravel yang berjalan di jaringan Docker internal.
- Laravel dapat terhubung ke container lain seperti MySQL/MariaDB, Redis, queue worker, dan service scheduler.
- Data database dan file penting disimpan pada Docker volume agar tidak hilang saat container di-recreate.
Pendekatan ini dipilih karena praktis untuk VPS tunggal. Anda bisa memulai dari satu server tanpa langsung masuk ke orchestration seperti Kubernetes. Trade-off-nya, manajemen skala dan high availability masih terbatas, tetapi untuk banyak aplikasi internal, MVP, atau produk tahap awal, pendekatan ini sudah sangat memadai.
2. Persiapan server Linux
2.1 Buat user non-root
Jangan gunakan user root untuk aktivitas harian deployment. Buat user baru, beri akses sudo, lalu gunakan SSH key.
adduser deploy
usermod -aG sudo deploy
mkdir -p /home/deploy/.ssh
nano /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keysSetelah public key dimasukkan ke authorized_keys, coba login:
ssh deploy@IP_VPSKenapa ini penting? Karena pemisahan user mengurangi risiko kesalahan fatal, misalnya menghapus file sistem saat deployment atau menjalankan proses sebagai root tanpa perlu.
2.2 Update sistem dan pasang paket dasar
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl git ufw ca-certificates gnupg lsb-release nginx certbot python3-certbot-nginxPaket di atas cukup umum untuk server Debian/Ubuntu. Jika Anda memakai distro lain, sesuaikan manajer paketnya.
2.3 Konfigurasi firewall
Minimal buka port SSH, HTTP, dan HTTPS.
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw statusJika Anda memakai port SSH non-standar, pastikan port tersebut dibuka sebelum mengaktifkan firewall agar tidak terkunci dari server sendiri.
3. Instalasi Docker dan Docker Compose
Gunakan repositori resmi Docker agar versi dan dependensinya konsisten.
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginTambahkan user deploy ke grup docker agar tidak perlu selalu memakai sudo:
sudo usermod -aG docker deployLogout lalu login kembali agar perubahan grup aktif.
Verifikasi:
docker --version
docker compose version4. Menyusun direktori deployment
Struktur direktori yang rapi memudahkan backup, rollback, dan troubleshooting. Salah satu pola yang cukup aman:
/var/www/myapp/
├── current/
├── releases/
│ └── 20260325-120000/
├── shared/
│ ├── .env
│ ├── storage/
│ └── logs/
└── docker/
├── compose.prod.yml
└── DockerfilePenjelasannya:
- releases/: hasil kode per deployment.
- current/: symlink ke release aktif.
- shared/: file yang harus persisten antar release, misalnya
.envdanstorage. - docker/: konfigurasi Docker production.
Buat direktori awal:
sudo mkdir -p /var/www/myapp/{releases,shared,docker}
sudo chown -R deploy:deploy /var/www/myapp5. Mengirim source code ke server
Ada beberapa pendekatan umum:
- git pull di server: paling sederhana.
- rsync/scp: cocok untuk server kecil atau pipeline manual.
- CI/CD: paling rapi untuk tim, misalnya GitHub Actions atau GitLab CI.
Untuk seri ini, kita gunakan pendekatan yang mudah dipahami: clone repository ke folder release.
cd /var/www/myapp/releases
git clone https://github.com/username/myapp.git 20260325-120000
ln -sfn /var/www/myapp/releases/20260325-120000 /var/www/myapp/currentJika repository private, gunakan deploy key atau token dengan akses minimum. Hindari menaruh kredensial Git sembarangan di shell history atau file world-readable.
6. Menyiapkan Dockerfile dan Compose production
6.1 Contoh Dockerfile production
Dockerfile sebaiknya fokus pada image runtime yang ramping dan tidak membawa dependency development yang tidak diperlukan.
FROM php:8.2-fpm-alpine
RUN apk add --no-cache \
bash curl git unzip libpng-dev libjpeg-turbo-dev freetype-dev \
icu-dev oniguruma-dev libzip-dev $PHPIZE_DEPS
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install pdo pdo_mysql mbstring intl gd zip opcache
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY . .
RUN composer install --no-dev --optimize-autoloader --no-interaction
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache
CMD ["php-fpm"]Jika aplikasi Anda menggunakan ekstensi lain seperti Redis, Swoole, atau PostgreSQL, tambahkan sesuai kebutuhan. Jangan menginstal paket berlebihan karena akan memperbesar image dan permukaan serangan.
6.2 Contoh docker compose production
services:
app:
build:
context: ../current
dockerfile: ../docker/Dockerfile
container_name: myapp-app
restart: unless-stopped
env_file:
- ../shared/.env
volumes:
- ../shared/storage:/var/www/html/storage
expose:
- "9000"
depends_on:
- db
- redis
web:
image: nginx:alpine
container_name: myapp-web
restart: unless-stopped
volumes:
- ../current:/var/www/html:ro
- ../shared/storage:/var/www/html/storage
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- "127.0.0.1:8080:80"
depends_on:
- app
db:
image: mysql:8
container_name: myapp-db
restart: unless-stopped
environment:
MYSQL_DATABASE: myapp
MYSQL_USER: myapp
MYSQL_PASSWORD: strongpassword
MYSQL_ROOT_PASSWORD: rootpassword
volumes:
- myapp_db_data:/var/lib/mysql
redis:
image: redis:7-alpine
container_name: myapp-redis
restart: unless-stopped
volumes:
- myapp_redis_data:/data
queue:
build:
context: ../current
dockerfile: ../docker/Dockerfile
container_name: myapp-queue
restart: unless-stopped
env_file:
- ../shared/.env
volumes:
- ../shared/storage:/var/www/html/storage
command: php artisan queue:work --sleep=3 --tries=3 --timeout=90
depends_on:
- app
- redis
- db
scheduler:
build:
context: ../current
dockerfile: ../docker/Dockerfile
container_name: myapp-scheduler
restart: unless-stopped
env_file:
- ../shared/.env
volumes:
- ../shared/storage:/var/www/html/storage
command: sh -c "while true; do php artisan schedule:run --verbose --no-interaction; sleep 60; done"
depends_on:
- app
- redis
- db
volumes:
myapp_db_data:
myapp_redis_data:Kenapa queue dan scheduler dibuat sebagai container terpisah? Karena tanggung jawab proses production sebaiknya dipisah. Web request, worker queue, dan scheduler memiliki karakteristik beban yang berbeda. Pemisahan ini memudahkan restart selektif dan observasi log.
7. Konfigurasi .env production
Buat file /var/www/myapp/shared/.env dan isi dengan konfigurasi production. Jangan salin mentah dari lokal tanpa audit.
APP_NAME=MyApp
APP_ENV=production
APP_DEBUG=false
APP_URL=https://example.com
LOG_CHANNEL=stack
LOG_LEVEL=info
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=myapp
DB_PASSWORD=strongpassword
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
REDIS_HOST=redis
REDIS_PORT=6379Beberapa catatan penting:
- APP_DEBUG=false wajib di production.
- APP_URL harus sesuai domain final.
- Host database dan Redis menggunakan nama service Docker, bukan
127.0.0.1. - Simpan secret seperti
APP_KEY, kredensial mail, dan API key secara aman.
Jika belum ada APP_KEY, generate sekali dari container:
docker compose -f /var/www/myapp/docker/compose.prod.yml run --rm app php artisan key:generate8. Menjalankan build, migration, dan optimasi Laravel
Setelah semua siap:
cd /var/www/myapp/docker
docker compose -f compose.prod.yml build
docker compose -f compose.prod.yml up -dJalankan migration setelah database hidup:
docker compose -f compose.prod.yml exec app php artisan migrate --forceTambahkan optimasi umum:
docker compose -f compose.prod.yml exec app php artisan config:cache
docker compose -f compose.prod.yml exec app php artisan route:cache
docker compose -f compose.prod.yml exec app php artisan view:cacheKenapa memakai --force saat migration? Laravel meminta flag ini di production agar perubahan schema tidak dieksekusi tanpa sengaja.
Sebelum migration besar yang berisiko, selalu backup database. Jangan mengandalkan rollback migration sebagai satu-satunya strategi pemulihan.
9. Reverse proxy Nginx di host VPS
Walaupun ada container Nginx internal untuk melayani aplikasi, kita tetap menggunakan Nginx di host sebagai pintu masuk utama dari internet. Keuntungannya, pengelolaan domain, SSL, dan reverse proxy lebih sederhana.
Buat file konfigurasi host Nginx, misalnya /etc/nginx/sites-available/myapp:
server {
listen 80;
server_name example.com www.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Aktifkan konfigurasi:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxPastikan DNS domain Anda sudah mengarah ke IP VPS sebelum lanjut ke SSL.
10. Menghubungkan domain dan memasang SSL Let's Encrypt
Di panel DNS penyedia domain, buat record:
- A record untuk
example.comke IP VPS - A record untuk
www.example.comke IP VPS
Setelah propagasi DNS cukup, pasang sertifikat:
sudo certbot --nginx -d example.com -d www.example.comCertbot akan memperbarui konfigurasi Nginx secara otomatis untuk HTTPS dan redirect jika Anda menyetujuinya. Verifikasi auto-renew:
sudo systemctl status certbot.timer
sudo certbot renew --dry-runKesalahan umum pada tahap ini biasanya:
- DNS belum mengarah ke VPS.
- Port 80 atau 443 tertutup firewall.
- Nginx salah konfigurasi sehingga challenge Let's Encrypt gagal.
11. Persistence database dan volume penting
Salah satu kesalahan paling fatal dalam deployment container adalah menyimpan data penting hanya di layer filesystem container. Saat container dihapus atau di-recreate, data ikut hilang. Karena itu, database dan data runtime tertentu harus memakai volume persisten.
Pada contoh Compose di atas:
myapp_db_datamenyimpan data MySQL.myapp_redis_datamenyimpan data Redis jika persistence diaktifkan.../shared/storagemenyimpan file Laravel seperti log, cache file tertentu, dan upload jika aplikasi Anda menyimpannya lokal.
Jika aplikasi menerima upload pengguna dalam jumlah penting, pertimbangkan memakai object storage seperti S3-compatible storage daripada disk lokal VPS. Disk lokal lebih sederhana, tetapi backup dan migrasinya lebih merepotkan.
12. Strategi update dengan zero/minimal downtime sederhana
Untuk VPS tunggal, zero downtime sempurna tidak selalu realistis, tetapi Anda tetap bisa menekan gangguan layanan menjadi sangat kecil.
12.1 Pola release baru + rebuild
- Clone kode ke folder release baru.
- Ubah symlink
currentke release baru. - Build image baru.
- Jalankan container baru atau recreate service terkait.
- Jalankan migration yang kompatibel dengan versi lama dan baru.
Kunci utamanya adalah membuat perubahan schema database yang bersifat backward compatible saat rolling update sederhana. Misalnya, menambah kolom baru lebih aman daripada langsung menghapus kolom lama yang masih dipakai versi aplikasi sebelumnya.
12.2 Perintah update sederhana
cd /var/www/myapp/releases
git clone https://github.com/username/myapp.git 20260326-090000
ln -sfn /var/www/myapp/releases/20260326-090000 /var/www/myapp/current
cd /var/www/myapp/docker
docker compose -f compose.prod.yml build app queue scheduler web
docker compose -f compose.prod.yml up -d --no-deps app web queue scheduler
docker compose -f compose.prod.yml exec app php artisan migrate --forcePendekatan ini biasanya menimbulkan downtime sangat singkat atau bahkan nyaris tidak terasa, tergantung waktu start container dan karakter aplikasi. Jika Anda ingin lebih halus, Anda bisa menambahkan health check dan hanya mengalihkan traffic setelah service sehat.
12.3 Rollback sederhana
Jika deployment bermasalah:
- Arahkan kembali symlink
currentke release sebelumnya. - Rebuild/restart service memakai release lama.
- Pastikan migration yang sudah terlanjur jalan tidak membuat versi lama gagal.
Di sinilah pentingnya desain migration yang aman. Rollback kode jauh lebih mudah daripada rollback data.
13. Debugging dan kesalahan yang sering terjadi
- 502 Bad Gateway dari Nginx host: cek apakah container
webberjalan dan port127.0.0.1:8080benar-benar terbuka. - Laravel tidak bisa konek database: pastikan
DB_HOST=db, bukan localhost. - Permission storage/bootstrap cache: pastikan owner dan permission sesuai user runtime.
- Queue tidak jalan: cek container worker dan koneksi Redis.
- SSL gagal: cek DNS, firewall, dan validasi konfigurasi Nginx dengan
nginx -t.
Perintah yang sering dipakai untuk troubleshooting:
docker compose -f compose.prod.yml ps
docker compose -f compose.prod.yml logs -f app
docker compose -f compose.prod.yml logs -f web
docker compose -f compose.prod.yml logs -f queue
sudo journalctl -u nginx -f14. Penutup
Sampai tahap ini, Anda sudah memiliki fondasi deployment Laravel Docker ke VPS yang cukup solid: server Linux dengan user non-root, firewall aktif, Docker dan Compose terpasang, struktur release yang rapi, Nginx sebagai reverse proxy, domain dengan SSL Let's Encrypt, service queue dan scheduler terpisah, serta persistence untuk database dan data penting.
Struktur ini belum bisa disebut final untuk production skala besar, tetapi sudah jauh lebih baik daripada menjalankan semua hal secara manual tanpa isolasi, tanpa SSL, dan tanpa persistence. Pada tahap berikutnya, Anda akan lebih siap membahas hardening, backup, log rotation, monitoring, fail2ban, pembatasan akses internal, strategi secret management, dan routine maintenance agar deployment tetap stabil dalam jangka panjang.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!