Client Area

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

10 min readPublished 4 Mar 2026Updated 16 Apr 20261,108 views

In this article

  • 1Short Summary
  • 2Table of Contents
  • 31) Baseline Server Prep (Once per VPS)
  • 41.1 Create a nonroot user & SSH keys
  • 51.2 System updates & essentials

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: PHPFPM / 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 nonroot 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, unattendedupgrades

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
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 (AutoTLS, 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 [email protected]

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/[email protected]
 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/[email protected]
 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 (PHPFPM / WordPress / Woo / Magento 2)

7.1 PHPFPM & 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:[email protected]:27017/appdbauthSource=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 / languagespecific 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 autoTLS.

  • 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, productiongrade 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


Was this article helpful?

Your feedback helps us improve our documentation

Still need help? Submit a support ticket

Still need help?

Our support team can assist you directly.

Submit Ticket