Environment Setup on VPS

The essential playbook for implementing environment setup on vps in your SaaS.

This page covers the base server setup required before deploying a Flask, FastAPI, or similar SaaS app on a VPS. The goal is a repeatable production environment with secure access, required system packages, predictable app paths, and service-level process management.

Quick Fix / Quick Setup

Use this Ubuntu/Debian baseline only after verifying SSH key access for the new user in a separate session.

bash
# Ubuntu/Debian VPS baseline setup
apt update && apt upgrade -y
apt install -y sudo ufw fail2ban git curl unzip build-essential \
  python3 python3-venv python3-pip nginx

# create deploy user
adduser deploy
usermod -aG sudo deploy
mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

# ssh hardening
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
systemctl restart ssh || systemctl restart sshd

# firewall
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw --force enable

# app directories
mkdir -p /srv/app/current /srv/app/shared /var/log/app
chown -R deploy:deploy /srv/app /var/log/app

# verify
ufw status
ss -tulpn
python3 --version
nginx -v

Do not disable root login or password auth until SSH key login for deploy is confirmed in a second terminal.

What’s happening

A fresh VPS usually lacks user separation, firewall rules, runtime packages, and standardized directories for app deployment.

Most deployment failures start before app code runs:

  • missing packages
  • wrong file ownership
  • broken SSH access
  • no service management
  • incorrect paths across deploy scripts, nginx, and systemd

A clean VPS setup should provide:

  • secure SSH access
  • a non-root deploy user
  • locked-down network access
  • required runtime dependencies
  • predictable filesystem paths
  • a safe base for nginx and systemd deployment

This page covers host preparation only. App serving, reverse proxy, HTTPS, and monitoring should be configured separately.

fresh VPS
secure access
packages
directories
services
deployment-ready state

Process Flow

Step-by-step implementation

1. Update the server

Connect as root or the provider default admin user.

bash
apt update && apt upgrade -y

Optional cleanup:

bash
apt autoremove -y
apt autoclean

Verify OS details:

bash
lsb_release -a || cat /etc/os-release
uname -a

2. Create a dedicated deploy user

Do not deploy apps as root.

bash
adduser deploy
usermod -aG sudo deploy
getent passwd deploy
id deploy

Test sudo membership:

bash
su - deploy
sudo whoami

Expected output:

bash
root

3. Install SSH keys for the deploy user

If your root user already has working keys, copy them. Otherwise, add your public key manually.

bash
mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

Validate permissions:

bash
ls -la /home/deploy/.ssh
stat /home/deploy/.ssh/authorized_keys

4. Verify deploy-user login before hardening SSH

Open a second terminal before changing sshd_config.

bash
ssh deploy@your-server-ip

If login fails, stop here and fix keys first.

Useful checks:

bash
sudo -u deploy -H bash -lc 'whoami && pwd'

5. Harden SSH

Edit /etc/ssh/sshd_config directly or use sed.

bash
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
systemctl restart ssh || systemctl restart sshd

Recommended values in /etc/ssh/sshd_config:

conf
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding no

Validate service state:

bash
systemctl status ssh || systemctl status sshd
journalctl -xeu ssh --no-pager | tail -n 50

6. Configure the firewall

Allow SSH before enabling UFW.

bash
apt install -y ufw
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw --force enable
ufw status verbose

If nginx is not yet public, you can start with SSH only:

bash
ufw reset
ufw allow OpenSSH
ufw --force enable

Expected open ports later:

  • 22 for SSH
  • 80 for HTTP
  • 443 for HTTPS

7. Install base runtime packages

For Flask/FastAPI deployments:

bash
apt install -y sudo ufw fail2ban git curl unzip build-essential \
  python3 python3-venv python3-pip nginx

Optional database/client packages:

bash
apt install -y postgresql-client
apt install -y default-mysql-client
apt install -y libpq-dev pkg-config

Why these matter:

  • build-essential: native Python dependencies
  • python3-venv: isolated virtualenvs
  • nginx: reverse proxy
  • git: code checkout
  • curl: health checks and install scripts

8. Set hostname and timezone

These affect logs, cron behavior, token debugging, and operational consistency.

bash
hostnamectl set-hostname app-prod-1
timedatectl set-timezone UTC
hostnamectl
timedatectl

Use UTC unless you have a clear reason not to.

9. Review memory, disk, and swap

On small VPS instances, memory and disk shortages cause failed builds and unstable services.

bash
df -h
free -m
swapon --show

If no swap exists on a low-memory server, add a small swap file.

Example for 1 GB swap:

bash
fallocate -l 1G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
swapon --show
free -m

10. Create standard deployment directories

Use stable paths so deploy scripts, nginx, and systemd all reference the same layout.

bash
mkdir -p /srv/app/current
mkdir -p /srv/app/shared
mkdir -p /var/log/app
chown -R deploy:deploy /srv/app /var/log/app

Recommended layout:

text
/srv/app/current   # current release or symlink target
/srv/app/shared    # .env, uploads, persistent assets
/var/log/app       # app logs if needed

Verify:

bash
ls -la /srv/app /srv/app/current /srv/app/shared /var/log/app

11. Store secrets outside the repository

Do not place .env inside the checked-out app directory.

bash
touch /srv/app/shared/.env
chown deploy:deploy /srv/app/shared/.env
chmod 600 /srv/app/shared/.env

Example:

env
APP_ENV=production
PORT=8000
DATABASE_URL=postgresql://user:pass@host:5432/dbname
SECRET_KEY=replace-me

Check permissions:

bash
stat /srv/app/shared/.env

12. Install and enable fail2ban

If SSH is exposed to the internet, install basic protection.

bash
apt install -y fail2ban
systemctl enable fail2ban
systemctl start fail2ban
fail2ban-client status

Minimal jail override:

bash
cat >/etc/fail2ban/jail.local <<'EOF'
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
backend = systemd
maxretry = 5
findtime = 10m
bantime = 1h
EOF

Restart and verify:

bash
systemctl restart fail2ban
fail2ban-client status
fail2ban-client status sshd

13. Prepare for systemd-managed app services

Do not run your app in an interactive shell in production. Use systemd in the deployment step.

Example unit file path:

text
/etc/systemd/system/app.service

Example skeleton:

ini
[Unit]
Description=App service
After=network.target

[Service]
User=deploy
Group=deploy
WorkingDirectory=/srv/app/current
EnvironmentFile=/srv/app/shared/.env
ExecStart=/srv/app/current/.venv/bin/gunicorn -w 2 -k uvicorn.workers.UvicornWorker app:app --bind 127.0.0.1:8000
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

You will configure this fully in the app deployment guide: Deploy SaaS with Nginx + Gunicorn.

14. Validate the server baseline

Run these checks before deploying code.

bash
whoami && id
hostnamectl
timedatectl
df -h
free -m
swapon --show
ss -tulpn
ufw status verbose
python3 --version
nginx -v
systemctl status nginx --no-pager
systemctl status ssh || systemctl status sshd
fail2ban-client status

If you plan to use this with a Flask or FastAPI codebase, align your file paths with your project structure guide: Structuring a Flask/FastAPI SaaS Project.

Common causes

  • PasswordAuthentication disabled before deploy-user SSH key access was verified
  • PermitRootLogin disabled before deploy had working keys
  • UFW enabled before allowing OpenSSH
  • app files or virtualenv owned by root
  • deployment scripts expect /srv/app but code was placed elsewhere
  • missing system libraries for compiled Python packages
  • nginx installed but not started or enabled
  • low disk space or memory during package install or app startup
  • .env permissions prevent app service access
  • timezone or clock mismatch affecting logs, token expiry, or scheduled tasks

Debugging tips

Keep one active SSH session open before restarting SSH or enabling firewall rules.

Check effective user and file ownership first:

bash
whoami && id
ls -la /srv/app /srv/app/shared /srv/app/current /var/log/app
ls -la /home/deploy/.ssh
stat /home/deploy/.ssh/authorized_keys

Test commands as the deploy user:

bash
sudo -u deploy -H bash -lc 'whoami && pwd && python3 --version'

Inspect services before editing more config:

bash
systemctl status ssh || systemctl status sshd
systemctl status nginx --no-pager
journalctl -xeu ssh --no-pager | tail -n 50
journalctl -p err -n 100 --no-pager

Verify listening ports:

bash
ss -tulpn

Check resource limits:

bash
df -h
free -m
swapon --show

If deployment later fails after this setup, use:

Build
Test
Migrate
Deploy
Health Check

Deployment Pipeline

Checklist

  • OS packages updated
  • non-root deploy user created
  • SSH key login verified for deploy user
  • root SSH login disabled
  • password SSH login disabled
  • firewall enabled with correct allowed ports
  • base runtime packages installed
  • app directories created under /srv/app
  • ownership and permissions validated
  • timezone and hostname configured
  • swap reviewed for low-memory instances
  • fail2ban installed if SSH is publicly exposed
  • secrets location defined outside repo
  • disk space and open ports verified
  • server ready for nginx, systemd, and app deployment

Before go-live, also run:

Product CTA

Related guides

FAQ

What is the minimum VPS setup for a small SaaS app?

A practical baseline is Ubuntu or Debian, a non-root deploy user, SSH key auth, firewall rules, Python runtime, build tools, nginx, app directories under /srv, and systemd for service management.

Should I disable password login on SSH?

Yes, but only after confirming SSH key access works for your non-root user in a separate session.

Where should application code live on the server?

Use a predictable path such as /srv/app/current for the active release and /srv/app/shared for persistent files and secrets.

Do I need fail2ban on a VPS?

It is recommended for internet-facing servers with exposed SSH, especially if you manage the host directly instead of using a private network or bastion setup.

Can I skip nginx and run Gunicorn directly on a public port?

You can for testing, but production setups usually place nginx in front for reverse proxying, static file handling, buffering, and TLS termination.

Final takeaway

A production VPS environment is mostly about consistency and safety:

  • secure access
  • correct packages
  • stable paths
  • non-root service execution

If the host is prepared correctly, deployment, monitoring, and incident response become much easier.

Treat VPS setup as a repeatable baseline, not an ad hoc list of shell commands.