KVM NVMe VPS • Ubuntu 22.04/24.04 LTS • Nginx/Caddy • SSL • CI/CD • Hardened Security
✍️ Short Summary
This playbook shows you how to provision and securely deploy production apps on a fresh Ubuntu KVM VPS for all major stacks: Node/MERN, Python (Django/FastAPI/Flask), PHP & CMS, Java/.NET, Go, Ruby on Rails, Elixir Phoenix, and Rust. You’ll set up reverse proxy + TLS, systemd services, CI/CD, databases/queues, and observability with hardened defaults.
📎 Table of Contents
-
🧭 Baseline Server Prep
-
🔐 OS Hardening & Auto Updates
-
🌐 Reverse Proxy & TLS (Nginx/Caddy)
-
🧱 Reusable Building Blocks (Nginx vhost, systemd, CI/CD)
-
🟢 Node.js: Express / Next.js SSR / NestJS / Socket.io
-
🐍 Python: Django (ASGI) / FastAPI / Flask
-
🐘 PHP & CMS: PHP‑FPM / WordPress / WooCommerce / Magento 2
-
☕ & 🧩 Java & .NET: Spring Boot / Tomcat / .NET on Linux
-
🦫 Go • 💎 Rails • 🔥 Phoenix • 🦀 Rust
-
🧠 Headless CMS: Strapi / Directus / Keystone
-
🗃️ Databases & Queues: PostgreSQL / MySQL / Redis / RabbitMQ / MongoDB
-
📊 Observability, Logs & Backups
-
🧪 Troubleshooting Cheats
-
✅ Conclusion / Next Steps
-
🔗 Related Articles
1) 🧭 Baseline Server Prep (Once per VPS)
Assumptions: Ubuntu 22.04/24.04 LTS, public IPv4, domain example.com
→ VPS IP, sudo access.
1.1 Create a non‑root user & SSH keys
adduser deploy
usermod -aG sudo deploy
mkdir -p /home/deploy/.ssh && chmod 700 /home/deploy/.ssh
nano /home/deploy/.ssh/authorized_keys # paste your public key
chmod 600 /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
1.2 System updates & essentials
apt-get update && apt-get -y upgrade
apt-get -y install curl git jq tar gnupg2 ca-certificates lsb-release build-essential
2) 🔐 OS Hardening & Auto Updates
2.1 UFW, Fail2ban, unattended‑upgrades
apt-get -y install ufw fail2ban unattended-upgrades
ufw allow OpenSSH && ufw allow 80/tcp && ufw allow 443/tcp
ufw --force enable && ufw status
systemctl enable --now fail2ban
# enable security updates
dpkg-reconfigure --priority=low unattended-upgrades
2.2 SSH hardening (optional but recommended)
sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#PermitRootLogin yes/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
systemctl reload ssh
3) 🌐 Reverse Proxy & TLS (Choose One)
3.1 Nginx + Let’s Encrypt (Flexible)
apt-get -y install nginx certbot python3-certbot-nginx
systemctl enable --now nginx
3.2 Caddy (Auto‑TLS, simplest)
apt-get -y install debian-keyring debian-archive-keyring
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | tee /usr/share/keyrings/caddy-stable-archive-keyring.gpg >/dev/null
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt-get update && apt-get -y install caddy
systemctl enable --now caddy
Tip: Choose Nginx for maximum control and community examples, or Caddy for effortless HTTPS.
4) 🧱 Reusable Building Blocks
4.1 Nginx vhost template (proxy → app on port 3000)
Create /etc/nginx/sites-available/example.com
:
server {
listen 80; server_name example.com www.example.com;
location /.well-known/acme-challenge/ { root /var/www/letsencrypt; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl http2; server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
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_read_timeout 60s;
}
location ~* \.(png|jpg|jpeg|gif|css|js|ico|svg|woff2?)$ {
expires 30d; access_log off;
}
}
Enable + issue cert:
ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled
nginx -t && systemctl reload nginx
certbot --nginx -d example.com -d www.example.com --redirect --agree-tos -m admin@example.com
4.2 Generic systemd
service
Create /etc/systemd/system/myapp.service
:
[Unit]
Description=My App Service
After=network.target
[Service]
User=deploy
Group=deploy
WorkingDirectory=/var/www/myapp
EnvironmentFile=/var/www/myapp/.env
ExecStart=/usr/local/bin/start-myapp.sh
Restart=always
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
[Install]
WantedBy=multi-user.target
systemctl daemon-reload && systemctl enable --now myapp
journalctl -u myapp -f
4.3 CI/CD (GitHub Actions pattern)
.github/workflows/deploy.yml
:
name: Deploy
on:
push:
branches: [ "main" ]
jobs:
build-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# build/test steps vary per stack
- name: Package artifact
run: tar -czf artifact.tar.gz ./
- name: Copy to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
source: "artifact.tar.gz"
target: "/var/www/myapp/"
- name: Unpack & restart
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/myapp
tar -xzf artifact.tar.gz && rm artifact.tar.gz
sudo systemctl restart myapp
5) 🟢 Node.js (Express / Next.js SSR / NestJS / Socket.io)
5.1 Install Node LTS & PM2
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
apt-get -y install nodejs build-essential
npm i -g pm2
mkdir -p /var/www/nodeapp && chown -R deploy:deploy /var/www/nodeapp
su - deploy && cd /var/www/nodeapp
# clone your repo then:
npm ci
npm run build # if SSR
5.2 Run with PM2 (simple) or systemd (lean)
PM2:
pm2 start "npm start" --name nodeapp
pm2 save
pm2 startup systemd -u deploy --hp /home/deploy # run printed command as root
systemd: create /etc/systemd/system/nodeapp.service
:
[Service]
User=deploy
WorkingDirectory=/var/www/nodeapp
Environment=NODE_ENV=production
ExecStart=/usr/bin/node server.js
Restart=always
systemctl daemon-reload && systemctl enable --now nodeapp
WebSockets: Nginx block above already includes
Upgrade
/Connection
headers for Socket.io.
6) 🐍 Python (Django / FastAPI / Flask)
6.1 Toolchain & venv
apt-get -y install python3-pip python3-venv python3-dev build-essential
mkdir -p /var/www/pyapp && chown -R deploy:deploy /var/www/pyapp
su - deploy && cd /var/www/pyapp
python3 -m venv venv && source venv/bin/activate
6.2 Django (ASGI) + Gunicorn(UvicornWorker)
pip install django gunicorn uvicorn[standard] psycopg2-binary whitenoise
python manage.py collectstatic --noinput
/etc/systemd/system/django.service
:
[Service]
User=deploy
WorkingDirectory=/var/www/pyapp
Environment="DJANGO_SETTINGS_MODULE=project.settings"
EnvironmentFile=/var/www/pyapp/.env
ExecStart=/var/www/pyapp/venv/bin/gunicorn project.asgi:application \
-k uvicorn.workers.UvicornWorker --bind 127.0.0.1:8000 --workers 3 --timeout 60
Restart=always
6.3 FastAPI + Uvicorn/Gunicorn
pip install fastapi "uvicorn[standard]" gunicorn
/etc/systemd/system/fastapi.service
:
[Service]
User=deploy
WorkingDirectory=/var/www/pyapp
EnvironmentFile=/var/www/pyapp/.env
ExecStart=/var/www/pyapp/venv/bin/gunicorn app.main:app \
-k uvicorn.workers.UvicornWorker --bind 127.0.0.1:8001 --workers 3 --timeout 60
Restart=always
6.4 Flask (WSGI or ASGI)
pip install flask gunicorn
/etc/systemd/system/flask.service
:
[Service]
User=deploy
WorkingDirectory=/var/www/pyapp
EnvironmentFile=/var/www/pyapp/.env
ExecStart=/var/www/pyapp/venv/bin/gunicorn "app:app" --bind 127.0.0.1:8002 --workers 3
Restart=always
Celery: add
celery.service
+celerybeat.service
using Redis/RabbitMQ.
7) 🐘 PHP & CMS (PHP‑FPM / WordPress / Woo / Magento 2)
7.1 PHP‑FPM & Nginx
apt-get -y install php-fpm php-cli php-mysql php-xml php-curl php-zip php-gd php-intl php-bcmath php-mbstring
systemctl enable --now php8.2-fpm # adjust version
Nginx server block (root → /var/www/phpapp/public
):
location / { try_files $uri $uri/ /index.php?$query_string; }
location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.2-fpm.sock; }
7.2 WordPress quickstart
apt-get -y install mariadb-server && mysql_secure_installation
mysql -u root -p -e "CREATE DATABASE wpdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -u root -p -e "CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'StrongPass!'; GRANT ALL ON wpdb.* TO 'wpuser'@'localhost'; FLUSH PRIVILEGES;"
mkdir -p /var/www/phpapp && cd /var/www/phpapp
curl -O https://wordpress.org/latest.tar.gz && tar -xzf latest.tar.gz
mv wordpress/* public && rm -rf wordpress latest.tar.gz
chown -R www-data:www-data /var/www/phpapp
Performance: enable OPcache, install Redis server, and use a Redis object cache plugin.
7.3 WooCommerce & Magento 2 (notes)
-
Woo: persistent object cache (Redis), safe checkout cache rules, DB hygiene.
-
Magento 2: PHP extensions (soap,intl,bcmath,xsl,gd,mbstring,zip), OpenSearch/Elasticsearch, Redis, Varnish for FPC, Composer install, correct file perms.
8) ☕ & 🧩 Java & .NET
8.1 Spring Boot (JAR) + Nginx
apt-get -y install openjdk-17-jre-headless # or 21
mkdir -p /var/www/springapp && chown -R deploy:deploy /var/www/springapp
# copy your JAR to /var/www/springapp/app.jar
/etc/systemd/system/spring.service
:
[Service]
User=deploy
WorkingDirectory=/var/www/springapp
Environment="JAVA_OPTS=-Xms256m -Xmx512m"
ExecStart=/usr/bin/java $JAVA_OPTS -jar app.jar --server.port=9000
Restart=always
8.2 Tomcat/Jakarta
apt-get -y install tomcat9
# deploy WAR to /var/lib/tomcat9/webapps/app.war
systemctl enable --now tomcat9
Reverse proxy to 127.0.0.1:8080
(HTTP proxy, not AJP).
8.3 .NET on Linux (Kestrel + Nginx)
wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
apt-get update && apt-get -y install dotnet-sdk-8.0
mkdir -p /var/www/dotnetapp && chown -R deploy:deploy /var/www/dotnetapp
# publish to /var/www/dotnetapp/out
/etc/systemd/system/dotnet.service
:
[Service]
User=deploy
WorkingDirectory=/var/www/dotnetapp/out
Environment=ASPNETCORE_URLS=http://127.0.0.1:7000
ExecStart=/usr/bin/dotnet MyApp.dll
Restart=always
9) 🦫 Go • 💎 Rails • 🔥 Phoenix • 🦀 Rust
9.1 Go
apt-get -y install golang
mkdir -p /var/www/goapp && chown -R deploy:deploy /var/www/goapp
cd /var/www/goapp && go mod tidy
go build -ldflags "-s -w" -o app
/etc/systemd/system/go.service
→ ExecStart=/var/www/goapp/app
9.2 Ruby on Rails (Puma)
apt-get -y install rbenv ruby-build nodejs yarnpkg
# install Ruby via rbenv, then:
gem install bundler && bundle install
RAILS_ENV=production bin/rails assets:precompile && bin/rails db:migrate
/etc/systemd/system/puma.service
→ run Puma with your config.
9.3 Elixir Phoenix (releases)
apt-get -y install erlang-base erlang-ssl elixir
mix deps.get
MIX_ENV=prod mix assets.deploy
MIX_ENV=prod mix release
/etc/systemd/system/phoenix.service
→ run release binary.
9.4 Rust (Actix/Axum)
apt-get -y install cargo
mkdir -p /var/www/rustapp && chown -R deploy:deploy /var/www/rustapp
cd /var/www/rustapp && cargo build --release
/etc/systemd/system/rust.service
→ run target/release/myapp
.
10) 🧠 Headless CMS: Strapi / Directus / Keystone
Pattern: Node LTS + PM2/systemd + Nginx TLS, S3/B2 for uploads, hardened admin.
mkdir -p /var/www/headless && chown -R deploy:deploy /var/www/headless
su - deploy && cd /var/www/headless
npx create-strapi-app@latest cms --quickstart
cd cms && npm run build
pm2 start "npm run start" --name headless && pm2 save
Proxy to 127.0.0.1:1337
, restrict admin route if needed, configure CORS/JWT and storage.
11) 🗃️ Databases & Queues
11.1 PostgreSQL (with sane defaults)
apt-get -y install postgresql
systemctl enable --now postgresql
sudo -u postgres psql -c "CREATE ROLE appuser WITH LOGIN PASSWORD 'StrongPass!';"
sudo -u postgres psql -c "CREATE DATABASE appdb OWNER appuser;"
Backups: pg_dump -U appuser appdb > /backups/appdb_$(date +%F).sql
11.2 MySQL/MariaDB
apt-get -y install mariadb-server && mysql_secure_installation
mysql -u root -p -e "CREATE DATABASE appdb;"
mysql -u root -p -e "CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'StrongPass!'; GRANT ALL ON appdb.* TO 'appuser'@'localhost'; FLUSH PRIVILEGES;"
11.3 Redis
apt-get -y install redis-server
sed -i 's/^# requirepass .*/requirepass VeryStrongPass!/' /etc/redis/redis.conf
sed -i 's/^supervised no/supervised systemd/' /etc/redis/redis.conf
systemctl enable --now redis-server
11.4 RabbitMQ
apt-get -y install rabbitmq-server
rabbitmq-plugins enable rabbitmq_management
rabbitmqctl add_user appuser VeryStrongPass!
rabbitmqctl add_vhost appvhost
rabbitmqctl set_permissions -p appvhost appuser ".*" ".*" ".*"
Expose management UI via reverse proxy; don’t bind directly to public interfaces.
11.5 MongoDB
# 1) Repo & install (MongoDB 7.0 example)
sudo apt-get update && sudo apt-get install -y gnupg curl
curl -fsSL https://pgp.mongodb.com/server-7.0.asc | sudo gpg --dearmor -o /usr/share/keyrings/mongodb-server-7.0.gpg
echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu \
$(. /etc/os-release; echo $VERSION_CODENAME)/mongodb-org/7.0 multiverse" \
| sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
sudo apt-get update && sudo apt-get install -y mongodb-org
# 2) Bind locally + enable auth
sudo cp /etc/mongod.conf /etc/mongod.conf.bak
sudo sed -i 's/^ *bindIp:.*/ bindIp: 127.0.0.1/' /etc/mongod.conf
sudo sed -i 's/^#security:/security:\n authorization: enabled/' /etc/mongod.conf
sudo systemctl enable --now mongod
# 3) Create admin user
mongosh --eval 'db.getSiblingDB("admin").createUser({user:"admin",pwd:"REPLACE_ME_STRONG",roles:[{role:"userAdminAnyDatabase",db:"admin"},{role:"readWriteAnyDatabase",db:"admin"}]})'
# 4) (Optional) Single-node replica set (needed for Change Streams/transactions)
sudo tee -a /etc/mongod.conf >/dev/null <<'EOF'
replication:
replSetName: rs0
EOF
sudo systemctl restart mongod
mongosh -u admin -p 'REPLACE_ME_STRONG' --authenticationDatabase admin --eval 'rs.initiate()'
# 5) App database + user
mongosh -u admin -p 'REPLACE_ME_STRONG' --authenticationDatabase admin --eval \
'db.getSiblingDB("appdb").createUser({user:"appuser",pwd:"REPLACE_APP_PASS",roles:[{role:"readWrite",db:"appdb"}]})'
.env example (local):
MONGODB_URI=mongodb://appuser:REPLACE_APP_PASS@127.0.0.1:27017/appdb?authSource=appdb
12) 📊 Observability, Logs & Backups
-
Logs:
journalctl -u <service> -f
, Nginx in/var/log/nginx/
. -
Logrotate: Ensure app logs rotate if writing to files.
-
Metrics: Add Node Exporter / language‑specific exporters; health endpoints (
/healthz
,/readyz
). -
Backups: Nightly DB dumps, offsite copies (S3/B2) via
rclone
; periodic restore tests. -
Uptime: External pings + web transaction checks.
13) 🧪 Troubleshooting Cheats
-
502/Bad Gateway: app not listening/crashed →
journalctl -u service -n 100
. -
SSL fails: verify DNS A/AAAA, port 80 open; rerun
certbot --nginx
or confirm Caddy auto‑TLS. -
Permission denied: fix ownership/user in
systemd
service and web root. -
High CPU: reduce workers/threads; enable caches; inspect slow queries.
-
Memory pressure: fewer workers; add swap (
/swapfile
); watch for OOM.
✅ Conclusion / Next Steps
You now have a repeatable, production‑grade blueprint for deploying modern apps on a KVM VPS with HTTPS, hardened security, and CI/CD. Start with the baseline, pick your stack section, and deploy confidently. Next, add monitoring, backups, and blue/green releases for safe iterations.
Ready to move?
-
🛡️ Harden now → UFW, Fail2ban, auto updates
-
🔗 Add SSL & reverse proxy → Nginx/Caddy
-
🚀 Deploy your first stack → Node/Python/PHP/Java/.NET/Go/Rails/Phoenix/Rust
-
📈 Add metrics & alerts → catch issues early
-
♻️ Automate CI/CD → ship faster with confidence
🔗 Related Articles (Topics)
-
CI/CD Patterns for Zero‑Downtime Deploys
-
Object Caching for WordPress & WooCommerce
-
Database Backup Strategies with Restore Testing
-
Observability 101: Logs, Metrics, Traces