Deployment guide

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 admin with a password generated on your instance, and the two keys that sign every session and cookie (encryption_key and session_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 admin password, 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.php and the control panel through admin.php. Every other PHP file is never run, and the whole system folder (configuration, framework, templates and caches) is never served. The folders that PHP can write to and the web serves, images and themes/user (which holds the default theme and its upload folders), are static only. Every file gets nosniff and 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-IP and the other forwarding headers before PHP sees them, so ExpressionEngine records the address of the connection.
  • Hardened defaults. The ExpressionEngine code belongs to root and 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.
1

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}]'
2

Connect to your instance

Version SSH user

ExpressionEngine 7.5.28 on Ubuntu 24.04 ubuntu

ssh -i <your-key.pem> ubuntu@<public-ip>
3

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-firstboot
active active active active

nginx 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 --version
define('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 wrapper

To see how the download was verified, run:

cat /usr/share/doc/expressionengine/RELEASE
ExpressionEngine 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 dbebeba87e57e3bab14913ad0aa39ccee5138f040911555f1f5d49f7b4bf9335
4

Get 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.txt
EE_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>
5

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.

6

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.

7

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.

8

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/user

ExpressionEngine's own database backup (Tools > Utilities > Back Up Database) also works. For a whole-instance copy, snapshot the EBS volume.

9

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/ || true
port 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: 000

A 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.php
600 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.php

An 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.html
HTTP/2 200 x-content-type-options: nosniff content-security-policy: default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; sandbox content-disposition: attachment

The 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 subjectAltName
subject=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:localhost

These 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 Authorization header 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.conf
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 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-roundtrip
OK https 301 + no installer + config.php 404 + admin login + wrong and default passwords refused

Links 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: 200

The 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.

10

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.

11

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 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/sac-expressionengine-headers.conf. The image does not send one, because browsers ignore HSTS from a site whose certificate they do not trust.

12

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 --stdin

The 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.php

Email. 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, images and themes/user.
  • Let its system/ee/installer run the database update through the control panel.
  • Remove system/ee/installer again.
  • Give system/user/cache back to www-data.
  • Check that config.php is 0640 root:www-data once more.
13

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)

14

Security recommendations

  • Allow port 22 in the security group from your own networks only.
  • Keep the admin password 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.php and your database backups secret (Step 8). The encryption keys in config.php sign 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 upgrade applies everything else. MariaDB, PHP-FPM and PHP's zip extension come from Ubuntu's universe repository. sudo pro security-status --esm-apps shows 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

Deploy on AWS