Linux Server Essentials: What Every Developer Should Know (2026)
Sooner or later, you'll need to manage a server. Whether it's a VPS, cloud instance, or CI runner — here's what you actually need to know.
First Things First: Connect & Secure
# === SSH Connection ===
# Basic:
ssh user@your-server-ip
# With key (recommended):
ssh -i ~/.ssh/mykey.pem user@your-server-ip
# SSH config (~/.ssh/config) — save typing every time:
Host myserver
HostName 43.135.172.134
User root
IdentityFile ~/.ssh/id_ed25519
Port 22
ServerAliveInterval 60 # Keep connection alive
# Now just: ssh myserver
# === IMMEDIATE Security Steps (do this on EVERY new server!) ===
# 1. Change root password:
passwd
# 2. Create non-root user (never run as root!):
adduser deploy
usermod -aG sudo deploy # Add to sudo group
# 3. Set up SSH key auth (disable password login):
# On your LOCAL machine:
ssh-copy-id deploy@myserver # Copies your public key to server
# On the server, edit SSH config:
sudo nano /etc/ssh/sshd_config
# Change these:
PermitRootLogin no # No direct root login
PasswordAuthentication no # Only key-based auth
PubkeyAuthentication yes
Port 2222 # Optional: change from default 22
# Restart SSH:
sudo systemctl restart sshd
# ⚠️ Don't close your current session until you've tested the new one!
# 4. Enable firewall:
sudo ufw allow 2222/tcp # Your custom SSH port
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable # Activate firewall
sudo ufw status verbose # Check rules
# 5. Automatic security updates:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
Monitoring Your Server
# === Resource Usage (first commands to run!) ===
# Quick overview:
htop # Interactive process viewer (install first)
# Or built-in alternative:
top # Press '1' to see per-CPU usage
# Memory:
free -h # RAM usage (human-readable)
vmstat 1 5 # Memory stats every 1 second, 5 times
cat /proc/meminfo # Detailed memory info
# Disk:
df -h # Filesystem usage (human-readable)
du -sh /var/log/* # Directory sizes
ncdu / # Interactive disk usage browser (great for finding big files)
# Network:
ss -tlnp # Listening ports + PIDs (modern netstat)
nethogs # Real-time bandwidth per process
iftop # Network interface traffic
# System load:
uptime # Load averages (1min, 5min, 15min)
# Rule of thumb: keep under # of CPU cores on each average
# 4 cores → 4.0 is max sustainable; >4.0 means overloaded
# Logging:
journalctl -u nginx -f # Follow nginx service logs live
tail -f /var/log/syslog # Follow system log
dmesg | tail -20 # Recent kernel messages
Process Management
# === Finding processes ===
ps aux | grep node # Find all Node.js processes
pgrep -a node # Same, cleaner output
pidof node # Just PIDs
# === Managing processes ===
kill PID # Graceful stop (SIGTERM)
kill -9 PID # Force kill (SIGKILL) — last resort!
pkill -f "node server" # Kill by name pattern
killall node # Kill ALL processes named node
# Process states (from ps):
# R = Running (on CPU)
# S = Sleeping (waiting for I/O/event)
# D = Uninterruptible sleep (usually I/O wait)
# Z = Zombie (dead but parent hasn't reaped it!)
# T = Stopped (by job control signal)
# Zombie processes (should be rare, fix if many):
ps aux | awk '$8 ~ /Z/' # List zombies
# Fix: kill the parent process (PPID column)
# Background jobs:
npm start & # Run in background
jobs # List background jobs
fg %1 # Bring job 1 to foreground
Ctrl+Z # Suspend current job
bg %1 # Resume suspended in background
nohup npm start & # Survives terminal disconnection
Service Management (systemd)
# === Essential Commands ===
systemctl status nginx # Check service status
systemctl start nginx # Start service
systemctl stop nginx # Stop service
systemctl restart nginx # Restart
systemctl reload nginx # Reload config (no downtime!)
systemctl enable nginx # Start on boot
systemctl disable nginx # Don't start on boot
# View logs:
journalctl -u nginx # All logs for this service
journalctl -u nginx -f # Follow logs live
journalctl -u nginx --since "2026-06-01" # Logs since date
journalctl -u nginx -p err // Only error-level logs
# Failed services:
systemctl --failed # List services that failed
# === Creating Your Own systemd Service ===
# /etc/systemd/system/myapp.service:
[Unit]
Description=My Node.js Application
After=network.target postgresql.service
[Service]
Type=simple
User=deploy
WorkingDirectory=/home/deploy/app
ExecStart=/home/deploy/.nvm/v22.0.0/bin/node server.js
Restart=always # Auto-restart on crash
RestartSec=10 # Wait 10s between restarts
Environment=NODE_ENV=production
Environment=PORT=3000
Environment=DATABASE_URL=postgresql://...
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
[Install]
WantedBy=multi-user.target
# After creating:
sudo systemctl daemon-reload # Reload systemd configs
sudo systemctl enable myapp # Enable auto-start
sudo systemctl start myapp # Start it!
sudo journalctl -u myapp -f # Watch logs
Nginx: The Web Server You Need
# Install:
sudo apt install nginx
# Key directories:
/etc/nginx/nginx.conf # Main config
/etc/nginx/sites-available/ # Site configs (stored here)
/etc/nginx/sites-enabled/ # Active sites (symlinks to available)
/var/log/nginx/ # Access + error logs
/usr/share/nginx/html/ # Default static files
# === Basic Site Configuration ===
# /etc/nginx/sites-available/myapp.conf:
server {
listen 80;
server_name example.com www.example.com;
# Redirect HTTP → HTTPS:
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Reverse proxy to your app:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
# Required headers for WebSocket and proper forwarding:
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;
# Timeouts:
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Static files (serve directly, faster than through Node):
location /static/ {
alias /home/deploy/app/public/;
expires 30d; # Cache for 30 days
add_header Cache-Control "public, immutable";
}
# Rate limiting:
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
# ... same headers as above ...
}
}
# Activate site:
sudo ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/
sudo nginx -t # Test configuration (always do before restart!)
sudo systemctl reload nginx # Apply changes without dropping connections!
# SSL with Let's Encrypt (free HTTPS):
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
# Auto-renews automatically!
Automation & Maintenance
# === Cron Jobs (Scheduled Tasks) ===
crontab -e # Edit your cron jobs
# Format: minute hour day-of-month month day-of-week command
# Examples:
0 3 * * * /usr/bin/docker system prune -af >> /var/log/cleanup.log 2>&1 # Daily 3 AM cleanup
*/15 * * * * /home/deploy/scripts/healthcheck.sh # Every 15 minutes
0 6 * * 1 /usr/bin/pg_dump -U app mydb > /backups/db_$(date +\%Y\%m\%d).sql # Weekly Monday backup
# === Useful One-Liners ===
# Find large files (>100MB):
find / -type f -size +100M 2>/dev/null | head -20
# Find recently modified files (last 7 days):
find /var/www -type f -mtime -7
# Monitor real-time file access:
inotifywait -m -r /var/www/html
# Port check (is something listening?):
ss -tlnp | grep :3000
# Connection count per IP (detect attacks):
netstat -an | grep ':80' | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -10
# Top CPU consumers:
ps aux --sort=-%cpu | head -11
# Free memory aggressively (sync before!):
sync && echo 3 > /proc/sys/vm/drop_caches
# System info summary:
echo "=== OS ===" && cat /etc/os-release && echo "=== CPU ===" && nproc && \
echo "=== RAM ===" && free -h | grep Mem && echo "=== Disk ===" && df -h / && \
echo "=== Uptime ===" && uptime
# === Backup Strategy ===
#!/bin/bash
# Simple backup script (save as /home/deploy/scripts/backup.sh)
BACKUP_DIR="/backups"
DATE=$(date +%Y%m%d_%H%M%S)
# Database backup
pg_dump -U app mydb > "$BACKUP_DIR/db_$DATE.sql"
# Application files
tar -czf "$BACKUP_DIR/app_$DATE.tar.gz" -C /home/deploy app/
# Clean backups older than 30 days
find "$BACKUP_DIR" -mtime +30 -delete
echo "Backup complete: $DATE"
What's your go-to server command? What server nightmare have you survived?
Follow @armorbreak for more practical developer guides.
Top comments (0)