Applications AWS

Strapi 5.60.0 on AWS User Guide

| Product: Strapi 5.60.0 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Strapi 5.60.0 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg CMS, Blogs and Wikis listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

Strapi is a free and open source headless content management system built on Node.js. Editors work in an admin panel at /admin; your web sites, apps and services read and write content through Strapi's REST API under /api. It has a media library, roles and permissions for admin users and for API users, draft and publish, internationalisation and API tokens. This image ships Strapi's own JavaScript project with a SQLite database, ready for your content types.

The cloudimg image adds:

  • No setup page anyone could claim. A fresh Strapi project lets whoever opens the admin panel first register as the super administrator. On this image your instance's first boot creates the super administrator itself, with a password generated on your instance, before the web server starts, so that page never opens.
  • Secrets of your instance's own, never on a command line. Strapi's application keys, API token salt, admin and users JWT secrets, transfer token salt and encryption key, the administrator password and the TLS key are generated on your instance's first boot; nothing from the image build is reused. The secrets live in one root-owned file that only the Strapi service can read, not in the environment or a .env file, and the password reaches Strapi in a file, never as a command line argument (Strapi's own admin:create-user command takes it on the command line).
  • HTTPS from the first boot. nginx serves Strapi on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS. Strapi itself listens only on 127.0.0.1:1337.
  • Host from configuration, never from the request. Strapi's public URL (used for links such as password reset) and its CORS origin are your instance's public DNS name by default, set by cloudimg's strapi-set-hostname helper. nginx answers only that name and the instance's own addresses; any other host name gets no answer.
  • Closed by default. Public end-user sign-up (/api/auth/local/register) is off until you turn it on. Strapi's usage telemetry, its npm update check, its MCP endpoint and Strapi AI are off, and the admin panel does not fetch release notes from GitHub. No mail is sent, and no mail content is written to the log, until you configure an e-mail provider (Step 9).
  • Community Edition. Strapi's Enterprise code ships inside its npm packages, as in every Strapi install, but it is never enabled: no licence key ships and the service runs with STRAPI_DISABLE_EE=true.
  • Sign-in protection. nginx limits every admin and API sign-in, registration, password reset and token refresh route to 10 requests a minute per client address, on top of Strapi's own rate limit. The admin refresh cookie is Secure, HttpOnly and SameSite=Lax.
  • Sandboxed uploads. Every file under /uploads/ gets nosniff and a sandboxing content security policy; only passive types (images, PDF, fonts, audio and video) open in the browser, anything else is sent as a download. Strapi refuses HTML, SVG and executable uploads.
  • A hardened service. Strapi runs as its own unprivileged user. The application in /opt/strapi belongs to root and is read only to Strapi; Strapi can write only its data folder, /var/lib/strapi. Instance metadata (169.254.169.254) is blocked for Strapi and nginx, and Strapi refuses webhooks to non-public addresses. WebSocket upgrades are accepted only for Strapi's remote data transfer, and never from another site.
  • Checked dependencies. Strapi's dependency tree is installed exactly from a reviewed lockfile; the one native module (better-sqlite3) is compiled from source on the build. Only the Node.js runtime itself ships, not npm.
  • Fail closed. nginx and Strapi do not start until first boot has finished; if first boot cannot complete, the site stays offline rather than open.

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (or a private route you use for management).
  • A security group that allows TCP 22 (SSH), TCP 443 (Strapi) and TCP 80 (redirects to HTTPS) from the networks that should reach them. Keep 22 to your own networks. Open 443 to your editors and API clients.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Strapi 5.60.0 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair and a security group as above.

With the AWS CLI (replace the values in angle brackets):

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=strapi}]'

Step 2: Connect to your instance

Version SSH user
Strapi 5.60.0 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

Step 3: Check the services

First boot takes under a minute (it creates the secrets, the database and the administrator). Then nginx, Strapi and the first boot unit are active:

systemctl is-active nginx strapi strapi-firstboot
active
active
active

nginx serves ports 80 and 443; Strapi listens on the loopback address only, so the only open ports are 22, 80 and 443 (port 53 on 127.0.0.53/54 is the system's local DNS resolver):

sudo ss -tlnH | awk '{print $4}' | sort -V
0.0.0.0:22
0.0.0.0:80
0.0.0.0:443
127.0.0.1:1337
127.0.0.53%lo:53
127.0.0.54:53
[::]:22
[::]:80
[::]:443

The Node.js, Strapi, SQLite and nginx versions:

/opt/node/bin/node --version
cd /opt/strapi && for m in @strapi/strapi @strapi/admin better-sqlite3 sharp react; do printf '%s %s\n' "$m" "$(/opt/node/bin/node -p "require('./node_modules/$m/package.json').version")"; done
/opt/node/bin/node -e "const D=require('/opt/strapi/node_modules/better-sqlite3'); console.log('SQLite', new D(':memory:').prepare('select sqlite_version() v').get().v)"
nginx -v 2>&1
v24.22.2
@strapi/strapi 5.60.0
@strapi/admin 5.60.0
better-sqlite3 12.11.1
sharp 0.35.5
react 18.3.1
SQLite 3.53.2
nginx version: nginx/1.24.0 (Ubuntu)

Step 4: Get your sign-in details

The credentials file holds the address Strapi is served as (your instance's public DNS name by default), the admin panel address and the administrator's e-mail and password, all generated on this instance. Show it with:

sudo cat /root/strapi-credentials.txt
STRAPI_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
STRAPI_ADMIN_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/admin
STRAPI_ADMIN_EMAIL=admin@example.com
STRAPI_ADMIN_PASSWORD=<24 letters and digits>

Strapi admin users sign in with an e-mail address. The administrator starts as admin@example.com; change it to your own address under your profile (top left of the admin panel) once you have configured an e-mail provider (Step 9), because password reset mails go to that address.

Step 5: Sign in to the admin panel

Open STRAPI_ADMIN_URL in your browser. The certificate is self signed (generated on your instance), so your browser warns once: compare the certificate's SHA-256 fingerprint with the one on the instance (sudo openssl x509 -in /etc/nginx/ssl/strapi.crt -noout -fingerprint -sha256) and continue, or install a certificate from your own certificate authority (Step 9). Sign in with STRAPI_ADMIN_EMAIL and STRAPI_ADMIN_PASSWORD:

The Strapi admin sign in page with the Email and Password fields

The home page greets the administrator and links to the Content Manager, the Content-Type Builder, the Media Library and the settings:

The Strapi admin home page after signing in

Step 6: Media and content

Open the Media Library and choose Add assets to upload images, videos and documents. Strapi stores the files in /var/lib/strapi/uploads and serves them from https://<your host>/uploads/<name>:

The Strapi Media Library with an uploaded image

Uploads are limited to 256 MB. Strapi refuses HTML, SVG and executable files, and nginx sends every other non-passive type as a download.

Content types. Strapi runs here in production mode (strapi start), in which Strapi's Content-Type Builder is read only: you can see the content types but not create or change them. That is how Strapi is designed to run in production. You model content types in a Strapi development project (on your workstation, with npm run develop), then deploy their code to this instance and rebuild the admin panel (Step 10). Entries, media, users, roles and settings are managed here in the admin panel as usual.

The admin API works with the same account. For example, from a machine that trusts your certificate (or with -k while you test), sign in and read your own profile:

curl -s -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","password":"<your password>"}' https://<your host>/admin/login
curl -s -H 'Authorization: Bearer <data.token from the answer>' https://<your host>/admin/users/me

Your sites and apps read content through the REST API with an API token (Settings, API Tokens) or with the Users & Permissions roles (Settings, Users & Permissions plugin, Roles).

Step 7: The closed-by-default settings

The super administrator registration route is closed, public end-user sign-up is off, anonymous clients cannot list users, Enterprise Edition and Strapi AI are off, and Strapi carries no telemetry identity:

H=$(sed -n 's/^set \$strapi_host "\(.*\)";$/\1/p' /etc/nginx/strapi-host.conf)
curl -sk --resolve "$H:443:127.0.0.1" -H 'Content-Type: application/json' -d '{"email":"someone@example.com","password":"Example12345678901234567","firstname":"Someone"}' "https://$H/admin/register-admin"; echo
curl -sk --resolve "$H:443:127.0.0.1" -H 'Content-Type: application/json' -d '{"username":"someone","email":"someone@example.com","password":"Example12345678901234567"}' "https://$H/api/auth/local/register"; echo
curl -sk -o /dev/null -w 'anonymous user list: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/api/users"
curl -sk --resolve "$H:443:127.0.0.1" "https://$H/admin/project-type"; echo
curl -sk --resolve "$H:443:127.0.0.1" "https://$H/admin/init"; echo
{"data":null,"error":{"status":400,"name":"ApplicationError","message":"You cannot register a new super admin","details":{}}}
{"data":null,"error":{"status":400,"name":"ApplicationError","message":"Register action is currently disabled","details":{}}}
anonymous user list: 403
{"data":{"isEE":false,"isTrial":false,"features":[],"flags":{"nps":false,"promoteEE":false,"docLinks":true},"ai":{"enabled":false}}}
{"data":{"uuid":false,"hasAdmin":true,"menuLogo":null,"authLogo":null}}

To let your app's users register themselves, turn Enable sign-ups on under Settings, Users & Permissions plugin, Advanced settings (choose the default role first):

The Users and Permissions advanced settings with Enable sign-ups off

Settings, Overview shows the Strapi and Node.js versions and the Community plan:

The Strapi settings overview showing Strapi v5.60.0 on the Community plan

Step 8: Your data and configuration

Strapi's data is the SQLite database /var/lib/strapi/data.db and the upload folder /var/lib/strapi/uploads; its configuration is /etc/strapi/strapi.env, and its secrets are /etc/strapi/secrets.json:

sudo grep -v '^#' /etc/strapi/strapi.env | sed -E 's#https://[^/]+#https://<your host>#'
sudo stat -c '%n %U:%G %a' /root/strapi-credentials.txt /etc/strapi /etc/strapi/strapi.env /etc/strapi/secrets.json /opt/strapi /var/lib/strapi /var/lib/strapi/data.db /var/lib/strapi/uploads
sudo python3 -c 'import json; print(sorted(json.load(open("/etc/strapi/secrets.json"))))'
DATABASE_FILENAME=/var/lib/strapi/data.db
STRAPI_SECRETS_FILE=/etc/strapi/secrets.json
STRAPI_PUBLIC_URL=https://<your host>
/root/strapi-credentials.txt root:root 600
/etc/strapi root:strapi 750
/etc/strapi/strapi.env root:strapi 640
/etc/strapi/secrets.json root:strapi 640
/opt/strapi root:root 755
/var/lib/strapi strapi:strapi 750
/var/lib/strapi/data.db strapi:strapi 640
/var/lib/strapi/uploads strapi:strapi 750
['ADMIN_JWT_SECRET', 'API_TOKEN_SALT', 'APP_KEYS', 'ENCRYPTION_KEY', 'JWT_SECRET', 'TRANSFER_TOKEN_SALT']

To back up, take an EBS snapshot of the volume, or copy the database consistently with SQLite's own backup command and copy the upload folder:

sudo -u strapi sqlite3 /var/lib/strapi/data.db ".backup '/var/lib/strapi/data-backup.db'"
sudo tar -C /var/lib/strapi -czf /root/strapi-uploads.tgz uploads

Keep /etc/strapi/secrets.json with your backups: ENCRYPTION_KEY decrypts the API tokens Strapi stores, and the JWT secrets sign every session. To keep the data on a separate EBS volume, stop strapi, mount the volume at /var/lib/strapi (owner strapi:strapi, mode 0750), copy the files across and start strapi again. To move to PostgreSQL or MySQL, use Strapi's data export and import (strapi export / strapi import) with a project configured for that database.

Step 9: Your own DNS name, certificate and e-mail

Port 80 sends browsers to HTTPS, the health endpoint answers through Strapi for load balancer checks, and an unknown host name gets no answer:

H=$(sed -n 's/^set \$strapi_host "\(.*\)";$/\1/p' /etc/nginx/strapi-host.conf)
curl -s -o /dev/null -w 'port 80: %{http_code} -> %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/admin" | sed -E 's#https://[^/]+#https://<your host>#'
curl -sk -o /dev/null -w 'admin panel: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/admin/auth/login"
curl -sk -o /dev/null -w 'health: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/healthz"
curl -sk -o /dev/null -w 'unknown host name: %{http_code}\n' -H 'Host: example.com' https://127.0.0.1/ || true
port 80: 301 -> https://<your host>/admin
admin panel: 200
health: 204
unknown host name: 000

(000 means nginx closed the connection without an answer.) To serve Strapi under your own DNS name (point an A record at the instance's Elastic IP first):

sudo strapi-set-hostname cms.example.com

This generates a new certificate for that name, makes nginx answer it, sets Strapi's public URL and CORS origin to https://cms.example.com and restarts Strapi. To use a certificate from your own certificate authority (for example one you obtain with certbot for your-domain), replace /etc/nginx/ssl/strapi.crt and /etc/nginx/ssl/strapi.key (root, 0600 for the key) and run sudo systemctl reload nginx.

E-mail: the image's e-mail provider (/opt/strapi/cloudimg/email-none.js) sends nothing, so password reset and confirmation mails are dropped and only a line saying a message was not sent is logged. To send mail, add an e-mail provider such as @strapi/provider-email-nodemailer with your SMTP server to the project, set it in config/plugins.js, and rebuild (Step 10).

Step 10: Passwords, content types and upgrades

Change the administrator's password (a new random password is written to the credentials file, and every session of that user ends; Strapi restarts for the change):

sudo strapi-set-password

For another admin user, sudo strapi-set-password --email <user e-mail> prints the new password; with --stdin it reads one (24 to 72 letters and digits, with upper and lower case and a digit) from standard input.

Adding content types, plugins or providers, or upgrading Strapi, is a rebuild of the project. The image ships the Node.js runtime but not npm; install the Node.js 24 toolchain with npm in a working copy, then:

cp -a /opt/strapi ~/strapi && cd ~/strapi
npm ci
# copy your content types into src/api/ and src/components/, edit config/, or raise the @strapi packages in package.json
python3 -c 'import base64,json,secrets; r=lambda: base64.b64encode(secrets.token_bytes(32)).decode(); print(json.dumps({"APP_KEYS":[r() for _ in range(4)], **{k: r() for k in ("API_TOKEN_SALT","ADMIN_JWT_SECRET","TRANSFER_TOKEN_SALT","JWT_SECRET","ENCRYPTION_KEY")}}))' > ~/build-secrets.json
NODE_ENV=production STRAPI_SECRETS_FILE=$HOME/build-secrets.json DATABASE_FILENAME=/tmp/build.db STRAPI_PUBLIC_URL=https://<your host> npx strapi build
rm -f ~/build-secrets.json /tmp/build.db
sudo systemctl stop strapi
sudo rsync -a --delete --exclude public/uploads ~/strapi/ /opt/strapi/ && sudo chown -R root:root /opt/strapi
sudo systemctl start strapi

The build needs a secrets file only to load the configuration, so it uses a throwaway one; the running service keeps reading /etc/strapi/secrets.json. Strapi updates the database schema for new or changed content types when it starts; back up the database first (Step 8) and test changes on a copy.

Step 11: Security checks

Strapi runs as its own user with a read-only system and only its data folder writable, and instance metadata is blocked for Strapi and nginx:

systemctl show strapi -p User -p ProtectSystem -p ReadWritePaths -p NoNewPrivileges -p IPAddressDeny
systemctl show nginx -p IPAddressDeny
IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32
User=strapi
ReadWritePaths=/var/lib/strapi
ProtectSystem=strict
NoNewPrivileges=yes
IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32

The certificate generated on first boot is a server certificate, not a certificate authority:

sudo openssl x509 -in /etc/nginx/ssl/strapi.crt -noout -ext basicConstraints,extendedKeyUsage | sed 's/^ *//'
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Extended Key Usage: 
TLS Web Server Authentication

The database holds exactly the administrator and no end users:

sudo -u strapi sqlite3 -readonly /var/lib/strapi/data.db 'SELECT email FROM admin_users;' 'SELECT count(*) FROM up_users;'
admin@example.com
0

Step 12: Server components

Component Where
Strapi project (code and admin build, root owned) /opt/strapi
Node.js 24.22.2 LTS /opt/node/bin/node
Strapi service strapi.service (strapi start on 127.0.0.1:1337, user strapi)
Web server nginx.service, site /etc/nginx/sites-available/cloudimg-strapi
Data /var/lib/strapi (SQLite data.db, uploads/)
Configuration and secrets /etc/strapi/strapi.env, /etc/strapi/secrets.json
Helpers strapi-set-hostname, strapi-set-password, strapi-cred-roundtrip
Package list, licence, Enterprise code note /usr/share/doc/strapi/PACKAGES.txt, LICENSE, EE-CODE.txt

Logs: sudo journalctl -u strapi (Strapi) and /var/log/nginx/.

Remote data transfer (strapi transfer --to https://<your host>/admin from another Strapi project, with a transfer token from Settings, Transfer Tokens) is served at /admin/transfer/runner/; it is the only WebSocket endpoint, and nginx refuses it from any browser on another site.

Step 13: Security recommendations

  • Allow port 22 in the security group from your own networks only.
  • Configure an e-mail provider (Step 9), then change the administrator e-mail to your own (Step 4); keep its password secret and rotate it if it may have been shared (Step 10).
  • Use your own DNS name and a certificate your browsers trust (Step 9).
  • Give admin users the narrowest role they need, and give each app its own API token with only the permissions it needs; leave public sign-up off unless your app needs it.
  • Keep Strapi on a current release (Step 10) and keep the operating system patched; unattended security upgrades are on, and sudo apt-get update && sudo apt-get upgrade applies everything else.
  • The admin panel's Marketplace page loads the plugin list from Strapi's servers in the browser of whoever opens it.

Support and licensing

Strapi is free software under the MIT licence for everything outside its ee/ directories (a copy is at /usr/share/doc/strapi/LICENSE); the ee/ directories, which ship dormant inside Strapi's npm packages, are under the Strapi Enterprise licence and are not enabled on this image (/usr/share/doc/strapi/EE-CODE.txt). Node.js, React and the other bundled packages carry their own open source licences (listed in /usr/share/doc/strapi/PACKAGES.txt). There is no per user or per CPU licence fee and no licence key. One installed dependency, zod 4.4.3, which Strapi 5.60.0 pins, has a published advisory about memory use when validating very large arrays; on this image it is reachable only by signed-in admin users, request bodies are capped at 1 MB, and the image is rebuilt when Strapi moves to the fixed release. The AMI price covers cloudimg's hardening, first boot setup and support.

  • Email: support@cloudimg.co.uk
  • Website: https://www.cloudimg.co.uk
  • Support: 24/7, with a 24 hour response SLA