ExpressionEngine 7.5.28
Overview
ExpressionEngine is a free, open source content management platform written in PHP. Instead of a fixed page or blog model, it gives you channels and custom fields so you model content the way your site needs, plus a template language to present it. Members, roles and permissions, categories, a file manager and a full control panel at /admin.php come with it.
This guide covers the ExpressionEngine 7.5.28 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud BookStack, CMS, Blogs and Wikis listing on AWS Marketplace. You select it as the software version when you subscribe and launch.
What the SecureAnyCloud image adds:
- No reachable installer, no shared database, no shared keys. ExpressionEngine's browser installer creates the site, its encryption keys and the first Super Admin for whoever opens it first. This image ships no database at all (the MariaDB data directory is empty), only the empty configuration file ExpressionEngine itself ships, and no installer in the web root. Your instance's first boot initializes its own database and runs ExpressionEngine's own installer before the web server starts, over a temporary server on a random local port that answers only requests carrying a token generated for that boot. The installer creates the schema, the default site theme, the Super Admin
adminwith a password generated on your instance, and the two keys that sign every session and cookie (encryption_keyandsession_crypt_key). First boot then stops that server and removes the installer again. - Secrets unique to your instance, never on a command line. The database password, the
adminpassword, the encryption keys and the TLS key are all generated on your instance, and nothing from the image build is reused. The installer's form, which carries the passwords, is handed over in files only root can read, never as command-line arguments, and its reply is never logged. - The current, verified release. ExpressionEngine 7.5.28 is the newest stable release and includes the security fixes of 7.5.27 and 7.5.28. The download is checked against a pinned checksum and against the release's own signature, using ExpressionEngine's update-signing key.
- No calls home. A stock ExpressionEngine checks expressionengine.com for new versions while you use the control panel, loads a news feed on the dashboard and can share analytics. The control panel's scripts also post your site's address to ExpressionEngine's license service from your browser on every page. On this image, the version check, the news feed and analytics are off, and the control panel's pages may talk only to your own server (Step 7). The server makes none of those calls while you use the control panel.
- No third-party scripts on your site. ExpressionEngine's default site theme loads an old jQuery (1.11.2) from Google's CDN in every visitor's browser. On this image it loads the jQuery 3.6.0 (with jQuery Migrate) that ExpressionEngine itself ships, from your own server. The unused old jQuery 2.2.4 copy is removed.
- HTTPS from the first boot. nginx serves ExpressionEngine on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS.
- Fail closed. nginx does not start until first boot has finished. If first boot cannot complete, the site stays offline rather than open.
- Only ExpressionEngine's entry points run PHP. The site goes through
index.phpand the control panel throughadmin.php. Every other PHP file is never run, and the wholesystemfolder (configuration, framework, templates and caches) is never served. The folders that PHP can write to and the web serves,imagesandthemes/user(which holds the default theme and its upload folders), are static only. Every file getsnosniffand a sandboxing content security policy, and only passive types (images, PDF, fonts, audio and video) open in the browser. Anything else, SVG and HTML included, is sent as a download. PHP in templates is off. - A rate-limited sign-in. nginx gives every request that sends data to ExpressionEngine (the control panel sign-in, the forgotten password form, the front-end member sign-in and every other form) one shared budget of 30 a minute from one address, after a burst of 30. ExpressionEngine locks out an address after four wrong passwords for a user in a minute. nginx uses the address of the connection, never a header a client sends.
- The real host and client address. nginx hands ExpressionEngine the host name you configure and nothing a request claims. The configuration pins the site and control panel addresses to that host name, so every link, including password reset emails, uses it. nginx removes
X-Forwarded-For,Client-IPand the other forwarding headers before PHP sees them, so ExpressionEngine records the address of the connection. - Hardened defaults. The ExpressionEngine code belongs to
rootand is read-only to PHP, the configuration file is read-only to PHP, and only ExpressionEngine's runtime folders are writable (and kept private to the web server, not world-readable). PHP cannot run shell commands and is confined to the site folder. MariaDB has no network listener at all (ExpressionEngine talks to it over a local socket), and its root account signs in only through the socket as the operating system's root. nginx, PHP, MariaDB and the first-boot service are blocked from the instance metadata service, the AMI requires IMDSv2, and requests for a host name the instance does not serve are dropped. - Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
- 24/7 SecureAnyCloud support.
What is on the image: ExpressionEngine 7.5.28 (the upstream release, checksum and signature verified), MariaDB 10.11, PHP 8.3 FPM, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.
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 (ExpressionEngine) and TCP 80 (redirects to HTTPS) from the networks that should reach them. Keep port 22 open to your own networks only. Open 443 to your visitors for a public site, or only to your team while you build it.
Launch the instance
In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Pick ExpressionEngine 7.5.28 on Ubuntu 24.04 as the software version, choose your Region, then choose Launch through EC2.
Choose m5.large, your key pair and a security group as described above.
To launch with the AWS CLI instead, 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=expressionengine}]'Connect to your instance
Version SSH user
ExpressionEngine 7.5.28 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>Check the services
First boot takes under a minute, because it creates the database and runs ExpressionEngine's installer. After that, nginx, PHP, MariaDB and the first-boot unit are all active:
systemctl is-active nginx php8.3-fpm mariadb expressionengine-firstbootactive active active activenginx serves ports 80 and 443. PHP and MariaDB listen on local sockets only, and the installer's temporary server is gone, so the only open ports are 22, 80 and 443:
sudo ss -tlnp | awk 'NR==1 || /nginx|sshd|php|maria/' | awk '{print $1, $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g'State Local Peer LISTEN 0.0.0.0:22 users:(("sshd"),("systemd")) LISTEN 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx")) LISTEN 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx")) LISTEN [::]:22 users:(("sshd"),("systemd")) LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx")) LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx"))Check the ExpressionEngine release and the PHP and MariaDB versions:
grep "define('APP_VER'" /var/www/expressionengine/system/ee/legacy/libraries/Core.php | sed 's/^ *//' php -v | head -1 mariadb --versiondefine('APP_VER', '7.5.28'); PHP 8.3.6 (cli) (built: Oct 19 2026 15:16:58) (NTS) mariadb Ver 15.1 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64) using EditLine wrapperTo see how the download was verified, run:
cat /usr/share/doc/expressionengine/RELEASEExpressionEngine 7.5.28 source https://github.com/ExpressionEngine/ExpressionEngine/releases/download/7.5.28/ExpressionEngine7.5.28.zip commit cc658dd16f555ac76c064324c20a99bc93a7ad7b sha256 0d830ba2cc908ef67d89dff8cf70e677859de29c34a32b21dab810de0bccc57c signature verified with ExpressionEngineUpdate.pub sha256 dbebeba87e57e3bab14913ad0aa39ccee5138f040911555f1f5d49f7b4bf9335Get your sign-in details
The credentials file holds the address ExpressionEngine is served as (your instance's public DNS name by default), the control panel address, the Super Admin login and password, and the database credentials. All of them are generated on this instance. Show the file with:
sudo cat /root/expressionengine-credentials.txtEE_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/ EE_ADMIN_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/admin.php EE_ADMIN_USER=admin EE_ADMIN_PASSWORD=<24 letters and digits> EE_DB_NAME=expressionengine EE_DB_USER=expressionengine EE_DB_PASSWORD=<48 hexadecimal characters>Visit the site and sign in
Open EE_URL in your browser. The certificate is self-signed and generated on your instance, so accept the browser warning once, or install your own certificate (Step 11). A fresh site shows ExpressionEngine's default theme, a small sample site with a blog.
Open EE_ADMIN_URL and sign in with the user name admin and EE_ADMIN_PASSWORD.
The control panel opens on its Overview dashboard, with the menu on the left.
admin is a Super Admin. Create a separate account for each editor under Members, and give each one a role (Roles, under Members in the menu) with only the permissions it needs.
The control panel shows Super Admins a warning that the configuration file "does not have the proper permissions" and suggests mode 666. The image sets this permission deliberately, so do not make config.php world-writable. Step 12 explains why, and how to save the few settings that live in that file.
Channels, entries and templates
Content lives in channels, and each channel has its own fields. The default theme comes with the channels About, Blog and Contact.
Open Entries to list, edit and publish entries, or open Entries > Blog > + to write a new blog post.
Use Channels and Fields (under Developer in the menu) to model your own content. Use Templates to write the templates that present it on the site. The default theme's templates are in the home, blog, about and contact groups. Files and images you upload go into upload folders you manage under Files.
The default theme is sample content. Its 404 page shows an animated image hosted on giphy.com, and its blog entry template can embed YouTube, SoundCloud and Bandcamp players when an entry has them. Edit or remove those templates under Templates when you build your own site.
The closed-by-default settings
SecureAnyCloud sets the following ExpressionEngine configuration overrides in system/user/config/config.php:
Setting Image default What it does
new_version_check n ExpressionEngine would otherwise ask expressionengine.com for the newest version while you use the control panel. Settings > General Settings > Check now still checks on demand.
show_ee_news n The dashboard would otherwise load ExpressionEngine's news feed from expressionengine.com.
share_analytics n No usage analytics are sent to ExpressionEngine.
allow_php n Templates cannot run PHP, whatever a template's own setting says.
cookie_secure y Session and CSRF cookies are only sent over HTTPS.
proxy_ips empty ExpressionEngine trusts no forwarded client address (Step 10).
password_lockout, password_lockout_interval y, 1 Four wrong passwords for a user from one address lock that address out for a minute.
base_url, site_url, cp_url https://<your host>/ Every link, including password reset emails, uses your host name (Step 11 changes it).
sudo grep -E "^\\\$config\['(new_version_check|show_ee_news|share_analytics|allow_php|cookie_secure|proxy_ips|password_lockout|password_lockout_interval)'\]" /var/www/expressionengine/system/user/config/config.php$config['new_version_check'] = 'n'; $config['show_ee_news'] = 'n'; $config['share_analytics'] = 'n'; $config['allow_php'] = 'n'; $config['cookie_secure'] = 'y'; $config['proxy_ips'] = ''; $config['password_lockout'] = 'y'; $config['password_lockout_interval'] = '1';The control panel also sends a content security policy that lets its pages connect only to your own server. Without it, the control panel's scripts post your site's name and address, the ExpressionEngine version and your add-on list from your browser to updates.expressionengine.com on every control panel page you open. This is ExpressionEngine's license and add-on check:
H=$(sed -n 's/^set \$ee_host "\(.*\)";$/\1/p' /etc/nginx/expressionengine-host.conf)
curl -sk -D - -o /dev/null --resolve "$H:443:127.0.0.1" "https://$H/admin.php?/cp/login" | tr -d '\r' | grep -i '^content-security-policy'content-security-policy: connect-src 'self'; frame-ancestors 'self'If you add commercial add-ons that check their licenses from the control panel, or you use ExpressionEngine Pro, remove that add_header line from /etc/nginx/sites-available/sac-expressionengine and reload nginx.
If you switch the version check or the news feed back on, the server needs outbound HTTPS to expressionengine.com.
Your data
Your site lives in three places: the expressionengine database in MariaDB, your templates in /var/www/expressionengine/system/user/templates, and the uploads under /var/www/expressionengine/images and /var/www/expressionengine/themes/user. ExpressionEngine rebuilds its caches in system/user/cache automatically.
Keep system/user/config/config.php with your backup. It holds this site's encryption keys and database password.
Back up the database to a file only root can read. MariaDB's root signs in over the local socket without a password, so no password appears on the command line:
sudo sh -c 'umask 077; mysqldump --single-transaction expressionengine > /root/expressionengine-$(date +%F).sql'
sudo tar -C /var/www/expressionengine -czf /root/expressionengine-files-$(date +%F).tgz system/user/config system/user/templates images themes/userExpressionEngine's own database backup (Tools > Utilities > Back Up Database) also works. For a whole-instance copy, snapshot the EBS volume.
Security checks
These checks confirm that port 80 sends browsers to HTTPS and that the health endpoint answers for load balancer checks. They also confirm that the installer, the configuration, the command-line tool, the templates, the bundled libraries and stray PHP files in the upload folders are not reachable:
H=$(sed -n 's/^set \$ee_host "\(.*\)";$/\1/p' /etc/nginx/expressionengine-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.php"
curl -sk --resolve "$H:443:127.0.0.1" "https://$H/healthz" for p in /system/ee/installer/ /system/user/config/config.php /system/ee/eecli.php /system/user/templates/ /system/ee/vendor-build/composer/installed.json /themes/user/x.php /images/uploads/x.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/ || trueport 80: 301 -> https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/admin.php ok /system/ee/installer/: 404 /system/user/config/config.php: 404 /system/ee/eecli.php: 404 /system/user/templates/: 404 /system/ee/vendor-build/composer/installed.json: 404 /themes/user/x.php: 404 /images/uploads/x.php: 404 unknown host name: 000A status of 000 means nginx closed the connection without an answer.
The credentials file is readable by root only, and the configuration file by root and the web server. ExpressionEngine's runtime and upload folders belong to the web server, and the code belongs to root:
sudo stat -c '%a %U:%G %n' /root/expressionengine-credentials.txt /var/www/expressionengine/system/user/config/config.php /var/www/expressionengine/system/user/cache /var/www/expressionengine/system/user/templates /var/www/expressionengine/themes/user /var/www/expressionengine/images/uploads /var/www/expressionengine/admin.php600 root:root /root/expressionengine-credentials.txt 640 root:www-data /var/www/expressionengine/system/user/config/config.php 750 www-data:www-data /var/www/expressionengine/system/user/cache 750 www-data:www-data /var/www/expressionengine/system/user/templates 750 www-data:www-data /var/www/expressionengine/themes/user 750 www-data:www-data /var/www/expressionengine/images/uploads 644 root:root /var/www/expressionengine/admin.phpAn uploaded HTML file is sent as a download with nosniff and the sandboxing policy. It is never rendered as a page of your site:
H=$(sed -n 's/^set \$ee_host "\(.*\)";$/\1/p' /etc/nginx/expressionengine-host.conf)
echo '<b>demo</b>' | sudo -u www-data tee /var/www/expressionengine/images/uploads/guide-demo.html >/dev/null curl -sk -D - -o /dev/null --resolve "$H:443:127.0.0.1" "https://$H/images/uploads/guide-demo.html" | tr -d '\r' | grep -iE '^(HTTP|content-disposition|content-security-policy|x-content-type)'
sudo rm -f /var/www/expressionengine/images/uploads/guide-demo.htmlHTTP/2 200 x-content-type-options: nosniff content-security-policy: default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; sandbox content-disposition: attachmentThe certificate is generated on this instance and names its own addresses:
H=$(sed -n 's/^set \$ee_host "\(.*\)";$/\1/p' /etc/nginx/expressionengine-host.conf)
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltNamesubject=CN = ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, O = sac 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:localhostThese checks confirm the following:
- nginx, PHP, MariaDB and the first-boot service (and the installer's temporary server, which ran inside it) cannot reach the instance metadata service.
- PHP cannot run shell commands and is confined to the site folder.
- nginx hands PHP the configured host name, removes forwarding headers and any
Authorizationheader a client sends, and rate-limits requests.
for u in nginx php8.3-fpm mariadb expressionengine-firstboot; do systemctl show $u -p IPAddressDeny; done php-fpm8.3 -i 2>/dev/null | grep -E '^(open_basedir|disable_functions|expose_php) ' | cut -d' ' -f1-3 grep -E 'fastcgi_param (HTTP_HOST|SERVER_NAME|HTTP_X_FORWARDED_FOR|HTTP_CLIENT_IP|HTTP_X_FORWARDED_HOST|HTTP_AUTHORIZATION) ' /etc/nginx/snippets/sac-expressionengine-fastcgi.conf grep -h 'limit_req_zone\|limit_req_status' /etc/nginx/conf.d/sac-expressionengine-guard.confIPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32 IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32 IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32 IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32 disable_functions => exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec expose_php => Off open_basedir => /var/www/expressionengine/:/var/lib/php/sessions/:/tmp/ fastcgi_param HTTP_HOST $ee_host; fastcgi_param SERVER_NAME $ee_host; fastcgi_param HTTP_X_FORWARDED_HOST ""; fastcgi_param HTTP_X_FORWARDED_FOR ""; fastcgi_param HTTP_CLIENT_IP ""; fastcgi_param HTTP_AUTHORIZATION ""; limit_req_zone $ee_post_key zone=ee_post:10m rate=30r/m; limit_req_zone $binary_remote_addr zone=ee_php:10m rate=300r/m; limit_req_status 429;The ee_post limit counts every request that sends data (any method other than GET and HEAD) to index.php or admin.php from one address. That covers signing in, the forgotten password form, front-end member forms and saving your work in the control panel. The limit is 30 a minute after a burst of 30. The ee_php limit allows 300 PHP requests a minute.
If your editors need more, raise the limits in /etc/nginx/conf.d/sac-expressionengine-guard.conf and /etc/nginx/snippets/sac-expressionengine-fastcgi.conf.
Because Authorization headers are removed, ExpressionEngine's template HTTP authentication is off. Restrict templates to member roles instead.
If four wrong passwords lock you out ("You are only permitted to make four login attempts every 1 minute(s)"), wait a minute or run sudo expressionengine-set-password (Step 12), which clears the lockout.
sudo expressionengine-cred-roundtrip checks the whole sign-in path on the instance. It verifies the redirect to HTTPS, that no installer is offered and the configuration is refused, a sign-in through the control panel's login form with the password from the credentials file, and that a wrong password and a common password are refused:
sudo expressionengine-cred-roundtripOK https 301 + no installer + config.php 404 + admin login + wrong and default passwords refusedLinks opened from another site. A control panel link that arrives from another site (an email in webmail, a page elsewhere) can reach only /admin.php, the sign-in and forgotten password pages, the password reset link and the dashboard. Anything else in the control panel (sign out, sidebar and banner switches, the update check, every other page) needs the request to come from the control panel itself, so another site cannot sign you out or trigger an action through a link. The same goes for the front-end editing toggle of ExpressionEngine Pro's dock.
nginx decides this from the browser's Sec-Fetch-Site, Origin and Referer headers, and a request with none of them is refused too. A refused link shows a page with a link back into the control panel. Scripts that call the control panel must send Sec-Fetch-Site: same-origin:
H=$(sed -n 's/^set \$ee_host "\(.*\)";$/\1/p' /etc/nginx/expressionengine-host.conf)
curl -sk -o /dev/null -w 'sign out link from another site: %{http_code}\n' -H 'Sec-Fetch-Site: cross-site' --resolve "$H:443:127.0.0.1" "https://$H/admin.php?/cp/login/logout"
curl -sk -o /dev/null -w 'sign in page from another site: %{http_code}\n' -H 'Sec-Fetch-Site: cross-site' --resolve "$H:443:127.0.0.1" "https://$H/admin.php?/cp/login"sign out link from another site: 403 sign in page from another site: 200The rules are in /etc/nginx/conf.d/sac-expressionengine-xsite.conf. expressionengine-set-hostname writes that file for your host name and the add-on actions installed at the time. Run it again after you install an add-on whose links must work from other sites.
Behind a proxy or load balancer
nginx removes X-Forwarded-For, X-Real-IP, Client-IP and Forwarded from every request, and proxy_ips is empty. ExpressionEngine's logs, sessions and lockouts therefore use the address of the connection.
Behind an AWS load balancer, that is the load balancer's address, and nginx's own access log (/var/log/nginx/access.log) records it the same way. To record visitors' addresses, use nginx's real_ip module with set_real_ip_from set to your load balancer's subnet only.
Your own DNS name and certificate
Point a DNS record at the instance, then set the name ExpressionEngine is served as. This command creates a new self-signed certificate for the name and updates nginx, which hands the name to ExpressionEngine. It also updates the site, control panel and theme addresses in config.php, clears ExpressionEngine's caches and updates the addresses in /root/expressionengine-credentials.txt. The name persists across reboots.
sudo expressionengine-set-hostname <your-domain>To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx. Running expressionengine-set-hostname again replaces it with a self-signed certificate.
sudo install -m 0644 <your-domain>.crt /etc/nginx/ssl/expressionengine.crt
sudo install -m 0600 <your-domain>.key /etc/nginx/ssl/expressionengine.key
sudo nginx -t && sudo systemctl reload nginxOnce browsers trust the certificate, you can also add an HSTS header (add_header Strict-Transport-Security "max-age=31536000" always;) to /etc/nginx/snippets/sac-expressionengine-headers.conf. The image does not send one, because browsers ignore HSTS from a site whose certificate they do not trust.
Passwords, settings, email, add-ons and upgrades
expressionengine-set-password gives admin a new random password using ExpressionEngine's own password hashing. It also clears a sign-in lockout, signs out every session of the account and records the password in /root/expressionengine-credentials.txt.
Run it without arguments, or pass your own password (24 to 96 letters and digits) on stdin. Members change their own password in the control panel under My Profile (their account menu).
sudo expressionengine-set-password printf '%s\n' '<your-new-password>' | sudo expressionengine-set-password --stdinThe configuration file is read-only to PHP. config.php is executed on every request, so the image does not let the web server rewrite it. Otherwise, a compromised PHP process could change the site permanently.
ExpressionEngine keeps most settings in its database and saves them normally. A few live in config.php, and saving one of those reports that the configuration file is not writable. For that change only, make the file writable, save the setting, then put it back:
sudo chmod 0660 /var/www/expressionengine/system/user/config/config.php
sudo chmod 0640 /var/www/expressionengine/system/user/config/config.phpEmail. By default, ExpressionEngine sends password resets and notifications with PHP's mail function. The image has no mail server, so set an SMTP server under Settings > Outgoing Email before you rely on email.
Add-ons. The code folders belong to root, so install add-ons from the shell. Copy the add-on into /var/www/expressionengine/system/user/addons/<name> and any theme files into themes/user (owned by www-data). Then install it under Add-Ons.
Upgrades. ExpressionEngine releases are published at https://github.com/ExpressionEngine/ExpressionEngine/releases with a signature.txt. Take a snapshot first, then follow ExpressionEngine's documented manual update:
- Lay the new release's files over the site, keeping
system/user,imagesandthemes/user. - Let its
system/ee/installerrun the database update through the control panel. - Remove
system/ee/installeragain. - Give
system/user/cacheback towww-data. - Check that
config.phpis0640 root:www-dataonce more.
Server components
Component Path
ExpressionEngine code (root owned) /var/www/expressionengine
Configuration (this instance's keys and database password) /var/www/expressionengine/system/user/config/config.php
File and folder modes ExpressionEngine writes with (0640 / 0750) /var/www/expressionengine/system/user/config/constants.php
Templates /var/www/expressionengine/system/user/templates
Uploads and the site theme /var/www/expressionengine/images, /var/www/expressionengine/themes/user
Caches (rebuilt automatically) /var/www/expressionengine/system/user/cache
Database (MariaDB datadir) /var/lib/mysql
nginx site, request limits and cross-site link rules /etc/nginx/sites-available/sac-expressionengine, /etc/nginx/conf.d/sac-expressionengine-guard.conf, /etc/nginx/conf.d/sac-expressionengine-xsite.conf, /etc/nginx/snippets/sac-expressionengine-*.conf
PHP settings /etc/php/8.3/fpm/conf.d/99-sac-expressionengine.ini
MariaDB settings /etc/mysql/mariadb.conf.d/99-sac-expressionengine.cnf
Certificate and key /etc/nginx/ssl/
First boot script, installer copy and helpers /usr/local/sbin/expressionengine-firstboot.sh, /usr/local/lib/sac/expressionengine/
Helpers /usr/local/sbin/expressionengine-set-hostname, expressionengine-set-password, expressionengine-cred-roundtrip
Licence, release and image notes /usr/share/doc/expressionengine/
Credentials /root/expressionengine-credentials.txt (mode 0600)
Security recommendations
- Allow port 22 in the security group from your own networks only.
- Keep the
adminpassword secret, and rotate it if it may have been shared (Step 12). Give each editor their own account with only the permissions they need. - Keep
config.phpand your database backups secret (Step 8). The encryption keys inconfig.phpsign every session and cookie of your site. - Use your own DNS name and a certificate your browsers trust (Step 11).
- Keep ExpressionEngine on the newest release (Step 12), and install only add-ons you trust.
- Keep the operating system patched. Unattended security upgrades are on, and
sudo apt-get update && sudo apt-get upgradeapplies everything else. MariaDB, PHP-FPM and PHP's zip extension come from Ubuntu's universe repository.sudo pro security-status --esm-appsshows whether Ubuntu Pro (ESM Apps) has security updates for them. None were pending when the image was built.
Support and licensing
ExpressionEngine is free software under the Apache License, Version 2.0, and a copy is at /usr/share/doc/expressionengine/LICENSE. The bundled libraries carry their own open source licenses.
There is no per-user or per-CPU license fee, and no license key is needed. ExpressionEngine Pro is a separate paid add-on that this image does not include.
This image installs the unmodified upstream release, without its unused copy of jQuery 2.2.4, plus the SecureAnyCloud first-boot setup, settings and helpers described above. 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 ExpressionEngine 7.5.28 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