# WEBONICTECH — SERVER HANDBOOK — PART 3
## Sections 9 through 12

---

# SECTION 9 — SSL, DOMAINS, AND DNS

---

## 9.1 Understanding Domains

A **domain** is a human-readable address for a website. `example.com` is a domain.

Domain hierarchy:
```
.com                          ← TLD (Top-Level Domain)
example.com                   ← Second-level domain
www.example.com               ← Subdomain
api.example.com               ← Subdomain
blog.example.com              ← Subdomain
staging.example.com           ← Subdomain (for testing)
```

**Where to register domains:**
- GoDaddy, Namecheap, Google Domains, Cloudflare Registrar

Once purchased, you control DNS records for the domain.

---

## 9.2 DNS — How It Works

DNS (Domain Name System) translates domain names to IP addresses.

```
User types: example.com
    ↓
Browser checks local cache → OS /etc/hosts → ISP DNS Resolver
    ↓ (if not cached)
DNS Resolver asks Root DNS Server (.com)
    ↓
Root Server says: "ask ns1.cloudflare.com" (the authoritative nameserver)
    ↓
Authoritative nameserver returns: 203.0.113.42
    ↓
Browser connects to 203.0.113.42
```

**DNS Record Types Explained:**

| Record | Purpose | Example |
|--------|---------|---------|
| **A** | Domain → IPv4 address | `example.com → 203.0.113.42` |
| **AAAA** | Domain → IPv6 address | `example.com → 2001:db8::1` |
| **CNAME** | Alias to another domain | `www → example.com` |
| **MX** | Email server | `@ → mail.example.com (priority 10)` |
| **TXT** | Text records (SPF, DKIM, verification) | `v=spf1 include:sendgrid.net ~all` |
| **NS** | Nameservers for this domain | `@ → ns1.cloudflare.com` |
| **PTR** | Reverse DNS (IP → domain) | `42.113.0.203.in-addr.arpa → example.com` |
| **SRV** | Service location | For SIP, XMPP, etc. |
| **CAA** | Which CAs can issue SSL for domain | `0 issue "letsencrypt.org"` |

---

## 9.3 Setting Up DNS Records

### For a New VPS Server

Scenario: You have a VPS with IP `203.0.113.42` and domain `mysite.com`

**Steps in your DNS control panel (Cloudflare/GoDaddy/Namecheap):**

```
Type    Name            Value               TTL
────────────────────────────────────────────────
A       @               203.0.113.42        Auto
A       www             203.0.113.42        Auto
A       api             203.0.113.42        Auto
A       mail            203.0.113.42        Auto
CNAME   blog            mysite.com          Auto
MX      @               mail.mysite.com     10
TXT     @               v=spf1 ip4:203.0.113.42 ~all   Auto
```

**DNS Propagation:** After changing DNS records, wait 15 minutes to 48 hours. Use these tools to check:
```bash
# Check DNS from command line
dig mysite.com A
dig www.mysite.com A
nslookup mysite.com 8.8.8.8    # Ask Google's DNS

# Online tools:
# https://dnschecker.org (check from multiple locations)
# https://mxtoolbox.com
```

---

## 9.4 Cloudflare — What Is It and Why Use It?

Cloudflare is a **Content Delivery Network (CDN)** and **DNS provider** that sits in front of your server.

```
Without Cloudflare:
User → your-server-ip (direct connection)

With Cloudflare:
User → Cloudflare's global network → your-server-ip
```

**Benefits of Cloudflare:**
- **DDoS Protection** — Absorbs attack traffic
- **CDN** — Caches static files at edge locations worldwide (faster loading)
- **Free SSL** — Automatic HTTPS without Certbot
- **WAF** — Web Application Firewall (blocks common attacks)
- **DNS Management** — Fast DNS with easy management
- **Analytics** — Traffic statistics
- **Bot Protection** — Blocks bad bots

**Setup:**
1. Create account at cloudflare.com
2. Add your domain
3. Cloudflare scans existing DNS records
4. Update nameservers at your registrar to Cloudflare's (e.g., `ns1.cloudflare.com`)
5. Wait for propagation (up to 48 hours)

**Orange Cloud vs Grey Cloud:**
- 🟠 Orange cloud = Traffic goes through Cloudflare (CDN + protection active)
- ⬜ Grey cloud = DNS only, traffic goes direct to server (no CDN)

---

## 9.5 Let's Encrypt and Certbot — Free SSL

Let's Encrypt is a **free Certificate Authority** that issues SSL certificates valid for 90 days. Certbot automatically renews them.

### Installing and Using Certbot

```bash
# Install Certbot
sudo apt install certbot

# For Nginx
sudo apt install python3-certbot-nginx

# For Apache
sudo apt install python3-certbot-apache

# Get certificate for Nginx
sudo certbot --nginx -d mysite.com -d www.mysite.com

# Get certificate for Apache
sudo certbot --apache -d mysite.com -d www.mysite.com

# Get certificate without auto-configuring web server (manual)
sudo certbot certonly --webroot -w /var/www/mysite -d mysite.com -d www.mysite.com

# Wildcard certificate (covers *.mysite.com) — requires DNS challenge
sudo certbot certonly --manual --preferred-challenges=dns -d "*.mysite.com" -d mysite.com
```

### Certificate Files Location

```bash
/etc/letsencrypt/live/mysite.com/
├── cert.pem            ← Your certificate
├── chain.pem           ← Intermediate certificates
├── fullchain.pem       ← cert.pem + chain.pem (use this in Nginx)
└── privkey.pem         ← Your private key (PROTECT THIS!)
```

### Auto-Renewal

Certbot installs a systemd timer or cron job that renews certificates automatically.

```bash
# Check renewal timer
sudo systemctl status certbot.timer
sudo systemctl list-timers | grep certbot

# Test renewal without actually renewing
sudo certbot renew --dry-run

# Force renew all certificates
sudo certbot renew --force-renewal

# Renew a specific domain
sudo certbot renew --cert-name mysite.com

# View all certificates
sudo certbot certificates
```

### Certbot Hooks (Run Commands After Renewal)

```bash
# Reload Nginx after certificate renewal
sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
```
```bash
#!/bin/bash
systemctl reload nginx
```
```bash
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
```

---

## 9.6 HTTP to HTTPS Redirect

**Nginx:**
```nginx
server {
    listen 80;
    server_name mysite.com www.mysite.com;
    return 301 https://$server_name$request_uri;
}
```

**Apache:**
```apache
<VirtualHost *:80>
    ServerName mysite.com
    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</VirtualHost>
```

---

## 9.7 SSL Troubleshooting

### Certificate Not Trusted
```
Symptom: Browser shows "Your connection is not private"
Causes:
1. Using self-signed certificate
2. Certificate expired
3. Wrong domain on certificate
4. Mixed content (HTTPS page loading HTTP resources)

Fix:
sudo certbot certificates    # Check expiry dates
sudo certbot renew            # Renew if expired
```

### Mixed Content Issues
```
Symptom: SSL padlock shows "Not Secure" or warning symbol despite HTTPS
Cause: Some resources (images, scripts, CSS) are still loaded over HTTP

Fix in WordPress:
# Install "Better Search Replace" plugin
# Replace http://mysite.com with https://mysite.com in database
# Or use wp-cli:
sudo -u www-data wp search-replace 'http://mysite.com' 'https://mysite.com'

Fix in Laravel (.env):
APP_URL=https://mysite.com
```

### SSL Certificate Error After Cloudflare Setup
```
Cause: Cloudflare SSL mode mismatch
Cloudflare SSL modes:
- Off: No SSL
- Flexible: Cloudflare ↔ Browser is HTTPS, but Cloudflare ↔ Server is HTTP
- Full: Cloudflare ↔ Browser is HTTPS, Cloudflare ↔ Server is HTTPS (self-signed OK)
- Full (Strict): Cloudflare ↔ Browser is HTTPS, Cloudflare ↔ Server needs valid SSL cert

Recommendation: Use "Full (Strict)" with Let's Encrypt on your server.
Avoid "Flexible" — it creates redirect loops with HTTPS redirects on the server.
```

---

---

# SECTION 10 — FILE PERMISSIONS & SECURITY

---

## 10.1 Linux File Permissions Deep Dive

### Permission String Anatomy
```
-rwxr-xr--
│││ │││ │││
│││ │││ └└└─ Others:  r-- = read only (4)
│││ └└└───── Group:   r-x = read + execute (5)
└└└───────── Owner:   rwx = read + write + execute (7)
│
└─────────── File type: - = regular file, d = directory, l = symlink
```

### Octal Permission Table

| Octal | Binary | Permissions | Meaning |
|-------|--------|-------------|---------|
| 0 | 000 | --- | No permissions |
| 1 | 001 | --x | Execute only |
| 2 | 010 | -w- | Write only |
| 3 | 011 | -wx | Write + Execute |
| 4 | 100 | r-- | Read only |
| 5 | 101 | r-x | Read + Execute |
| 6 | 110 | rw- | Read + Write |
| 7 | 111 | rwx | Read + Write + Execute |

### Most Common Permission Settings

```
644 = rw-r--r--   Standard file permissions
                   Owner: read+write, Group: read, Others: read
                   Use for: PHP files, HTML, CSS, configuration files

755 = rwxr-xr-x   Standard directory permissions
                   Owner: all, Group: read+execute, Others: read+execute
                   Use for: directories, executable scripts

600 = rw-------   Sensitive files
                   Owner: read+write only
                   Use for: .env, private keys, wp-config.php

700 = rwx------   Private executables
                   Owner: all. Nobody else.
                   Use for: private scripts, SSH directory

775 = rwxrwxr-x   Shared write directory
                   Owner+Group: all, Others: read+execute
                   Use for: Laravel storage/, bootstrap/cache/

777 = rwxrwxrwx   FULL ACCESS — EVERYONE
                   NEVER use in production.
                   Security risk: Anyone can modify/execute files
```

---

## 10.2 Why 777 Is Dangerous

If a directory or file has `777` permissions:
1. The web server (`www-data`) can write to it
2. This means a **PHP script can also write to it**
3. If your site gets hacked via a PHP injection, the attacker can:
   - Upload malicious PHP files
   - Execute arbitrary code
   - Install backdoors
   - Access other sites on the server

**Real-world scenario:**
```
Developer sets uploads/ to 777 because "images weren't uploading"
↓
Attacker uploads a PHP webshell (looks like image.php)
↓
Attacker executes: http://mysite.com/uploads/image.php
↓
Now has full server access
```

**Correct approach for uploads:**
```bash
# Owner www-data, permissions 755
# www-data can write (since it's the owner)
# Others can read (for serving images) but cannot write
sudo chown www-data:www-data /var/www/mysite/uploads
sudo chmod 755 /var/www/mysite/uploads
```

---

## 10.3 www-data User

`www-data` is the **default user that Apache2 and Nginx run as** on Ubuntu/Debian.

```bash
# See www-data user info
id www-data

# Files served by web server should be owned by www-data
sudo chown -R www-data:www-data /var/www/mysite

# For development, add your user to www-data group
sudo usermod -aG www-data ubuntu
# Then log out and log back in for group change to take effect
```

**Best practice for file ownership:**

```bash
# Option 1: Pure www-data (nginx serves, writes via webserver)
sudo chown -R www-data:www-data /var/www/mysite

# Option 2: Your user owns, www-data is group (SSH + FTP access easier)
sudo chown -R ubuntu:www-data /var/www/mysite
sudo chmod -R 750 /var/www/mysite      # Owner: all, Group: read+exec
sudo chmod -R 770 /var/www/mysite/storage  # Allow www-data to write
```

---

## 10.4 UFW Firewall

**UFW** (Uncomplicated Firewall) is the standard firewall tool on Ubuntu.

```bash
# Install UFW (usually pre-installed)
sudo apt install ufw

# Check status
sudo ufw status
sudo ufw status verbose

# Default policies (start with deny everything)
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH (ALWAYS do this BEFORE enabling UFW or you'll lock yourself out!)
sudo ufw allow ssh          # Allow port 22
sudo ufw allow 22/tcp       # Same thing

# If you use a non-standard SSH port
sudo ufw allow 2222/tcp

# Allow web traffic
sudo ufw allow http         # Port 80
sudo ufw allow https        # Port 443
sudo ufw allow 'Nginx Full' # Both 80 and 443 for Nginx

# Allow specific port
sudo ufw allow 3306/tcp     # MySQL (only if remote access needed)

# Allow from specific IP
sudo ufw allow from 203.0.113.10 to any port 3306   # MySQL only from your IP
sudo ufw allow from 203.0.113.10 to any port 22     # SSH only from your IP

# Deny a port
sudo ufw deny 8080

# Enable UFW (DO NOT run without allowing SSH first!)
sudo ufw enable

# Disable UFW
sudo ufw disable

# Delete a rule
sudo ufw delete allow 3306
sudo ufw delete allow from 203.0.113.10

# Reset all rules
sudo ufw reset
```

**Standard production firewall setup:**
```bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw enable
```

---

## 10.5 Fail2Ban — Brute Force Protection

**Fail2ban** monitors log files and automatically **bans IPs** that show malicious signs (too many failed login attempts).

```bash
# Install Fail2ban
sudo apt install fail2ban

# Main configuration (don't edit this directly)
# /etc/fail2ban/jail.conf

# Create local override
sudo nano /etc/fail2ban/jail.local
```

```ini
[DEFAULT]
# Ban for 1 hour
bantime = 3600
# Find window: 10 minutes
findtime = 600
# Max retries before ban
maxretry = 5
# Email notifications
destemail = admin@mysite.com
sendername = Fail2ban

# SSH protection
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3

# Apache brute force
[apache-auth]
enabled = true
port = http,https
logpath = %(apache_error_log)s

# Nginx bad requests
[nginx-http-auth]
enabled = true
logpath = %(nginx_error_log)s
maxretry = 6

# WordPress login protection
[wordpress]
enabled = true
filter = wordpress
logpath = /var/log/nginx/mysite-access.log
maxretry = 5
port = http,https
```

```bash
# Start Fail2ban
sudo systemctl start fail2ban
sudo systemctl enable fail2ban

# Check ban status
sudo fail2ban-client status
sudo fail2ban-client status sshd

# Unban an IP
sudo fail2ban-client set sshd unbanip 203.0.113.42

# View banned IPs
sudo fail2ban-client get sshd banned
```

---

## 10.6 SSH Security

```bash
# Edit SSH configuration
sudo nano /etc/ssh/sshd_config
```

**Recommended SSH security settings:**
```bash
# Change default port (makes scanners work harder)
Port 2222

# Disable root login (use sudo instead)
PermitRootLogin no

# Disable password authentication (use SSH keys only)
PasswordAuthentication no
PubkeyAuthentication yes

# Disable empty passwords
PermitEmptyPasswords no

# Limit login attempts
MaxAuthTries 3

# Disconnect idle sessions after 5 minutes
ClientAliveInterval 300
ClientAliveCountMax 2

# Allow only specific users
AllowUsers ubuntu developer

# Use only strong protocols
Protocol 2
```

```bash
# After editing, test config
sudo sshd -t
# Should produce no output if OK

# Restart SSH
sudo systemctl restart sshd
```

**Setting up SSH key authentication:**
```bash
# On your LOCAL machine (not server):
# Generate SSH key pair
ssh-keygen -t ed25519 -C "your-email@example.com"
# Creates: ~/.ssh/id_ed25519 (private key) and ~/.ssh/id_ed25519.pub (public key)

# Copy public key to server
ssh-copy-id ubuntu@server-ip
# Or manually:
cat ~/.ssh/id_ed25519.pub | ssh ubuntu@server-ip "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"

# Now you can login without password
ssh ubuntu@server-ip
```

---

## 10.7 Security Scanning and Monitoring

```bash
# Check for rootkits
sudo apt install rkhunter
sudo rkhunter --check

# Check for vulnerabilities in packages
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades

# Monitor authentication attempts
sudo tail -f /var/log/auth.log
sudo grep "Failed password" /var/log/auth.log | tail -20
sudo grep "Accepted password" /var/log/auth.log | tail -20

# Check for suspicious processes
ps aux | grep -i php
ps aux | sort -rk 3,3 | head -20   # Sort by CPU usage

# Check open network connections
sudo ss -tlnp
sudo netstat -tlnp

# Check cron jobs (look for unauthorized ones)
crontab -l
sudo crontab -l
ls /etc/cron.d/
ls /etc/cron.daily/
```

---

## 10.8 Server Hardening Checklist

```
□ Change default SSH port
□ Disable root SSH login
□ Enable SSH key-only authentication
□ Install and configure UFW firewall
□ Install Fail2ban for brute force protection
□ Keep packages updated: sudo apt update && sudo apt upgrade
□ Remove unused packages and services
□ Disable directory listing in web server
□ Never use 777 permissions in production
□ Secure .env files (600 permissions, never in Git)
□ Set APP_DEBUG=false in Laravel production
□ Disable PHP display_errors in production
□ Use strong MySQL passwords
□ Restrict MySQL to localhost (no remote root access)
□ Keep SSL certificates up to date
□ Enable security headers in web server
□ Restrict phpMyAdmin access by IP
□ Regular backups to off-server location
□ Monitor logs regularly
□ Use HTTPS everywhere
```

---

---

# SECTION 11 — SERVER TROUBLESHOOTING MASTER GUIDE

---

## 11.1 The Debugging Mindset

Before panicking, follow this **structured debugging approach**:

```
Problem appears
    ↓
1. WHAT exactly is the symptom?
   (Browser error? Server down? Slow? 404? 502?)
    ↓
2. WHEN did it start?
   (After deployment? After server reboot? Randomly?)
    ↓
3. WHAT changed?
   (Code deployed? Server updated? Config edited? DNS changed?)
    ↓
4. CHECK THE LOGS first
   (Error logs almost always tell you exactly what's wrong)
    ↓
5. ISOLATE the problem
   (Is it the web server? PHP? Database? DNS? Application code?)
    ↓
6. FIX and TEST
    ↓
7. DOCUMENT what caused it and how you fixed it
```

---

## 11.2 What Logs to Check First

### Priority Order When Debugging

```bash
# 1. Check the web server error log (MOST important)
sudo tail -n 50 /var/log/nginx/error.log
sudo tail -n 50 /var/log/apache2/error.log

# 2. Check the site-specific error log
sudo tail -n 50 /var/log/nginx/mysite-error.log

# 3. Check PHP-FPM error log
sudo tail -n 50 /var/log/php8.2-fpm.log
sudo tail -n 50 /var/log/php/error.log

# 4. Check application logs
tail -n 50 /var/www/mylaravelapp/storage/logs/laravel.log
pm2 logs my-node-app

# 5. Check service status
sudo systemctl status nginx
sudo systemctl status php8.2-fpm
sudo systemctl status mysql

# 6. Check system logs
sudo journalctl -u nginx -n 50
sudo tail -n 50 /var/log/syslog

# 7. Check disk space (often overlooked!)
df -h

# 8. Check memory
free -h
```

---

## 11.3 HTTP Status Code Complete Reference

### 1xx — Informational
| Code | Name | Meaning |
|------|------|---------|
| 100 | Continue | Server received request headers, client should proceed |
| 101 | Switching Protocols | Upgrade to WebSocket, etc. |

### 2xx — Success
| Code | Name | Meaning |
|------|------|---------|
| 200 | OK | Request succeeded. Normal successful response. |
| 201 | Created | Resource was created (e.g., POST to API) |
| 204 | No Content | Success but no response body (e.g., DELETE) |

### 3xx — Redirects
| Code | Name | Meaning & Use |
|------|------|---------------|
| **301** | Moved Permanently | Page moved forever. Browsers cache this. Use for domain migrations. SEO-safe. |
| **302** | Found (Temporary) | Page moved temporarily. Not cached. Use for maintenance redirects. |
| 304 | Not Modified | Browser's cached version is still valid. Don't re-download. |
| 307 | Temporary Redirect | Same as 302 but method is preserved (POST stays POST) |
| 308 | Permanent Redirect | Same as 301 but method is preserved |

### 4xx — Client Errors
| Code | Name | Common Causes | How to Fix |
|------|------|---------------|------------|
| **400** | Bad Request | Malformed request, invalid JSON | Check request format |
| **401** | Unauthorized | Not logged in, invalid token | Login or provide valid auth |
| **403** | Forbidden | No permission to access resource | Check file permissions, .htaccess |
| **404** | Not Found | File/page doesn't exist | Check URL, file exists, routing configured |
| **405** | Method Not Allowed | Wrong HTTP method (GET vs POST) | Check API route method |
| **408** | Request Timeout | Client too slow to send request | Check client connection |
| **413** | Payload Too Large | File upload too big | Increase upload limits in PHP/Nginx |
| **422** | Unprocessable Entity | Validation failed | Check form/API input data |
| **429** | Too Many Requests | Rate limit exceeded | Wait, or increase rate limit |

### 5xx — Server Errors
| Code | Name | Common Causes | How to Fix |
|------|------|---------------|------------|
| **500** | Internal Server Error | PHP error, .htaccess error, app crash | Check error log |
| **502** | Bad Gateway | Upstream not running (PHP-FPM/Node.js down) | Restart upstream service |
| **503** | Service Unavailable | Server overloaded or in maintenance mode | Check resources, disable maintenance |
| **504** | Gateway Timeout | Upstream too slow to respond | Increase timeout, fix slow query/code |

---

## 11.4 Debugging 500 Internal Server Error

```
Symptom: Browser shows "500 Internal Server Error"
This is the most common error freshers encounter.

Step 1: Check the error log
sudo tail -n 100 /var/log/apache2/error.log
sudo tail -n 100 /var/log/nginx/error.log
sudo tail -n 100 /var/www/myapp/storage/logs/laravel.log

Step 2: Common causes and fixes:

A) PHP Fatal Error
   Log shows: "PHP Fatal error: Uncaught Error: ..."
   Fix: Fix the PHP code error on the line mentioned

B) PHP Parse Error (syntax error)
   Log shows: "PHP Parse error: syntax error, unexpected..."
   Fix: Check the PHP file for syntax errors
   Test: php -l /var/www/myapp/problematic-file.php

C) .htaccess error
   Log shows: "Invalid command 'RewriteEngine'..."
   Fix: sudo a2enmod rewrite && sudo systemctl reload apache2
   Or: Check .htaccess syntax

D) Missing PHP extension
   Log shows: "Call to undefined function curl_init()"
   Fix: sudo apt install php8.2-curl && sudo systemctl restart php8.2-fpm

E) Permission denied for log/cache files
   Log shows: "Unable to write to /var/www/app/storage"
   Fix: sudo chmod -R 775 storage && sudo chown -R www-data:www-data storage

F) Laravel: Missing APP_KEY
   Log shows: "No application encryption key has been specified"
   Fix: php artisan key:generate

G) Database connection error
   Log shows: "SQLSTATE[HY000] [2002] Connection refused"
   Fix: Check MySQL is running, check DB credentials in .env
```

---

## 11.5 Debugging 502 Bad Gateway

```
Symptom: Browser shows "502 Bad Gateway"
Meaning: Nginx received a bad/no response from the application (PHP-FPM, Node.js, etc.)

Step 1: Check Nginx error log
sudo tail -n 50 /var/log/nginx/error.log
# Common messages:
# "connect() failed (111: Connection refused) while connecting to upstream"
# → The upstream service isn't running or isn't listening on expected port/socket

# "no live upstreams while connecting to upstream"
# → All servers in an upstream block are down

Step 2: Check if the upstream is running

For PHP-FPM:
sudo systemctl status php8.2-fpm
sudo systemctl restart php8.2-fpm
# Check socket exists:
ls -la /run/php/php8.2-fpm.sock

For Node.js:
pm2 status
pm2 restart my-app

For custom port:
sudo ss -tlnp | grep :3000   # Is anything listening on port 3000?

Step 3: Check the Nginx config matches reality
# In nginx config: fastcgi_pass unix:/run/php/php8.2-fpm.sock;
# Does that socket file actually exist?
ls -la /run/php/

# Or: proxy_pass http://127.0.0.1:3000;
# Is the app actually running on port 3000?
```

---

## 11.6 Debugging 403 Forbidden

```
Symptom: Browser shows "403 Forbidden"

Common causes:

1. Wrong file/directory permissions
   Check: ls -la /var/www/mysite/
   Fix: sudo chown -R www-data:www-data /var/www/mysite
        sudo chmod -R 755 /var/www/mysite

2. Missing index file
   If directory listing is disabled and no index.php/index.html exists
   Check: ls /var/www/mysite/
   Fix: Create an index.php file

3. Apache: AllowOverride not set
   In VirtualHost, AllowOverride is "None" but .htaccess tries to override
   Fix: Change to AllowOverride All in VirtualHost config

4. .htaccess denying access
   Check: cat /var/www/mysite/.htaccess | grep -i "deny"
   Check for: Deny from all

5. SELinux (not on Ubuntu usually)
   ls -Z /var/www/mysite/

Debugging command:
sudo tail -n 20 /var/log/apache2/error.log | grep "403\|forbidden\|Forbidden"
```

---

## 11.7 Debugging 404 Not Found

```
Symptom: Page returns 404

Common causes:

1. File genuinely doesn't exist
   Check: ls /var/www/mysite/the-page.php

2. URL rewriting not configured (mod_rewrite / try_files)
   For WordPress: Check .htaccess exists and mod_rewrite is enabled
   For Laravel: DocumentRoot must point to public/
   For Nginx: Check try_files $uri $uri/ /index.php line exists

3. DocumentRoot/root points to wrong directory
   Check Nginx: grep -r "root " /etc/nginx/sites-enabled/
   Check Apache: grep -r "DocumentRoot" /etc/apache2/sites-enabled/

4. React Router: Refresh on dynamic URL returns 404
   Fix in Nginx: try_files $uri $uri/ /index.html;

5. Domain not pointing to this server
   Check: dig mysite.com A
   Should return your server's IP
```

---

## 11.8 Debugging DNS Issues

```
Symptom: Domain not loading, or loading wrong site

Check:
# What IP does the domain resolve to?
dig mysite.com A

# Is that your server IP?
curl -I http://your-server-ip -H "Host: mysite.com"

# Is DNS propagated everywhere?
# Visit: https://dnschecker.org

# Check from different DNS servers
nslookup mysite.com 8.8.8.8      # Google DNS
nslookup mysite.com 1.1.1.1      # Cloudflare DNS
nslookup mysite.com 208.67.222.222  # OpenDNS

# Clear local DNS cache (on your machine, not server)
# Windows: ipconfig /flushdns
# Mac: sudo dscacheutil -flushcache
# Linux: sudo systemctl restart systemd-resolved
```

---

## 11.9 Debugging SSL Issues

```
Symptom: SSL errors, "Not Secure" in browser

Commands:
# Check certificate details
sudo certbot certificates
openssl s_client -connect mysite.com:443 -servername mysite.com

# Check certificate expiry
echo | openssl s_client -servername mysite.com -connect mysite.com:443 2>/dev/null | openssl x509 -noout -dates

# Test SSL configuration
# https://www.ssllabs.com/ssltest/

# Renew expired certificate
sudo certbot renew --force-renewal --cert-name mysite.com

# After renewal, reload web server
sudo systemctl reload nginx
# or
sudo systemctl reload apache2
```

---

## 11.10 Debugging Permission Denied Issues

```
Quick diagnosis:
# See who owns the files
ls -la /var/www/mysite/

# See who Nginx/Apache runs as
ps aux | grep nginx | head -3
ps aux | grep apache | head -3
# Should show www-data

# See the exact error
sudo tail -n 20 /var/log/nginx/error.log | grep permission

Common fix:
sudo chown -R www-data:www-data /var/www/mysite
sudo chmod -R 755 /var/www/mysite
sudo chmod -R 644 /var/www/mysite/*.php

# For Laravel storage
sudo chmod -R 775 /var/www/mylaravelapp/storage
sudo chmod -R 775 /var/www/mylaravelapp/bootstrap/cache
```

---

## 11.11 Debugging PHP Issues

```bash
# Check PHP version
php -v

# Check which php.ini is loaded
php --ini

# Check PHP syntax of a file
php -l /var/www/myapp/index.php

# Check PHP modules loaded
php -m
php -m | grep mysql
php -m | grep curl
php -m | grep mbstring

# Check PHP configuration values
php -r "echo ini_get('memory_limit');"
php -r "echo ini_get('upload_max_filesize');"
php -r "phpinfo();" | grep -i "memory"

# PHP error log location
php -i | grep "error_log"

# Enable PHP error display temporarily (debug only)
php -r "ini_set('display_errors', 1); include('problematic-script.php');"
```

---

## 11.12 Debugging Database Connection Issues

```
Symptom: "Connection refused" or "Access denied"

Step 1: Is MySQL running?
sudo systemctl status mysql

Step 2: Can you connect locally?
mysql -u myapp_user -p myapp_db
# Enter password. If this fails, the credentials are wrong.

Step 3: Check the exact error message in your application log
# For Laravel:
tail -n 50 /var/www/myapp/storage/logs/laravel.log
# For WordPress:
# The wp-config.php error is shown in browser (during install) or logged

Step 4: Common fixes:
# Wrong host: DB_HOST should be '127.0.0.1' not '0.0.0.0'
# Wrong database name: Check the DB exists
# Wrong username/password: Reset with:
sudo mysql
ALTER USER 'myapp_user'@'localhost' IDENTIFIED BY 'newpassword';
FLUSH PRIVILEGES;

# MySQL bind address (if connecting from different IP):
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
# Change: bind-address = 127.0.0.1
# To allow all: bind-address = 0.0.0.0
sudo systemctl restart mysql
```

---

## 11.13 Disk Full Issue

Disk full is a **silent killer** — it causes all sorts of strange errors.

```bash
# Check disk usage
df -h

# Find what's taking space
du -sh /var/log/*       # Logs often grow huge
du -sh /var/www/*       # Sites
du -sh /tmp             # Temp files

# Find large files
find / -type f -size +100M 2>/dev/null | head -20
find /var -type f -size +50M 2>/dev/null

# Clear PHP sessions
sudo rm -rf /var/lib/php/sessions/*

# Clear old logs
sudo truncate -s 0 /var/log/nginx/access.log
sudo truncate -s 0 /var/log/apache2/access.log

# Set up log rotation (usually pre-configured)
sudo nano /etc/logrotate.d/nginx
# Default config rotates logs weekly

# Clear apt cache
sudo apt clean
sudo apt autoremove

# Remove old PM2 logs
pm2 flush
```

---

## 11.14 Server Slow / High CPU or Memory

```bash
# Check CPU and memory usage
htop
top

# Find memory-hungry processes
ps aux --sort=-%mem | head -20

# Find CPU-hungry processes
ps aux --sort=-%cpu | head -20

# Check MySQL slow queries
sudo tail /var/log/mysql/slow.log

# Check PHP-FPM status
sudo curl --unix-socket /run/php/php8.2-fpm.sock http://localhost/fpm-status

# Check Nginx connection stats
sudo nginx -V 2>&1 | grep with-http_stub_status_module
# Add to server block: location /nginx-status { stub_status; allow 127.0.0.1; deny all; }
curl http://127.0.0.1/nginx-status
```

---

## 11.15 The Troubleshooting Checklist Template

When something breaks, work through this:

```
□ What is the exact error message? (Read it carefully)
□ When did it start?
□ What was the last change made to the server/code?
□ Check web server error log (nginx/apache)
□ Check application error log (laravel.log, pm2 logs)
□ Check PHP-FPM log
□ Check MySQL error log if DB related
□ Is the web server running? (systemctl status nginx)
□ Is PHP-FPM running? (systemctl status php8.2-fpm)
□ Is MySQL running? (systemctl status mysql)
□ Is the Node app running? (pm2 status)
□ Is disk space OK? (df -h)
□ Are file permissions correct? (ls -la /var/www/mysite)
□ Is .env configured correctly?
□ Is the domain pointing to this server? (dig domain.com)
□ Is SSL valid? (certbot certificates)
□ Can you access the server at all? (ping server-ip)
```

---

---

# SECTION 12 — DEPLOYMENT WORKFLOWS

---

## 12.1 Git Basics for Deployments

Every professional deployment uses **Git** for version control and code management.

```bash
# Install Git on server
sudo apt install git

# Configure Git identity
git config --global user.name "Your Name"
git config --global user.email "you@example.com"

# Clone a repository
git clone https://github.com/yourorg/myproject.git
git clone git@github.com:yourorg/myproject.git   # SSH clone (no password prompts)

# Check current status
git status

# Pull latest changes from remote
git pull origin main
git pull origin master

# Fetch without merging
git fetch origin

# Check which branch you're on
git branch

# Switch branch
git checkout main
git checkout feature/new-feature

# View commit history
git log --oneline -20
```

### Setting Up SSH Key for GitHub/GitLab

```bash
# Generate SSH key on server
ssh-keygen -t ed25519 -C "server@mysite.com"
# Press Enter for all prompts (default location, no passphrase for automated deployments)

# View the public key
cat ~/.ssh/id_ed25519.pub

# Add this key to GitHub:
# GitHub → Settings → SSH Keys → New SSH Key → Paste key

# Test connection
ssh -T git@github.com
# Should say: "Hi username! You've successfully authenticated"
```

---

## 12.2 Laravel Deployment Workflow

### Full Laravel Deployment Step-by-Step

```bash
# =============================================
# FIRST-TIME DEPLOYMENT
# =============================================

# 1. SSH into server
ssh ubuntu@your-server-ip

# 2. Create project directory
sudo mkdir -p /var/www/mylaravelapp
sudo chown ubuntu:ubuntu /var/www/mylaravelapp
cd /var/www/mylaravelapp

# 3. Clone repository
git clone git@github.com:yourorg/mylaravelapp.git .

# 4. Install dependencies (skip dev, optimize autoloader)
composer install --no-dev --optimize-autoloader

# 5. Set up environment
cp .env.example .env
nano .env    # Fill in all production values

# 6. Generate app key
php artisan key:generate

# 7. Run migrations
php artisan migrate --force

# 8. Seed if needed (rarely in production)
php artisan db:seed --force

# 9. Create storage symlink (for public uploads)
php artisan storage:link

# 10. Set correct permissions
sudo chown -R www-data:www-data /var/www/mylaravelapp
sudo chmod -R 755 /var/www/mylaravelapp
sudo chmod -R 775 /var/www/mylaravelapp/storage
sudo chmod -R 775 /var/www/mylaravelapp/bootstrap/cache

# 11. Cache config/routes/views for production
php artisan config:cache
php artisan route:cache
php artisan view:cache

# 12. Set up web server (Nginx or Apache)
# See Section 3/4 for configuration

# 13. Set up SSL
sudo certbot --nginx -d mylaravelapp.com

# 14. Set up cron
sudo crontab -e
# Add: * * * * * cd /var/www/mylaravelapp && php artisan schedule:run >> /dev/null 2>&1

# 15. Set up queue workers (if using queues)
# Install and configure Supervisor (see Section 6.6)
```

### Laravel Update Deployment (Subsequent Deployments)

```bash
# Create a deployment script: /usr/local/bin/deploy-laravel.sh

#!/bin/bash
set -e    # Exit if any command fails

APP_DIR="/var/www/mylaravelapp"
echo "Starting deployment..."

cd $APP_DIR

# Enable maintenance mode
php artisan down --message="Deploying update. Back in a minute." --retry=60

# Pull latest code
git fetch origin
git reset --hard origin/main

# Install dependencies
composer install --no-dev --optimize-autoloader

# Run migrations
php artisan migrate --force

# Clear and rebuild caches
php artisan config:clear
php artisan config:cache
php artisan route:clear
php artisan route:cache
php artisan view:clear
php artisan view:cache

# Clear application cache
php artisan cache:clear

# Restart queue workers
php artisan queue:restart

# Set permissions
chown -R www-data:www-data $APP_DIR/storage $APP_DIR/bootstrap/cache

# Disable maintenance mode
php artisan up

echo "Deployment complete!"
```

```bash
# Make script executable
chmod +x /usr/local/bin/deploy-laravel.sh

# Run deployment
sudo /usr/local/bin/deploy-laravel.sh
```

---

## 12.3 WordPress Deployment Workflow

### Deploying a WordPress Site from Scratch

```bash
# 1. Create database (see Section 7)

# 2. Download and extract WordPress
cd /tmp
wget https://wordpress.org/latest.tar.gz
tar xzvf latest.tar.gz
sudo mv wordpress /var/www/myclientsite

# 3. Set permissions
sudo chown -R www-data:www-data /var/www/myclientsite
sudo chmod -R 755 /var/www/myclientsite

# 4. Configure wp-config.php
cd /var/www/myclientsite
sudo -u www-data cp wp-config-sample.php wp-config.php
sudo -u www-data nano wp-config.php

# 5. Configure web server (see Section 3 or 4)

# 6. Get SSL
sudo certbot --nginx -d myclientsite.com -d www.myclientsite.com

# 7. Visit the domain and complete WordPress installation wizard
```

### WordPress Update/Maintenance Workflow

```bash
# ALWAYS BACKUP FIRST
# Backup database
mysqldump -u root -p wordpress_db > /backup/wordpress_$(date +%Y%m%d).sql

# Backup files
tar -czvf /backup/wordpress_files_$(date +%Y%m%d).tar.gz /var/www/myclientsite

# Update via WP-CLI (recommended for production)
cd /var/www/myclientsite
sudo -u www-data wp core update
sudo -u www-data wp plugin update --all
sudo -u www-data wp theme update --all

# Clear cache after updates
sudo -u www-data wp cache flush
sudo -u www-data wp rewrite flush

# Verify site is working
curl -I https://myclientsite.com
```

---

## 12.4 React App Deployment Workflow

```bash
# =============================================
# REACT APP DEPLOYMENT
# =============================================

# Option 1: Build locally, upload to server
# On your local machine:
npm run build
# Produces: ./build/ directory

# Upload build to server
scp -r ./build/* ubuntu@server-ip:/var/www/myreactapp/
# Or using rsync (faster for updates):
rsync -avz --delete ./build/ ubuntu@server-ip:/var/www/myreactapp/

# On server, just ensure permissions
sudo chown -R www-data:www-data /var/www/myreactapp

# Option 2: Build on server
cd /var/www
git clone git@github.com:yourorg/react-app.git
cd react-app
npm install
npm run build
# Files are in react-app/build/

# Create Nginx config (see Section 4.10)
# Get SSL
sudo certbot --nginx -d myreactapp.com
```

### React App Update Script

```bash
#!/bin/bash
# /usr/local/bin/deploy-react.sh

APP_DIR="/var/www/react-app"
WEB_DIR="/var/www/myreactapp"

echo "Deploying React app..."

cd $APP_DIR

# Pull latest code
git fetch origin
git reset --hard origin/main

# Install dependencies (only if package.json changed)
npm install --production=false

# Build
npm run build

# Copy build to web directory
cp -r build/* $WEB_DIR/

# Set permissions
chown -R www-data:www-data $WEB_DIR

echo "React deployment complete!"
```

---

## 12.5 Node.js App Deployment Workflow

```bash
# =============================================
# NODE.JS APP DEPLOYMENT
# =============================================

# First time:
cd /var/www
git clone git@github.com:yourorg/nodeapp.git
cd nodeapp
npm install --production
cp .env.example .env
nano .env   # Set production values

pm2 start ecosystem.config.js --env production
pm2 save

# Configure Nginx reverse proxy (see Section 4.8)
sudo certbot --nginx -d api.mysite.com
```

### Node.js Update Script

```bash
#!/bin/bash
# /usr/local/bin/deploy-node.sh

APP_DIR="/var/www/nodeapp"
APP_NAME="my-api"

echo "Deploying Node.js app..."

cd $APP_DIR

# Pull latest code
git fetch origin
git reset --hard origin/main

# Install/update dependencies
npm install --production

# Reload app with zero downtime
pm2 reload $APP_NAME --update-env

echo "Node.js deployment complete!"
```

---

## 12.6 Next.js App Deployment Workflow

```bash
# =============================================
# NEXT.JS DEPLOYMENT
# =============================================

# First time:
cd /var/www
git clone git@github.com:yourorg/nextjs-app.git
cd nextjs-app
npm install
nano .env.local    # or .env.production

npm run build    # Build Next.js app

pm2 start npm --name "nextjs-app" -- start
pm2 save

# Configure Nginx (see Section 4.9)
sudo certbot --nginx -d mysite.com
```

### Next.js Update Script

```bash
#!/bin/bash
# /usr/local/bin/deploy-nextjs.sh

APP_DIR="/var/www/nextjs-app"
APP_NAME="nextjs-app"

echo "Deploying Next.js app..."

cd $APP_DIR

# Pull latest
git fetch origin
git reset --hard origin/main

# Install dependencies
npm install

# Build
npm run build

# Reload PM2 with zero downtime
pm2 reload $APP_NAME

echo "Next.js deployment complete!"
```

---

## 12.7 Rollback Strategy

**Always be able to rollback.** If something breaks after deployment:

### Git Rollback

```bash
# View recent commits
git log --oneline -10

# Rollback to previous commit
git reset --hard HEAD~1       # Go back 1 commit
git reset --hard <commit-hash>  # Go back to specific commit

# After rollback, rebuild/restart
composer install --no-dev --optimize-autoloader  # For PHP
npm install && npm run build                      # For Node.js
pm2 reload myapp                                  # Restart
php artisan config:cache                          # For Laravel
```

### Database Rollback

```bash
# Laravel migrations rollback
php artisan migrate:rollback       # Undo last migration
php artisan migrate:rollback --step=3  # Undo last 3 migrations

# Restore from backup
mysql -u root -p mydb < /backup/mydb_before_deploy.sql
```

---

## 12.8 Staging vs Production Environment

**NEVER test directly on the production server.** Always have a staging environment.

```
Development (Local)    →    Staging Server    →    Production Server
  Your laptop               (Mirrors prod)          (Live site)
  localhost:8000             staging.mysite.com      mysite.com
  .env debug=true            .env debug=true         .env debug=false
  Test all changes           Final testing           Customer traffic
```

```bash
# Staging server setup is identical to production
# But with different .env values:
APP_ENV=staging
APP_DEBUG=true
APP_URL=https://staging.mysite.com
DB_DATABASE=staging_mydb

# Protect staging from public access:
# In Nginx: restrict to your IP
allow 203.0.113.10;  # Your office/home IP
deny all;
```

---

> **End of Part 3 — Sections 9 to 12**  
> Continue to Part 4 for Sections 13 through 16, Checklists, Roadmap, and Labs.
