🚀 From Zero to Production: The Complete VPS Setup Guide for Popular Stacks Print

  • 0

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

  1. 🧭 Baseline Server Prep

  2. 🔐 OS Hardening & Auto Updates

  3. 🌐 Reverse Proxy & TLS (Nginx/Caddy)

  4. 🧱 Reusable Building Blocks (Nginx vhost, systemd, CI/CD)

  5. 🟢 Node.js: Express / Next.js SSR / NestJS / Socket.io

  6. 🐍 Python: Django (ASGI) / FastAPI / Flask

  7. 🐘 PHP & CMS: PHP‑FPM / WordPress / WooCommerce / Magento 2

  8. ☕ & 🧩 Java & .NET: Spring Boot / Tomcat / .NET on Linux

  9. 🦫 Go • 💎 Rails • 🔥 Phoenix • 🦀 Rust

  10. 🧠 Headless CMS: Strapi / Directus / Keystone

  11. 🗃️ Databases & Queues: PostgreSQL / MySQL / Redis / RabbitMQ / MongoDB 

  12. 📊 Observability, Logs & Backups

  13. 🧪 Troubleshooting Cheats

  14. ✅ Conclusion / Next Steps

  15. 🔗 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.serviceExecStart=/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)


Was this answer helpful?

« Back