Strapi is an open-source headless CMS built on Node.js: editors manage content in an admin panel, and your websites or apps read it through an automatically generated REST API. In this tutorial you will install Strapi 5 on Ubuntu 24.04 with PostgreSQL as the database, create a content type and expose it through the public API, and then run Strapi in production mode as a systemd service behind Nginx with a Let's Encrypt certificate.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 2 GB of RAM. Building the admin panel fails on smaller servers unless you add swap.
  • A non-root user with sudo privileges, called your_user in this guide.
  • A domain name (your_domain) with an A record pointing to the server's public IP.
  • UFW enabled with SSH allowed (sudo ufw allow OpenSSH && sudo ufw enable).
  • SSH access from your local computer, used to open the admin panel through a tunnel during setup.

Step 1 - Installing Node.js 22

Strapi 5 runs on the Active and Maintenance LTS releases of Node.js. Install Node.js 22 from the NodeSource repository. Download the setup script and review it before running it:

cd ~
curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh
less nodesource_setup.sh
sudo bash nodesource_setup.sh
sudo apt install nodejs

Verify the installation:

node -v
npm -v
v22.19.0
10.9.3

Step 2 - Creating the PostgreSQL database

Install PostgreSQL:

sudo apt install postgresql

Open a PostgreSQL shell and create a role and a database owned by it. Replace your_strong_password with a long random password:

sudo -u postgres psql
CREATE ROLE strapi WITH LOGIN PASSWORD 'your_strong_password';
CREATE DATABASE strapi OWNER strapi;
\q

Owning the database lets Strapi create its tables in the public schema. Test the login over TCP, the way Strapi will connect:

psql -h 127.0.0.1 -U strapi -d strapi -c 'SELECT current_user;'
 current_user
--------------
 strapi
(1 row)

Step 3 - Creating the Strapi project

Create a new project called cms in your home directory with the official installer:

cd ~
npx create-strapi@latest cms

The installer asks several questions. Answer them as follows:

  • Strapi Cloud login: choose Skip.
  • Use the default database (SQLite): No, then choose postgres.
  • Database name, host, port and username: strapi, 127.0.0.1, 5432, strapi, and the password you set in Step 2. Do not enable SSL for a local connection.
  • Start with an example structure and data: No.
  • TypeScript, install dependencies with npm and initialize a Git repository: Yes.

When it finishes, the project contains a .env file with the database settings and randomly generated secrets (APP_KEYS, API_TOKEN_SALT, ADMIN_JWT_SECRET, JWT_SECRET and others). Keep this file private and back it up: if the secrets change, existing sessions and API tokens stop working.

cd ~/cms
chmod 600 .env

Step 4 - Creating a content type in development mode

In Strapi 5, content types are defined as files in your project and can only be edited with the Content-Type Builder in development mode. Production mode disables the builder on purpose. For this first setup you will run development mode on the server and reach it through an SSH tunnel, so port 1337 never has to be open to the Internet.

From your local computer, open a tunnel that forwards local port 1337 to the server:

ssh -L 1337:127.0.0.1:1337 your_user@your_server_ip

In that SSH session, start Strapi in development mode:

cd ~/cms
npm run develop

Once the log shows that the server has started, open http://localhost:1337/admin in your local browser and fill in the form to create the first administrator account.

Create a content type for blog articles:

  1. Open Content-Type Builder and click Create new collection type.
  2. Enter Article as the display name and continue.
  3. Add a Text field called title and a Rich text (Blocks) field called content.
  4. Click Save. Strapi writes the schema to src/api/article/ and restarts.

Next, go to Content Manager, select Article, create an entry with a title and some content, and click Publish. Only published entries are returned by the API.

Finally, allow anonymous visitors to read articles. Go to Settings, then Users & Permissions plugin, Roles, Public. Under Article, tick find and findOne and click Save.

From a second SSH session on the server, query the API:

curl -s http://127.0.0.1:1337/api/articles
{"data":[{"id":2,"documentId":"k3f9a1b2c4d5e6f7g8h9i0jk","title":"Hello from Strapi","content":[...],"createdAt":"2026-09-24T10:31:07.412Z","updatedAt":"2026-09-24T10:31:07.412Z","publishedAt":"2026-09-24T10:31:07.431Z"}],"meta":{"pagination":{"page":1,"pageSize":25,"pageCount":1,"total":1}}}

Stop the development server with CTRL+C. Commit the new content type so it is part of your project history:

git add -A
git commit -m "Add Article content type"

Step 5 - Configuring Strapi for production behind a proxy

Strapi needs to know its public URL (used in emails and the admin panel) and that it sits behind a reverse proxy, so it trusts the X-Forwarded-Proto header and sets secure cookies correctly. Open the server configuration:

nano ~/cms/config/server.ts

Add the url and proxy lines inside the returned object, next to the existing host and port settings:

  url: env('PUBLIC_URL', 'http://localhost:1337'),
  proxy: { koa: env.bool('IS_PROXIED', false) },

Now edit .env:

nano ~/cms/.env

Change HOST so Strapi listens only on localhost, and add the two new variables:

HOST=127.0.0.1
PORT=1337
PUBLIC_URL=https://your_domain
IS_PROXIED=true

Leave the generated secrets and database settings as they are. Build the admin panel for production:

cd ~/cms
NODE_ENV=production npm run build

The build takes a minute or two and ends without errors.

Step 6 - Running Strapi with systemd

Create a systemd unit so Strapi starts at boot and restarts if it crashes:

sudo nano /etc/systemd/system/strapi.service
[Unit]
Description=Strapi CMS
After=network.target postgresql.service

[Service]
Type=simple
User=your_user
Group=your_user
WorkingDirectory=/home/your_user/cms
Environment=NODE_ENV=production
ExecStart=/usr/bin/npm run start
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target

Strapi reads the rest of its configuration from the .env file in the working directory. Start the service and enable it at boot:

sudo systemctl daemon-reload
sudo systemctl enable --now strapi

Check its status and the API:

sudo systemctl status strapi --no-pager
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:1337/api/articles
200

Logs are available with sudo journalctl -u strapi -f.

Step 7 - Configuring Nginx and HTTPS

Install Nginx and create a server block that forwards all requests to Strapi:

sudo apt install nginx
sudo nano /etc/nginx/sites-available/strapi
server {
    listen 80;
    listen [::]:80;
    server_name your_domain;

    client_max_body_size 100M;

    location / {
        proxy_pass http://127.0.0.1:1337;
        proxy_http_version 1.1;
        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;
    }
}

client_max_body_size controls the largest file editors can upload to the Media Library; Nginx rejects anything bigger with a 413 error.

Enable the site, test and reload Nginx, and open the web ports:

sudo ln -s /etc/nginx/sites-available/strapi /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
sudo ufw allow 'Nginx Full'

Request a certificate with Certbot. It configures HTTPS in the server block and redirects HTTP to HTTPS:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your_domain
sudo certbot renew --dry-run

Verify the public API over HTTPS:

curl -s https://your_domain/api/articles | head -c 120

Then open https://your_domain/admin and log in with the administrator account you created. The admin panel must be reached over HTTPS in production, because its session cookie is marked secure.

Step 8 - Deploying changes

When you change content types or code locally, commit and push them, then update the server:

cd ~/cms
git pull
npm ci
NODE_ENV=production npm run build
sudo systemctl restart strapi

Content created by editors lives in PostgreSQL and uploaded files live in ~/cms/public/uploads, so neither is affected by deployments. Include both in your backups.

Troubleshooting

Cannot send secure cookie over unencrypted connection when logging in: Strapi does not know the request came in over HTTPS. Check that IS_PROXIED=true is in .env, that config/server.ts contains the proxy line and that Nginx sets X-Forwarded-Proto. Then restart the service.

403 Forbidden from /api/articles: the Public role has no permission on the content type. Enable find and findOne for it in the Users & Permissions settings.

Empty data array: the entries exist but are drafts. Publish them in the Content Manager.

The build is killed or fails with JavaScript heap out of memory: the server does not have enough RAM for the admin build. Add a swap file or build on a larger machine.

413 Request Entity Too Large on uploads: raise client_max_body_size in the Nginx server block and reload Nginx.

Conclusion

Strapi 5 now runs in production mode on Ubuntu 24.04, stores its content in PostgreSQL, and serves both the admin panel and the REST API through Nginx over HTTPS. Content types are managed in development and deployed with Git, which keeps the production schema under version control.

As next steps you can:

  • Create API tokens under Settings, API Tokens for frontends that need access beyond the public role.
  • Move uploads to S3-compatible object storage with an upload provider plugin.
  • Schedule pg_dump backups of the database together with the public/uploads directory.