Technology Encyclopedia Home >How to Deploy a Laravel PHP App on a Cloud Server — Nginx, PHP-FPM, and MySQL

How to Deploy a Laravel PHP App on a Cloud Server — Nginx, PHP-FPM, and MySQL

Laravel development is pleasantly smooth — php artisan serve, everything works locally. Production is where things get interesting.

File permissions trip up most people first. Laravel needs to write to storage/ and bootstrap/cache/, but those folders need specific ownership to work with PHP-FPM. Get it wrong and you'll see 500 errors with no obvious cause.

Then there's the .env file, Composer dependencies, the application key, queue workers, scheduled tasks — each piece has a right way to set it up in production. I've debugged enough Laravel deployments to know where people get stuck, and this guide addresses each one.

The stack: Nginx, PHP-FPM, MySQL. Deploy from a GitHub repo. Production-ready in one go.

I run this on Tencent Cloud Lighthouse with Ubuntu 22.04. The 2 vCPU / 4 GB RAM plan handles a production Laravel app comfortably. Two Lighthouse features I use specifically for Laravel deployments: the snapshot feature lets me take a full server backup before running database migrations (Laravel migrations are reversible in theory, but having a full snapshot is better), and CBS cloud disk expansion means I can increase storage capacity as the app's uploaded files grow without provisioning a new server. For caching and queues, Redis runs efficiently on the same instance without needing a separate cache server.


Table of Contents

  1. What You're Building
  2. Prerequisites
  3. Part 1 — Server Setup
  4. Part 2 — Install PHP and Extensions
  5. Part 3 — Install Composer
  6. Part 4 — Set Up MySQL Database
  7. Part 5 — Deploy the Laravel App
  8. Part 6 — Configure Nginx
  9. Part 7 — Set File Permissions
  10. Part 8 — Enable HTTPS
  11. Part 9 — Set Up Queue Workers and Scheduler
  12. The Gotcha: Storage and Bootstrap Cache Permissions
  13. Common Commands

Key Takeaways

  • Set correct ownership: www-data:www-data for storage/ and bootstrap/cache/
  • Never commit .env to version control — it contains secrets
  • Run php artisan key:generate and php artisan migrate after deployment
  • Set up systemd services for queue workers to keep them running persistently
  • Add one cron entry * * * * * php artisan schedule:run for all scheduled tasks

What You're Building {#stack}

Internet → Nginx → PHP-FPM → Laravel
                ↓
           MySQL database
           Redis (optional, for cache/queue)

Prerequisites {#prerequisites}

Requirement Notes
Cloud server Tencent Cloud Lighthouse Ubuntu 22.04
PHP 8.2+ Laravel 11 requires PHP 8.2
Composer PHP dependency manager
MySQL Or PostgreSQL — both supported
A Laravel project on GitHub Or we'll create a fresh one

Part 1 — Server Setup {#part-1}

ssh ubuntu@YOUR_SERVER_IP
sudo apt update && sudo apt upgrade -y

sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enable

Part 2 — Install PHP and Extensions {#part-2}

Laravel 11 requires PHP 8.2 and specific extensions:

# Add PHP repository for latest versions
sudo apt install -y software-properties-common
sudo add-apt-repository ppa:ondrej/php -y
sudo apt update

# Install PHP 8.2 and required extensions
sudo apt install -y \
  php8.2-fpm \
  php8.2-mysql \
  php8.2-xml \
  php8.2-curl \
  php8.2-mbstring \
  php8.2-zip \
  php8.2-gd \
  php8.2-intl \
  php8.2-bcmath \
  php8.2-redis \
  nginx

# Verify
php8.2 --version

Check PHP-FPM socket path:

ls /run/php/
# php8.2-fpm.sock

Part 3 — Install Composer {#part-3}

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

composer --version

Part 4 — Set Up MySQL Database {#part-4}

sudo apt install -y mysql-server
sudo systemctl start mysql && sudo systemctl enable mysql
sudo mysql_secure_installation

Create database and user:

sudo mysql
CREATE DATABASE laravel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'laravel_user'@'localhost' IDENTIFIED BY 'choose_strong_password';
GRANT ALL PRIVILEGES ON laravel_db.* TO 'laravel_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Part 5 — Deploy the Laravel App {#part-5}

cd /var/www
sudo git clone https://github.com/your-username/your-laravel-app.git mylaravel
sudo chown -R ubuntu:www-data /var/www/mylaravel
cd /var/www/mylaravel

# Install PHP dependencies
composer install --no-dev --optimize-autoloader

# Create and configure .env
cp .env.example .env
nano .env

Update the .env file:

APP_NAME="My App"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_db
DB_USERNAME=laravel_user
DB_PASSWORD=your_password

CACHE_DRIVER=file
QUEUE_CONNECTION=database
SESSION_DRIVER=database
# Generate application key
php artisan key:generate

# Run migrations
php artisan migrate --force

# Optimize for production
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan optimize

Part 6 — Configure Nginx {#part-6}

sudo nano /etc/nginx/sites-available/mylaravel
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    root /var/www/mylaravel/public;
    index index.php index.html;

    access_log /var/log/nginx/mylaravel_access.log;
    error_log  /var/log/nginx/mylaravel_error.log;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Deny access to hidden files
    location ~ /\.(?!well-known).* {
        deny all;
    }

    # Cache static assets
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, no-transform";
    }
}
sudo ln -s /etc/nginx/sites-available/mylaravel /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Part 7 — Set File Permissions {#part-7}

Laravel needs write access to storage/ and bootstrap/cache/:

sudo chown -R ubuntu:www-data /var/www/mylaravel
sudo chmod -R 755 /var/www/mylaravel
sudo chmod -R 775 /var/www/mylaravel/storage
sudo chmod -R 775 /var/www/mylaravel/bootstrap/cache

Part 8 — Enable HTTPS {#part-8}

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

After enabling HTTPS, update .env:

APP_URL=https://yourdomain.com

Clear cache:

php artisan config:clear && php artisan config:cache

Part 9 — Set Up Queue Workers and Scheduler {#part-9}

Queue workers with Supervisor

sudo apt install -y supervisor

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

Task scheduler (cron)

crontab -e
# Add:
* * * * * cd /var/www/mylaravel && php artisan schedule:run >> /dev/null 2>&1

The Gotcha: Storage and Bootstrap Cache Permissions {#gotcha}

The most common Laravel error after deployment is a 500 error with this in the logs:

The stream or file "storage/logs/laravel.log" could not be opened: 
failed to open stream: Permission denied

This means Nginx (running as www-data) can't write to the storage directory.

Fix:

sudo chown -R ubuntu:www-data /var/www/mylaravel/storage
sudo chown -R ubuntu:www-data /var/www/mylaravel/bootstrap/cache
sudo chmod -R 775 /var/www/mylaravel/storage
sudo chmod -R 775 /var/www/mylaravel/bootstrap/cache

Also common: forgetting to run php artisan config:cache after changing .env. Laravel reads from the cache in production, so changes to .env don't take effect until you clear and rebuild the cache.

php artisan config:clear
php artisan config:cache

Common Commands {#commands}

# Deploy a new version
cd /var/www/mylaravel
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan optimize
sudo systemctl reload nginx

# Clear all caches
php artisan optimize:clear

# View logs
tail -f /var/www/mylaravel/storage/logs/laravel.log

# Queue status
sudo supervisorctl status

Troubleshooting {#troubleshooting}

Issue Likely Cause Fix
Connection refused Service not running or wrong port Check systemctl status SERVICE and verify firewall rules
Permission denied Wrong file ownership or permissions Check file ownership with ls -la and use chown/chmod to fix
502 Bad Gateway Backend service not running Restart the backend service; check logs with journalctl -u SERVICE
SSL certificate error Certificate expired or domain mismatch Run sudo certbot renew and verify domain DNS points to server IP
Service not starting Config error or missing dependency Check logs with journalctl -u SERVICE -n 50 for specific error
Out of disk space Logs or data accumulation Run df -h to identify usage; clean logs or attach CBS storage
High memory usage Too many processes or memory leak Check with htop; consider upgrading instance plan if consistently high
Firewall blocking traffic Port not open in UFW or Lighthouse console Open port in Lighthouse console firewall AND sudo ufw allow PORT

Frequently Asked Questions {#faq}

What PHP version does Laravel require?
Laravel 11 requires PHP 8.2+. Laravel 10 requires PHP 8.1+. Check composer.json in your project for the exact requirement. Use php -v on the server to verify the installed version.

Why do I get "permission denied" errors with Laravel?
Laravel needs write access to storage/ and bootstrap/cache/. Set correct ownership with sudo chown -R www-data:www-data /path/to/app and permissions with sudo chmod -R 775 storage bootstrap/cache.

How do I run Laravel queue workers in production?
Create a systemd service for php artisan queue:work. Set it to restart automatically. The queue worker must keep running — if it stops, queued jobs won't be processed.

What is APP_KEY in Laravel and how do I generate it?
APP_KEY is a 32-character encryption key used for secure cookies and encrypted data. Generate it with php artisan key:generate. Never commit it to version control.

How do I schedule Laravel tasks (cron) on a server?
Add one cron entry to run the Laravel scheduler every minute: * * * * * cd /path/to/app && php artisan schedule:run >> /dev/null 2>&1. Laravel handles the actual task scheduling logic internally.

Deploy your Laravel app today:
👉 Tencent Cloud Lighthouse — PHP-ready Ubuntu VPS
👉 View current pricing and promotions
👉 Explore all active deals and offers