Stack Setup — Node.js, PHP & Databases
Deploy Node.js, Express, Next.js, React. Master PHP-FPM, Laravel, WordPress. Manage MySQL, MongoDB and Redis.
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
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
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 Feature | Benefit |
|---|---|
| Auto-restart on crash | App stays running even after exceptions |
| Startup script | App restarts after server reboot |
| Cluster mode | Uses all CPU cores for better performance |
| Log management | Centralised logs for all your apps |
| Zero-downtime reload | Deploy 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
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
| Issue | Cause | Fix |
|---|---|---|
| App exits after closing terminal | Not using PM2 | pm2 start server.js |
| App doesn't restart after reboot | PM2 startup not configured | pm2 startup && pm2 save |
| 502 from Nginx | Node.js app not running | pm2 status, pm2 restart app |
| Port already in use | Previous process still running | pm2 delete app, pkill node |
| Memory keeps growing | Memory leak in code | pm2 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
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
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
| Command | Purpose |
|---|---|
php artisan migrate | Run pending database migrations |
php artisan migrate --force | Force migrations in production |
php artisan migrate:rollback | Undo last migration |
php artisan cache:clear | Clear application cache |
php artisan config:clear | Clear config cache |
php artisan route:clear | Clear route cache |
php artisan view:clear | Clear compiled views |
php artisan optimize | Cache config + routes + views (production) |
php artisan storage:link | Create symlink for public storage |
php artisan key:generate | Generate APP_KEY |
php artisan down | Enable maintenance mode |
php artisan up | Disable maintenance mode |
php artisan queue:work | Process queued jobs |
php artisan route:list | Show all registered routes |
php artisan make:controller | Create a new controller |
php artisan make:model Product -m | Create 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;
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
| Path | Permission | Why |
|---|---|---|
/var/www/wordpress/ | 755 | Directories need execute bit |
*.php, *.css, *.js | 644 | Readable but not writeable by others |
wp-config.php | 640 or 644 | Config file — read only |
wp-content/uploads/ | 755 | WordPress needs to write here |
wp-content/themes/ | 755 | Theme files |
wp-content/plugins/ | 755 | Plugin 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>
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