You're a developer, not a sysadmin. But your project needs to be deployed somewhere, and that somewhere is usually a Linux server - whether it's a VPS, a cPanel hosting account, or a bare metal machine your client already has.
Here's the practical minimum you need to know to deploy and keep alive Node.js and Python apps without becoming a DevOps engineer.
cPanel: The Reality
A lot of client projects end up on shared cPanel hosting. It's not glamorous, but it's what many clients already pay for. Here's how to make it work.
Node.js on cPanel
Most modern cPanel installations include a Node.js app manager. The setup:
- Go to Setup Node.js App in cPanel
- Select your Node.js version (18+ recommended)
- Set the application root (where your code lives)
- Set the startup file (usually
server.jsorapp.js) - Click Create
cPanel uses Phusion Passenger under the hood. Your app runs behind Apache, so you don't configure ports - Passenger handles the reverse proxy.
For a Next.js app, the startup file looks like:
// server.js - production Next.js on cPanel
const { createServer } = require('http');
const next = require('next');
const app = next({ dev: false });
const handle = app.getRequestHandler();
app.prepare().then(() => {
createServer((req, res) => handle(req, res)).listen(0); // Passenger manages the port
});Python on cPanel
cPanel's Python support uses Passenger WSGI. For Django:
- Go to Setup Python App in cPanel
- Select Python version (3.10+)
- Set application root and startup file
- Create, then install requirements via the cPanel terminal
The WSGI entry point:
# passenger_wsgi.py
import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
os.environ['DJANGO_SETTINGS_MODULE'] = 'config.settings.production'
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()cPanel Cron Jobs
Need scheduled tasks? cPanel's Cron Jobs interface is straightforward:
# Run a Django management command every hour
0 * * * * cd /home/user/myproject && /home/user/virtualenv/myproject/bin/python manage.py process_queue >> /home/user/logs/cron.log 2>&1
# Run a Node.js script daily at midnight
0 0 * * * cd /home/user/myapp && /opt/cpanel/ea-nodejs18/bin/node scripts/cleanup.js >> /home/user/logs/cleanup.log 2>&1Always redirect output to a log file. When a cron job fails silently, the log is your only clue.
VPS: The Full Control Option
When cPanel isn't enough (or isn't available), a VPS gives you complete control. Here's the minimum viable setup.
Initial Server Setup
# Update everything
sudo apt update && sudo apt upgrade -y
# Create a non-root user
sudo adduser deploy
sudo usermod -aG sudo deploy
# Set up SSH key auth (from your local machine)
ssh-copy-id deploy@your-server-ip
# Disable password auth
sudo nano /etc/ssh/sshd_config
# Set: PasswordAuthentication no
sudo systemctl restart sshd
# Basic firewall
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enableThat's the security baseline: no root login, no password auth, firewall allowing only SSH and HTTP/HTTPS.
Nginx as Reverse Proxy
Nginx sits in front of your application and handles SSL, static files, and request routing:
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name myapp.com www.myapp.com;
# Redirect to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name myapp.com www.myapp.com;
ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
# Static files (served directly by Nginx - fast)
location /static/ {
alias /home/deploy/myapp/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
location /media/ {
alias /home/deploy/myapp/media/;
expires 7d;
}
# Proxy to application
location / {
proxy_pass http://127.0.0.1:8000;
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;
}
}Enable it:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t # Test configuration
sudo systemctl reload nginxSSL with Let's Encrypt
Free SSL in two commands:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d myapp.com -d www.myapp.comCertbot automatically configures Nginx and sets up auto-renewal. Done.
Process Management with Supervisor
Your app needs to stay running after you close the SSH session, restart if it crashes, and start on server boot. Supervisor handles this:
; /etc/supervisor/conf.d/myapp.conf
[program:myapp]
command=/home/deploy/myapp/venv/bin/gunicorn config.wsgi:application -b 127.0.0.1:8000 -w 3
directory=/home/deploy/myapp
user=deploy
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/home/deploy/logs/myapp.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5
environment=DJANGO_SETTINGS_MODULE="config.settings.production"For a Node.js app:
[program:myapp-node]
command=/usr/bin/node server.js
directory=/home/deploy/myapp
user=deploy
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/home/deploy/logs/node.log
environment=NODE_ENV="production",PORT="3000"Control your app:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status myapp # Check status
sudo supervisorctl restart myapp # Restart
sudo supervisorctl tail -f myapp # Live logsDatabase: PostgreSQL Setup
sudo apt install postgresql postgresql-contrib
# Create database and user
sudo -u postgres psql
postgres=# CREATE DATABASE myapp_db;
postgres=# CREATE USER myapp_user WITH PASSWORD 'secure_password';
postgres=# GRANT ALL PRIVILEGES ON DATABASE myapp_db TO myapp_user;
postgres=# \qAutomated Backups
# /home/deploy/scripts/backup_db.sh
#!/bin/bash
BACKUP_DIR="/home/deploy/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
pg_dump -U myapp_user myapp_db | gzip > "$BACKUP_DIR/myapp_$TIMESTAMP.sql.gz"
# Keep only last 7 days
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +7 -deleteAdd to cron:
0 3 * * * /home/deploy/scripts/backup_db.shDeployment Script
I use a simple bash script instead of CI/CD for most projects:
#!/bin/bash
# deploy.sh - run from your local machine
set -e
SERVER="deploy@your-server-ip"
APP_DIR="/home/deploy/myapp"
echo "Deploying..."
# Push code
rsync -avz --exclude='node_modules' --exclude='venv' --exclude='.git' \
./ $SERVER:$APP_DIR/
# Run remote commands
ssh $SERVER << 'EOF'
cd /home/deploy/myapp
# Python
source venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py collectstatic --noinput
# Restart
sudo supervisorctl restart myapp
EOF
echo "Done!"Run ./deploy.sh and your changes are live. No Docker, no CI pipeline, no YAML files. Just rsync and SSH.
Monitoring: The Minimum
You need to know when your app is down. At minimum:
1. Log Rotation
# /etc/logrotate.d/myapp
/home/deploy/logs/*.log {
daily
rotate 14
compress
missingok
notifempty
}2. Basic Health Check
A simple cron job that checks if your app responds:
# Check every 5 minutes
*/5 * * * * curl -sf https://myapp.com/health/ > /dev/null || echo "APP DOWN" | mail -s "Alert" [email protected]3. Disk Space Alerts
# Alert if disk is over 90%
0 */6 * * * df -h / | awk 'NR==2 && int($5)>90 {print "Disk usage: "$5}' | mail -s "Disk Alert" [email protected]Key Takeaways
- cPanel is perfectly fine for many projects - use its Node.js/Python app managers
- For VPS, the minimum stack is: Nginx + Supervisor + Let's Encrypt
- Always disable root login and password authentication
- A simple rsync + SSH deployment script beats complex CI/CD for solo projects
- Automated PostgreSQL backups are non-negotiable
- Monitor at minimum: is the app responding, and is the disk full
- Don't use Docker unless you have a specific reason to - it adds complexity for solo deployments
The goal isn't a perfect infrastructure - it's an infrastructure you can debug, maintain, and hand off without a 50-page runbook.
These patterns work for small-to-medium projects deployed by a single developer. If you're running a team or handling serious traffic, invest in proper DevOps.