Cockpit CMS 2.14.1 on AWS User Guide
Overview
This guide covers launching and using the Cockpit CMS 2.14.1 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg BookStack, CMS, Blogs and Wikis listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Cockpit CMS is an open source, headless (API first) content platform. You model your content in its admin interface as collections, singletons and trees made of typed fields, manage images and files in its asset manager, and read everything back through its REST and GraphQL Content API. Because it is headless it never dictates how your site or app is built: the same content can feed a website, a mobile app, a static site generator or several frontends at once. Cockpit keeps its content in a file based store (MongoLite, on SQLite), so there is no database server to run.
The cloudimg image adds:
- No web installer, no default login. Out of the box Cockpit ships an
/installpage that creates the first administrator for whoever opens it while the system has no user, and even suggests a password. This image does not ship the installer at all, and nginx refuses/installas well. Your instance's first boot sets Cockpit up before the web server starts: an administratoradminwith a password generated on your instance (stored only as a bcrypt hash) and a security key of its own. The password is recorded in/root/cockpit-credentials.txt. Nothing from the image build ships in the image. - A security key of your instance's own. Cockpit signs its sign-in and CSRF tokens and encrypts stored data with a
sec-key. Upstream's default key is the same published value on every installation that does not change it; on this image first boot writes a random 256-bit key into/var/www/cockpit/config/config.php. - The current release. Cockpit 2.14.1 is the newest stable release. It fixes the security issues reported against 2.14.0 and earlier, among them missing access checks and path traversal in the bucket file API, command injection through video file names, stored cross-site scripting in Set fields, and a login timing leak. The download is verified against a pinned checksum.
- HTTPS from the first boot. nginx serves Cockpit on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS.
- Fail closed. nginx and PHP refuse to start until first boot has finished.
- Only Cockpit's own front controller runs. Every request that is not a plain asset goes through Cockpit's router, so no file in the installation (its configuration, libraries, the content store, caches and logs) is ever served or run directly. Uploaded files and thumbnails are served as static files only, never run as PHP, with a sandboxing content security policy so an uploaded SVG or HTML file cannot run script in your admin session.
- A rate-limited login. Cockpit itself does not throttle sign in attempts. On this image nginx limits sign in, two-factor and password reset requests to 6 a minute from one address after a burst of 10, and the address comes from the connection, never from a header a client sends. Cockpit's own login log records the same real address.
- The Content API is closed until you open it. No API key exists on a new instance, so the REST and GraphQL API refuses every request until you create a key (Step 8). A small cloudimg addon also switches off Cockpit's optional
?rspcAPI response cache, which in 2.14.1 answers a cached request before it checks the API key (fixed upstream after the 2.14.1 release). - Hardened defaults. The Cockpit code is owned by root and read only to PHP; PHP can write only Cockpit's storage, cannot run shell commands or open URLs as files and is confined to Cockpit's folders. nginx and PHP are blocked from the instance metadata service, the AMI requires IMDSv2, requests for a host name the instance does not serve are dropped, and Cockpit's links and API addresses always use the host name you configure, never the one a request claims.
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: Cockpit CMS 2.14.1 (the upstream core release with its bundled dependencies, checksum verified), PHP 8.3 FPM, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. Cockpit's storage (the content store, uploads, thumbnails, cache and logs) is on a separate 20 GiB EBS volume mounted at /var/www/cockpit/storage.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large
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 (Cockpit and its API) and TCP 80 (redirects to HTTPS) from the networks that should reach them. Keep 22 to your own networks; open 443 to the editors and to the frontends that read the API.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Cockpit CMS 2.14.1 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (Cockpit is light; a smaller instance is fine for a small team), your key pair and a security group as above. Keep the second volume (Cockpit's storage) in the launch settings; you can make it larger there.
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=cockpit}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Cockpit CMS 2.14.1 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot takes a few seconds. Then nginx, PHP and the first boot unit are active:
systemctl is-active nginx php8.3-fpm cockpit-firstboot
active
active
active
nginx serves ports 80 and 443; PHP listens on a local socket only, and there is no database server, so the only open ports are 22, 80 and 443:
sudo ss -tlnp | awk 'NR==1 || /nginx|sshd|php/' | awk '{print $1, $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g'
State Local Peer
LISTEN 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
LISTEN 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN 0.0.0.0:22 users:(("sshd"),("systemd"))
LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx"))
LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN [::]:22 users:(("sshd"),("systemd"))
The Cockpit release, the PHP version and the storage volume:
grep "^const APP_VERSION" /var/www/cockpit/bootstrap.php
php -v | head -1
findmnt -no TARGET,FSTYPE,SIZE /var/www/cockpit/storage
const APP_VERSION = '2.14.1';
PHP 8.3.6 (cli) (built: Oct 19 2026 15:16:58) (NTS)
/var/www/cockpit/storage ext4 19.5G
Step 4: Get your sign-in details
The credentials file holds the address Cockpit is served as (your instance's public DNS name by default), the administrator user name and the password generated on this instance. Show it with:
sudo cat /root/cockpit-credentials.txt
COCKPIT_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
COCKPIT_ADMIN_USER=admin
COCKPIT_ADMIN_PASSWORD=<24 letters and digits>
Step 5: Sign in
Open COCKPIT_URL in your browser. The certificate is self signed (generated on your instance), so accept the browser warning once, or install your own certificate (Step 12). Sign in as admin with COCKPIT_ADMIN_PASSWORD:

The dashboard shows the latest content and assets. The icons on the left open Content, Assets, API & Security and search; the icon at the bottom left opens all Settings.

Open your own account from the avatar at the top right to change the administrator's e-mail address (it starts as admin@localhost.localdomain) and password, and to switch on two-factor authentication.
Step 6: Model your content
Under Content, create a model for each kind of content you publish:
- a collection for many entries of one shape (blog posts, products, team members),
- a singleton for one-off content (the site settings, the home page),
- a tree for nested entries (a navigation menu, documentation pages).
Give the model a name (it becomes the name in the API, for example posts) and add typed fields: text, rich text, numbers, dates, booleans, assets, links to other models, sets and repeaters. Save it, then add entries with Create item.


Step 7: Assets
Assets holds the images, videos and documents your content uses. Upload files there (or straight from an asset field), organise them in folders and edit their titles and alt texts; Cockpit makes thumbnails of images itself. Uploaded files live on the storage volume under /var/www/cockpit/storage/uploads.

Step 8: Read your content through the API
A new instance has no API key, so the Content API refuses every request:
H=$(sed -n 's/^set \$cockpit_host "\(.*\)";$/\1/p' /etc/nginx/cockpit-host.conf)
curl -sk -w '\n' --resolve "$H:443:127.0.0.1" "https://$H/api/content/items/posts"
{"error":"Authentication failed"}
To open it, go to API & Security (the API icon on the left), choose Add key and give the key a role. A key with the built-in admin role can do everything, so for a public website create a role (Settings > Users > Manage roles) that may only read the models it shows, and give the key that role. Access > Configure on the same page sets what requests without a key may read; nothing is allowed on a new instance. The page also shows your REST and GraphQL endpoints, which always use the host name you configured:

Send the key in the api-key header:
curl -sk -H 'api-key: <API_KEY>' 'https://<your-host>/api/content/items/posts'
curl -sk -H 'api-key: <API_KEY>' -H 'Content-Type: application/json' \
-d '{"query":"{ postsModel { title } }"}' 'https://<your-host>/api/gql'
Keep keys out of browser code unless the key's role only reads public content. Cockpit's optional ?rspc=1 response cache is switched off on this image by the root-owned addon /var/www/cockpit/addons/CloudimgGuard (see /usr/share/doc/cockpit/CLOUDIMG-GUARD.txt): in 2.14.1 a cached response is answered before the API key is checked. The fix is in Cockpit's development branch; remove the addon once a release with it is installed.
Step 9: Users and roles
Settings > Users adds editors and other administrators; Manage roles on the same page defines what each role may do (which models it may read, create, edit or publish, and whether it may manage assets). Give each person their own account with the narrowest role that works.

Email: Cockpit sends password reset and magic sign in links by e-mail. The image has no mail server, so configure a mailer (SMTP) in config/config.php (Cockpit's mailer setting) before you rely on them; the administrator's password can always be reset from the shell (Step 13).
Step 10: Your Cockpit storage
Cockpit keeps everything it writes on its own EBS volume at /var/www/cockpit/storage, readable only by the web server: data holds the content store (app.sqlite with your models and entries, system.sqlite with users, roles, API keys and the log, app.memory.sqlite with caches), uploads your assets, tmp the thumbnails, and cache and logs working files. The per-instance configuration is /var/www/cockpit/config/config.php, where everything else belongs to root:
sudo ls -l /var/www/cockpit/storage /var/www/cockpit/storage/data | sed -E 's/ +/ /g' | cut -d' ' -f1,3,4,9-
/var/www/cockpit/storage:
total
drwxr-x--- www-data www-data cache
drwxr-x--- www-data www-data data
drwxr-x--- www-data www-data logs
drwx------ root root lost+found
drwxr-x--- www-data www-data tmp
drwxr-x--- www-data www-data uploads
/var/www/cockpit/storage/data:
total
-rw-r--r-- www-data www-data app.memory.sqlite
-rw-r--r-- www-data www-data app.sqlite
-rw-r--r-- www-data www-data system.sqlite
Back Cockpit up with an EBS snapshot of the storage volume (and of the root volume for the configuration), or as files with the services stopped:
sudo systemctl stop nginx php8.3-fpm
sudo tar -czf cockpit-backup.tar.gz -C / var/www/cockpit/storage var/www/cockpit/config/config.php
sudo systemctl start php8.3-fpm nginx
To restore onto a new instance, let its first boot finish, stop nginx and PHP, replace the contents of /var/www/cockpit/storage with your copy (owned by www-data, leaving lost+found alone) and config/config.php with yours (owned by root:www-data, mode 0640: its sec-key must match the store), and start them again. The restored store brings its own users and passwords.
Step 11: Security checks
Port 80 sends browsers to HTTPS, the health endpoint answers for load balancer checks, and the installer, the configuration, the content store, the module cache and Cockpit's code are not reachable:
H=$(sed -n 's/^set \$cockpit_host "\(.*\)";$/\1/p' /etc/nginx/cockpit-host.conf)
curl -s -o /dev/null -w 'port 80: %{http_code} -> %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/auth/login"
curl -sk --resolve "$H:443:127.0.0.1" "https://$H/healthz"
for p in /install/ /config/config.php /storage/data/system.sqlite /storage/cache/modules.cache.php /lib/vendor/autoload.php /modules/App/bootstrap.php /bootstrap.php /cron.php; do
curl -sk -o /dev/null -w "$p: %{http_code}\n" --resolve "$H:443:127.0.0.1" "https://$H$p"
done
curl -s -o /dev/null -w 'unknown host name: %{http_code}\n' -H 'Host: example.com' http://127.0.0.1/ || true
port 80: 301 -> https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/auth/login
ok
/install/: 404
/config/config.php: 404
/storage/data/system.sqlite: 404
/storage/cache/modules.cache.php: 404
/lib/vendor/autoload.php: 404
/modules/App/bootstrap.php: 404
/bootstrap.php: 404
/cron.php: 404
unknown host name: 000
(000 means nginx closed the connection without an answer.) Cockpit's own view: one account, admin, with no personal API key, no API keys at all, the configured HTTPS address as its site URL, debug mode off:
sudo -u www-data php /usr/local/lib/cloudimg/cockpit/cockpit-admin.php summary
users=admin:admin:nokey
apikeys=0
site_url=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com
debug=off
version=2.14.1
The credentials file is readable by root only, the configuration by root and the web server's group, the storage by the web server only, and the code belongs to root:
sudo stat -c '%a %U:%G %n' /root/cockpit-credentials.txt /var/www/cockpit/config/config.php /var/www/cockpit/storage /var/www/cockpit/index.php
600 root:root /root/cockpit-credentials.txt
640 root:www-data /var/www/cockpit/config/config.php
750 www-data:www-data /var/www/cockpit/storage
644 root:root /var/www/cockpit/index.php
The certificate was generated on this instance and names its own addresses:
H=$(sed -n 's/^set \$cockpit_host "\(.*\)";$/\1/p' /etc/nginx/cockpit-host.conf)
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN = ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name:
DNS:ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost
nginx and PHP cannot reach the instance metadata service; PHP cannot run shell commands or open URLs as files and is confined to Cockpit's folders; nginx hands PHP the configured host name and the real client address from the connection, never values a client sends, and rate-limits the sign in requests:
systemctl show nginx -p IPAddressDeny
systemctl show php8.3-fpm -p IPAddressDeny
php-fpm8.3 -i 2>/dev/null | grep -E '^(open_basedir|disable_functions|expose_php|allow_url_fopen) ' | cut -d' ' -f1-3
grep -E 'fastcgi_param (HTTP_HOST|SERVER_NAME|HTTP_X_FORWARDED_FOR|HTTP_CLIENT_IP) ' /etc/nginx/snippets/cloudimg-cockpit-php.conf
grep -h 'limit_req' /etc/nginx/conf.d/cloudimg-cockpit-login-guard.conf /etc/nginx/sites-available/cloudimg-cockpit
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
allow_url_fopen => Off
disable_functions => exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec
expose_php => Off
open_basedir => /var/www/cockpit/:/var/lib/php/sessions/:/tmp/
fastcgi_param HTTP_HOST $cockpit_host;
fastcgi_param SERVER_NAME $cockpit_host;
fastcgi_param HTTP_X_FORWARDED_FOR $remote_addr;
fastcgi_param HTTP_CLIENT_IP $remote_addr;
limit_req_zone $cockpit_auth_key zone=cockpit_auth:10m rate=6r/m;
limit_req_status 429;
limit_req zone=cockpit_auth burst=10 nodelay;
If a burst of wrong passwords rate-limits you (HTTP 429), wait a minute and try again: the limit only slows down repeated attempts from one address.
Step 12: Your own DNS name and certificate
Point a DNS record at the instance, then set the name Cockpit is served as. This mints a new self signed certificate for the name, updates nginx, Cockpit's site_url (every absolute link and API address Cockpit shows, in config/config.php) and the URL in /root/cockpit-credentials.txt, and keeps the name across reboots:
sudo cockpit-set-hostname <your-domain>
To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running cockpit-set-hostname again replaces it with a self signed one):
sudo install -m 0644 <your-domain>.crt /etc/nginx/ssl/cockpit.crt
sudo install -m 0600 <your-domain>.key /etc/nginx/ssl/cockpit.key
sudo nginx -t && sudo systemctl reload nginx
Once browsers trust the certificate you can also add an HSTS header (add_header Strict-Transport-Security "max-age=31536000" always;) to /etc/nginx/snippets/cloudimg-cockpit-headers.conf. The image does not send one: browsers ignore HSTS from a site whose certificate they do not trust, so it only takes effect with a trusted certificate.
Step 13: Passwords, extensions and upgrades
cockpit-set-password gives admin a new random password and records it in /root/cockpit-credentials.txt. Run it without arguments, or pass your own password (24 to 96 letters and digits) on stdin. Other users change their own password under Account, and administrators can set any user's password in Settings > Users.
sudo cockpit-set-password
printf '%s\n' '<your-new-password>' | sudo cockpit-set-password --stdin
Things that need a writable code tree. Cockpit's built-in Updater and Spaces (extra Cockpit instances inside one installation) write into the Cockpit code folder. On this image that folder belongs to root, so both stop with an error instead of changing the code from the browser. The Finder file manager works, except its "unzip" action, which needs PHP's zip extension; add it if you use that action (it comes from Ubuntu's universe repository):
sudo apt-get install php8.3-zip && sudo systemctl restart php8.3-fpm
Upgrades. Snapshot the instance, download the new core release (cockpit-core.zip) from Cockpit's GitHub releases, compare its SHA-256 checksum with the one shown on the release page, read the release notes, and replace the code as root, keeping config and storage:
sudo apt-get install unzip
sudo systemctl stop nginx php8.3-fpm
unzip -q cockpit-core.zip -d /tmp/cockpit-new
cd /var/www/cockpit && sudo rm -rf index.php bootstrap.php cron.php favicon.png robots.txt lib modules
sudo cp -a /tmp/cockpit-new/cockpit-core/{index.php,bootstrap.php,cron.php,favicon.png,robots.txt,lib,modules} /var/www/cockpit/
sudo chown -R root:root /var/www/cockpit/{index.php,bootstrap.php,cron.php,favicon.png,robots.txt,lib,modules}
sudo rm -f /var/www/cockpit/storage/cache/modules.cache.php
sudo systemctl start php8.3-fpm nginx
Do not copy the release's install folder into the web root. Your content in /var/www/cockpit/storage is untouched.
Step 14: Server components
| Component | Path |
|---|---|
| Cockpit code (root owned) | /var/www/cockpit |
| Storage: content store, uploads, thumbnails, cache, logs | /var/www/cockpit/storage (its own EBS volume) |
| Per-instance configuration (sec-key, site URL) | /var/www/cockpit/config/config.php |
cloudimg addon (switches off the ?rspc cache) |
/var/www/cockpit/addons/CloudimgGuard |
| nginx site and login rate limit | /etc/nginx/sites-available/cloudimg-cockpit, /etc/nginx/conf.d/cloudimg-cockpit-login-guard.conf |
| PHP settings | /etc/php/8.3/fpm/conf.d/99-cloudimg-cockpit.ini |
| Certificate and key | /etc/nginx/ssl/ |
| First boot script | /usr/local/sbin/cockpit-firstboot.sh |
| Helpers | /usr/local/sbin/cockpit-set-hostname, cockpit-set-password |
| Licence, changelog and release notes | /usr/share/doc/cockpit/ |
| Credentials | /root/cockpit-credentials.txt (mode 0600) |
Step 15: Security recommendations
- Allow port 22 in the security group from your own networks only, and 443 from the networks that edit content or read the API.
- Keep the administrator password secret, and rotate it if it may have been shared (Step 13). Give each person their own account with the narrowest role, and switch on two-factor authentication for administrators.
- Give API keys the narrowest role that works, and never put an admin-role key in a browser or mobile app.
- Use your own DNS name and a certificate your browsers trust (Step 12).
- Keep
config/config.phpsecret: itssec-keysigns sign-in tokens. - Keep the operating system patched; unattended security upgrades are on, and
sudo apt-get update && sudo apt-get upgradeapplies everything else. PHP-FPM comes from Ubuntu's universe repository;sudo pro security-status --esm-appsshows whether Ubuntu Pro (ESM Apps) has security updates for it (none were pending when the image was built). - Snapshot the storage volume regularly (Step 10).
Support and licensing
Cockpit CMS is open source under the MIT License (a copy is at /usr/share/doc/cockpit/LICENSE); there is no per user or per CPU licence fee. This image installs the unmodified upstream core release, without its web installer and without one bundled library's README (it carries a sample private key), plus the small cloudimg addon described in Step 8. 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