Skip to content
KhaiziNam Blog KhaiziNam Blog
Go back
Đọc bằng tiếng Việt

Deploy PHP, Laravel, CodeIgniter on Linux VPS with Nginx + MySQL Docker + SSL 2026

A complete step-by-step guide to installing PHP 8.2, Nginx, MySQL via Docker, configuring PHP-FPM, UFW firewall, and Let’s Encrypt SSL to deploy plain PHP, Laravel, or CodeIgniter on an Ubuntu VPS from scratch.

You just spun up a Linux VPS and you’re staring at a blank terminal not knowing where to start? Shared hosting works fine for simple PHP — but the moment you need to control PHP versions, configure MySQL your way, or run a Laravel queue worker, shared hosting falls short. This guide goes straight to hands-on: from your first SSH login to your domain running HTTPS with PHP 8.2, Nginx, MySQL (via Docker), and auto-renewing SSL.

Table of Contents:


1. Why Nginx Instead of Apache for PHP on a VPS?

Apache has been the default web server for PHP for decades — and it still works well — but Nginx has a clear advantage on resource-constrained VPS environments. Nginx handles concurrent requests using an event-driven asynchronous model, consuming significantly less RAM than Apache’s process-per-request model under load. On a 1–2GB RAM VPS, this difference is substantial.

See more: How to Deploy Node.js and React Apps on Linux Hosting with Nginx Reverse Proxy

Nginx doesn’t execute PHP directly — it delegates PHP processing to PHP-FPM (FastCGI Process Manager) over a Unix socket or TCP port. This separation means:


2. Prerequisites


3. Step 1 — SSH In, Update the Server, and Enable UFW Firewall

3.1. SSH into the server

# SSH with password (first login) ssh root@YOUR_SERVER_IP

SSH with key (recommended for production)

ssh -i ~/.ssh/your_key.pem ubuntu@YOUR_SERVER_IP

3.2. Update the server and install essential tools

# Update all packages sudo apt update && sudo apt upgrade -y

Install essential tools

sudo apt install -y curl git unzip software-properties-common apt-transport-https ca-certificates gnupg

3.3. Configure UFW firewall

# Enable UFW sudo ufw enable

Allow SSH (critical — missing this step will lock you out)

sudo ufw allow ssh sudo ufw allow 22/tcp

Allow HTTP and HTTPS

sudo ufw allow ‘Nginx Full’

Check status

sudo ufw status verbose

Critical warning: Always run sudo ufw allow ssh BEFORE sudo ufw enable. Enabling UFW without opening the SSH port first will completely lock you out of the server — you’ll need to use your VPS provider’s console to recover.


4. Step 2 — Install PHP 8.2 and Required Extensions

4.1. Add PPA and install PHP 8.2

# Add Ondřej Surý’s PHP PPA (most trusted source for up-to-date PHP on Ubuntu) sudo add-apt-repository ppa:ondrej/php -y sudo apt update

Install PHP 8.2 and PHP-FPM

sudo apt install -y php8.2 php8.2-fpm

Verify version

php -v

4.2. Install required PHP extensions

# Full extension set for plain PHP, Laravel, and CodeIgniter sudo apt install -y \ php8.2-cli \ php8.2-common \ php8.2-mysql \ php8.2-pgsql \ php8.2-sqlite3 \ php8.2-xml \ php8.2-xmlrpc \ php8.2-curl \ php8.2-gd \ php8.2-imagick \ php8.2-mbstring \ php8.2-zip \ php8.2-bcmath \ php8.2-intl \ php8.2-redis \ php8.2-opcache

Start and enable PHP-FPM

sudo systemctl enable php8.2-fpm sudo systemctl start php8.2-fpm sudo systemctl status php8.2-fpm

4.3. Install Composer

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


5. Step 3 — Install Nginx

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

Remove the default config — you’ll create per-site configs instead

sudo rm /etc/nginx/sites-enabled/default

At this point, visiting http://YOUR\_SERVER\_IP in a browser should show the Nginx welcome page, confirming it’s running correctly.


6. Step 4 — Install MySQL via Docker (and Why You Should)

You can install MySQL directly with apt install mysql-server — that works perfectly fine. However, running MySQL inside a Docker container has practical advantages, especially when managing multiple projects on a single server:

6.1. Install Docker

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg —dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

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

sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

sudo usermod -aG docker $USER newgrp docker docker —version

6.2. Run the MySQL 8 container

# Create a named volume so data survives container restarts docker volume create mysql8_data

Run MySQL 8

- Binds port 33060 on localhost only (not exposed to the internet)

- Data persisted in docker volume

- Restarts automatically on server reboot

docker run -d \ —name mysql8 \ -e MYSQL_ROOT_PASSWORD=your_strong_root_password \ -e MYSQL_DATABASE=your_db_name \ -e MYSQL_USER=your_db_user \ -e MYSQL_PASSWORD=your_db_password \ -p 127.0.0.1:33060:3306 \ -v mysql8_data:/var/lib/mysql \ —restart always \ mysql:8.0

Important: The -p 127.0.0.1:33060:3306 flag binds MySQL only to the loopback interface — only processes on the same server can connect. Never use 0.0.0.0:3306 on a production server as it exposes MySQL to the public internet.

6.3. Create a database and user per project

# Connect into the MySQL container docker exec -it mysql8 mysql -u root -p

Inside the MySQL shell

CREATE DATABASE laravel_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER ‘laravel_user’@’%’ IDENTIFIED BY ‘strong_password_here’; GRANT ALL PRIVILEGES ON laravel_app.* TO ‘laravel_user’@’%’; FLUSH PRIVILEGES; EXIT;

In your Laravel / CodeIgniter .env file:

DB_HOST=127.0.0.1 DB_PORT=33060 DB_DATABASE=laravel_app DB_USERNAME=laravel_user DB_PASSWORD=strong_password_here


7. Step 5 — Configure Nginx Server Block for PHP

Nginx doesn’t process PHP — it hands PHP requests to PHP-FPM via fastcgi_pass. This is the PHP equivalent of proxy_pass for Node.js, but using the FastCGI protocol instead of HTTP.

7.1. Create the web directory and upload code

sudo mkdir -p /var/www/yourdomain.com sudo chown -R $USER:www-data /var/www/yourdomain.com sudo chmod -R 755 /var/www/yourdomain.com

cd /var/www/yourdomain.com git clone https://github.com/youruser/your-php-project.git .

7.2. Nginx config for plain PHP

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

Paste the following. This is the HTTP port 80 config — Certbot will add the HTTPS section in the next step:

server { listen 80; listen [::]:80; server_name yourdomain.com www.yourdomain.com;

root /var/www/yourdomain.com;
index index.php index.html index.htm;

access\_log /var/log/nginx/yourdomain.access.log;
error\_log  /var/log/nginx/yourdomain.error.log;

location / {
    try\_files $uri $uri/ =404;
}

# Pass all .php requests to PHP-FPM
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;
}

location ~ /\\.ht  { deny all; }
location ~ /\\.env { deny all; }

}

7.3. Nginx config for Laravel

server { listen 80; listen [::]:80; server_name yourdomain.com www.yourdomain.com;

# Laravel: document root must point to the public/ subdirectory
root /var/www/yourdomain.com/public;
index index.php index.html;

access\_log /var/log/nginx/yourdomain.access.log;
error\_log  /var/log/nginx/yourdomain.error.log;

location / {
    # Laravel routing depends on this line
    try\_files $uri $uri/ /index.php?$query\_string;
}

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;
}

location ~ /\\.ht  { deny all; }
location ~ /\\.env { deny all; }

location ~\* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
    expires 1y;
    add\_header Cache-Control "public, immutable";
}

}

7.4. Enable the site and test

sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx


8. Step 6 — Enable Free HTTPS with Certbot + Let’s Encrypt

Certbot reads your existing Nginx config, adds an HTTPS server block for port 443, and configures an HTTP-to-HTTPS redirect — no manual Nginx editing required.

sudo apt install -y certbot python3-certbot-nginx

sudo certbot —nginx -d yourdomain.com -d www.yourdomain.com

Follow the prompts:

1. Enter your email for renewal notifications

2. Agree to the Terms of Service (A)

3. Select redirect HTTP to HTTPS (option 2) — recommended

# Test auto-renewal (dry run — does not affect the real certificate) sudo certbot renew —dry-run

Verify the systemd renewal timer is active

sudo systemctl status certbot.timer


9. Step 7 — Deploy Laravel and CodeIgniter

9.1. Complete Laravel setup

cd /var/www/yourdomain.com composer install —optimize-autoloader —no-dev cp .env.example .env nano .env php artisan key:generate php artisan migrate —force sudo chown -R www-data:www-data storage bootstrap/cache sudo chmod -R 775 storage bootstrap/cache php artisan config:cache php artisan route:cache php artisan view:cache

9.2. Run Laravel Queue Worker 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/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/log/supervisor/laravel-worker.log stopwaitsecs=3600

sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl start laravel-worker:*


10. Common Errors and Fixes

502 Bad Gateway: Nginx cannot communicate with PHP-FPM. Check that PHP-FPM is running (sudo systemctl status php8.2-fpm) and verify the socket path in your Nginx config (/var/run/php/php8.2-fpm.sock) matches the actual socket file (ls /var/run/php/).

403 Forbidden: Nginx can’t read your files due to wrong ownership. Fix with sudo chown -R www-data:www-data /var/www/yourdomain.com and ensure directories are 755 and files are 644.

404 on all Laravel/CodeIgniter routes: Missing or incorrect try_files $uri $uri/ /index.php?$query_string directive. Framework routing depends on this to forward all non-static requests to index.php.

PHP can’t connect to MySQL Docker: Verify the port in .env matches the mapped port in your docker run command. Check the container is running with docker ps. Test the connection manually: mysql -h 127.0.0.1 -P 33060 -u your_user -p.

Certbot: “Could not automatically find a matching server block”: Your Nginx config’s server_name doesn’t match the domain you passed to Certbot. Check /etc/nginx/sites-available/yourdomain.com and ensure server_name is correct.

Laravel: “No application encryption key has been specified”: You haven’t run php artisan key:generate, or the .env file is missing the APP_KEY variable.

Permission denied on storage/ or bootstrap/cache/: Run sudo chown -R www-data:www-data storage bootstrap/cache && sudo chmod -R 775 storage bootstrap/cache from the Laravel root directory.


FAQ — Frequently Asked Questions

Should I install MySQL directly or use Docker on a VPS?

Both work. A direct install is simpler if you only have one project. Docker makes more sense when managing multiple projects that need different MySQL versions, or when you want to keep the OS clean and simplify data backup and migration.

What’s the difference between a Unix socket and a TCP port for PHP-FPM?

A Unix socket (unix:/var/run/php/php8.2-fpm.sock) is faster than TCP because it communicates through the kernel without going through the network stack. Use Unix sockets when Nginx and PHP-FPM are on the same server — this is the standard setup. Use TCP (127.0.0.1:9000) only when PHP-FPM runs on a separate server.

Can I run multiple PHP versions on the same server?

Yes. Install PHP 7.4 and PHP 8.2 in parallel from Ondřej’s PPA — each version gets its own PHP-FPM socket. In each site’s Nginx config, simply point fastcgi_pass to the correct socket: php7.4-fpm.sock or php8.2-fpm.sock.

What’s the deployment process for new code?

For plain PHP: upload files via SFTP or run git pull — no server restart needed. For Laravel: git pull && composer install —no-dev && php artisan migrate —force && php artisan config:cache && php artisan route:cache. If using queue workers: sudo supervisorctl restart laravel-worker:* after deployment.

What happens when my Let’s Encrypt certificate expires?

Nothing manual is required. Certbot installs a systemd timer that runs certbot renew twice daily. Certificates are renewed automatically when less than 30 days remain. To verify: sudo certbot renew —dry-run.

Conclusion — Your PHP Production Stack Is Ready

You now have a complete PHP production stack: PHP 8.2 with all required extensions running through PHP-FPM, Nginx as the web server with FastCGI pass, MySQL 8 isolated in a Docker container with persistent volume storage, UFW firewall controlling inbound traffic, and auto-renewing HTTPS via Let’s Encrypt. This stack handles plain PHP, Laravel, CodeIgniter, and any other PHP framework.

Next step: set up automated CI/CD with GitHub Actions — SSH into the server, pull the latest code, run composer install and php artisan migrate automatically on every merge to main. Automating deployments from day one eliminates manual errors and reduces each update to a matter of seconds.

See more: How to Deploy Node.js and React Apps on Linux Hosting with Nginx Reverse Proxy


Share this post:

Related Posts

Secrets to Deploying React/Node.js on a 1GB RAM VPS with PM2: Never Run Out of Memory 2026

For web developers, owning a 1GB RAM VPS at a low cost is an ideal starting point. However, in practice, deployment often turns into a "nightmare" when applications constantly crash for no apparent reason. The core of the problem lies in the following two key factors:

How to Deploy Node.js and React Apps on Linux Hosting with Nginx Reverse Proxy (PM2 + SSL Guide 2025)

You built a Node.js API or React app and now you're staring at a blank Linux VPS wondering how to actually get it live. Uploading files to shared hosting won't work here — Node.js needs a process manager, a reverse proxy, and proper server configuration to run reliably in production. This guide walks you through the complete deployment stack: PM2 to keep your app alive

Optimizing Project Environment Setup Time with Docker for Low-Config VPS

How to optimize project environment setup time with Docker for a 2-Core 4GB RAM VPS. Guide to the smoothest Nginx, PHP, MySQL, and Redis configuration. Check it out now!

IT Fresher & Junior Salary 2026: PHP, Node.js, React, Flutter - Real Market Data

Real salary benchmarks for IT freshers and junior developers in Vietnam in 2026, broken down by tech stack - so you know exactly what number to quote in interviews, or whether you're being underpaid right now.

IT Salary Levels in Ho Chi Minh City and Hanoi (2025 - 2026 Overview)

The IT industry in Vietnam continues to maintain a higher salary range compared to many other industries, especially in Ho Chi Minh City and Hanoi. These two cities are currently the country’s largest technology recruitment hubs, attracting outsourcing companies, product-based firms, fintech businesses, AI startups, and international tech corporations.

Salary Negotiation for Fresh IT Graduates: Stop Leaving Money on the Table

Salary negotiation for IT freshers is the process of discussing and adjusting your compensation after receiving a job offer — a step that most new graduates skip entirely because they believe "freshers have no bargaining power." The reality is the opposite: nearly every company builds a negotiation buffer into their offers even for entry-level candidates, and not negotiating means you're leaving m

Junior Developer Portfolio: What to Include to Get Interview Calls

A junior developer portfolio is the collection of real projects, skills, and professional information you present so employers can evaluate your abilities — substituting for the work experience section of your CV that's currently empty. For freshers and junior developers, a portfolio isn't something that's "nice to have" — it's the only evidence you can offer to prove you can actually build things

How to Write a Junior Developer Cover Letter With No Experience

Learn how to write a junior developer cover letter with no experience, including practical templates, real examples, common mistakes, and tips to pass the CV screening round.

I Failed My IT Job Interview — Here's What I Changed to Get Hired

Getting rejected from an IT job interview is something almost every fresher and junior developer goes through at least once — usually more. That first rejection can make you question your abilities, but in the vast majority of cases, interview failures have specific, identifiable, fixable causes that can be addressed in a matter of weeks if you know where to look.

Body Language in Tech Interviews: 7 Mistakes That Cost You the Offer

Body language in tech interviews refers to the non-verbal signals — posture, eye contact, hand gestures, and vocal tone — that recruiters observe alongside your technical answers. Mastering these signals helps you project confidence and professionalism from the very first second you walk into the room.


Previous Post
Secrets to Deploying React/Node.js on a 1GB RAM VPS with PM2: Never Run Out of Memory 2026
Next Post
Cheap AI Accounts: The Dark Side of MMO Reselling and 4 Hidden Risks You Need to Know (2026)