Menu

How to Deploy Laravel on a VPS: DigitalOcean and Hostinger CLI Guide

How to Deploy Laravel on a VPS: DigitalOcean and Hostinger CLI Guide

This guide takes a fresh Ubuntu 22.04 VPS on DigitalOcean or Hostinger and ends with a Laravel app running in production: key-only SSH, fail2ban, a locked-down UFW firewall, Nginx (or Apache), free SSL, supervised queue workers and Laravel's caches warmed up. Every command is here, in the order I run them.

I wrote it as the guide I wish I'd had when I first set up production servers. Along the way I explain why each security step matters, so you understand your server instead of pasting commands blindly. The commands are nearly identical on DigitalOcean Droplets and Hostinger VPS plans.

If you'd rather not do it yourself, I also offer Laravel server deployment as a service.

Table of Contents

  1. Prerequisites
  2. Initial Server Access
  3. Create a Non-Root Sudo User
  4. Change the Default SSH Port (and Why It Matters)
  5. Disable Ping on Your Server IP (and Why You Should)
  6. Install and Configure fail2ban
  7. Configure the UFW Firewall
  8. Install the LEMP Stack (Nginx, PHP and MySQL)
  9. Configure an Nginx Virtual Host for Laravel
  10. Alternative: Apache Virtual Host for Laravel
  11. Deploy Your Laravel Application
  12. Install an SSL Certificate with Let's Encrypt
  13. Configure Laravel Queue Workers and the Scheduler
  14. Final Checks and Performance Tuning

1. Prerequisites

Have these ready before you start:

  • A fresh Ubuntu 22.04 LTS VPS on DigitalOcean or Hostinger. I'd use at least 1 GB of RAM for Laravel.
  • Your domain pointed at the server's IP address, with the A record set in your DNS.
  • Your Laravel project in a Git repository (GitHub, GitLab or Bitbucket).
  • SSH access. Both providers hand you root credentials when the server is first created.
  • A local terminal: Terminal on Mac or Linux, or WSL or PuTTY on Windows.

A note on versions: everything here uses Ubuntu 22.04 LTS (Jammy Jellyfish), and the commands also work on Ubuntu 20.04. On CentOS or Debian the package manager commands differ slightly.

2. Initial Server Access

When the VPS is created you get a root password (Hostinger) or you use the SSH key you added (DigitalOcean). Connect for the first time:

ssh root@YOUR_SERVER_IP

With a DigitalOcean SSH key, point SSH at the key file:

ssh -i ~/.ssh/your_key root@YOUR_SERVER_IP

The first job on a new server is updating every package. Don't skip it:

apt update && apt upgrade -y

That refreshes the package list and upgrades what's installed, security patches included. Running a server with outdated packages is like leaving the front door unlocked.

3. Create a Non-Root Sudo User

Working as root all day is risky. One typo, such as rm -rf /, can wipe the whole server with no warning and no confirmation. So create a separate user with sudo rights and turn off direct root login.

# Create a new user (replace 'zeeshan' with your username)
adduser zeeshan

# Add the user to the sudo group
usermod -aG sudo zeeshan

Copy your SSH keys over so the new user can log in:

# While still logged in as root
rsync --archive --chown=zeeshan:zeeshan ~/.ssh /home/zeeshan

Open a second terminal window and log in as the new user before you close the root session:

ssh zeeshan@YOUR_SERVER_IP

Check that sudo works:

sudo whoami
# Should output: root

Now disable root login over SSH. Open the SSH config:

sudo nano /etc/ssh/sshd_config

Find these lines and change them:

PermitRootLogin no
PasswordAuthentication no

With PasswordAuthentication no, only people holding an SSH key can log in, so nobody can brute-force a password. Save the file (Ctrl+X, then Y, then Enter) and restart SSH:

sudo systemctl restart sshd

4. Change the Default SSH Port (and Why It Matters)

SSH listens on port 22 by default, and everyone knows it. Within minutes of a server going online, bots from all over the world start hammering port 22 with common usernames and passwords. It isn't theoretical. Check the auth log on any fresh server after about ten minutes:

sudo cat /var/log/auth.log | grep "Failed password"

You'll see hundreds of failed attempts from IPs everywhere. A different SSH port won't make you invincible, but most automated attacks scan port 22 specifically, so moving it removes the bulk of them. It's a small change that shrinks your attack surface right away.

How to Change the SSH Port

Open the SSH daemon config again:

sudo nano /etc/ssh/sshd_config

Find this line:

#Port 22

Uncomment it and set a port between 1024 and 65535. Pick something non-obvious, and skip popular alternatives like 2222 because bots scan those too:

Port 4827

Before restarting SSH, allow the new port in UFW, or you'll lock yourself out:

sudo ufw allow 4827/tcp

Now restart SSH:

sudo systemctl restart sshd

Open a new terminal and test the new port before you close the current session:

ssh -p 4827 zeeshan@YOUR_SERVER_IP

Once that works, close the old session. From now on connect with -p 4827 (or whichever port you chose). To avoid typing it every time, add the server to your SSH config on your own machine:

# On your LOCAL machine, edit ~/.ssh/config
Host myserver
    HostName YOUR_SERVER_IP
    User zeeshan
    Port 4827
    IdentityFile ~/.ssh/your_key

After that, ssh myserver is all you need.

Further reading: SSH Academy on changing the SSH port.

5. Disable Ping on Your Server IP (and Why You Should)

When someone pings your IP and gets a reply, they learn two things: the address is live and a server is running there. That's the first step of most network reconnaissance, and bots run ping sweeps across whole IP ranges to find live servers. If you drop ICMP echo requests, your server stays quiet to those scans.

It won't stop someone who already knows your IP and is determined. It does cut your exposure to opportunistic scanners, and a server that doesn't need to answer public diagnostic pings has no reason to keep replying.

How to Block Ping with UFW

Edit UFW's before.rules file:

sudo nano /etc/ufw/before.rules

Near the top, find this section:

# ok icmp codes for INPUT
-A ufw-before-input -p icmp --icmp-type destination-unreachable -j ACCEPT
-A ufw-before-input -p icmp --icmp-type time-exceeded -j ACCEPT
-A ufw-before-input -p icmp --icmp-type parameter-problem -j ACCEPT
-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT

Change the last line from ACCEPT to DROP:

-A ufw-before-input -p icmp --icmp-type echo-request -j DROP

Save the file and reload UFW:

sudo ufw reload

Test from your local machine:

ping YOUR_SERVER_IP
# Should now show: Request timeout

Leave destination-unreachable and time-exceeded on ACCEPT. TCP/IP routing and MTU discovery need them. Only the echo request gets dropped.

6. Install and Configure fail2ban

A custom SSH port and key-only login already do a lot, but I still run fail2ban. It watches your log files in real time and bans IP addresses that behave badly, such as repeated failed SSH logins or a flood of 404 errors on your web server.

Think of it as an automated bouncer. After a set number of failures inside a time window, fail2ban adds an iptables rule that blocks that IP for a while. That protects SSH, and also your web app, mail and any other service you point it at.

Install fail2ban

sudo apt install fail2ban -y

Configure fail2ban

Don't edit /etc/fail2ban/jail.conf directly, because updates overwrite it. Make a local copy instead:

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local

Find the [DEFAULT] section and set these values:

[DEFAULT]
# Ban an IP for 1 hour (in seconds)
bantime  = 3600

# Look for failures within a 10-minute window
findtime  = 600

# Ban after 5 failed attempts
maxretry = 5

# Your own IP to never ban (replace with your home/office IP)
ignoreip = 127.0.0.1/8 ::1 YOUR_HOME_IP

Then find the [sshd] section and enable it with your custom port:

[sshd]
enabled = true
port    = 4827
logpath = %(sshd_log)s
backend = %(sshd_backend)s

To have fail2ban guard Nginx against brute-force attempts too:

[nginx-http-auth]
enabled = true

[nginx-limit-req]
enabled = true
logpath = /var/log/nginx/error.log

Start it and enable it at boot:

sudo systemctl start fail2ban
sudo systemctl enable fail2ban

Check the status and the active jails:

sudo fail2ban-client status
sudo fail2ban-client status sshd

If you ever ban yourself by accident, unban your IP like this:

sudo fail2ban-client set sshd unbanip THE_IP_ADDRESS

Further reading: the fail2ban manual.

7. Configure the UFW Firewall

UFW (Uncomplicated Firewall) is Ubuntu's standard firewall tool. The rule is simple: deny everything by default, then allow only what you need.

# Set default rules
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow your custom SSH port (use whatever port you chose in Step 4)
sudo ufw allow 4827/tcp

# Allow HTTP and HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Enable the firewall
sudo ufw enable

# Check status
sudo ufw status verbose

You should see only ports 4827, 80 and 443 open, and nothing else. If an outside service needs to reach MySQL, open port 3306 for one trusted IP only:

sudo ufw allow from TRUSTED_IP to any port 3306

Never open port 3306 to the whole internet. It's a common mistake and a serious one.

8. Install the LEMP Stack (Nginx, PHP and MySQL)

LEMP stands for Linux, Nginx, MySQL (or MariaDB) and PHP. It's the standard stack for deploying Laravel on a VPS.

Install Nginx

sudo apt install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx

Install PHP and the Required Extensions

Laravel 10 and newer need PHP 8.1 or higher. I'll install PHP 8.2 with the extensions Laravel uses:

# Add the Ondrej PHP PPA for current PHP versions
sudo apt install software-properties-common -y
sudo add-apt-repository ppa:ondrej/php -y
sudo apt update
# Install PHP 8.2 and Laravel's required extensions
sudo apt install php8.2 php8.2-fpm php8.2-mysql php8.2-mbstring php8.2-xml \
php8.2-bcmath php8.2-curl php8.2-zip php8.2-gd php8.2-intl php8.2-redis \
php8.2-tokenizer php8.2-fileinfo -y

Confirm PHP is installed:

php -v

Turn on OPcache next. It keeps compiled PHP in memory, so PHP runs noticeably faster. Open the FPM config:

sudo nano /etc/php/8.2/fpm/php.ini

Find these OPcache settings and set them:

opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.revalidate_freq=2
opcache.fast_shutdown=1

Install MySQL

sudo apt install mysql-server -y
sudo mysql_secure_installation

The secure installation script asks you to set a root password, remove anonymous users, disable remote root login and drop the test database. Answer yes to all of them.

Then create a database and a user for your Laravel app:

sudo mysql -u root -p
-- Inside MySQL shell
CREATE DATABASE your_app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'your_app_user'@'localhost' IDENTIFIED BY 'StrongPassword123!';
GRANT ALL PRIVILEGES ON your_app_db.* TO 'your_app_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Install Composer

curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
composer --version

Install Git

sudo apt install git -y

9. Configure an Nginx Virtual Host for Laravel

This is one of the most important steps in a Laravel deployment. A correct Nginx server block points the document root at Laravel's public/ directory, hands PHP to PHP-FPM, and sends every request through index.php. The root matters most: if you point it at the project folder instead, your .env and source code are exposed to the web.

If you see a 403 Forbidden or a 404 right after setup, check the root line and the try_files rule first.

Create a server block for your domain:

sudo nano /etc/nginx/sites-available/yourdomain.com

Paste in this complete configuration:

server {
    listen 80;
    listen [::]:80;

    server_name yourdomain.com www.yourdomain.com;

    # IMPORTANT: the document root points to Laravel's public/ directory
    # Never point this to your project root
    root /var/www/yourdomain.com/public;

    index index.php index.html;

    # Security headers: protect against common web attacks
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header X-XSS-Protection "1; mode=block";
    add_header Referrer-Policy "strict-origin-when-cross-origin";

    # Hide the Nginx version from response headers
    server_tokens off;

    # Maximum upload size: match your php.ini setting
    client_max_body_size 64M;

    # Laravel's front controller pattern
    # All requests that don't match a real file go to index.php
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # Block access to hidden files (like .env, .git)
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }

    # PHP-FPM configuration
    # A Unix socket is faster than TCP (127.0.0.1:9000)
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;

        # Pass the real IP to PHP (useful for logging and rate limiting)
        fastcgi_param REMOTE_ADDR $remote_addr;
    }

    # Cache static assets aggressively
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Don't log favicon or robots.txt requests
    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_log  /var/log/nginx/yourdomain.com_error.log;
    access_log /var/log/nginx/yourdomain.com_access.log;
}

Why These Configuration Choices Matter

  • root /var/www/.../public: pointing at public/ keeps your .env file, application code and vendor/ directory out of reach from the web. This is basic Laravel security.
  • Security headers: X-Frame-Options prevents clickjacking and X-Content-Type-Options stops MIME sniffing. They cost nothing and protect your users.
  • Unix socket instead of TCP: unix:/var/run/php/php8.2-fpm.sock is faster than 127.0.0.1:9000 because it skips the TCP stack. Use the socket whenever Nginx and PHP-FPM share a server, which is almost always.
  • location ~ /\.: this blocks every hidden file and folder. Without it, someone could open yourdomain.com/.env directly.

Enable the site with a symlink and test the configuration:

# Enable the site
sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/

# Test the Nginx config for syntax errors: always do this before reloading
sudo nginx -t

# If the test passes, reload Nginx
sudo systemctl reload nginx

Always run sudo nginx -t before a reload. Reloading a broken config can take your server down.

10. Alternative: Apache Virtual Host for Laravel

If your environment runs Apache (common on cPanel and some Hostinger plans), here's the virtual host setup. The main difference from Nginx is that Apache uses .htaccess files for URL rewriting, and Laravel already ships one in public/.

Enable the Apache modules you need:

sudo a2enmod rewrite
sudo a2enmod headers
sudo a2enmod ssl
sudo systemctl restart apache2

Install PHP for Apache:

sudo apt install libapache2-mod-php8.2 -y

Create the virtual host file:

sudo nano /etc/apache2/sites-available/yourdomain.com.conf
<VirtualHost *:80>
    ServerName yourdomain.com
    ServerAlias www.yourdomain.com

    # Document root points to Laravel's public/ directory
    DocumentRoot /var/www/yourdomain.com/public

    # Security: hide the Apache version
    ServerTokens Prod
    ServerSignature Off

    # Security headers
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-XSS-Protection "1; mode=block"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"

    <Directory /var/www/yourdomain.com/public>
        # AllowOverride All is required for Laravel's .htaccess to work
        # Without it, URL rewriting breaks and you get 404 errors
        AllowOverride All
        Require all granted

        Options -Indexes -MultiViews
    </Directory>

    # Block access to sensitive files
    <FilesMatch "^\.env|composer\.(json|lock)$">
        Require all denied
    </FilesMatch>

    ErrorLog ${APACHE_LOG_DIR}/yourdomain.com_error.log
    CustomLog ${APACHE_LOG_DIR}/yourdomain.com_access.log combined
</VirtualHost>

Why AllowOverride All Matters for Laravel on Apache

Laravel's public/.htaccess sends every request to index.php so the router can handle it. If AllowOverride All is missing, Apache ignores that file completely, and every route except the homepage returns a 404. It's the most common Laravel and Apache misconfiguration I see.

Enable the site:

sudo a2ensite yourdomain.com.conf

# Disable the default site if you haven't already
sudo a2dissite 000-default.conf

# Test the Apache config
sudo apache2ctl configtest

# Reload Apache
sudo systemctl reload apache2

11. Deploy Your Laravel Application

Now the app itself. Create the web directory and clone your repository:

# Create the directory for your app
sudo mkdir -p /var/www/yourdomain.com

# Give your user ownership
sudo chown -R $USER:$USER /var/www/yourdomain.com

# Clone your repository
cd /var/www
git clone https://github.com/yourusername/your-repo.git yourdomain.com

cd yourdomain.com

Install Composer Dependencies

# --no-dev skips development dependencies (testing tools, debugbars, etc.)
# --optimize-autoloader generates a faster class autoloader for production
composer install --optimize-autoloader --no-dev

Set Up the Environment File

cp .env.example .env
nano .env

Set these values in your .env:

APP_NAME="Your App Name"
APP_ENV=production
APP_KEY=                  # Leave blank, generate below
APP_DEBUG=false           # CRITICAL: always false in production
APP_URL=https://yourdomain.com

LOG_CHANNEL=stack
LOG_LEVEL=error           # Only log errors in production, not debug info

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_app_db
DB_USERNAME=your_app_user
DB_PASSWORD=StrongPassword123!

CACHE_DRIVER=file         # Change to redis if you have Redis installed
QUEUE_CONNECTION=database # Or redis for better performance
SESSION_DRIVER=file

Generate the application key:

php artisan key:generate

Set the Correct File Permissions

This is where a lot of deployments break. The web server runs as www-data on Ubuntu, and it needs write access to storage/ and bootstrap/cache/. Nothing else should be writable by it:

# Set ownership: your user owns the files, www-data is the group
sudo chown -R $USER:www-data /var/www/yourdomain.com

# Directories need execute permission to be traversable
sudo find /var/www/yourdomain.com -type d -exec chmod 755 {} \;

# Files should be readable but not executable
sudo find /var/www/yourdomain.com -type f -exec chmod 644 {} \;

# Storage and cache must be writable by www-data
sudo chmod -R 775 /var/www/yourdomain.com/storage
sudo chmod -R 775 /var/www/yourdomain.com/bootstrap/cache

Run the Database Migrations

php artisan migrate --force

Laravel asks for confirmation before migrating in production, and --force skips that prompt. Only use it when you're sure about the migrations you're running.

Seed the Database (if needed)

php artisan db:seed --force

Laravel Performance Optimizations

Run all of these on every deployment. They cache your routes, config and views so Laravel doesn't parse them again on each request:

# Cache configuration: reads .env once and caches the result
php artisan config:cache

# Cache routes: pre-compiles routes/web.php and routes/api.php
php artisan route:cache

# Cache views: pre-compiles Blade templates
php artisan view:cache

# Generate an optimized autoloader
composer dump-autoload --optimize

One thing to remember: after config:cache, Laravel stops reading your .env file. Whenever you change a value there, run php artisan config:cache again, or clear the cache first with php artisan config:clear.

Create the storage symlink so uploaded files can be served publicly:

php artisan storage:link

12. Install an SSL Certificate with Let's Encrypt

There's no good reason to run a production app without HTTPS today. Let's Encrypt gives you free certificates with automatic renewal, and Google also favors HTTPS sites in search.

# Install Certbot and the Nginx plugin
sudo apt install certbot python3-certbot-nginx -y
# Obtain and automatically configure SSL for your domain
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot asks for your email address (for renewal reminders) and whether to redirect HTTP to HTTPS. Choose option 2, Redirect. It then updates your Nginx config to serve HTTPS and redirect all HTTP traffic.

On Apache, use the Apache plugin instead:

sudo apt install python3-certbot-apache -y
sudo certbot --apache -d yourdomain.com -d www.yourdomain.com

Verify Auto-Renewal

Let's Encrypt certificates expire after 90 days. Certbot installs a systemd timer that renews them for you. Test it:

sudo certbot renew --dry-run

If the dry run succeeds, renewal is set up correctly.

Update the Laravel APP_URL

With HTTPS live, make sure your .env matches:

APP_URL=https://yourdomain.com

If your app sits behind a proxy or load balancer (common with DigitalOcean Load Balancers), trust the proxy. On older Laravel versions, add this to the App\Http\Middleware\TrustProxies middleware or the trusted proxies config:

# In config/trustedproxy.php or TrustProxies middleware
protected $proxies = '*';

On Laravel 11 and newer, set it in bootstrap/app.php with $middleware->trustProxies(at: '*'); instead. Without it, request()->secure() returns false even over HTTPS, and that can cause mixed content warnings.

13. Configure Laravel Queue Workers and the Scheduler

If your app sends email, processes images or does any other background work, you're using queues. Queue workers are long-running processes, so they have to survive reboots and restart on their own if they crash. Supervisor is the standard tool for that.

Install Supervisor

sudo apt install supervisor -y

Configure a Queue Worker

sudo nano /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/yourdomain.com/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/yourdomain.com/storage/logs/worker.log
stopwaitsecs=3600

What the key settings do:

  • numprocs=2 runs two worker processes. Raise it if your queue is busy.
  • --max-time=3600 makes a worker exit cleanly after one hour, which stops memory from building up in long-running processes. Supervisor starts a new one right away.
  • --tries=3 retries a failed job three times before marking it as failed.
  • autorestart=true means Supervisor restarts the worker if it crashes.

Load the config and start the workers:

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
# Check status
sudo supervisorctl status

Configure the Laravel Scheduler

Laravel's task scheduler runs from a single cron entry. Open the crontab for www-data:

sudo crontab -e -u www-data

Add this line:

* * * * * cd /var/www/yourdomain.com && php artisan schedule:run >> /dev/null 2>&1

Cron runs it every minute, and Laravel decides internally which scheduled tasks are due.

14. Final Checks and Performance Tuning

Verify Your Deployment

# Check PHP-FPM is running
sudo systemctl status php8.2-fpm

# Check Nginx is running
sudo systemctl status nginx

# Check MySQL is running
sudo systemctl status mysql

# Check Supervisor workers are running
sudo supervisorctl status

# Check fail2ban is running
sudo systemctl status fail2ban

# View your UFW rules
sudo ufw status verbose

Test Your Application

# Test that Laravel can connect to the database
php artisan tinker
# Inside Tinker:
# DB::connection()->getPdo();
# Exit with Ctrl+C
# Check for any configuration errors
php artisan about

Set Up Log Rotation

Laravel logs can grow large in production. Rotate them so they don't fill your disk:

sudo nano /etc/logrotate.d/laravel
/var/www/yourdomain.com/storage/logs/*.log {
    daily
    missingok
    rotate 14
    compress
    notifempty
    create 0664 www-data www-data
    sharedscripts
}

Check Server Response Time

Once everything is in place, measure how fast the server answers:

# On a Linux terminal:
curl -o /dev/null -s -w "Time to first byte: %{time_starttransfer}s\n" https://yourdomain.com
# On Windows PowerShell:
curl.exe -o null -s -w "Time to first byte: %{time_starttransfer}s\n" https://yourdomain.com

With OPcache, config caching and route caching on, a properly sized VPS should answer in well under 300 ms.

Monitor Your Server

A few commands worth keeping handy:

# Real-time server resource usage
htop

# Check disk usage
df -h

# Check memory usage
free -h

# Watch Nginx access logs in real time
sudo tail -f /var/log/nginx/yourdomain.com_access.log

# Watch Laravel logs in real time
tail -f /var/www/yourdomain.com/storage/logs/laravel.log

# Check fail2ban banned IPs
sudo fail2ban-client status sshd

Further reading:

Wrapping Up

Your Laravel app is now running on a production VPS with a hardened setup: SSH for a non-root user only, a custom SSH port, no ping replies, fail2ban watching the logs, a tight firewall, an Nginx or Apache virtual host, free SSL, supervised queue workers and Laravel's caches in place.

That's a production-grade server, not a stopgap. A plain upload-and-hope deployment can get compromised within a week. A server set up like this can run reliably for years.

If a step gave you trouble, or you'd rather have someone else handle the deployment, that's the work I do. See my Laravel and MERN stack server deployment service, or hire me directly on Upwork for a fast, fully documented setup. If something breaks on a live server, my maintenance and bug fixing service covers that too.

Muhammad Zeeshan

Muhammad Zeeshan

Full Stack Developer

I'm a full-stack developer in Islamabad, Pakistan, with 3+ years of experience building web applications with Laravel, PHP, React.js, and Node.js. I build REST APIs, tune MySQL for speed, and deploy to production on VPS servers, so one developer can take a project from idea to live. I currently work in a government IT Organization and take on freelance and contract projects alongside it. You can hire me directly or through Upwork for remote work with companies worldwide.