# How to install Strapi 5 with PostgreSQL, PM2 and HTTPS

> Run Strapi 5 in production on Ubuntu or Debian with PostgreSQL, Node.js LTS and PM2 as a non-root user, behind Caddy HTTPS, with backups and upgrades.

Difficulty: Intermediate\
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

Strapi is an open-source headless CMS built on Node.js. Editors manage content in an admin panel, and your websites and apps read it through REST or GraphQL APIs. This guide runs **Strapi 5** in production on a single server: PostgreSQL as the database, Node.js LTS from the NodeSource repository, PM2 as the process manager under a dedicated non-root user, and Caddy in front for automatic HTTPS. You also set up the development-to-production workflow Strapi expects, plus backups, upgrades and troubleshooting.

> **Note**
>
> Strapi treats the production server as a place to run your project, not to design it. Content types (the data schema) are created in development and deployed with your code. Step 7 shows the routine.

## Prerequisites

- A server running **Ubuntu 24.04 LTS**, **Ubuntu 26.04 LTS**, **Debian 12** or **Debian 13**. Strapi's requirements name Ubuntu 24.04 as the recommended Ubuntu release (20.04 is the minimum) and Debian 10 as the minimum Debian release.
- A non-root user with `sudo` rights and SSH key login. If you have not done this yet, follow [Secure a new Linux server](/guides/secure-a-new-linux-server) and [Set up SSH keys](/guides/ssh-keys) first.
- A domain name such as `cms.example.com` with an A record (and optionally an AAAA record) pointing at the server.
- Caddy installed as described in [Caddy reverse proxy](/guides/caddy-reverse-proxy). You need it in Step 6.

Strapi publishes its own hardware requirements:

| Resource | Minimum (official) | Suggested starting point |
|---|---|---|
| CPU | 1 core | 2 cores (Strapi's recommendation) |
| RAM | 2 GB | 4 GB (Strapi's recommendation) |
| Disk | 8 GB | 32 GB (Strapi's recommendation), plus space for uploads |

Strapi supports PostgreSQL 14 or newer and recommends 17. The PostgreSQL package that ships with each release above meets the minimum. Building the admin panel is the heaviest job on a small server; if a build gets killed, see Troubleshooting.

## Step 1 — Install PostgreSQL and create the database

Install PostgreSQL from your distribution's archive and check that the service runs:

```bash
sudo apt update
sudo apt install postgresql
sudo systemctl status postgresql --no-pager
```

On Ubuntu and Debian PostgreSQL listens only on `localhost` by default, so it needs no firewall rule. Generate a strong password for the database user and keep it for Step 3:

```bash
openssl rand -hex 24
```

Create a `strapi` role (paste the password when prompted), a database owned by it, and grant the schema permission Strapi's documentation asks for. Without the `GRANT`, a fresh database user cannot create tables in the `public` schema and the admin panel answers with a 500 error:

```bash
sudo -u postgres createuser --pwprompt strapi
sudo -u postgres createdb --owner=strapi strapi
sudo -u postgres psql -d strapi -c "GRANT ALL ON SCHEMA public TO strapi;"
```

Test the login over TCP, the way Strapi will connect. After you enter the password, the query prints `strapi`:

```bash
psql -h 127.0.0.1 -U strapi -d strapi -c "SELECT current_user;"
```

## Step 2 — Install Node.js LTS and PM2

On most of these releases the distribution's own `nodejs` package is older than Node.js 22, the oldest line Strapi supports. You have two common alternatives: **nvm**, which installs Node.js per user inside a home directory and is handy on a development machine where you switch versions often, and the **NodeSource apt repository**, which installs one system-wide Node.js under `/usr/bin` and updates it with `apt` together with your other security updates. For a server that runs a long-lived service, the apt repository is easier to keep patched and does not depend on a shell profile, so this guide uses NodeSource.

NodeSource's manual installation uses a distribution-independent suite called `nodistro`. Add the signing key and the repository for Node.js 24 LTS:

```bash
sudo apt install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
```

```bash
NODE_MAJOR=24
sudo tee /etc/apt/sources.list.d/nodesource.sources <<EOF
Types: deb
URIs: https://deb.nodesource.com/node_${NODE_MAJOR}.x/
Suites: nodistro
Components: main
Signed-By: /etc/apt/keyrings/nodesource.gpg
EOF
sudo apt update
sudo apt install nodejs
node -v
npm -v
```

`node -v` should print a `v24` version. Now install PM2 globally, so the `pm2` command is on every user's path and the boot script can find it:

```bash
sudo npm install -g pm2
pm2 --version
```

## Step 3 — Create the strapi user and the project

Run Strapi under its own system user, never as root. The user's home directory `/srv/strapi` holds the project and PM2's state:

```bash
sudo adduser --system --group --home /srv/strapi --shell /bin/bash strapi
sudo -iu strapi
```

You are now in a shell as `strapi`. Create the project with the official `create-strapi` tool. The flags answer the interactive questions in advance: skip the Strapi Cloud login, use npm and JavaScript, install dependencies, start a Git repository, skip example data and anonymous A/B tests, and connect to the PostgreSQL database from Step 1. Replace `change-me` with the password you generated:

```bash
cd /srv/strapi
npx create-strapi@latest app --skip-cloud --no-run --javascript --use-npm --install --no-example --git-init --no-enable-ab-tests --dbclient=postgres --dbhost=127.0.0.1 --dbport=5432 --dbname=strapi --dbusername=strapi --dbpassword=change-me
```

`npx` asks once whether it may download `create-strapi`; answer `y`. Answer any question the flags did not cover. Use `--typescript` instead of `--javascript` if your team prefers TypeScript; the configuration file in Step 4 is then `config/server.ts` with `export default` instead of `module.exports`. When the command finishes, list the project:

```bash
cd /srv/strapi/app
ls -a
```

You should see `config`, `src`, `public`, `package.json` and a `.env` file.

## Step 4 — Configure production settings

`create-strapi` writes a `.env` file with freshly generated values for `APP_KEYS`, `API_TOKEN_SALT`, `ADMIN_JWT_SECRET`, `TRANSFER_TOKEN_SALT`, `JWT_SECRET` and `ENCRYPTION_KEY`, plus the database settings. List the variable names without printing the secrets:

```bash
cut -d= -f1 .env
```

Open the file with `nano .env` and make sure it contains these lines. Bind Strapi to `127.0.0.1` so only Caddy can reach it, set your public URL, and turn off telemetry if you prefer:

```env
HOST=127.0.0.1
PORT=1337
PUBLIC_URL=https://cms.example.com
APP_KEYS=generated-value-1,generated-value-2,generated-value-3,generated-value-4
API_TOKEN_SALT=generated-value
ADMIN_JWT_SECRET=generated-value
TRANSFER_TOKEN_SALT=generated-value
JWT_SECRET=generated-value
ENCRYPTION_KEY=generated-value
DATABASE_CLIENT=postgres
DATABASE_HOST=127.0.0.1
DATABASE_PORT=5432
DATABASE_NAME=strapi
DATABASE_USERNAME=strapi
DATABASE_PASSWORD=change-me
DATABASE_SSL=false
STRAPI_TELEMETRY_DISABLED=true
```

If a secret is missing, for example because you cloned an existing project whose `.env` is not in Git, generate each value with `openssl rand -base64 32`; `APP_KEYS` takes several such values separated by commas. Then restrict the file to its owner:

```bash
chmod 600 .env
```

> **Warning**
>
> Do not regenerate `APP_KEYS`, `ADMIN_JWT_SECRET` or `JWT_SECRET` on a running site. Strapi's documentation warns that this invalidates existing sessions and API tokens. Keep a copy of `.env` with your backups.

Next, tell Strapi its public URL and that it runs behind one reverse proxy. Open `config/server.js` with `nano config/server.js` and replace its content with:

```text
module.exports = ({ env }) => ({
  host: env('HOST', '0.0.0.0'),
  port: env.int('PORT', 1337),
  url: env('PUBLIC_URL', 'https://cms.example.com'),
  proxy: {
    koa: true,
    maxIpsCount: 1,
  },
  app: {
    keys: env.array('APP_KEYS'),
  },
});
```

`proxy.koa: true` makes Strapi trust the `X-Forwarded-*` headers Caddy sends, and `maxIpsCount: 1` limits that trust to exactly one proxy. Strapi's Caddy guide notes that leaving `maxIpsCount` unset means an unlimited count. Changes to this file need a fresh admin build, which you run next.

## Step 5 — Build, start with PM2 and create the first admin

Build the admin panel. Set `NODE_ENV` on the build command itself only: Strapi's PM2 guide warns that exporting it for the whole session makes npm skip the development dependencies the build needs.

```bash
NODE_ENV=production npm run build
```

Create the PM2 configuration with `nano ecosystem.config.js` in `/srv/strapi/app`:

```text
module.exports = {
  apps: [
    {
      name: 'strapi',
      cwd: '/srv/strapi/app',
      script: 'npm',
      args: 'start',
      env: {
        NODE_ENV: 'production',
      },
    },
  ],
};
```

`npm start` runs `strapi start`, the production mode without auto-reload. Start the app and check its health endpoint, which answers `HTTP/1.1 204 No Content` once Strapi is ready:

```bash
pm2 start ecosystem.config.js
pm2 list
curl -I http://127.0.0.1:1337/_health
```

Until an administrator exists, the `/admin` page shows a registration form to anyone who opens it. Create the first admin from the command line before the site goes public. Run the command without options and answer the prompts for name, email and password:

```bash
npx strapi admin:create-user
```

Now make PM2 start at boot. Leave the `strapi` shell, generate the systemd unit for the `strapi` user, and save the current process list:

```bash
exit
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u strapi --hp /srv/strapi
sudo -iu strapi pm2 save
systemctl status pm2-strapi --no-pager
```

The unit is called `pm2-strapi` and should be `active`. Run `pm2 save` again whenever you add or rename an app. Finally, keep PM2's own logs from filling the disk, as the Strapi PM2 guide suggests:

```bash
sudo -iu strapi pm2 install pm2-logrotate
sudo -iu strapi pm2 set pm2-logrotate:max_size 10M
sudo -iu strapi pm2 set pm2-logrotate:retain 7
```

## Step 6 — Publish Strapi over HTTPS with Caddy

Add a site block to `/etc/caddy/Caddyfile`. Caddy obtains and renews the certificate on its own:

```caddyfile
cms.example.com {
    reverse_proxy 127.0.0.1:1337
}
```

```bash
sudo systemctl reload caddy
```

Allow only SSH and web traffic through the firewall. Port 1337 stays closed because Strapi listens on `127.0.0.1` only:

```bash
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ss -tlnp | grep 1337
```

The last command should show `127.0.0.1:1337`, not `0.0.0.0:1337`. Check the public endpoint with `curl -I https://cms.example.com/_health`, then open `https://cms.example.com/admin` and log in with the account from Step 5. Prefer Nginx? Use [Nginx with Certbot](/guides/nginx-reverse-proxy-certbot) with the same upstream; [Traefik](/guides/traefik-reverse-proxy) works too.

## Step 7 — Change content types in development, then deploy

On the server, Strapi runs in production mode, where the Content-Type Builder is disabled. Strapi's FAQ explains why: schemas are files such as `src/api/article/content-types/article/schema.json`, and changing them needs a restart. Treat the server as a deployment target:

1. Push the project to a private Git repository. Strapi's generated `.gitignore` should exclude `.env` and `node_modules`; confirm with `git check-ignore .env`.
2. Clone it on your workstation, run `npm install` and `npm run develop`, and design or change content types there.
3. Commit, push, and deploy on the server.

To connect the server copy to your repository, run as `strapi` in `/srv/strapi/app` (the `strapi` user needs read access, for example a read-only deploy key):

```bash
git status
git remote add origin git@git.example.com:team/cms.git
git push -u origin HEAD
```

Each deployment then looks like this:

```bash
sudo -iu strapi
cd /srv/strapi/app
git pull
npm ci
NODE_ENV=production npm run build
pm2 restart strapi --update-env
```

Strapi synchronises the database schema when it starts, so a removed field means removed data. Take a backup before you deploy schema changes.

## Back up and restore

Three things hold state: the PostgreSQL database, uploaded files in `public/uploads` (the default local upload provider), and the secrets in `.env`. The code itself lives in Git. Create a private backup folder and dump everything with dated names:

```bash
sudo mkdir -p /opt/backups
sudo chown $USER:$USER /opt/backups
chmod 700 /opt/backups
sudo -u postgres pg_dump -Fc strapi > /opt/backups/strapi-db-$(date +%F).dump
sudo tar czf /opt/backups/strapi-files-$(date +%F).tar.gz -C /srv/strapi/app public/uploads .env
```

`pg_dump` produces a consistent snapshot while Strapi keeps running. Strapi's own `strapi export` command is useful for moving content between environments, but it does not include admin users or API tokens, so it is an extra, not a replacement. If you want one, run it as `strapi` (the archive is unencrypted, so store it as carefully as the database dump):

```bash
sudo -iu strapi
cd /srv/strapi/app
npm run strapi export -- --no-encrypt -f /srv/strapi/export-$(date +%F)
exit
```

To restore, stop Strapi, recreate the database, load the dump, unpack the files and start again. Replace the dates with those of your backup:

> **Warning**
>
> `dropdb` deletes the current database. Make sure your backup file is complete before you run it.

```bash
sudo -iu strapi pm2 stop strapi
sudo -u postgres dropdb strapi
sudo -u postgres createdb --owner=strapi strapi
sudo -u postgres pg_restore -d strapi < /opt/backups/strapi-db-2026-10-09.dump
sudo tar xzf /opt/backups/strapi-files-2026-10-09.tar.gz -C /srv/strapi/app
sudo chown -R strapi:strapi /srv/strapi/app/public/uploads /srv/strapi/app/.env
sudo -iu strapi pm2 start strapi
```

On a new server, run Steps 1 to 3 first (cloning your repository instead of creating a project), then restore. Copy backups off the server as well, for example with `rsync` to another machine, and test a restore now and then.

## Update Strapi

Upgrade Strapi in development, not on the server, because the official upgrade tool edits your code. Read the release notes and back up first. In your development copy, preview the changes with a dry run, then apply them:

```bash
npx @strapi/upgrade minor --dry
npx @strapi/upgrade minor
```

`patch` and `minor` stay within your current major version. `major` moves to the next major but requires the latest minor and patch of your current one; `latest` can also cross majors after a confirmation. The tool updates dependencies and runs codemods; it does not migrate data. Review the changes, test with `npm run develop`, commit, and deploy with the routine from Step 7.

Node.js patch releases arrive with `sudo apt update && sudo apt upgrade`. To move to a new Node.js major line, change `NODE_MAJOR` in the repository file to another version Strapi supports, upgrade, rebuild, and regenerate PM2's boot script as PM2's documentation advises: run `pm2 unstartup`, then the `pm2 startup` command from Step 5 again. Update PM2 itself with `sudo npm install -g pm2`, then run `pm2 update` as `strapi`.

## Troubleshooting

### The admin panel answers with a 500 error on a fresh install

The database user cannot create tables in the `public` schema. Run the `GRANT ALL ON SCHEMA public TO strapi;` command from Step 1, then `pm2 restart strapi` as the `strapi` user.

### The build stops with JavaScript heap out of memory or the process is killed

The admin build needs more memory than is free. Stop other heavy processes, add a temporary swap file, or move to a server with at least the 4 GB Strapi recommends. Check `free -h` and `dmesg | tail` for out-of-memory kills.

### The Content-Type Builder is disabled on the server

This is expected in production mode. Create and change content types in a development copy and deploy them as described in Step 7.

### Admin requests go to localhost or plain HTTP

`PUBLIC_URL` is missing or wrong, or the admin panel was built before you changed `config/server.js`. Fix `.env` and the `url` setting, rebuild with `NODE_ENV=production npm run build`, and run `pm2 restart strapi --update-env`.

### Strapi does not come back after a reboot

The boot unit is missing or the process list was not saved. Check `systemctl status pm2-strapi`, repeat the `pm2 startup` and `pm2 save` commands from Step 5, and read the logs with `pm2 logs strapi --lines 100` as `strapi`.

## Next steps

- Compare another self-hosted headless CMS: [How to install Directus](/guides/install-directus).
- Learn more about automatic HTTPS in [Caddy reverse proxy](/guides/caddy-reverse-proxy).
- See server options for Strapi projects on the [Strapi hosting](/strapi-hosting) page.
- Read Strapi's [deployment documentation](https://docs.strapi.io/cms/deployment) for advanced topics such as cluster mode and object storage for uploads.

## Frequently asked questions

### Can I create or edit content types on the production server?

No. Strapi stores content-type schemas as files in the project and disables the Content-Type Builder when it runs in production mode. Make schema changes in a development copy with npm run develop, commit them to Git, then pull, build and restart on the server.

### Which Node.js version does Strapi 5 support?

Strapi supports only Active LTS and Maintenance LTS releases of Node.js; its deployment page lists versions 22, 24 and 26. Odd-numbered releases such as 23 or 25 are not supported. This guide installs Node.js 24 from the NodeSource repository.

### Should I use PM2 or a plain systemd service?

Strapi's deployment documentation highly recommends a process manager such as PM2. PM2's startup command generates a systemd unit, so you get both: PM2 keeps Strapi running and systemd starts PM2 at boot under the non-root strapi user.

### Is strapi export a full backup?

No. The export archive contains content, uploaded files, configuration and schemas, but Strapi documents that admin users and API tokens are not exported. Use pg_dump plus an archive of public/uploads and .env as your real backup, and keep exports for moving content between environments.

### Can I use SQLite instead of PostgreSQL?

SQLite is supported and is the default for new projects, but this guide uses PostgreSQL so the database runs as its own service and can be dumped with pg_dump while Strapi keeps running. MongoDB and other NoSQL databases are not supported.

### Which HyperDC servers can run Strapi?

Any HyperDC Linux VPS, VDS or dedicated server with root access and a supported Ubuntu or Debian release. Strapi lists 2 GB of RAM as the minimum and 4 GB or more as recommended, so size the server for the admin build as well as for traffic.

---

Source: <https://hyperdc.com/guides/tutorials/install-strapi>\
Updated: 2026-10-09
