Payload CMS 3.92.1
Overview
Payload is a free and open source headless content management system built on Next.js. You describe your content in TypeScript (collections, fields, access rules), and Payload gives you an admin panel at /admin, a REST API under /api, a GraphQL API at /api/graphql, a Lexical rich text editor, uploads with image resizing, versions and drafts.
This SecureAnyCloud AMI ships Payload CMS 3.92.1 on Ubuntu 24.04 as Payload's blank project, with two collections ready for you to extend: Users (who can sign in) and Media (uploads). It is one version of the SecureAnyCloud CMS, Blogs and Wikis listing on AWS Marketplace, selected as the software version when you subscribe and launch.
What you get:
- No setup page anyone could claim. A fresh Payload project shows a "create first user" page to whoever opens the admin panel first. On this image, your instance's first boot creates the first administrator itself, with a password generated on your instance, before the web server starts. That page is never open.
- Secrets of your instance's own, never on a command line. The Payload secret (which signs every sign-in session), the administrator password, the TLS key and the Next.js keys are generated on your instance's first boot, and nothing from the image build is reused. The Next.js keys are the Server Actions encryption key and the draft-mode keys, which a Next.js build would otherwise bake into the image and share between every instance. The password reaches Payload in a file only Payload can read, never as a command line argument or an environment variable.
- HTTPS from the first boot. nginx serves Payload on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS. Payload itself listens only on
127.0.0.1:3000. - Host from configuration, never from the request. Payload's server URL, used for every absolute link, is your instance's public DNS name by default, set by SecureAnyCloud's payload-set-hostname helper. nginx answers only that name and the instance's own addresses, and any other host name gets no answer. Payload honours the admin's session cookie only for requests from that URL.
- Closed by default. Payload's usage telemetry is off. No mail is sent, and no mail is written to the log, until you configure a mail adapter (Step 10). Payload's default would write password reset links into the system journal. The GraphQL playground and GraphQL schema introspection are off.
- Sign-in protection. nginx limits every sign-in, password reset and account route to 10 requests a minute per client address, and GraphQL requests to 60 a minute. Payload locks an account for 10 minutes after 5 wrong passwords. The session cookie is
Secure,HttpOnlyandSameSite=Lax. - Sandboxed upload files. Every file under
/api/<collection>/file/getsnosniffand a sandboxing content security policy. Only passive types (images, PDF, fonts, audio and video) open in the browser, and anything else is sent as a download. Payload itself refuses HTML uploads and SVG files that carry scripts. - A hardened service. Payload runs as its own unprivileged user. The application in
/opt/payloadbelongs to root and is read-only to Payload, which can write only its data folder,/var/lib/payload. Instance metadata (169.254.169.254) is blocked for Payload and nginx. - Current, checked dependencies. Payload 3.92.1's template pins Next.js 16.3.3 and sharp 0.35.4, which have published security fixes in Next.js 16.4.3 and sharp 0.35.5. This image ships the fixed releases. Only the Node.js runtime itself ships, not npm.
- Fail closed. nginx and Payload 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 (Payload) and TCP 80 (redirects to HTTPS) from the networks that should reach them. Keep port 22 limited to your own networks, and open port 443 to your editors and API clients.
Launch the instance
In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Pick Payload CMS 3.92.1 on Ubuntu 24.04 as the software version, select your Region, and choose Launch through EC2. Choose m5.large, your key pair and a security group as described above.
Alternatively, launch from 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=payload-cms}]'Connect to your instance
Version SSH user
Payload CMS 3.92.1 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>Check the services
First boot takes about a minute, because it creates the database and the administrator. Afterwards, nginx, Payload and the first-boot unit are all active:
systemctl is-active nginx payload payload-firstbootactive active activenginx serves ports 80 and 443, and Payload listens on the loopback address only. The only open ports are therefore 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 -V0.0.0.0:22 0.0.0.0:80 0.0.0.0:443 127.0.0.1:3000 127.0.0.53%lo:53 127.0.0.54:53 [::]:22 [::]:80 [::]:443Check the Node.js, Payload, Next.js, nginx and SQLite versions:
/opt/node/bin/node --version cd /opt/payload && for m in payload next react sharp @payloadcms/db-sqlite @payloadcms/richtext-lexical; do printf '%s %s\n' "$m" "$(/opt/node/bin/node -p "require('./node_modules/$m/package.json').version")"; done nginx -v 2>&1 sqlite3 --version | cut -d' ' -f1v24.22.2 payload 3.92.1 next 16.4.3 react 19.2.6 sharp 0.35.5 @payloadcms/db-sqlite 3.92.1 @payloadcms/richtext-lexical 3.92.1 nginx version: nginx/1.24.0 (Ubuntu) 3.45.1Get your sign-in details
The credentials file holds the address Payload is served on (your instance's public DNS name by default), the admin panel address, and the administrator's e-mail and password. All of these are generated on this instance. Print the file with:
sudo cat /root/payload-cms-credentials.txtPAYLOAD_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/ PAYLOAD_ADMIN_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/admin PAYLOAD_ADMIN_EMAIL=admin@example.com PAYLOAD_ADMIN_PASSWORD=<24 letters and digits>Payload signs in with an e-mail address. The administrator starts as admin@example.com. Once you have configured mail (Step 10), change it to your own address in the admin panel (Users, then the administrator), because password reset mails go to that address.
Sign in to the admin panel
Open PAYLOAD_ADMIN_URL in your browser. The certificate is self-signed and generated on your instance, so your browser warns you once. Compare the certificate's SHA-256 fingerprint with the one on the instance (sudo openssl x509 -in /etc/nginx/ssl/payload.crt -noout -fingerprint -sha256) and continue. Alternatively, install a certificate from your own certificate authority (Step 9).
Sign in with PAYLOAD_ADMIN_EMAIL and PAYLOAD_ADMIN_PASSWORD. The dashboard lists the project's collections, Users and Media.
Users holds the accounts that can sign in to the admin panel and the API. A fresh instance has exactly one, the administrator.
Uploads and content
Open Media and choose Create New to upload a file. The Alt text is required. Payload stores the file in /var/lib/payload/media and serves it from https://<your host>/api/media/file/<name>.
Uploads are limited to 32 MiB. Payload refuses HTML files and SVG files that carry scripts, and nginx sends every other non-passive type as a download.
Your content model lives in code. The project is in /opt/payload, with the configuration in src/payload.config.ts and the collections in src/collections/. To add collections and fields, edit those files and rebuild the project (Step 11).
The front end at https://<your host>/ is the blank template's welcome page. Replace src/app/(frontend)/ with your own site, or use Payload as a headless CMS from any front end through its APIs.
The REST API accepts the same accounts. For example, from a machine that trusts your certificate (or with -k while you test), sign in and read the current user:
curl -s -c jar.txt -H 'Content-Type: application/json' \
-d '{"email":"admin@example.com","password":"<your password>"}' https://<your host>/api/users/login
curl -s -b jar.txt -H 'Origin: https://<your host>' https://<your host>/api/users/meThe closed-by-default settings
The GraphQL playground and schema introspection are off, the first-user setup route is closed, and anonymous clients cannot list or create users. Verify each setting:
H=$(sed -n 's/^set \$payload_host "\(.*\)";$/\1/p' /etc/nginx/payload-host.conf)
curl -sk -o /dev/null -w 'GraphQL playground: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/api/graphql-playground"
curl -sk --resolve "$H:443:127.0.0.1" -H 'Content-Type: application/json' -d '{"query":"{__schema{types{name}}}"}' "https://$H/api/graphql" | cut -c1-95; echo
curl -sk -o /dev/null -w 'first-register: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Content-Type: application/json' -d '{"email":"someone@example.com","password":"Example12345678901234567"}' "https://$H/api/users/first-register"
curl -sk -o /dev/null -w 'anonymous user list: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/api/users"GraphQL playground: 404 {"errors":[{"message":"GraphQL introspection is not allowed, but the query contained __schema o first-register: 403 anonymous user list: 403These settings come from the project configuration, where you can change them:
grep -nE "telemetry: false|email: sacNoEmail|csrf:|push: false|disable(Introspection|Playground)InProduction" /opt/payload/src/payload.config.ts | sed -E 's/^([0-9]+): +/\1: /'32: csrf: serverURL ? [serverURL] : [], 51: telemetry: false, 54: email: sacNoEmail, 56: disableIntrospectionInProduction: true, 57: disablePlaygroundInProduction: true, 73: push: false,push: false means the database schema comes from migrations (src/migrations/) and never from an automatic schema push. This is Payload's recommended setup for production.
Your data and configuration
Payload's data is the SQLite database /var/lib/payload/payload.db and the upload folder /var/lib/payload/media. Its configuration is /etc/payload/payload.env, and its secret is /etc/payload/payload-secret. Review the configuration and the file permissions:
sudo grep -v '^#' /etc/payload/payload.env | sed -E 's#https://[^/]+#https://<your host>#'
sudo stat -c '%n %U:%G %a' /root/payload-cms-credentials.txt /etc/payload /etc/payload/payload.env /etc/payload/payload-secret /opt/payload /var/lib/payload /var/lib/payload/payload.db /var/lib/payload/mediaDATABASE_URL=file:/var/lib/payload/payload.db PAYLOAD_MEDIA_DIR=/var/lib/payload/media PAYLOAD_SECRET_FILE=/etc/payload/payload-secret PAYLOAD_SERVER_URL=https://<your host> /root/payload-cms-credentials.txt root:root 600 /etc/payload root:payload 750 /etc/payload/payload.env root:payload 640 /etc/payload/payload-secret root:payload 640 /opt/payload root:root 755 /var/lib/payload payload:payload 750 /var/lib/payload/payload.db payload:payload 640 /var/lib/payload/media payload:payload 750To back up, take an EBS snapshot of the volume. Alternatively, copy the database consistently with SQLite's own backup command, then copy the media folder:
sudo -u payload sqlite3 /var/lib/payload/payload.db ".backup '/var/lib/payload/payload-backup.db'"
sudo tar -C /var/lib/payload -czf /root/payload-media.tgz mediaKeep the Payload secret safe. It signs every session, and changing it signs everyone out.
To keep the data on a separate EBS volume, stop payload, mount the volume at /var/lib/payload (owner payload:payload, mode 0750), copy the files across and start payload again.
Security checks
Port 80 sends browsers to HTTPS, the health endpoint answers through Payload for load balancer checks, and an unknown host name gets no answer. Verify all three:
H=$(sed -n 's/^set \$payload_host "\(.*\)";$/\1/p' /etc/nginx/payload-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 sign-in page: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/admin/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/ || trueport 80: 301 -> https://<your host>/admin admin sign-in page: 200 health: 200 unknown host name: 000The status 000 means nginx closed the connection without an answer.
Payload runs as its own user with a read-only system and only its data folder writable. Instance metadata is blocked for Payload and nginx. Confirm the service settings:
systemctl show payload -p User -p ProtectSystem -p ReadWritePaths -p NoNewPrivileges -p IPAddressDeny
systemctl show nginx -p IPAddressDenyIPAddressDeny=168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128 User=payload ReadWritePaths=/var/lib/payload ProtectSystem=strict NoNewPrivileges=yes IPAddressDeny=168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128The certificate generated on first boot is a server certificate, not a certificate authority. Confirm it:
sudo openssl x509 -in /etc/nginx/ssl/payload.crt -noout -ext basicConstraints,extendedKeyUsage | sed 's/^ *//'X509v3 Basic Constraints: critical CA:FALSE X509v3 Extended Key Usage: TLS Web Server AuthenticationThe database holds exactly one user, the administrator. First boot created it after the shipped migration built the schema:
sudo -u payload sqlite3 -readonly /var/lib/payload/payload.db 'SELECT email FROM users;' 'SELECT count(*) FROM payload_migrations;'admin@example.com 1Your own DNS name, certificate and mail
To serve Payload under your own DNS name, first point an A record at the instance's Elastic IP. Then run:
sudo payload-set-hostname cms.example.comThe helper generates a new certificate for that name, makes nginx answer it, sets Payload's server URL to https://cms.example.com and restarts Payload.
To use a certificate from your own certificate authority (for example one you obtain with certbot for your-domain), replace /etc/nginx/ssl/payload.crt and /etc/nginx/ssl/payload.key (root, 0600 for the key) and run sudo systemctl reload nginx. Do not add the self-signed certificate to a trust store. Compare its fingerprint instead (Step 5).
Mail. The image's mail adapter (src/sac/no-email.ts) sends nothing, so password reset and verification mails are dropped, and only a line saying a message was dropped is logged. To send mail, add Payload's nodemailer adapter (@payloadcms/email-nodemailer) with your SMTP server. Replace email: sacNoEmail in src/payload.config.ts with it, then rebuild (Step 11).
Passwords, schema changes and upgrades
To change the administrator's password, run the command below. It writes a new random password to the credentials file and ends every session of that user:
sudo payload-set-passwordFor another user, sudo payload-set-password --email <user e-mail> prints the new password. With --stdin, it reads a password (24 to 72 letters and digits) from standard input. Either form also lifts a lockout after too many wrong passwords.
Changing the schema (collections, fields, access rules) or upgrading Payload means rebuilding the project, because Payload is a Next.js application. The image ships the Node.js runtime but not npm. To rebuild, install the Node.js 24 toolchain with npm in a working copy, then run:
cp -a /opt/payload ~/payload && cd ~/payload npm ci
# edit src/payload.config.ts or src/collections/*, or raise the payload packages in package.json npx payload migrate:create <name> npx next build
sudo systemctl stop payload
sudo rsync -a --delete ~/payload/ /opt/payload/ && sudo chown -R root:root /opt/payload
sudo payload-next-keys
sudo -u payload sh -c 'cd /opt/payload && /opt/node/bin/node node_modules/payload/bin.js migrate'
sudo systemctl start payloadsudo payload-next-keys writes your instance's Next.js keys (/etc/payload/next-keys.env) into the new build, because every next build mints new ones. sudo payload-next-keys --rotate makes new keys.
Run the migration command with the same environment that Payload uses (/etc/payload/payload.env). Test schema changes on a copy of your database first.
Server components
Component Where
Payload project (code, root owned) /opt/payload
Node.js 24.22.2 LTS /opt/node/bin/node
Payload service payload.service (next start -H 127.0.0.1 -p 3000, user payload)
Web server nginx.service, site /etc/nginx/sites-available/sac-payload
Data /var/lib/payload (SQLite payload.db, media/, next-cache/)
Configuration and secrets /etc/payload/payload.env, /etc/payload/payload-secret, /etc/payload/next-keys.env
Helpers payload-set-hostname, payload-set-password, payload-next-keys, payload-cred-roundtrip
Package list and licences /usr/share/doc/payload-cms/PACKAGES.txt
Logs: sudo journalctl -u payload (Payload, as JSON lines) and /var/log/nginx/.
Security recommendations
- Allow port 22 in the security group from your own networks only.
- Configure mail (Step 10), then change the administrator e-mail to your own (Step 4). Keep the password secret, and rotate it if it may have been shared (Step 11).
- Use your own DNS name and a certificate that your browsers trust (Step 10).
- Create editor accounts with only the access they need. Payload's access control is code in your collections.
- Keep Payload on a current release (Step 11) and keep the operating system patched. Unattended security upgrades are on, and
sudo apt-get update && sudo apt-get upgradeapplies everything else. - Payload's code editor loads the Monaco editor from cdn.jsdelivr.net in the browser of whoever edits a Code or JSON field. The shipped collections have no such fields.
Support and licensing
Payload is free software under the MIT licence (a copy is at /usr/share/doc/payload-cms/LICENSE.md). Node.js, Next.js, React and the other bundled packages carry their own open source licences, listed in /usr/share/doc/payload-cms/PACKAGES.txt. There is no per-user or per-CPU licence fee and no licence key.
This image ships Payload's blank project with the SecureAnyCloud configuration, first boot and helpers described above, and with Next.js and sharp raised to their fixed releases. One installed dependency, happy-dom 20.14.6, has published advisories that are fixed only in a later major version. It is used only by an optional part of the Lexical editor that Payload does not load, and it is never loaded on this image.
The AMI price covers SecureAnyCloud's hardening, first-boot setup and support.
- Email: sales@secureanycloud.com
- Website: https://secureanycloud.com
- Support: 24/7, with a 24 hour response SLA
Deploy on AWS
Launch Payload CMS 3.92.1 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com