How I Set Up a Secure Production VPS for a Dockerized Laravel App

1/31/2025

[!tip] πŸ’‘ This article documents how I deploy Laravel applications securely on a VPS using Docker, Cloudflare Tunnel, and SSH hardening β€” following real-world production practices.

Tech stack: Laravel Β· Docker Β· Nginx Β· MariaDB Β· Ubuntu Server Β· Cloudflare Tunnel

Category: DevOps Β· Backend Β· VPS Security

Level: Intermediate β†’ Advanced


Why I Built This Laravel VPS Setup

Deploying a Laravel application to a VPS is easy. Deploying it securely and correctly is where most setups fail.

I built this VPS configuration to follow real-world production practices, focusing on:

  • Minimal attack surface
  • Docker-first architecture
  • Secure database access
  • Clean, observable logging
  • No exposed infrastructure ports

This is the same approach I would use for a serious personal project or a small production system.


Core Design Principles

  • ❌ No public MySQL / MariaDB ports
  • ❌ No direct Nginx exposure
  • ❌ No password-based SSH
  • βœ… SSH key-only authentication
  • βœ… Dockerized Laravel services
  • βœ… Cloudflare Tunnel for HTTPS access
  • βœ… Logs written to stdout/stderr (cloud-native)

VPS Base Configuration

Operating system: Ubuntu Server 22.04+

User model: Non-root deploy user with sudo

Firewall: UFW with minimal rules

Create deploy user

adduser deploy
usermod -aG sudo deploy

Secure SSH configuration

PermitRootLogin no
PasswordAuthentication no

This significantly reduces brute-force and privilege escalation risks.


Using Docker for Laravel Deployment on a VPS

Docker is the foundation of the system. All services run inside containers:

  • Laravel (PHP-FPM)
  • Nginx
  • MariaDB
  • Optional observability tools
curl -fsSL <https://get.docker.com> | sudo sh
sudo usermod -aG docker deploy

Why Docker?

  • Consistent environments
  • Easy rebuilds
  • No dependency pollution on the host
  • Portable across VPS providers

Project Structure

/var/www/app
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ Dockerfile
β”œβ”€β”€ .env
β”œβ”€β”€ app/
β”œβ”€β”€ public/
└── storage/

This layout keeps everything contained, auditable, and easy to back up or migrate.


Dockerized Laravel (PHP-FPM)

Key decisions:

  • PHP 8.2
  • Minimal required extensions
  • Non-root container user
  • Logs sent to stderr
LOG_CHANNEL=stderr
LOG_LEVEL=debug

This ensures Laravel logs are Docker-friendly and production-correct.


Nginx Configuration (Private by Default)

Nginx is bound to localhost only:

ports:
  - "127.0.0.1:80:80"

Benefits:

  • No public web server ports
  • Reduced attack surface
  • Access only via Cloudflare Tunnel

Secure MariaDB Setup

The database runs inside Docker and is bound to localhost only:

ports:
  - "127.0.0.1:3306:3306"

Security benefits:

  • Database accessible only from the VPS
  • Safe for SSH tunneling
  • Zero public exposure

Secure Database Access with DBeaver

Administrative database access is handled through an SSH tunnel:

ssh -N -L 3307:127.0.0.1:3306 deploy@VPS_IP

DBeaver connects to:

  • Host: 127.0.0.1
  • Port: 3307

This allows full GUI access without exposing the database.


Cloudflare Tunnel for Public Access

Instead of opening ports 80 or 443, the application is exposed using Cloudflare Tunnel.

ingress:
  - hostname: app.example.com
    service: <http://localhost:80>

Benefits

  • No public IP exposure
  • Automatic HTTPS
  • Built-in DDoS protection
  • Zero-trust friendly

Laravel Logging Strategy

  • Laravel logs β†’ stderr
  • Docker captures logs
  • Inspect with:
docker logs laravel-app

Optional Upgrade

  • Grafana + Loki
  • Centralized log search
  • Error dashboards
  • Alerting

Centralized logging is optional and can be added later if needed.


Grafana Security (Optional)

If observability tools are enabled:

  • User registration disabled
  • Anonymous access disabled
  • Routed only through Cloudflare Tunnel

This keeps internal tools private.


Final Security Checklist

  • βœ” No public database ports
  • βœ” No public web ports
  • βœ” SSH key-only access
  • βœ” Root login disabled
  • βœ” Docker-native logging
  • βœ” Cloudflare Tunnel enabled

Final Thoughts

This Laravel VPS setup prioritizes security, simplicity, and realism.

It avoids unnecessary complexity while still following:

  • Modern DevOps practices
  • Docker-native deployment patterns
  • Zero-trust networking principles

It’s a setup I would confidently run in production and reuse for future projects.


[!tip] πŸ’‘ πŸ’Ό What this project demonstrates: β€’ Secure VPS configuration β€’ Docker-based Laravel deployment β€’ Cloudflare Tunnel usage β€’ Production-grade logging practices β€’ Real-world DevOps decision making