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
sudoprivileges, calledyour_userin 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:
- Open Content-Type Builder and click Create new collection type.
- Enter
Articleas the display name and continue. - Add a Text field called
titleand a Rich text (Blocks) field calledcontent. - 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"
NoteOn a real project, run
npm run developon your own computer, create and change content types there, commit them and deploy them to the server. The server only runs production mode.
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_dumpbackups of the database together with thepublic/uploadsdirectory.
