CodeIgniter 4.7.5
Overview
CodeIgniter is a lightweight, fast PHP framework for building web applications and APIs. This SecureAnyCloud AMI, CodeIgniter 4.7.5 Host on Ubuntu 24.04, is a ready-to-use CodeIgniter host for Amazon EC2: the framework, PHP 8.3-FPM, nginx, MariaDB and Composer are installed and wired together.
A small sample application is already serving, so you can see the whole stack working before you deploy your own code. The AMI is one version of the SecureAnyCloud app stacks and runtimes listing on AWS Marketplace. You select it as the software version when you subscribe and launch.
What you get:
- CodeIgniter 4.7.5, current with the security fixes. Version 4.7.5 fixes six framework advisories that 4.7.4 still carries, including a critical upload validation bypass (GHSA-4hwf-v4mp-hf6c). The framework version is pinned exactly, its dependency tree is three permissive packages (MIT and BSD-3-Clause), and
composer auditwas clean at build time. - HTTPS only, from the first boot. nginx serves the application on port 443 with a certificate generated on your instance. There is no plain HTTP listener, and requests for a host name the instance does not serve are dropped.
- No default password. The admin password (HTTP Basic auth in front of every page except
/health) and CodeIgniter'sencryption.keyare generated on first boot and written to the root-only file/root/codeigniter-credentials.txt. nginx and PHP-FPM refuse to start until first boot has finished. - No database password at all. The application's MariaDB account is the PHP-FPM user
www-data, which MariaDB authenticates by the local socket (unix_socket), so there is no database password to store, leak or rotate. The image ships a freshly initialised database directory. The application database and its account are created on your instance at first boot. - Host and client address from configuration. CodeIgniter's
app.baseURLis set from the instance's configured name. PHP receives that name, never theHosta client sends, so every link and redirect names your site. Forwarding headers such asX-Forwarded-ForandX-Forwarded-Hostare removed before PHP sees them, so the client address is the real connection address. - Only the application on the web. nginx serves the
public/directory only and runs exactly one PHP file, CodeIgniter's front controllerpublic/index.php. Any other.phpURL answers 404, andapp/,vendor/,writable/and.envare outside the web root. The code is owned by root and read-only to PHP. Onlywritable/is writable. - Production mode.
CI_ENVIRONMENT=productionturns off the debug toolbar, and errors never show a stack trace, a file path or SQL. - Hardened defaults. PHP-FPM and MariaDB listen on
127.0.0.1only. nginx, PHP-FPM, MariaDB and the first-boot service are blocked from the instance metadata service, so PHP code cannot read instance metadata, including code that fetches a URL a user supplies. The AMI requires IMDSv2. - Your code and data on their own volume. The application, its
writable/directory and the MariaDB database live on a dedicated 20 GiB EBS volume at/srv/codeigniterthat you can grow independently of the root volume. - A development host. Composer stays on the image so you can
composer installyour own applications. There are no compilers or other build tools. - Fully patched base. Ubuntu 24.04 LTS is patched at build time, with unattended security upgrades switched on.
What is on the image: CodeIgniter 4.7.5 (codeigniter4/appstarter and codeigniter4/framework), PHP 8.3 FPM with OPcache, nginx 1.24, MariaDB 10.11, Composer 2.7, 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) from your admin network and TCP 443 (the application) from your users. Nothing else is needed. Never open 3306 or 9000.
Launch the instance
In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Pick CodeIgniter 4.7.5 Host on Ubuntu 24.04 as the software version, choose your Region, and select Launch through EC2.
Choose m5.large, your key pair and a security group as described above. The AMI brings two volumes: an 8 GiB root volume and a 20 GiB application volume. You can make either larger at launch.
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=codeigniter}]'First boot finishes a few seconds after the instance is running. Creating the database and its account and running the application's migration takes about 3 seconds.
Connect to your instance
Connect with SSH as the ubuntu user, the default user for CodeIgniter 4.7.5 Host on Ubuntu 24.04:
ssh -i <your-key.pem> ubuntu@<public-ip>Check the services
On first boot the image creates the certificate, the admin password, the encryption key, and the database with its socket-authenticated account. It then runs the application's migration and starts nginx and PHP-FPM. Confirm that all four services are active and first boot is done:
systemctl is-active codeigniter-firstboot nginx php8.3-fpm mariadb
sudo test -f /var/lib/sac/data/codeigniter-firstboot.done && echo FIRSTBOOT_DONEactive active active active FIRSTBOOT_DONEOnly SSH and nginx listen on network ports, and nginx listens on 443 only. PHP-FPM (9000) and MariaDB (3306) listen on the loopback address:
sudo ss -Hltn | awk '{print $4}' | sort -u0.0.0.0:22 0.0.0.0:443 127.0.0.1:3306 127.0.0.1:9000 127.0.0.53%lo:53 127.0.0.54:53 [::]:22 [::]:443Get your URL and passwords
Read the credentials file with sudo. Only root can read it:
sudo cat /root/codeigniter-credentials.txtThe file looks like this:
# CodeIgniter 4.7.5 Host - generated on first boot by codeigniter-firstboot.service
# These credentials are unique to this instance; none was baked into the image. Keep this file secret. CI_APP_URL=https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/ CI_ADMIN_USER=admin CI_ADMIN_PASSWORD=<CI_ADMIN_PASSWORD> CI_DB_NAME=codeigniter CI_DB_USER=www-data CI_TLS_CERT=/etc/nginx/tls/codeigniter.crt CI_CERT_SHA256=04:51:85:89:...:13:0F:5A:2B:51:6C CI_APP_DIR=/srv/codeigniter/app
#
# nginx (HTTPS on 443, HTTP Basic auth) is the only public entry point. PHP-FPM (127.0.0.1:9000) and MariaDB
# (127.0.0.1:3306) listen on loopback only. /health is an unauthenticated probe answered by nginx.
# The application connects to MariaDB as the PHP-FPM user www-data over the local socket; MariaDB authenticates it
# by the socket peer (unix_socket), so there is no database password. On-box maintenance:
sudo mariadb
# The CodeIgniter encryption key is in /srv/codeigniter/app/.env.
# New admin password:
sudo codeigniter-rotate-password
# Change the name it serves as:
sudo codeigniter-set-hostname <your-dns-name>
# User guide: https://secureanycloud.com/stacks/codeigniter-4-7-5/The URL uses the instance's public DNS name from the EC2 instance metadata. The certificate is generated on your instance and also names the public and private addresses. Inspect it with:
openssl x509 -noout -subject -ext subjectAltName -enddate -in /etc/nginx/tls/codeigniter.crtsubject=CN = ec2-32-197-202-84.compute-1.amazonaws.com, O = sac X509v3 Subject Alternative Name: DNS:ec2-32-197-202-84.compute-1.amazonaws.com, IP Address:32.197.202.84, IP Address:172.31.84.216, IP Address:127.0.0.1, DNS:localhost notAfter=Jan 22 12:46:19 2029 GMTOpen the application
Open CI_APP_URL in your browser and sign in as admin with CI_ADMIN_PASSWORD. The certificate is self-signed, so your browser asks you to confirm it the first time. You can install your own certificate in Step 9.
The sample application shows the CodeIgniter, PHP and MariaDB versions and a request counter. The counter is a row in a MariaDB table that a CodeIgniter migration created on first boot. Every request reads and writes it, so a number that goes up when you reload proves that the migration ran and the database round trip works.
The same facts are available as JSON for monitoring. From the instance itself, run the following. The password is passed to curl on standard input, never on the command line:
H=$(sudo sed -n 's#^CI_APP_URL=https://\(.*\)/$#\1#p' /root/codeigniter-credentials.txt) printf 'user = "admin:%s"\n' "$(sudo sed -n 's/^CI_ADMIN_PASSWORD=//p' /root/codeigniter-credentials.txt)" \
| curl -s -K - --cacert /etc/nginx/tls/codeigniter.crt --resolve "$H:443:127.0.0.1" "https://$H/api/info" | jq .{ "product": "sac CodeIgniter 4 Host", "codeigniterVersion": "4.7.5", "phpVersion": "8.3.6", "databaseServer": "10.11.14-MariaDB-0ubuntu0.24.04.1", "dbConnected": true, "environment": "production", "baseUrl": "https://ec2-32-197-202-84.compute-1.amazonaws.com/", "selfUrl": "https://ec2-32-197-202-84.compute-1.amazonaws.com/", "requestHost": "ec2-32-197-202-84.compute-1.amazonaws.com", "requestScheme": "https", "clientIp": "127.0.0.1", "requestsServed": 1 }Without the password, nothing is reachable except the health probe, which load balancers and monitoring can use:
H=$(sudo sed -n 's#^CI_APP_URL=https://\(.*\)/$#\1#p' /root/codeigniter-credentials.txt)
curl -s -o /dev/null -w 'anonymous -> HTTP %{http_code}\n' --cacert /etc/nginx/tls/codeigniter.crt --resolve "$H:443:127.0.0.1" "https://$H/"
curl -s --cacert /etc/nginx/tls/codeigniter.crt --resolve "$H:443:127.0.0.1" "https://$H/health"anonymous -> HTTP 401 okAdd your own controller
The application lives in /srv/codeigniter/app (a standard CodeIgniter 4 app starter layout), and nginx serves /srv/codeigniter/app/public. Add a controller and a route exactly as on any CodeIgniter install. The code is owned by root, so edit it with sudo:
sudo tee /srv/codeigniter/app/app/Controllers/Hello.php >/dev/null <<'PHP' <?php namespace App\Controllers; class Hello extends BaseController { public function index(): string { return 'Hello from my own CodeIgniter controller on ' . site_url('/'); } } PHP
sudo tee -a /srv/codeigniter/app/app/Config/Routes.php >/dev/null <<'PHP' $routes->get('hello', 'Hello::index'); PHP
sudo php -l /srv/codeigniter/app/app/Controllers/Hello.php
sudo php -l /srv/codeigniter/app/app/Config/Routes.php
sudo systemctl reload php8.3-fpmNo syntax errors detected in /srv/codeigniter/app/app/Controllers/Hello.php No syntax errors detected in /srv/codeigniter/app/app/Config/Routes.phpUse a quoted here-document (<<'PHP') so that $routes reaches the file literally. The php -l check catches a broken Routes.php before you reload.
Open https://<your-instance>/hello in your browser, or fetch it over HTTPS from the instance:
H=$(sudo sed -n 's#^CI_APP_URL=https://\(.*\)/$#\1#p' /root/codeigniter-credentials.txt) printf 'user = "admin:%s"\n' "$(sudo sed -n 's/^CI_ADMIN_PASSWORD=//p' /root/codeigniter-credentials.txt)" \
| curl -s -K - --cacert /etc/nginx/tls/codeigniter.crt --resolve "$H:443:127.0.0.1" "https://$H/hello"; echoHello from my own CodeIgniter controller on https://ec2-32-197-202-84.compute-1.amazonaws.com/Note that site_url() returns the configured https:// address. CodeIgniter builds every link from app.baseURL, never from the request.
Deploy your own application
Replace the sample with your own code in /srv/codeigniter/app. Keep the existing .env, which holds this instance's encryption key and database connection settings.
Run Composer as root, because the code is owned by root and read-only to PHP. Install without development packages, then reload PHP-FPM:
cd /srv/codeigniter/app
sudo COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader --no-interaction sudo systemctl reload php8.3-fpmComposer is part of the image. Check its version:
composer --versionComposer version 2.7.1 2024-02-09 15:26:28Keep writable/ owned by www-data and everything else owned by root. Put your migrations in app/Database/Migrations/ and run them as the web user:
cd /srv/codeigniter/app && sudo -u www-data php spark migrate:statusCodeIgniter v4.7.5 Command Line Tool - Server Time: 2026-10-20 12:46:46 UTC+00:00 +-----------+-------------------+---------------------+---------+---------------------+-------+ | Namespace | Version | Filename | Group | Migrated On | Batch | +-----------+-------------------+---------------------+---------+---------------------+-------+ | App | 2026-01-01-000000 | CreateStackCounters | default | 2026-10-20 12:46:21 | 1 | +-----------+-------------------+---------------------+---------+---------------------+-------+Apply new migrations with sudo -u www-data php spark migrate.
The application's database account is the PHP-FPM user www-data, and MariaDB authenticates it by the local socket (unix_socket). As a result, .env carries no database password and none exists on the instance:
sudo grep '^database' /srv/codeigniter/app/.env
sudo mariadb -t -e "SELECT CONCAT(user,'@',host) AS account, JSON_VALUE(Priv,'$.plugin') AS auth FROM mysql.global_priv WHERE user='www-data';"database.default.hostname = localhost database.default.database = codeigniter database.default.username = www-data database.default.DBDriver = MySQLi +--------------------+-------------+ | account | auth | +--------------------+-------------+ | www-data@localhost | unix_socket | +--------------------+-------------+hostname = localhost means the local socket. Only the www-data user can use that account. Any other user is refused, and the account cannot log in over TCP at all:
sudo -u www-data mariadb -t -e 'SELECT counter_key, hits FROM codeigniter.stack_counters;'
sudo mariadb -u www-data -e 'SELECT 1' 2>&1 || true+-------------+------+ | counter_key | hits | +-------------+------+ | requests | 2 | +-------------+------+ ERROR 1698 (28000): Access denied for user 'www-data'@'localhost'For maintenance, root reaches every database over the same socket with sudo mariadb. If your own application needs a password or a TCP account, create it with sudo mariadb and store the password outside the web root.
Storage layout
Inspect the two mounts that hold your application and database:
findmnt -no SOURCE,TARGET,FSTYPE,SIZE,OPTIONS /srv/codeigniter
findmnt -no SOURCE,TARGET,FSTYPE /var/lib/mysql/dev/nvme1n1 /srv/codeigniter ext4 19.5G rw,nosuid,nodev,relatime /dev/nvme1n1[/mysql] /var/lib/mysql ext4/srv/codeigniter is the application volume. /var/lib/mysql is a bind mount of /srv/codeigniter/mysql, so the database files live on the same volume while MariaDB keeps its standard path.
To grow the volume, resize it in EC2 and then run sudo resize2fs /dev/nvme1n1.
Your own DNS name and certificate
Point a DNS name at the instance. An Elastic IP keeps the address stable. Then tell the instance to serve that name. This issues a new self-signed certificate for it, pins nginx to it and sets CodeIgniter's app.baseURL:
sudo codeigniter-set-hostname <your-domain>To use a certificate from a public CA, replace /etc/nginx/tls/codeigniter.crt and /etc/nginx/tls/codeigniter.key, keeping the key 0600 root. Then run sudo systemctl reload nginx.
Requests for any other host name are dropped, and allowedHostnames stays empty. CodeIgniter therefore never builds a link from the Host a client sends.
Security checks
Send a request with a host name the instance does not serve. The connection is closed without an answer:
curl -sk -o /dev/null -w 'forged Host -> HTTP %{http_code} (000 = connection closed)\n' -H 'Host: evil.example' https://127.0.0.1/health || trueforged Host -> HTTP 000 (000 = connection closed)Forged forwarding headers change nothing. The application still builds its URLs from the configured name and sees the real client address:
H=$(sudo sed -n 's#^CI_APP_URL=https://\(.*\)/$#\1#p' /root/codeigniter-credentials.txt) printf 'user = "admin:%s"\n' "$(sudo sed -n 's/^CI_ADMIN_PASSWORD=//p' /root/codeigniter-credentials.txt)" \
| curl -s -K - --cacert /etc/nginx/tls/codeigniter.crt --resolve "$H:443:127.0.0.1" \
-H 'X-Forwarded-Host: evil.example' -H 'X-Forwarded-For: 203.0.113.9' "https://$H/api/info" | jq '{selfUrl, requestHost, clientIp}'{ "selfUrl": "https://ec2-32-197-202-84.compute-1.amazonaws.com/", "requestHost": "ec2-32-197-202-84.compute-1.amazonaws.com", "clientIp": "127.0.0.1" }Only the front controller runs. The configuration and writable/ are not on the web, and there is no port 80 listener:
H=$(sudo sed -n 's#^CI_APP_URL=https://\(.*\)/$#\1#p' /root/codeigniter-credentials.txt) for p in /.env /writable/ /app/Config/App.php /vendor/autoload.php; do printf 'user = "admin:%s"\n' "$(sudo sed -n 's/^CI_ADMIN_PASSWORD=//p' /root/codeigniter-credentials.txt)" \
| curl -s -o /dev/null -w "GET $p -> HTTP %{http_code}\n" -K - --cacert /etc/nginx/tls/codeigniter.crt --resolve "$H:443:127.0.0.1" "https://$H$p" done curl -s -o /dev/null -m 5 http://127.0.0.1/; echo "port 80 -> curl exit $? (7 = nothing listening)"GET /.env -> HTTP 404 GET /writable/ -> HTTP 404 GET /app/Config/App.php -> HTTP 404 GET /vendor/autoload.php -> HTTP 404 port 80 -> curl exit 7 (7 = nothing listening)The web stack is blocked from the instance metadata service. Your own shell is not:
systemctl show -p IPAddressDeny nginx php8.3-fpm mariadb codeigniter-firstbootIPAddressDeny=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/32The image also ships the credential check that the build runs: sudo /usr/local/sbin/codeigniter-credential-round-trip.sh.
Change the admin password
Run the rotate command to set a new admin password:
sudo codeigniter-rotate-passwordThis writes a new random password for admin as a bcrypt hash, never on a command line. It records the password in /root/codeigniter-credentials.txt and reloads nginx.
To serve the application publicly without the password prompt, for example once your app has its own login, delete the two lines in /etc/nginx/codeigniter-auth.conf and run sudo systemctl reload nginx.
Versions and licences
List the pinned component versions and the licence of each Composer package:
grep -E '^PINNED_' /opt/sac/codeigniter-versions.txt cd /srv/codeigniter/app && php -r '$l=json_decode(file_get_contents("composer.lock"),true); foreach($l["packages"] as $p) printf("%-26s %-8s %s\n",$p["name"],$p["version"],implode(",",(array)$p["license"]));'PINNED_CODEIGNITER=4.7.5 PINNED_PHP=8.3.6 PINNED_MARIADB=10.11.14-MariaDB-0ubuntu0.24.04.1 PINNED_COMPOSER=2.7.1 codeigniter4/framework v4.7.5 MIT laminas/laminas-escaper 2.18.0 BSD-3-Clause psr/log 3.0.2 MITTroubleshooting
Nothing answers on 443. First boot has not finished or has failed. nginx and PHP-FPM stay down until it succeeds, so a failed first boot leaves the instance closed rather than open. Check systemctl status codeigniter-firstboot and sudo journalctl -u codeigniter-firstboot -n 40.
The browser says the connection was closed. You used a host name the instance does not serve. Use CI_APP_URL, or set your own name with codeigniter-set-hostname (Step 9).
A 500 error with no detail. This is production mode working as intended. The detail is in sudo tail /srv/codeigniter/app/writable/logs/log-$(date +%F).log.
The address changed after a stop and start. Without an Elastic IP, the public name changes. Run sudo codeigniter-set-hostname <new-name> with the new public DNS name.
Security recommendations
- Limit network access. Allow port 22 from your admin network only, and port 443 from your users. Never open 3306 or 9000.
- Use a trusted certificate. Use your own DNS name and a certificate your browsers trust (Step 9).
- Protect the configuration. Keep
CI_ENVIRONMENT = productionand keep the.envfile private, because it holds the encryption key. - Stay current. Keep the operating system patched, update CodeIgniter with Composer when new releases fix advisories, and snapshot the application volume regularly.
Support and licensing
CodeIgniter is open source under the MIT licence (Copyright British Columbia Institute of Technology and the CodeIgniter Foundation). The framework vendors Kint (MIT), PSR Log (MIT) and Laminas Escaper (BSD-3-Clause), and their licence files ship with the image. CodeIgniter is a trademark of the CodeIgniter Foundation; SecureAnyCloud is not affiliated with or endorsed by the CodeIgniter Foundation. 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 CodeIgniter 4.7.5 Host 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