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.
# 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 -vDo 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.
Process Flow
Step-by-step implementation
1. Update the server
Connect as root or the provider default admin user.
apt update && apt upgrade -yOptional cleanup:
apt autoremove -y
apt autocleanVerify OS details:
lsb_release -a || cat /etc/os-release
uname -a2. Create a dedicated deploy user
Do not deploy apps as root.
adduser deploy
usermod -aG sudo deploy
getent passwd deploy
id deployTest sudo membership:
su - deploy
sudo whoamiExpected output:
root3. Install SSH keys for the deploy user
If your root user already has working keys, copy them. Otherwise, add your public key manually.
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_keysValidate permissions:
ls -la /home/deploy/.ssh
stat /home/deploy/.ssh/authorized_keys4. Verify deploy-user login before hardening SSH
Open a second terminal before changing sshd_config.
ssh deploy@your-server-ipIf login fails, stop here and fix keys first.
Useful checks:
sudo -u deploy -H bash -lc 'whoami && pwd'5. Harden SSH
Edit /etc/ssh/sshd_config directly or use sed.
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 sshdRecommended values in /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding noValidate service state:
systemctl status ssh || systemctl status sshd
journalctl -xeu ssh --no-pager | tail -n 506. Configure the firewall
Allow SSH before enabling UFW.
apt install -y ufw
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw --force enable
ufw status verboseIf nginx is not yet public, you can start with SSH only:
ufw reset
ufw allow OpenSSH
ufw --force enableExpected open ports later:
- 22 for SSH
- 80 for HTTP
- 443 for HTTPS
7. Install base runtime packages
For Flask/FastAPI deployments:
apt install -y sudo ufw fail2ban git curl unzip build-essential \
python3 python3-venv python3-pip nginxOptional database/client packages:
apt install -y postgresql-client
apt install -y default-mysql-client
apt install -y libpq-dev pkg-configWhy these matter:
build-essential: native Python dependenciespython3-venv: isolated virtualenvsnginx: reverse proxygit: code checkoutcurl: health checks and install scripts
8. Set hostname and timezone
These affect logs, cron behavior, token debugging, and operational consistency.
hostnamectl set-hostname app-prod-1
timedatectl set-timezone UTC
hostnamectl
timedatectlUse 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.
df -h
free -m
swapon --showIf no swap exists on a low-memory server, add a small swap file.
Example for 1 GB swap:
fallocate -l 1G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
swapon --show
free -m10. Create standard deployment directories
Use stable paths so deploy scripts, nginx, and systemd all reference the same layout.
mkdir -p /srv/app/current
mkdir -p /srv/app/shared
mkdir -p /var/log/app
chown -R deploy:deploy /srv/app /var/log/appRecommended layout:
/srv/app/current # current release or symlink target
/srv/app/shared # .env, uploads, persistent assets
/var/log/app # app logs if neededVerify:
ls -la /srv/app /srv/app/current /srv/app/shared /var/log/app11. Store secrets outside the repository
Do not place .env inside the checked-out app directory.
touch /srv/app/shared/.env
chown deploy:deploy /srv/app/shared/.env
chmod 600 /srv/app/shared/.envExample:
APP_ENV=production
PORT=8000
DATABASE_URL=postgresql://user:pass@host:5432/dbname
SECRET_KEY=replace-meCheck permissions:
stat /srv/app/shared/.env12. Install and enable fail2ban
If SSH is exposed to the internet, install basic protection.
apt install -y fail2ban
systemctl enable fail2ban
systemctl start fail2ban
fail2ban-client statusMinimal jail override:
cat >/etc/fail2ban/jail.local <<'EOF'
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
backend = systemd
maxretry = 5
findtime = 10m
bantime = 1h
EOFRestart and verify:
systemctl restart fail2ban
fail2ban-client status
fail2ban-client status sshd13. 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:
/etc/systemd/system/app.serviceExample skeleton:
[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.targetYou 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.
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 statusIf 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
PasswordAuthenticationdisabled before deploy-user SSH key access was verifiedPermitRootLogindisabled beforedeployhad working keys- UFW enabled before allowing
OpenSSH - app files or virtualenv owned by
root - deployment scripts expect
/srv/appbut 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
.envpermissions 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:
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_keysTest commands as the deploy user:
sudo -u deploy -H bash -lc 'whoami && pwd && python3 --version'Inspect services before editing more config:
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-pagerVerify listening ports:
ss -tulpnCheck resource limits:
df -h
free -m
swapon --showIf deployment later fails after this setup, use:
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
- ✓
fail2baninstalled 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
- Deploy the app and reverse proxy with Deploy SaaS with Nginx + Gunicorn
- Add HTTPS with HTTPS Setup (Let’s Encrypt)
- Configure logs with Logging Setup (Application + Server)
- Troubleshoot failed deploys with App Crashes on Deployment
- Validate launch readiness with Deployment Checklist
- Review broader production readiness with SaaS Production Checklist
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.