Back to Article List

Self-host Strapi 5 on a VPS with PostgreSQL

Self-host Strapi 5 on a VPS with PostgreSQL

Why self-host Strapi instead of using managed SaaS

Strapi's managed hosting is convenient but costly. You pay per seat, per API call, and per storage increment. A team of three editors with moderate content usage can run 500+ dollars monthly. Self-hosting on a VPS costs a fixed 20-40 dollars per month, with no surprise usage charges.

More importantly, self-hosting gives you complete control. Your content lives in a database you control. You can integrate Strapi directly with private systems, run custom plugins without restrictions, and back up data whenever you want. There's no vendor lock-in. You own the infrastructure.

Strapi 5 is particularly well-suited for self-hosting. It's built to run on modest hardware and doesn't require extensive DevOps expertise. With PostgreSQL as your database and Nginx handling traffic, you get a production-grade content management system that scales from your first user to thousands.

Resource requirements for Strapi and PostgreSQL

Running both Strapi and PostgreSQL on the same server demands more resources than a typical Node.js app. Plan for at least 4 GB of RAM to run both comfortably. With 2 GB, you'll hit memory limits quickly under any real traffic. A 2-core CPU handles small deployments, but 4 cores is better if you expect regular traffic or large media libraries.

Disk space depends on your media. If you're managing images or video, plan for 50+ GB on a production setup. Strapi itself takes only 500 MB after installation and build.

For larger deployments or to avoid resource contention, put PostgreSQL on a separate VPS. This guide assumes a combined deployment, which works fine for development and small production sites. If your team is larger or traffic is heavy, separate the database.

Install PostgreSQL on Ubuntu 24.04 LTS

SSH into your VPS and update the package list:

ssh root@your-vps-ip

apt update
apt install -y postgresql postgresql-contrib

Start the PostgreSQL service and enable it to run at boot:

systemctl start postgresql
systemctl enable postgresql

Verify the installation:

sudo -u postgres psql --version

You should see PostgreSQL 16 or later. If the service fails to start, check the logs with journalctl -u postgresql -n 50.

Create a database and dedicated user for Strapi

PostgreSQL uses role-based access. Create a database and a user that Strapi will authenticate as:

sudo -u postgres createdb strapi_db

sudo -u postgres createuser -P strapi_user

The -P flag prompts you to set a password. Choose something strong and save it securely. You'll reference this password in Strapi's configuration.

Grant all privileges on the database to your new user:

sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE strapi_db TO strapi_user;"

Also grant schema privileges to ensure Strapi can create tables:

sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON SCHEMA public TO strapi_user;" strapi_db

Understanding connection strings and authentication

PostgreSQL connection strings take this form: postgres://username:password@host:port/database. For local connections on your VPS, use localhost. The default PostgreSQL port is 5432.

Your Strapi connection string will look like:

postgres://strapi_user:your-strong-password@localhost:5432/strapi_db

PostgreSQL uses password authentication by default for local connections, configured in pg_hba.conf. You shouldn't need to edit this file unless you're using socket authentication or SSL certificates.

Verifying the database is ready

Test that your new user can connect:

psql -U strapi_user -d strapi_db -h localhost -c "SELECT version();"

If that command returns the PostgreSQL version, your database setup is complete. If it fails with "password authentication failed", double-check the password you created and ensure you're connecting to the right host.

Deploy Strapi 5 to your VPS

You have two options: clone an existing Strapi project from Git, or create a fresh Strapi installation directly on the server. For most deployments, cloning from Git is cleaner because it includes your content types and plugins.

Create a directory for your app and clone your repository:

mkdir -p /var/www/my-strapi-app
cd /var/www/my-strapi-app

git clone https://github.com/yourusername/my-strapi-app.git .
npm install

If you're starting fresh and don't have an existing Strapi project, create one on the VPS:

npm create strapi-app@latest my-strapi-app

The installer will prompt you to choose a database. Select PostgreSQL and enter your connection details. The installer runs database migrations automatically and creates an initial admin user.

For cloned projects, the next section covers database configuration.

Configure the Strapi database connection

Strapi 5 uses a configuration file at config/database.ts (or config/database.js if not using TypeScript) to define database connections. Open that file and configure PostgreSQL.

export default ({ env }) => ({
  connection: {
    client: 'postgres',
    connection: {
      host: env('DATABASE_HOST', 'localhost'),
      port: env('DATABASE_PORT', 5432),
      database: env('DATABASE_NAME', 'strapi_db'),
      user: env('DATABASE_USERNAME', 'strapi_user'),
      password: env('DATABASE_PASSWORD'),
      ssl: false
    },
    pool: {
      min: env.int('DATABASE_POOL_MIN', 2),
      max: env.int('DATABASE_POOL_MAX', 10)
    },
    useNullAsDefault: true,
  },
});

The pool settings control how many database connections Strapi maintains. With min: 2 and max: 10, Strapi keeps 2 connections open for fast requests and allows up to 10 total connections during traffic spikes. A common formula is (CPU cores * 2) + 1 for the maximum.

Connection pooling and performance tuning

The connection pool acts as a reservoir of ready-to-use database connections, avoiding the latency of creating a new connection for every request. The minimum value keeps idle connections open, while the maximum prevents exhausting your database server's resources.

If you see "Timeout acquiring a connection" errors, increase DATABASE_POOL_MAX. If you're running in Docker with automatic cleanup of idle connections, set DATABASE_POOL_MIN to 0 in your Docker environment.

PostgreSQL vs SQLite comparison

SQLite is simpler to set up and works for development, but it's not recommended for production. PostgreSQL handles concurrent users better, supports advanced features like transactions and full-text search, and scales more reliably. You're making the right choice by using PostgreSQL on a VPS.

Create a production environment file with your database credentials:

nano .env.production

Add these variables:

DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_NAME=strapi_db
DATABASE_USERNAME=strapi_user
DATABASE_PASSWORD=your-strong-password
DATABASE_POOL_MIN=2
DATABASE_POOL_MAX=10
NODE_ENV=production
HOST=0.0.0.0
PORT=1337

Restrict file permissions so only the app can read this file:

chmod 600 .env.production

Never commit .env.production to Git. Add it to your .gitignore.

Build Strapi for production

Strapi includes a build process that compiles the admin dashboard and optimizes plugins. This step is essential before running in production.

npm run build

The build process creates a dist/ folder and prepares the admin interface. It might take 2-3 minutes on first run. Verify your package.json includes a start script:

cat package.json | grep "\"start\""

It should show "start": "strapi start" or similar. If it's missing, add it:

"start": "strapi start"

Test the build locally before deploying:

npm run build
npm start

Press Ctrl+C to stop. If the app starts without errors, you're ready for PM2.

Create a PM2 ecosystem configuration file

PM2 will manage your Strapi process and restart it if it crashes. Create ecosystem.config.js in your project root:

module.exports = {
  apps: [
    {
      name: 'strapi',
      script: 'npm',
      args: 'start',
      cwd: '/var/www/my-strapi-app',
      instances: 1,
      exec_mode: 'fork',
      env: {
        NODE_ENV: 'production'
      },
      error_file: '/var/log/pm2/strapi-err.log',
      out_file: '/var/log/pm2/strapi-out.log',
      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
      max_memory_restart: '1G'
    }
  ]
};

Strapi runs in fork mode with a single instance. Cluster mode isn't supported because Strapi synchronizes its database schema at startup, which breaks when multiple instances try to do this simultaneously. The max_memory_restart setting restarts the process if memory exceeds 1 GB, protecting against memory leaks.

Test the configuration:

cd /var/www/my-strapi-app
npm run build
pm2 start ecosystem.config.js
pm2 logs strapi
pm2 delete strapi

Commit this file to version control so future deploys use the same configuration.

Configure Nginx as a reverse proxy for Strapi

Create an Nginx configuration file:

sudo nano /etc/nginx/sites-available/strapi

Add this configuration:

server {
    listen 80;
    server_name your-domain.com;

    client_max_body_size 50M;

    location / {
        proxy_pass http://localhost:1337;
        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_cache_bypass $http_upgrade;
    }
}

Strapi runs on port 1337 by default. The client_max_body_size 50M directive allows file uploads up to 50 MB. Adjust based on your needs (content/media/image files).

Enable this configuration:

sudo ln -s /etc/nginx/sites-available/strapi /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

Nginx reverse proxy headers and caching static assets

The proxy headers forward the original client IP and protocol to Strapi, so logs show real IPs instead of 127.0.0.1. The Upgrade header is needed for WebSocket support if you use real-time features.

To cache static assets served by Strapi (like the admin dashboard), add this location block:

location ~* ^/admin/(.*)$ {
    proxy_pass http://localhost:1337;
    proxy_cache_valid 200 1h;
    expires 1h;
}

Setting up SSL with Certbot

Secure your domain with a free Let's Encrypt certificate:

sudo certbot --nginx -d your-domain.com

Certbot automatically modifies your Nginx config to use HTTPS and sets up auto-renewal. Verify renewal works:

sudo certbot renew --dry-run

Your Nginx config will now include:

server {
    listen 443 ssl http2;
    server_name your-domain.com;

    ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    client_max_body_size 50M;

    location / {
        proxy_pass http://localhost:1337;
        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_cache_bypass $http_upgrade;
    }
}

server {
    listen 80;
    server_name your-domain.com;
    return 301 https://$server_name$request_uri;
}

Start Strapi with PM2

Now launch your Strapi application:

cd /var/www/my-strapi-app
npm run build
pm2 start ecosystem.config.js
pm2 save
pm2 startup

The pm2 save command saves the current process list so PM2 restarts it after a reboot. The pm2 startup command configures the system to start PM2 at boot.

Monitor the logs:

pm2 logs strapi

Visit your domain in a browser. If this is a fresh installation, you'll see the Strapi admin setup screen to create your first user.

Media uploads and file storage strategies

By default, Strapi stores uploaded files locally in public/uploads/. This works for small deployments, but has limits. If your disk fills up or you scale to multiple servers, local storage becomes problematic.

For most VPS deployments, local storage is sufficient. Monitor disk usage:

df -h /var/www/my-strapi-app

If you outgrow local storage, Strapi plugins support S3-compatible providers. Install the S3 upload plugin and configure your credentials. Services like AWS S3, DigitalOcean Spaces, Cloudflare R2, or Scaleway Object Storage all work. Configuration involves setting environment variables for your bucket name, region, access key and secret.

S3-compatible provider setup

Install the official Strapi S3 provider:

npm install @strapi/provider-upload-aws-s3

Configure it in config/plugins.js:

export default {
  upload: {
    provider: 'aws-s3',
    config: {
      credentials: {
        accessKeyId: env('AWS_ACCESS_KEY_ID'),
        secretAccessKey: env('AWS_SECRET_ACCESS_KEY'),
      },
      region: env('AWS_REGION'),
      params: {
        Bucket: env('AWS_BUCKET'),
      },
    },
  },
};

Add these to your .env.production:

AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret
AWS_REGION=us-east-1
AWS_BUCKET=your-bucket-name

With S3 storage, uploaded files are stored off-server, which scales better for large media libraries.

Disk monitoring and alerting

Set up automated alerts if disk usage climbs. A simple cron job can warn you:

0 */6 * * * df -h /var/www/my-strapi-app | grep -E '[8-9][0-9]%|100%' && echo "Disk almost full" | mail -s "Disk Alert" [email protected]

Backup strategies for Strapi content and database

Your Strapi setup has two critical components: the PostgreSQL database (your content) and uploaded media files. Both need regular backups.

Database backup with pg_dump

Create a backup script:

#!/bin/bash
BACKUP_DIR="/backups/strapi"
mkdir -p $BACKUP_DIR
DATE=$(date +%Y%m%d_%H%M%S)
sudo -u postgres pg_dump strapi_db | gzip > $BACKUP_DIR/strapi_db_$DATE.sql.gz
echo "Database backed up to $BACKUP_DIR/strapi_db_$DATE.sql.gz"

Save as /usr/local/bin/backup-strapi-db.sh and make executable:

chmod +x /usr/local/bin/backup-strapi-db.sh

Media file backup with tar

Back up your uploads directory:

tar -czf /backups/strapi/uploads_$(date +%Y%m%d_%H%M%S).tar.gz /var/www/my-strapi-app/public/uploads/

Automating backups with cron

Schedule daily backups. Edit your crontab:

crontab -e

Add these entries:

0 2 * * * /usr/local/bin/backup-strapi-db.sh
0 3 * * * tar -czf /backups/strapi/uploads_$(date +\%Y\%m\%d).tar.gz /var/www/my-strapi-app/public/uploads/

Database backup runs at 2 AM, media backup at 3 AM daily.

Off-site storage for disaster recovery

Don't keep backups on the same server. Copy them to another VPS or cloud storage:

rsync -avz /backups/strapi/ backup-user@backup-server:/var/backups/strapi/

Or use a cloud provider's CLI tools. For example, with DigitalOcean Spaces (S3-compatible):

aws s3 sync /backups/strapi s3://my-backup-bucket/strapi-backups --no-progress

Schedule this as a daily cron job to keep off-site copies current.

Updating Strapi to new versions

Test updates on a staging environment first. When ready for production:

cd /var/www/my-strapi-app
git pull origin main
npm install
npm run build
pm2 restart strapi

PM2 will gracefully restart the app. Visit your Strapi admin panel after the restart to run any required migrations.

If the update breaks something, roll back:

git revert HEAD
npm install
npm run build
pm2 restart strapi

Performance tuning for Strapi in production

Monitor your Strapi instance with pm2 monit to watch CPU and memory usage in real time. Check the Strapi admin panel's performance section to identify slow content queries.

Database indexing makes a huge difference. If your content types have fields you query frequently (like published status or category), add indexes in PostgreSQL:

sudo -u postgres psql strapi_db -c "CREATE INDEX idx_published ON strapi_db (published_at);"

For detailed metrics over time, set up Prometheus and Grafana monitoring on your VPS.

Troubleshooting common Strapi deployment issues

If Strapi won't start, check PM2 logs first:

pm2 logs strapi --lines 100

Common issues are database connection failures, missing environment variables, or out-of-memory errors. Ensure your .env.production file has all required variables and correct permissions (600).

If your database feels slow, check PostgreSQL logs:

tail -f /var/log/postgresql/postgresql-16-main.log

Slow query logs can be enabled to identify bottlenecks. Query Strapi's database documentation for advanced tuning.

If media uploads fail, verify the public/uploads/ directory exists and is writable by the Node.js process:

ls -la /var/www/my-strapi-app/public/ | grep uploads

Check available disk space with df -h.

Your idea deserves better hosting

24/7 support 30-day money-back guarantee Cancel anytime
Ciclo de Pagamento

1 GB RAM VPS

$3.99 Save  50 %
$1.99 Mensalmente
  • 1 vCPU AMD EPYC
  • 30 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte IPv6 não está disponível em França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Gestão de firewall
  • Monitor grátis

2 GB RAM VPS

$5.99 Save  17 %
$4.99 Mensalmente
  • 2 vCPU AMD EPYC
  • 30 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte IPv6 não está disponível em França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Gestão de firewall
  • Monitor grátis

6 GB RAM VPS

$14.99 Save  33 %
$9.99 Mensalmente
  • 6 vCPU AMD EPYC
  • 70 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte IPv6 não está disponível em França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Gestão de firewall
  • Monitor grátis

AMD EPYC VPS.P1

$7.99 Save  25 %
$5.99 Mensalmente
  • 2 vCPU AMD EPYC
  • 4 GB memória RAM
  • 40 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível em França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Cópia automática incluída
  • Gestão de firewall
  • Monitor grátis

AMD EPYC VPS.P2

$14.99 Save  27 %
$10.99 Mensalmente
  • 2 vCPU AMD EPYC
  • 8 GB memória RAM
  • 80 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível em França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Cópia automática incluída
  • Gestão de firewall
  • Monitor grátis

AMD EPYC VPS.P4

$29.99 Save  20 %
$23.99 Mensalmente
  • 4 vCPU AMD EPYC
  • 16 GB memória RAM
  • 160 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível em França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Cópia automática incluída
  • Gestão de firewall
  • Monitor grátis

AMD EPYC VPS.P5

$36.49 Save  21 %
$28.99 Mensalmente
  • 8 vCPU AMD EPYC
  • 16 GB memória RAM
  • 180 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível em França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Cópia automática incluída
  • Gestão de firewall
  • Monitor grátis

AMD EPYC VPS.P6

$56.99 Save  21 %
$44.99 Mensalmente
  • 8 vCPU AMD EPYC
  • 32 GB memória RAM
  • 200 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível em França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Cópia automática incluída
  • Gestão de firewall
  • Monitor grátis

AMD EPYC VPS.P7

$69.99 Save  20 %
$55.99 Mensalmente
  • 16 vCPU AMD EPYC
  • 32 GB memória RAM
  • 240 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível em França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Cópia automática incluída
  • Gestão de firewall
  • Monitor grátis

EPYC Genoa VPS.G1

$4.99 Save  20 %
$3.99 Mensalmente
  • 1 vCPU AMD EPYC Gen4 AMD EPYC Genoa 4.ª geração 9xx4 com 3.25 GHz ou equivalente, baseada na arquitetura Zen 4.
  • 1 GB DDR5 memória RAM
  • 25 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível em França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Cópia automática incluída
  • Gestão de firewall
  • Monitor grátis

EPYC Genoa VPS.G2

$12.99 Save  23 %
$9.99 Mensalmente
  • 2 vCPU AMD EPYC Gen4 AMD EPYC Genoa 4.ª geração 9xx4 com 3.25 GHz ou equivalente, baseada na arquitetura Zen 4.
  • 4 GB DDR5 memória RAM
  • 50 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível em França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Cópia automática incluída
  • Gestão de firewall
  • Monitor grátis

EPYC Genoa VPS.G4

$25.99 Save  27 %
$18.99 Mensalmente
  • 4 vCPU AMD EPYC Gen4 AMD EPYC Genoa 4.ª geração 9xx4 com 3.25 GHz ou equivalente, baseada na arquitetura Zen 4.
  • 8 GB DDR5 memória RAM
  • 100 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível em França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Cópia automática incluída
  • Gestão de firewall
  • Monitor grátis

EPYC Genoa VPS.G5

$44.99 Save  33 %
$29.99 Mensalmente
  • 4 vCPU AMD EPYC Gen4 AMD EPYC Genoa 4.ª geração 9xx4 com 3.25 GHz ou equivalente, baseada na arquitetura Zen 4.
  • 16 GB DDR5 memória RAM
  • 150 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível em França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Cópia automática incluída
  • Gestão de firewall
  • Monitor grátis

EPYC Genoa VPS.G6

$48.99 Save  31 %
$33.99 Mensalmente
  • 8 vCPU AMD EPYC Gen4 AMD EPYC Genoa 4.ª geração 9xx4 com 3.25 GHz ou equivalente, baseada na arquitetura Zen 4.
  • 16 GB DDR5 memória RAM
  • 200 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível em França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Cópia automática incluída
  • Gestão de firewall
  • Monitor grátis

EPYC Genoa VPS.G7

$74.99 Save  27 %
$54.99 Mensalmente
  • 8 vCPU AMD EPYC Gen4 AMD EPYC Genoa 4.ª geração 9xx4 com 3.25 GHz ou equivalente, baseada na arquitetura Zen 4.
  • 32 GB DDR5 memória RAM
  • 250 GB NVMe disco
  • Ilimitada largura de banda
  • IPv4 e IPv6 incluídos O suporte a IPv6 não está disponível em França, Finlândia ou Países Baixos.
  • 1 Gbps rede
  • Cópia automática incluída
  • Gestão de firewall
  • Monitor grátis

FAQ

How much RAM does Strapi need on a VPS?

Strapi and PostgreSQL together typically use 1-2 GB at rest. Under traffic, especially with large content models, expect 3-4 GB. A 4 GB VPS is the minimum for stable operation. For larger teams or high traffic, consider 8 GB or separate the database onto its own VPS.

Skip the setup, start deploying

Your server comes ready with PM2, Nginx and Certbot. Pick a datacenter and push your first build in minutes.

GPU products are in high demand at the moment. Fill the form to get notified as soon as your preferred GPU server is back in stock.