Section 5 — Node.js Server Deployment

Node.js is a JavaScript runtime that lets you run JavaScript on the server. It is NOT a web server like Apache — it is an application runtime. You run your Node.js app on a port (e.g., 3000), then Nginx acts as a reverse proxy in front.

Internet → Nginx (port 80/443) → Node.js App (port 3000)

5.1 Installing Node.js

⚠️
Don't Use the Default Ubuntu Package

The default apt Node.js package is usually very outdated. Use NodeSource or NVM instead.

Method 1: NodeSource (Recommended for Production)

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install nodejs
node -v   # Verify
npm -v

Method 2: NVM (Recommended for Development Flexibility)

# Install NVM
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
source ~/.bashrc

# Install and use a specific version
nvm install 20
nvm use 20
nvm alias default 20   # Set as default

# List installed versions
nvm list

package.json — Key Commands

npm install                    # Install all dependencies
npm install --production       # Skip devDependencies
npm install express            # Add a package
npm install --save-dev nodemon # Add a dev dependency
npm start                      # Run start script
npm run build                  # Run build script
🚨
Protect Your .env File

NEVER commit .env to Git. Always add .env to .gitignore. Database passwords and API keys must never be in source code.

# Always add to .gitignore
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
echo "node_modules/" >> .gitignore

5.2 PM2 — The Production Process Manager

When you run node server.js directly, the process dies if you close the terminal, there's an error, or the server reboots. PM2 solves all of this.

PM2 FeatureBenefit
Auto-restart on crashApp stays running even after exceptions
Startup scriptApp restarts after server reboot
Cluster modeUses all CPU cores for better performance
Log managementCentralised logs for all your apps
Zero-downtime reloadDeploy without dropping requests
# Install PM2 globally
sudo npm install -g pm2

# Start an application
pm2 start server.js --name "my-api"
pm2 start server.js --name "my-api" -i max   # Cluster mode (all CPU cores)

# For Next.js (runs 'npm start')
pm2 start npm --name "nextjs-app" -- start

# List all processes
pm2 list
pm2 status

# View logs
pm2 logs
pm2 logs my-api --lines 100

# Monitor in real-time
pm2 monit

# Restart / Stop / Delete
pm2 restart my-api
pm2 stop my-api
pm2 delete my-api

# Zero-downtime reload
pm2 reload my-api
💡
Make PM2 Survive Reboots

Always run these two commands after setting up PM2, otherwise your app won't start after a server reboot.

# Generate and run the startup command
pm2 startup
# Copy and run the command it prints, then:

pm2 save   # Save current process list

PM2 Ecosystem File (Multiple Apps)

// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'api-server',
      script: 'server.js',
      instances: 'max',         // Use all CPU cores
      exec_mode: 'cluster',
      env_production: {
        NODE_ENV: 'production',
        PORT: 3000
      },
      error_file: '/var/log/pm2/api-error.log',
      out_file: '/var/log/pm2/api-out.log',
      max_memory_restart: '500M',  // Restart if memory exceeds 500MB
      watch: false,
    }
  ]
};
pm2 start ecosystem.config.js --env production
pm2 reload ecosystem.config.js --env production

5.3 Express.js Deployment

# 1. Create project directory
sudo mkdir -p /var/www/my-express-api
sudo chown ubuntu:ubuntu /var/www/my-express-api
cd /var/www/my-express-api

# 2. Clone from Git
git clone https://github.com/yourorg/my-express-api.git .

# 3. Install production dependencies
npm install --production

# 4. Create .env file with production values
nano .env

# 5. Start with PM2
pm2 start server.js --name "my-express-api"
pm2 save

# 6. Create Nginx reverse proxy
sudo nano /etc/nginx/sites-available/my-express-api.conf
server {
    listen 80;
    server_name api.mysite.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}
sudo ln -s /etc/nginx/sites-available/my-express-api.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d api.mysite.com

5.4 Next.js Deployment (SSR)

cd /var/www
git clone https://github.com/yourorg/nextjs-app.git
cd nextjs-app

npm install
nano .env.local    # Set NEXT_PUBLIC_API_URL etc.

npm run build      # Build the application
pm2 start npm --name "nextjs-app" -- start
pm2 save

5.5 React App Deployment (Static)

cd /var/www
git clone https://github.com/yourorg/react-app.git
cd react-app

npm install
npm run build      # Creates build/ folder

# Nginx serves the build/ folder as static files
# See Part 1 Section 4.6 for the Nginx config

Common Node.js Deployment Issues

IssueCauseFix
App exits after closing terminalNot using PM2pm2 start server.js
App doesn't restart after rebootPM2 startup not configuredpm2 startup && pm2 save
502 from NginxNode.js app not runningpm2 status, pm2 restart app
Port already in usePrevious process still runningpm2 delete app, pkill node
Memory keeps growingMemory leak in codepm2 start app --max-memory-restart 500M

Section 6 — PHP Ecosystem

PHP is the backbone of most web applications — WordPress, Laravel, CodeIgniter are all PHP-based. On a production server, PHP should run via PHP-FPM (FastCGI Process Manager) — a separate service that is much more efficient than mod_php.

6.1 PHP 8.2 & PHP-FPM Installation

sudo apt update
sudo apt install php8.2 php8.2-fpm php8.2-mysql php8.2-curl php8.2-gd \
    php8.2-mbstring php8.2-xml php8.2-zip php8.2-bcmath php8.2-intl \
    php8.2-imagick php8.2-redis php8.2-opcache

php -v                           # Check version
sudo systemctl status php8.2-fpm

# For multiple PHP versions on same server
sudo add-apt-repository ppa:ondrej/php
sudo apt update
sudo apt install php7.4 php7.4-fpm php7.4-mysql
sudo apt install php8.2 php8.2-fpm

# Switch CLI default
sudo update-alternatives --config php

6.2 PHP Configuration (php.ini)

# Find which php.ini is loaded
php -i | grep "Loaded Configuration File"

sudo nano /etc/php/8.2/fpm/php.ini
; File upload limits
upload_max_filesize = 64M
post_max_size = 64M

; Memory & execution limits
memory_limit = 256M
max_execution_time = 300
max_input_vars = 3000

; CRITICAL: Turn off error display in production
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/php-error.log

; Timezone
date.timezone = "Asia/Kolkata"

; OPcache (speeds up PHP significantly)
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0   ; 0 = always use cache (production)
sudo systemctl restart php8.2-fpm  # Apply changes
⚠️
Always Restart PHP-FPM After php.ini Changes

Changes to php.ini do NOT take effect until PHP-FPM is restarted. Don't forget this step.

6.3 Composer — PHP Dependency Manager

Composer is to PHP what npm is to Node.js.

# Install Composer
cd ~
curl -sS https://getcomposer.org/installer -o composer-setup.php
sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer
composer --version

# Project commands
composer install                              # Install from composer.json
composer install --no-dev --optimize-autoloader  # Production install
composer require guzzlehttp/guzzle            # Add a package
composer update                               # Update packages
composer dump-autoload --optimize             # Regenerate autoload files

6.4 Laravel Full Deployment

# 1. Create and set up directory
sudo mkdir -p /var/www/mylaravelapp
sudo chown ubuntu:ubuntu /var/www/mylaravelapp
cd /var/www/mylaravelapp

# 2. Clone repository
git clone https://github.com/yourorg/laravel-app.git .

# 3. Install dependencies (no dev packages in production)
composer install --no-dev --optimize-autoloader

# 4. Create environment file
cp .env.example .env
nano .env
## Production .env example
APP_NAME="My Laravel App"
APP_ENV=production
APP_KEY=          # Generated in next step
APP_DEBUG=false   # CRITICAL: NEVER true in production!
APP_URL=https://mylaravelapp.com

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=mylaravelapp
DB_USERNAME=mylaravel_user
DB_PASSWORD=strongpassword

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

# 6. Run migrations
php artisan migrate --force

# 7. Set 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

# 8. Storage symlink (for uploaded files)
php artisan storage:link

# 9. Cache for performance (always do this in production)
php artisan config:cache
php artisan route:cache
php artisan view:cache
🚨
APP_DEBUG Must Be false on Production

If APP_DEBUG=true on production, Laravel will show detailed error pages with your database credentials, file paths, and application secrets to anyone who triggers an error.

6.5 Laravel Artisan Command Reference

CommandPurpose
php artisan migrateRun pending database migrations
php artisan migrate --forceForce migrations in production
php artisan migrate:rollbackUndo last migration
php artisan cache:clearClear application cache
php artisan config:clearClear config cache
php artisan route:clearClear route cache
php artisan view:clearClear compiled views
php artisan optimizeCache config + routes + views (production)
php artisan storage:linkCreate symlink for public storage
php artisan key:generateGenerate APP_KEY
php artisan downEnable maintenance mode
php artisan upDisable maintenance mode
php artisan queue:workProcess queued jobs
php artisan route:listShow all registered routes
php artisan make:controllerCreate a new controller
php artisan make:model Product -mCreate model + migration

6.6 Supervisor — Laravel Queue Workers

For processing queued jobs (emails, notifications), you need a queue worker running permanently in the background. Supervisor manages it.

sudo apt install supervisor

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

# Check status
sudo supervisorctl status

Laravel Cron Setup

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

6.7 WordPress Setup

# Create MySQL database first
mysql -u root -p
CREATE DATABASE wordpress_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'strongpassword';
GRANT ALL PRIVILEGES ON wordpress_db.* TO 'wp_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
# Download and install WordPress
cd /tmp
wget https://wordpress.org/latest.tar.gz
tar -xzvf latest.tar.gz
sudo mv wordpress /var/www/mywordpresssite

cd /var/www/mywordpresssite
cp wp-config-sample.php wp-config.php
nano wp-config.php

# Set permissions
sudo chown -R www-data:www-data /var/www/mywordpresssite
sudo chmod -R 755 /var/www/mywordpresssite
sudo chmod 644 /var/www/mywordpresssite/wp-config.php
// Key wp-config.php settings
define('DB_NAME', 'wordpress_db');
define('DB_USER', 'wp_user');
define('DB_PASSWORD', 'strongpassword');
define('DB_HOST', 'localhost');
define('DB_CHARSET', 'utf8mb4');

define('WP_MEMORY_LIMIT', '256M');
define('DISALLOW_FILE_EDIT', true);       // Security: disable admin file editing
define('AUTOMATIC_UPDATER_DISABLED', true); // Manage updates manually

// If behind SSL proxy (e.g., Cloudflare)
define('FORCE_SSL_ADMIN', true);
if (strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false) {
    $_SERVER['HTTPS'] = 'on';
}

6.8 CodeIgniter Deployment

cd /var/www
git clone https://github.com/yourorg/ci-project.git
cd ci-project

# CodeIgniter 4
nano app/Config/App.php     # Set $baseURL = 'https://mycisite.com/'
nano app/Config/Database.php  # Set DB credentials

# Permissions
sudo chown -R www-data:www-data /var/www/ci-project
sudo chmod -R 755 /var/www/ci-project
sudo chmod -R 775 /var/www/ci-project/writable

Section 7 — Database Management

7.1 MySQL / MariaDB Installation

# Install MySQL
sudo apt install mysql-server

# OR Install MariaDB (drop-in replacement, slightly improved)
sudo apt install mariadb-server

# Secure the installation — run immediately after install
sudo mysql_secure_installation
# Prompts:
# - Set root password: YES
# - Remove anonymous users: YES
# - Disallow remote root login: YES
# - Remove test database: YES
# - Reload privilege tables: YES

# Login
sudo mysql              # As system root
mysql -u root -p        # With password

mysql --version

7.2 Essential MySQL Commands

-- Show all databases
SHOW DATABASES;

-- Create a database (always use utf8mb4!)
CREATE DATABASE myapp_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- Select database
USE myapp_db;

-- Show tables
SHOW TABLES;

-- Describe table structure
DESCRIBE users;

-- Create a dedicated user (NEVER use root for apps!)
CREATE USER 'myapp_user'@'localhost' IDENTIFIED BY 'strongpassword123';

-- Grant permissions
GRANT ALL PRIVILEGES ON myapp_db.* TO 'myapp_user'@'localhost';
FLUSH PRIVILEGES;

-- Show user permissions
SHOW GRANTS FOR 'myapp_user'@'localhost';

-- Change password
ALTER USER 'myapp_user'@'localhost' IDENTIFIED BY 'newpassword';

-- Drop user
DROP USER 'myapp_user'@'localhost';

-- Drop database (CAREFUL!)
DROP DATABASE myapp_db;
⚠️
Never Use Root for Application Databases

Always create a dedicated database user with only the permissions needed. If the app is compromised, a limited user restricts damage.

7.3 Database Backups & Restore

Always backup before any deployment or database change.

# Backup a single database
mysqldump -u root -p myapp_db > /backup/myapp_db_$(date +%Y%m%d_%H%M%S).sql

# Backup with compression
mysqldump -u root -p myapp_db | gzip > /backup/myapp_db_$(date +%Y%m%d).sql.gz

# Backup all databases
mysqldump -u root -p --all-databases > /backup/all_$(date +%Y%m%d).sql

# Restore
mysql -u root -p myapp_db < /backup/myapp_db_20260101.sql

# Restore compressed
gunzip < /backup/myapp_db_20260101.sql.gz | mysql -u root -p myapp_db

Automated Daily Backup Script

sudo nano /usr/local/bin/backup_databases.sh
#!/bin/bash
BACKUP_DIR="/backup/mysql"
DATE=$(date +%Y%m%d_%H%M%S)
KEEP_DAYS=7

mkdir -p $BACKUP_DIR

for DB in $(mysql -uroot -pYOURPASS -e "SHOW DATABASES;" | grep -Ev "(Database|information_schema|performance_schema|sys)"); do
    mysqldump -uroot -pYOURPASS $DB | gzip > $BACKUP_DIR/${DB}_${DATE}.sql.gz
    echo "Backed up: $DB"
done

# Delete old backups
find $BACKUP_DIR -name "*.sql.gz" -mtime +$KEEP_DAYS -delete
echo "Done. Old backups cleaned."
sudo chmod +x /usr/local/bin/backup_databases.sh

# Add to cron (daily at 2 AM)
sudo crontab -e
# 0 2 * * * /usr/local/bin/backup_databases.sh >> /var/log/db_backup.log 2>&1

7.4 Redis

Redis is an in-memory key-value store used for caching, sessions, and queues. Laravel can use Redis as a cache driver or queue driver.

sudo apt install redis-server

sudo systemctl enable redis-server
sudo systemctl start redis-server
sudo systemctl status redis-server

# Test Redis
redis-cli ping     # Should return: PONG

# Basic commands
redis-cli
> SET mykey "hello"
> GET mykey
> DEL mykey
> KEYS *             # List all keys
> FLUSHALL           # Clear all data
# Configure Redis password (recommended for production)
sudo nano /etc/redis/redis.conf
# Find and set: requirepass YourStrongPassword

sudo systemctl restart redis-server

# Connect with auth
redis-cli -a YourStrongPassword

Laravel + Redis

# In .env
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=YourStrongPassword
REDIS_PORT=6379

7.5 MongoDB

MongoDB is a NoSQL document database. Used with Node.js apps and APIs.

# Install MongoDB 7
curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | sudo gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
sudo apt update
sudo apt install mongodb-org

sudo systemctl enable mongod
sudo systemctl start mongod
mongosh   # Connect to MongoDB shell
# Basic MongoDB Shell commands
mongosh
> show dbs
> use myapp_db
> db.users.insertOne({ name: "John", email: "john@example.com" })
> db.users.find()
> db.users.findOne({ name: "John" })
> db.users.deleteOne({ name: "John" })

Section 8 — WordPress Mastery

8.1 wp-config.php Deep Dive

The wp-config.php file is the heart of every WordPress installation.

<?php
// Database settings
define('DB_NAME', 'wordpress_db');
define('DB_USER', 'wp_user');
define('DB_PASSWORD', 'strongpassword');
define('DB_HOST', 'localhost');
define('DB_CHARSET', 'utf8mb4');
define('DB_COLLATE', '');

// Security keys — GENERATE FRESH ones at:
// https://api.wordpress.org/secret-key/1.1/salt/
define('AUTH_KEY',         'unique phrase here');
define('SECURE_AUTH_KEY',  'unique phrase here');
define('LOGGED_IN_KEY',    'unique phrase here');
define('NONCE_KEY',        'unique phrase here');

// Table prefix — change for security
$table_prefix = 'wp_';

// Performance
define('WP_MEMORY_LIMIT', '256M');
define('WP_MAX_MEMORY_LIMIT', '512M');

// Security hardening
define('DISALLOW_FILE_EDIT', true);        // Disable theme/plugin editor
define('DISALLOW_FILE_MODS', false);       // true = disable plugin/theme installs
define('AUTOMATIC_UPDATER_DISABLED', false); // Keep updates on

// Debug — OFF on production
define('WP_DEBUG', false);
define('WP_DEBUG_LOG', false);
define('WP_DEBUG_DISPLAY', false);

// Custom paths
define('WP_HOME', 'https://mywordpresssite.com');
define('WP_SITEURL', 'https://mywordpresssite.com');

8.2 WordPress File Permissions

PathPermissionWhy
/var/www/wordpress/755Directories need execute bit
*.php, *.css, *.js644Readable but not writeable by others
wp-config.php640 or 644Config file — read only
wp-content/uploads/755WordPress needs to write here
wp-content/themes/755Theme files
wp-content/plugins/755Plugin files
sudo chown -R www-data:www-data /var/www/mywordpresssite
find /var/www/mywordpresssite -type d -exec chmod 755 {} \;
find /var/www/mywordpresssite -type f -exec chmod 644 {} \;
# Special: uploads directory
chmod 755 /var/www/mywordpresssite/wp-content/uploads

8.3 WP-CLI — Command Line for WordPress

WP-CLI lets you manage WordPress from the terminal — no need to log into the admin panel.

# Install WP-CLI
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
wp --version

# Usage (run from WordPress root directory)
cd /var/www/mywordpresssite

# User management
wp user list
wp user create johndoe john@example.com --role=editor --user_pass=password123

# Plugin management
wp plugin list
wp plugin install contact-form-7 --activate
wp plugin deactivate akismet
wp plugin update --all

# Theme management
wp theme list
wp theme activate twentytwentyfour

# Core updates
wp core update
wp core update-db

# Database operations
wp db export /backup/wordpress_$(date +%Y%m%d).sql
wp db import /backup/wordpress_20260101.sql

# Search and replace (useful for domain migrations)
wp search-replace 'http://old-domain.com' 'https://new-domain.com' --precise --skip-columns=guid

# Cache
wp cache flush

# Run as www-data (avoids permission issues)
sudo -u www-data wp plugin update --all

8.4 WordPress Security Hardening

# In .htaccess — Protect wp-config.php
<Files wp-config.php>
    Order deny,allow
    Deny from all
</Files>

# Block XML-RPC (often abused for brute force)
<Files xmlrpc.php>
    Order deny,allow
    Deny from all
</Files>

# Disable directory listing
Options -Indexes

# Prevent PHP execution in uploads
<Directory "/var/www/wordpress/wp-content/uploads">
    php_flag engine off
</Directory>
💡
WordPress Security Essentials

1) Change default wp_ table prefix  |  2) Use strong unique passwords  |  3) Keep core, themes, plugins updated  |  4) Install Wordfence or Sucuri  |  5) Use Cloudflare for DDoS protection