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

🚨
No Backup = No Recovery

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
ItemFrequencyMethod
DatabaseDailymysqldump + gzip
Website filesDaily / after deploytar + S3
Nginx/Apache configAfter changesGit or backup copy
.env filesAfter changesEncrypted backup
SSL certificatesAuto (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

💡
Write Server Documentation for Every Client

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
⚠️
Never Share Credentials Over Slack or WhatsApp

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

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 SaysCauseFix
Class not foundComposer autoload not rebuiltcomposer dump-autoload
SQLSTATEDB error or migration missingphp artisan migrate --force
Permission deniedWrong ownership on storage/chown -R www-data:www-data storage
APP_KEY not setMissing .env keyphp artisan key:generate
No such fileNew env variable not in production .envAdd 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

15.2 Laravel Deployment Checklist

15.3 WordPress Deployment Checklist

15.4 Node.js Deployment Checklist

15.5 SSL Checklist

15.6 Security Checklist

Section 16 — Glossary

TermDefinition
Apache2Open-source web server software. Processes HTTP requests. Supports .htaccess per-directory config.
A RecordDNS record that maps a domain name to an IPv4 address.
ArtisanLaravel's command-line interface. Runs migrations, clears caches, generates boilerplate.
aptAdvanced Package Tool. Package manager for Ubuntu/Debian Linux systems.
BashDefault command-line shell on most Linux systems.
CacheTemporary storage of frequently accessed data to speed up future requests.
CDNContent Delivery Network. Global network serving static content from the server closest to the user.
CertbotFree tool that automates obtaining and renewing Let's Encrypt SSL certificates.
chmodLinux command to change file/directory permissions.
chownLinux command to change file/directory ownership.
CNAMEDNS record that maps one domain to another domain name (an alias).
ComposerDependency manager for PHP. Like npm but for PHP projects.
cron / crontabLinux utility for scheduling commands to run automatically at specific times.
DaemonBackground process that runs continuously. Apache, Nginx, MySQL are all daemons.
DNSDomain Name System. Translates domain names to IP addresses.
DocumentRootApache directive specifying which directory contains the website files.
.envFile containing environment-specific config variables. Never commit to version control.
Fail2banTool that bans IPs after too many failed login/auth attempts.
FastCGIProtocol for interfacing external applications (like PHP) with web servers. PHP-FPM uses FastCGI.
FirewallControls incoming and outgoing network traffic based on rules. UFW manages it on Ubuntu.
GitDistributed version control system. Tracks code changes and enables collaboration.
GzipCompression algorithm that reduces file sizes for faster network transfer.
htopInteractive system process viewer. A better version of top.
HTTPHypertext Transfer Protocol. Unencrypted web protocol, port 80.
HTTPSHTTP with SSL/TLS encryption. Port 443. Mandatory for all production websites.
IP AddressUnique numerical address assigned to each device on a network (e.g., 203.0.113.42).
journalctlCommand to read logs from systemd's logging service.
LaravelPopular PHP web framework known for elegant syntax and rich features (ORM, routing, queues).
Let's EncryptFree, automated Certificate Authority that provides 90-day SSL certificates.
Load BalancerDistributes incoming traffic across multiple backend servers.
MariaDBA drop-in MySQL replacement with improved performance and open-source governance.
MiddlewareSoftware layer that processes requests before they reach application code.
MySQLThe world's most popular open-source relational database management system.
NginxHigh-performance web server and reverse proxy. Handles massive concurrency efficiently.
NVMNode Version Manager. Lets you install and switch between multiple Node.js versions.
OPcachePHP extension that caches compiled bytecode in memory, speeding up PHP significantly.
PHP-FPMPHP FastCGI Process Manager. Separate PHP service used with Nginx for better performance.
PM2Production process manager for Node.js. Keeps apps running and restarts on crash.
PortNumbered 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.
RedisIn-memory data structure store used for caching, sessions, and message queuing.
rootSuperuser account with full system access on Linux. Use sudo instead of logging in as root.
Server BlockNginx's equivalent of Apache VirtualHost. Defines how Nginx handles a domain's requests.
SFTPSecure File Transfer Protocol. Transfers files over SSH. Use instead of FTP.
SSHSecure Shell. Protocol for secure remote server access over an unsecured network.
SSL/TLSEncryption protocols securing data in transit. TLS is the current standard (SSL is the old name).
StagingEnvironment mirroring production, used for testing before going live.
sudoAllows permitted users to run commands as root without logging in as root.
SupervisorLinux process control system. Manages long-running processes like Laravel queue workers.
SymlinkA file that points to another file or directory. Nginx's sites-enabled uses symlinks.
systemctlCommand to control systemd services — start, stop, restart, check status.
UFWUncomplicated Firewall. Easy interface for managing firewall rules on Ubuntu.
VirtualHostApache config block allowing one server to host multiple websites on different domains.
VPSVirtual Private Server. Gives you root access and dedicated resources on shared hardware.
www-dataDefault 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)

PhaseWeeksFocusGoal
Phase 11–2Linux & Terminal BasicsComfortable with SSH, files, permissions, services
Phase 23–4Web Servers (Nginx & Apache)Set up virtual hosts, SSL, serve real sites
Phase 35–7PHP EcosystemDeploy Laravel, WordPress, CodeIgniter
Phase 48–9Node.js & Modern AppsDeploy Express, React, Next.js with PM2
Phase 510–11Databases & SecurityMySQL, backups, UFW, Fail2ban, SSH keys
Phase 612–14Troubleshooting MasteryDebug any issue independently using logs

Phase 1 — Foundation (Weeks 1–2)

Phase 2 — Web Servers (Weeks 3–4)

Phase 3 — PHP & Frameworks (Weeks 5–7)

Phase 4 — Node.js (Weeks 8–9)

Phase 5 — Databases & Security (Weeks 10–11)

Phase 6 — Troubleshooting (Weeks 12–14)

Appendix B — Daily Practice Plan for Interns

WeekDayPractice Topic
1–2MondaySSH login, pwd, ls, cd, mkdir, touch
Tuesdaynano/vim, cat, less, tail -f
Wednesdaychmod, chown, user management, sudo
Thursdayapt, systemctl, service management
Fridayps, htop, cron jobs — practice quiz
3–4MondayInstall Nginx, understand nginx.conf structure
TuesdayCreate server block, deploy HTML page
WednesdayInstall Apache2, VirtualHost, deploy PHP page
ThursdaySSL with Certbot, HTTP→HTTPS redirect
FridayReview logs, fix deliberately broken configs
5–7Week 5 MonInstall PHP 8.2, PHP-FPM, configure php.ini
Week 5 TueDeploy plain PHP project
Week 5 WedInstall Composer, explore composer.json
Week 5 Thu–FriDeploy fresh Laravel: clone → composer → .env → migrate → permissions
Weeks 6–7WordPress setup, WP-CLI, Supervisor queue workers
8–9MondayInstall Node.js via NVM, npm basics
TuesdayDeploy Express.js app, understand PM2
WednesdayNginx reverse proxy for Node.js
ThursdayDeploy React static build
FridayDeploy Next.js with PM2 startup
10–12AllUFW, 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

🎤
Q: What is the difference between 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.

🎤
Q: What does chmod 755 mean?

Owner: read+write+execute (7), Group: read+execute (5), Others: read+execute (5). Standard for directories and executable scripts.

🎤
Q: What command checks which process is on port 80?

sudo ss -tlnp | grep :80

Web Server Questions

🎤
Q: What is the difference between Apache and Nginx?

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.

🎤
Q: Why does Laravel's DocumentRoot point to 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.

🎤
Q: What is PHP-FPM and why use it over mod_php?

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

🎤
Q: What is PM2 and why use it?

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.

🎤
Q: Why must 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.

🎤
Q: What is the difference between 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

🎤
Q: A site returns 502. What do you check first?

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.

🎤
Q: A site showed 403 Forbidden today but worked yesterday. What happened?

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.

🎤
Q: A file upload fails silently. What could be the cause?

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.

  1. Provision a Ubuntu 22.04 VPS (DigitalOcean, Hetzner, or Vultr — use free credits)
  2. SSH in as root; create a new user developer with sudo access
  3. Update all packages
  4. Install Nginx
  5. Configure UFW — allow ports 22, 80, 443
  6. Create a custom index.html at /var/www/html/
  7. 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.

  1. Install PHP 8.2 + PHP-FPM + extensions
  2. Install MySQL, create a database and user
  3. Install Composer
  4. Clone the laravel/laravel starter project
  5. Configure .env with DB credentials
  6. Run php artisan key:generate and php artisan migrate
  7. Configure Nginx server block (DocumentRoot → public/)
  8. Set correct file permissions
  9. Install SSL with Certbot
  10. 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.

  1. Install Node.js via NVM
  2. Create a simple Express.js "Hello World" app on port 3000
  3. Start it with PM2 (pm2 start server.js --name hello-api)
  4. Configure Nginx to proxy port 80 → port 3000
  5. Add SSL with Certbot
  6. Test with curl https://api.yourdomain.com
  7. Run pm2 startup && pm2 save
  8. Do sudo reboot and verify PM2 restarts automatically

Lab 4 — WordPress Site Setup

Objective: Install a secure WordPress site from scratch.

  1. Create MySQL database and user
  2. Download and extract WordPress to /var/www/mysite
  3. Configure wp-config.php (DB settings, security keys, debug=false)
  4. Set file permissions (755 dirs, 644 files, 644 wp-config.php)
  5. Configure Nginx server block
  6. Install SSL with Certbot
  7. Complete WordPress setup wizard
  8. Install Wordfence security plugin
  9. Add .htaccess protection for wp-config.php and block XML-RPC

Lab 5 — Server Hardening

Objective: Secure a working server against common attacks.

  1. Generate an SSH key pair on your local machine
  2. Copy public key to server with ssh-copy-id
  3. Test key-based login
  4. Set PasswordAuthentication no and PermitRootLogin no in sshd_config
  5. Configure UFW — only allow 22, 80, 443
  6. Install and configure Fail2ban for SSH (maxretry = 3)
  7. Verify Fail2ban is running: sudo fail2ban-client status sshd
  8. 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.

  1. Break 1: Change ownership of /var/www/mysite to root:root → observe 403 → fix
  2. Break 2: Stop PHP-FPM (sudo systemctl stop php8.2-fpm) → observe 502 → fix
  3. Break 3: Add a syntax error to an Nginx config → observe 500 or reload failure → fix
  4. Break 4: Delete Laravel's .env → observe 500 → fix
  5. Break 5: Set wrong DB credentials in .env → observe DB error → fix
  6. For each: read the relevant log before taking action

Lab 7 — Backup and Restore

Objective: Prove your backups actually work by restoring one.

  1. Create a MySQL backup with mysqldump
  2. Create a files backup with tar
  3. Drop the database: DROP DATABASE myapp_db;
  4. Restore from backup: mysql -u root -p myapp_db < backup.sql
  5. Verify data is intact
  6. Set up a cron job to backup daily at 2 AM

Lab 8 — Full Stack Deployment

Objective: Deploy a multi-component application (API + Frontend).

  1. Deploy a Laravel API at api.yourdomain.com
  2. Deploy a React frontend at yourdomain.com
  3. Configure CORS in Laravel to allow requests from the frontend domain
  4. Configure SSL on both domains
  5. Set up PM2 for the API (if using Node.js instead of PHP)
  6. Verify the frontend can make API calls
  7. Write a deployment script for each component