Advanced & Career
Best practices, real-world case studies, deployment checklists, a full glossary, learning roadmap, and interview preparation.
Section 13 — Best Practices
13.1 Server Hygiene
Think of your server like a car. If you ignore warning lights long enough, it breaks down at the worst possible time — when a client's site is under heavy traffic.
Daily Habits
# Check disk space every morning
df -h
# Check if services are running
sudo systemctl status nginx php8.2-fpm mysql
# Scan for errors in logs
sudo tail -n 50 /var/log/nginx/error.log
Weekly Tasks
sudo apt update && sudo apt upgrade -y # Update packages
sudo certbot certificates # Check SSL expiry
ls -lht /backup/mysql/ | head -10 # Verify backups exist
sudo fail2ban-client status # Review security
du -sh /var/www/* && du -sh /var/log/* # Monitor disk usage trend
Monthly Tasks
sudo apt autoremove # Remove unused packages
sudo ufw status verbose # Review firewall rules
sudo crontab -l && crontab -l # Review all cron jobs
# CRITICAL: Test backup restoration to a different location
13.2 Backup Policies
Backups are not optional. A single disk failure, accidental rm -rf, or ransomware attack can destroy everything without backups.
3-2-1 Backup Rule:
3 copies of data
2 different storage media/services
1 off-site backup
Example implementation:
Copy 1: /backup on same server (fast restore)
Copy 2: AWS S3 or Backblaze B2 (off-site)
Copy 3: Weekly download to local workstation
| Item | Frequency | Method |
|---|---|---|
| Database | Daily | mysqldump + gzip |
| Website files | Daily / after deploy | tar + S3 |
| Nginx/Apache config | After changes | Git or backup copy |
| .env files | After changes | Encrypted backup |
| SSL certificates | Auto (Certbot) | Let's Encrypt timer |
# Backup database to S3
sudo apt install awscli
aws configure # Set your AWS credentials
DB_BACKUP="/backup/mysql/mydb_$(date +%Y%m%d).sql.gz"
mysqldump -u root -p mydb | gzip > $DB_BACKUP
aws s3 cp $DB_BACKUP s3://my-backup-bucket/mysql/
# Backup website files to S3
tar -czvf /tmp/website_$(date +%Y%m%d).tar.gz /var/www/mysite
aws s3 cp /tmp/website_$(date +%Y%m%d).tar.gz s3://my-backup-bucket/websites/
13.3 Naming Conventions
# Web directories
/var/www/clientname-projecttype/ # Pattern
/var/www/raju-electricals-website/
/var/www/abc-corp-api/
/var/www/webonictech-admin/
# Nginx config files
/etc/nginx/sites-available/clientname-project.conf
# Backup files
/backup/mysql/dbname_YYYYMMDD_HHMMSS.sql.gz
# Custom log files
/var/log/nginx/clientname-project-error.log
/var/log/nginx/clientname-project-access.log
# PM2 app names
pm2 start server.js --name "clientname-api"
pm2 start npm --name "clientname-nextjs" -- start
13.4 Folder Organization for Web Projects
/var/www/
├── html/ ← Default page (don't put real sites here)
├── raju-electricals/ ← Plain HTML/PHP site
│ ├── public/ ← DocumentRoot
│ │ ├── index.php
│ │ ├── css/
│ │ └── images/
│ └── .htaccess
├── abc-corp-laravel/ ← Laravel app
│ ├── app/
│ ├── public/ ← DocumentRoot (NOT project root!)
│ ├── storage/
│ ├── .env
│ └── artisan
├── xyz-react-app/ ← React static build
│ ├── build/ ← DocumentRoot
│ └── src/
└── techstartup-api/ ← Node.js API
├── server.js
├── package.json
└── .env
13.5 Documentation Habits
A senior developer should not need to be called at midnight because nobody knows the database password or where the deployment script is.
# SERVER DOCUMENTATION — CLIENT NAME
## Server Details
- Provider: DigitalOcean / Hetzner
- IP Address: 203.0.113.42
- OS: Ubuntu 22.04 LTS
- RAM: 4 GB, CPU: 2 vCPU, Disk: 80 GB SSD
- Created: 2026-01-01
## Domains & Sites
| Domain | Project | Root | Stack |
|-----------------|------------|-------------------|-------------|
| mysite.com | Main site | /var/www/mysite | WordPress |
| api.mysite.com | API Server | PM2 port 3000 | Node.js |
## Database
- MySQL User: mysite_user / DB: mysite_db
- Password: [stored in Bitwarden — NOT here]
## Deployment
- Script: /usr/local/bin/deploy-mysite.sh
- Repo: git@github.com:webonictech/mysite.git
## History
- 2026-01-01: Server created
- 2026-01-15: WordPress installed
- 2026-02-01: SSL configured, Fail2ban set up
Use a team password manager: Bitwarden, 1Password, or Keeper. Credentials sent in chat are stored forever and can be leaked.
13.6 Monitoring Basics
# Install basic monitoring tools
sudo apt install htop iotop iftop
htop # CPU + memory + process viewer
sudo iotop # Disk I/O per process
sudo iftop # Network traffic per connection
# Free uptime monitoring
# UptimeRobot (uptimerobot.com) — pings every 5 min
# Sends email/SMS when site goes down
# Check Nginx active connections
curl http://127.0.0.1/nginx-status # Requires stub_status module
Client Server Safety Rules
- Back up database BEFORE touching anything
- Do maintenance during off-peak hours (2 AM – 5 AM)
- Enable maintenance mode before deploying (
php artisan down) - Work on one thing at a time — test each change
- Keep a list of every change you make
- If something breaks: don't panic, check logs, rollback if needed
- After work: verify the site, test critical features, disable maintenance mode
- Document what was done and why
Section 14 — Real-World Scenarios & Case Studies
14.1 Laravel 500 Error After Deployment
Symptom: Site was working. You deployed new code. Now showing 500.
| Log Says | Cause | Fix |
|---|---|---|
Class not found | Composer autoload not rebuilt | composer dump-autoload |
SQLSTATE | DB error or migration missing | php artisan migrate --force |
Permission denied | Wrong ownership on storage/ | chown -R www-data:www-data storage |
APP_KEY not set | Missing .env key | php artisan key:generate |
No such file | New env variable not in production .env | Add missing variable to .env |
tail -n 50 /var/www/mylaravelapp/storage/logs/laravel.log
# Standard fix sequence
php artisan config:clear && php artisan cache:clear && php artisan route:clear && php artisan view:clear
composer dump-autoload
php artisan config:cache
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache
14.2 React App Shows Blank Page
Symptom: Domain loads, but page is completely white.
# Step 1: Open browser DevTools → Console
# Look for: "Failed to load resource: 404" on .js or .css files
# Step 2: Check if build folder is deployed
ls -la /var/www/myreactapp/
# Must have: index.html + static/ folder (NOT src/)
# Step 3: Check Nginx root
grep -r "root " /etc/nginx/sites-enabled/myreactapp.conf
# Step 4: Check try_files is configured
# Must have: try_files $uri $uri/ /index.html; (for React Router)
14.3 WordPress Plugin Crash (WSOD)
Symptom: "There has been a critical error" after activating a plugin.
# Check PHP error log
tail -n 50 /var/log/php/php-error.log
# Deactivate all plugins via database (when you can't access admin)
mysql -u root -p wordpress_db
UPDATE wp_options SET option_value = 'a:0:{}' WHERE option_name = 'active_plugins';
# Or use WP-CLI
sudo -u www-data wp plugin deactivate problematic-plugin --allow-root
# If memory error
# In wp-config.php: define('WP_MEMORY_LIMIT', '512M');
14.4 Node.js App Doesn't Restart After Server Reboot
Symptom: API was running. Server rebooted. Now 502 from Nginx.
Root Cause: PM2 startup was never configured.
pm2 status # Shows empty — app not running
# Restart the app
pm2 start ecosystem.config.js --env production
# Set up startup (only needed once)
pm2 startup
# Run the command it outputs exactly as shown
pm2 save
# Verify after next reboot
sudo reboot
# SSH back in after 60 seconds
pm2 status # Should show app running
14.5 SSL Certificate Expired
Symptom: Browser shows "Your connection is not private" (NET::ERR_CERT_DATE_INVALID).
sudo certbot certificates # Check expiry
# Force renew
sudo certbot renew --force-renewal --cert-name mysite.com
sudo systemctl reload nginx
# Set up auto-renewal (if not already)
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
sudo certbot renew --dry-run # Test it works
14.6 Disk Full — Server Out of Space
Symptom: Strange errors everywhere — PHP crashes, DB writes fail, logs stop updating. df -h shows 100%.
# Find what's taking space
du -sh /var/log/* 2>/dev/null | sort -rh | head -10
du -sh /var/www/* 2>/dev/null | sort -rh | head -10
find / -type f -size +100M 2>/dev/null | head -20
# Safe cleanups (in order)
sudo apt clean # Clear apt cache (~0.5-1 GB)
sudo truncate -s 0 /var/log/nginx/access.log # Truncate (don't delete) logs
find /backup/mysql -name "*.sql.gz" -mtime +7 -delete # Remove old backups
sudo rm -rf /var/lib/php/sessions/* # Clear PHP sessions
pm2 flush # Clear PM2 logs
sudo apt autoremove # Remove old kernel packages
14.7 Database Connection Refused
Symptom: SQLSTATE[HY000] [2002] Connection refused
# Is MySQL running?
sudo systemctl status mysql
sudo systemctl start mysql # If not running
# If it fails to start, read the error
sudo journalctl -u mysql -n 50
# Test manual connection
mysql -u myapp_user -p myapp_db
# Check .env settings
cat /var/www/myapp/.env | grep "^DB_"
# DB_HOST must be 127.0.0.1 (not 0.0.0.0)
# Check MySQL error log
sudo tail -n 50 /var/log/mysql/error.log
# Common causes:
# - Disk full → free up space, restart MySQL
# - Corrupted tables → sudo mysqlcheck --all-databases -u root -p
# - Wrong permissions on /var/lib/mysql → sudo chown -R mysql:mysql /var/lib/mysql
Section 15 — Deployment Checklists
15.1 New Server Setup Checklist
- Update system:
sudo apt update && sudo apt upgrade -y - Set server timezone:
sudo timedatectl set-timezone Asia/Kolkata - Create a non-root user with sudo:
sudo adduser developer - Set up SSH keys for the new user
- Disable root SSH login (
PermitRootLogin no) - Disable password auth after setting up keys (
PasswordAuthentication no) - Test SSH with new user before closing root session
- Configure UFW: allow SSH, HTTP, HTTPS; enable
- Install Nginx / Apache2
- Install PHP + required extensions + Composer
- Install MySQL/MariaDB; run
mysql_secure_installation - Install Node.js + PM2 (if needed)
- Install Git
- Install Certbot
- Install Fail2ban; configure
jail.local - Create backup directory and backup scripts
- Add to monitoring tool (UptimeRobot)
- Document server details in password manager
15.2 Laravel Deployment Checklist
- Take database backup before deploying
- Check disk space on server (
df -h) - Enable maintenance mode:
php artisan down - Pull latest code:
git fetch && git reset --hard origin/main - Install dependencies:
composer install --no-dev --optimize-autoloader - Update
.envwith any new variables from.env.example - Run migrations:
php artisan migrate --force - Create storage symlink:
php artisan storage:link - Set permissions:
chown -R www-data:www-data storage bootstrap/cache - Clear and rebuild caches:
php artisan optimize:clear && php artisan optimize - Restart queue workers:
php artisan queue:restart - Disable maintenance mode:
php artisan up - Test site in browser — check login, forms, uploads
- Monitor error log for 15 minutes:
tail -f storage/logs/laravel.log
15.3 WordPress Deployment Checklist
- Take full database backup:
mysqldump - Back up
wp-content/folder - Create MySQL database and user
- Download WordPress and extract to
/var/www/<site> - Configure
wp-config.php(DB, keys, debug=false, DISALLOW_FILE_EDIT) - Set file permissions: directories 755, files 644
- Configure Nginx/Apache VirtualHost
- Install SSL:
sudo certbot --nginx -d domain.com - Run WordPress installation wizard
- Set permalink structure
- Install caching plugin (LiteSpeed Cache / WP Super Cache)
- Install security plugin (Wordfence)
- Disable XML-RPC if not needed
- Configure automated backups (UpdraftPlus)
15.4 Node.js Deployment Checklist
- Verify Node.js version matches local environment
- All dependencies listed in
package.json(not just installed locally) - Pull latest code
- Install dependencies:
npm install --production - Create / update
.envwith production values - Start / restart with PM2:
pm2 start ecosystem.config.js - Verify app is running:
pm2 status - Check logs:
pm2 logs app-name - Verify Nginx reverse proxy is routing correctly
- Test API endpoints with
curl - Confirm PM2 startup is set:
pm2 startup && pm2 save
15.5 SSL Checklist
- Domain DNS pointing to this server (
dig domain.com A) - Ports 80 and 443 open in UFW
- Web server running and serving domain on port 80
- Install Certbot:
sudo apt install certbot python3-certbot-nginx - Get certificate:
sudo certbot --nginx -d domain.com -d www.domain.com - Verify HTTPS works in browser
- Verify HTTP redirects to HTTPS
- Check certificate:
sudo certbot certificates - Test auto-renewal:
sudo certbot renew --dry-run - Confirm renewal timer is active:
sudo systemctl status certbot.timer - Test SSL quality at ssllabs.com/ssltest
- Check for mixed content (HTTP resources on HTTPS page)
15.6 Security Checklist
- SSH key authentication only (no password login)
- Root SSH disabled (
PermitRootLogin no) - UFW enabled — only required ports open
- Fail2ban installed and configured
- System packages up to date
- Server tokens hidden (
server_tokens offin Nginx) - Directory listing disabled
- .env files not accessible from web
- Upload directories block PHP execution
APP_DEBUG=falsein production- No 777 permissions on production server
- Strong, unique database passwords
- MySQL bound to
127.0.0.1 - .env not committed to Git
- Daily automated backups to off-server location
- Backup restoration tested
Section 16 — Glossary
| Term | Definition |
|---|---|
| Apache2 | Open-source web server software. Processes HTTP requests. Supports .htaccess per-directory config. |
| A Record | DNS record that maps a domain name to an IPv4 address. |
| Artisan | Laravel's command-line interface. Runs migrations, clears caches, generates boilerplate. |
| apt | Advanced Package Tool. Package manager for Ubuntu/Debian Linux systems. |
| Bash | Default command-line shell on most Linux systems. |
| Cache | Temporary storage of frequently accessed data to speed up future requests. |
| CDN | Content Delivery Network. Global network serving static content from the server closest to the user. |
| Certbot | Free tool that automates obtaining and renewing Let's Encrypt SSL certificates. |
| chmod | Linux command to change file/directory permissions. |
| chown | Linux command to change file/directory ownership. |
| CNAME | DNS record that maps one domain to another domain name (an alias). |
| Composer | Dependency manager for PHP. Like npm but for PHP projects. |
| cron / crontab | Linux utility for scheduling commands to run automatically at specific times. |
| Daemon | Background process that runs continuously. Apache, Nginx, MySQL are all daemons. |
| DNS | Domain Name System. Translates domain names to IP addresses. |
| DocumentRoot | Apache directive specifying which directory contains the website files. |
| .env | File containing environment-specific config variables. Never commit to version control. |
| Fail2ban | Tool that bans IPs after too many failed login/auth attempts. |
| FastCGI | Protocol for interfacing external applications (like PHP) with web servers. PHP-FPM uses FastCGI. |
| Firewall | Controls incoming and outgoing network traffic based on rules. UFW manages it on Ubuntu. |
| Git | Distributed version control system. Tracks code changes and enables collaboration. |
| Gzip | Compression algorithm that reduces file sizes for faster network transfer. |
| htop | Interactive system process viewer. A better version of top. |
| HTTP | Hypertext Transfer Protocol. Unencrypted web protocol, port 80. |
| HTTPS | HTTP with SSL/TLS encryption. Port 443. Mandatory for all production websites. |
| IP Address | Unique numerical address assigned to each device on a network (e.g., 203.0.113.42). |
| journalctl | Command to read logs from systemd's logging service. |
| Laravel | Popular PHP web framework known for elegant syntax and rich features (ORM, routing, queues). |
| Let's Encrypt | Free, automated Certificate Authority that provides 90-day SSL certificates. |
| Load Balancer | Distributes incoming traffic across multiple backend servers. |
| MariaDB | A drop-in MySQL replacement with improved performance and open-source governance. |
| Middleware | Software layer that processes requests before they reach application code. |
| MySQL | The world's most popular open-source relational database management system. |
| Nginx | High-performance web server and reverse proxy. Handles massive concurrency efficiently. |
| NVM | Node Version Manager. Lets you install and switch between multiple Node.js versions. |
| OPcache | PHP extension that caches compiled bytecode in memory, speeding up PHP significantly. |
| PHP-FPM | PHP FastCGI Process Manager. Separate PHP service used with Nginx for better performance. |
| PM2 | Production process manager for Node.js. Keeps apps running and restarts on crash. |
| Port | Numbered communication endpoint (80=HTTP, 443=HTTPS, 3306=MySQL, 22=SSH). |
| Proxy (Reverse Proxy) | Server that forwards client requests to backend servers. Nginx commonly proxies to Node.js apps. |
| Redis | In-memory data structure store used for caching, sessions, and message queuing. |
| root | Superuser account with full system access on Linux. Use sudo instead of logging in as root. |
| Server Block | Nginx's equivalent of Apache VirtualHost. Defines how Nginx handles a domain's requests. |
| SFTP | Secure File Transfer Protocol. Transfers files over SSH. Use instead of FTP. |
| SSH | Secure Shell. Protocol for secure remote server access over an unsecured network. |
| SSL/TLS | Encryption protocols securing data in transit. TLS is the current standard (SSL is the old name). |
| Staging | Environment mirroring production, used for testing before going live. |
| sudo | Allows permitted users to run commands as root without logging in as root. |
| Supervisor | Linux process control system. Manages long-running processes like Laravel queue workers. |
| Symlink | A file that points to another file or directory. Nginx's sites-enabled uses symlinks. |
| systemctl | Command to control systemd services — start, stop, restart, check status. |
| UFW | Uncomplicated Firewall. Easy interface for managing firewall rules on Ubuntu. |
| VirtualHost | Apache config block allowing one server to host multiple websites on different domains. |
| VPS | Virtual Private Server. Gives you root access and dedicated resources on shared hardware. |
| www-data | Default user that Apache and Nginx run as on Ubuntu. Web files should be owned by this user. |
Appendix A — 6-Phase Learning Roadmap (14 Weeks)
| Phase | Weeks | Focus | Goal |
|---|---|---|---|
| Phase 1 | 1–2 | Linux & Terminal Basics | Comfortable with SSH, files, permissions, services |
| Phase 2 | 3–4 | Web Servers (Nginx & Apache) | Set up virtual hosts, SSL, serve real sites |
| Phase 3 | 5–7 | PHP Ecosystem | Deploy Laravel, WordPress, CodeIgniter |
| Phase 4 | 8–9 | Node.js & Modern Apps | Deploy Express, React, Next.js with PM2 |
| Phase 5 | 10–11 | Databases & Security | MySQL, backups, UFW, Fail2ban, SSH keys |
| Phase 6 | 12–14 | Troubleshooting Mastery | Debug any issue independently using logs |
Phase 1 — Foundation (Weeks 1–2)
- Read Section 1 (Introduction to Servers) completely
- Learn basic Linux commands (Sections 2.3–2.4)
- Practice: SSH into a test server
- Practice: Navigate, create, view, and edit files
- Practice: Install Nginx and visit the default page
- Understand: What happens when you type a URL
- Mini Lab: Set up a free VPS, SSH in, serve a custom HTML page
Phase 2 — Web Servers (Weeks 3–4)
- Set up an Apache2 VirtualHost for a PHP site
- Set up an Nginx server block
- Deploy a static HTML/CSS site and a basic PHP page
- Configure SSL with Certbot and force HTTP→HTTPS
- Practice: Create 3 different virtual hosts on one server
- Mini Lab: Set up two domains, SSL for both, verify each shows correct content
Phase 3 — PHP & Frameworks (Weeks 5–7)
- Install PHP 8.2 + extensions, configure php.ini
- Install Composer
- Deploy a fresh Laravel project from scratch
- Set correct Laravel permissions, run migrations
- Set up Laravel queue worker (Supervisor) and cron
- Deploy a WordPress site and configure wp-config.php
- Mini Lab: Deploy laravel/laravel with MySQL, run migrations, set up cron
Phase 4 — Node.js (Weeks 8–9)
- Install Node.js via NVM, install PM2
- Deploy an Express.js API, configure Nginx reverse proxy
- Deploy a React app (static build)
- Deploy a Next.js app with PM2, configure PM2 startup
- Mini Lab: Deploy Express API behind Nginx with SSL, verify PM2 restarts after reboot
Phase 5 — Databases & Security (Weeks 10–11)
- Create MySQL databases and users, run SQL commands
- Set up automated daily backup cron
- Simulate and verify backup restoration
- Configure UFW (allow SSH, HTTP, HTTPS only)
- Install and configure Fail2ban
- Set up SSH key authentication, disable password login
- Mini Lab: Restore a MySQL backup to verify it; trigger Fail2ban with failed logins
Phase 6 — Troubleshooting (Weeks 12–14)
- Read all of Section 11
- Practice reading error logs systematically
- Deliberately break and fix: wrong permissions (403), FPM stopped (502), disk full
- Know all HTTP status codes and their common causes
- Create your own personal debugging checklist
- Mini Lab: Break server in 5 ways, debug each using only logs, document findings
Appendix B — Daily Practice Plan for Interns
| Week | Day | Practice Topic |
|---|---|---|
| 1–2 | Monday | SSH login, pwd, ls, cd, mkdir, touch |
| Tuesday | nano/vim, cat, less, tail -f | |
| Wednesday | chmod, chown, user management, sudo | |
| Thursday | apt, systemctl, service management | |
| Friday | ps, htop, cron jobs — practice quiz | |
| 3–4 | Monday | Install Nginx, understand nginx.conf structure |
| Tuesday | Create server block, deploy HTML page | |
| Wednesday | Install Apache2, VirtualHost, deploy PHP page | |
| Thursday | SSL with Certbot, HTTP→HTTPS redirect | |
| Friday | Review logs, fix deliberately broken configs | |
| 5–7 | Week 5 Mon | Install PHP 8.2, PHP-FPM, configure php.ini |
| Week 5 Tue | Deploy plain PHP project | |
| Week 5 Wed | Install Composer, explore composer.json | |
| Week 5 Thu–Fri | Deploy fresh Laravel: clone → composer → .env → migrate → permissions | |
| Weeks 6–7 | WordPress setup, WP-CLI, Supervisor queue workers | |
| 8–9 | Monday | Install Node.js via NVM, npm basics |
| Tuesday | Deploy Express.js app, understand PM2 | |
| Wednesday | Nginx reverse proxy for Node.js | |
| Thursday | Deploy React static build | |
| Friday | Deploy Next.js with PM2 startup | |
| 10–12 | All | UFW, Fail2ban, SSH hardening, DB management, backups, deliberate break/fix exercises |
Daily Log Habit
Every day, write 5–10 lines about what you did and what you learned. This builds your personal knowledge base.
Date: 2026-01-15
Today I learned:
- How to check PHP version: php -v
- mod_rewrite needs AllowOverride All to work in .htaccess
- www-data needs write access to Laravel storage/
Commands used today:
- sudo a2enmod rewrite
- sudo systemctl reload apache2
- php artisan config:cache
- tail -f /var/log/apache2/error.log
Mistakes made:
- Forgot to reload Apache after changing VirtualHost
- AllowOverride was None — changed to All
Appendix C — Interview Q&A
Linux Fundamentals
rm and rm -rf?
rm removes a single file. rm -rf removes directories and all their contents recursively and forcefully with no confirmation. There is no undo.
chmod 755 mean?
Owner: read+write+execute (7), Group: read+execute (5), Others: read+execute (5). Standard for directories and executable scripts.
sudo ss -tlnp | grep :80
Web Server Questions
Apache uses a process/thread-based model and supports .htaccess per-directory config. Nginx uses an event-driven async model, handles massive concurrency better, doesn't support .htaccess, but is generally faster for static files and works well as a reverse proxy.
public/ and not the project root?
Security. The public/ folder is the only directory that should be web-accessible. The .env file, app/, database/, and config/ folders must never be reachable from a browser.
PHP-FPM (FastCGI Process Manager) runs PHP as a separate service communicating via FastCGI. Benefits: can run as a different user per site, better process management, required for Nginx, more stable under high load.
Deployment Questions
PM2 is a production process manager for Node.js. It keeps apps running (auto-restarts on crash), manages logs, enables cluster mode for multi-core use, and persists processes across server reboots.
APP_DEBUG=false in production Laravel?
When APP_DEBUG=true, Laravel shows detailed error pages with file paths, database structure, and full stack traces — giving attackers a map of your application.
git pull and git fetch?
git fetch downloads changes without merging. git pull = fetch + merge. In production deployments, git fetch + git reset --hard origin/main gives a clean, deterministic update.
Troubleshooting Questions
Check if the upstream service is running: sudo systemctl status php8.2-fpm or pm2 status. Then check the Nginx error log: sudo tail /var/log/nginx/error.log.
Someone likely changed file ownership or permissions — perhaps during a deployment as root. Check: ls -la /var/www/mysite/. Files owned by root instead of www-data cause 403. Fix: sudo chown -R www-data:www-data /var/www/mysite.
upload_max_filesize or post_max_size in php.ini is too small. Also check Nginx's client_max_body_size. After changing, restart PHP-FPM and reload Nginx.
Appendix D — Hands-on Mini Labs
Lab 1 — Your First Production Server
Objective: Set up a completely clean server with a working website.
- Provision a Ubuntu 22.04 VPS (DigitalOcean, Hetzner, or Vultr — use free credits)
- SSH in as root; create a new user
developerwith sudo access - Update all packages
- Install Nginx
- Configure UFW — allow ports 22, 80, 443
- Create a custom
index.htmlat/var/www/html/ - Access your server's IP in a browser — verify the page loads
Lab 2 — Deploy a Laravel Application
Objective: Go from an empty server to a running Laravel app with SSL.
- Install PHP 8.2 + PHP-FPM + extensions
- Install MySQL, create a database and user
- Install Composer
- Clone the
laravel/laravelstarter project - Configure
.envwith DB credentials - Run
php artisan key:generateandphp artisan migrate - Configure Nginx server block (DocumentRoot →
public/) - Set correct file permissions
- Install SSL with Certbot
- Verify site loads at your domain over HTTPS
Lab 3 — Node.js API with Nginx Proxy
Objective: Deploy a Node.js API, proxy it through Nginx, add SSL.
- Install Node.js via NVM
- Create a simple Express.js "Hello World" app on port 3000
- Start it with PM2 (
pm2 start server.js --name hello-api) - Configure Nginx to proxy port 80 → port 3000
- Add SSL with Certbot
- Test with
curl https://api.yourdomain.com - Run
pm2 startup && pm2 save - Do
sudo rebootand verify PM2 restarts automatically
Lab 4 — WordPress Site Setup
Objective: Install a secure WordPress site from scratch.
- Create MySQL database and user
- Download and extract WordPress to
/var/www/mysite - Configure
wp-config.php(DB settings, security keys, debug=false) - Set file permissions (755 dirs, 644 files, 644 wp-config.php)
- Configure Nginx server block
- Install SSL with Certbot
- Complete WordPress setup wizard
- Install Wordfence security plugin
- Add .htaccess protection for
wp-config.phpand block XML-RPC
Lab 5 — Server Hardening
Objective: Secure a working server against common attacks.
- Generate an SSH key pair on your local machine
- Copy public key to server with
ssh-copy-id - Test key-based login
- Set
PasswordAuthentication noandPermitRootLogin noin sshd_config - Configure UFW — only allow 22, 80, 443
- Install and configure Fail2ban for SSH (maxretry = 3)
- Verify Fail2ban is running:
sudo fail2ban-client status sshd - Attempt 4 failed SSH logins from another terminal — verify IP gets banned
Lab 6 — Deliberate Break and Fix
Objective: Practice debugging by breaking things intentionally.
- Break 1: Change ownership of
/var/www/mysitetoroot:root→ observe 403 → fix - Break 2: Stop PHP-FPM (
sudo systemctl stop php8.2-fpm) → observe 502 → fix - Break 3: Add a syntax error to an Nginx config → observe 500 or reload failure → fix
- Break 4: Delete Laravel's
.env→ observe 500 → fix - Break 5: Set wrong DB credentials in
.env→ observe DB error → fix - For each: read the relevant log before taking action
Lab 7 — Backup and Restore
Objective: Prove your backups actually work by restoring one.
- Create a MySQL backup with
mysqldump - Create a files backup with
tar - Drop the database:
DROP DATABASE myapp_db; - Restore from backup:
mysql -u root -p myapp_db < backup.sql - Verify data is intact
- Set up a cron job to backup daily at 2 AM
Lab 8 — Full Stack Deployment
Objective: Deploy a multi-component application (API + Frontend).
- Deploy a Laravel API at
api.yourdomain.com - Deploy a React frontend at
yourdomain.com - Configure CORS in Laravel to allow requests from the frontend domain
- Configure SSL on both domains
- Set up PM2 for the API (if using Node.js instead of PHP)
- Verify the frontend can make API calls
- Write a deployment script for each component