Application Servers AWS

CodeIgniter 4.7.5 on AWS User Guide

| Product: CodeIgniter 4.7.5 Host on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the CodeIgniter 4.7.5 Host on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg app stacks and runtimes listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

CodeIgniter is a lightweight, fast PHP framework for building web applications and APIs. This image is a ready to use CodeIgniter host: the framework, PHP 8.3-FPM, nginx, MariaDB and Composer are installed and wired together, and a small sample application is already serving so you can see the whole stack working before you deploy your own code.

What the cloudimg image adds:

  • CodeIgniter 4.7.5, current with the security fixes. 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 audit was 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 at all, 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), the MariaDB password the application uses and CodeIgniter's encryption.key are generated on first boot and written to /root/codeigniter-credentials.txt (root only). nginx and PHP-FPM refuse to start until first boot has finished.
  • Host and client address from configuration. CodeIgniter's app.baseURL is set from the instance's configured name, and PHP is handed that configured name, never the Host a client sends, so every link and redirect names your site. Forwarding headers such as X-Forwarded-For and X-Forwarded-Host are 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 controller public/index.php. Any other .php URL answers 404, and app/, vendor/, writable/ and .env are outside the web root. The code is owned by root and read only to PHP; only writable/ is writable.
  • Production mode. CI_ENVIRONMENT=production: no 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.1 only. nginx, PHP-FPM, MariaDB and the first boot service are blocked from the instance metadata service, so PHP code (including anything that fetches a URL a user supplies) cannot read instance metadata. 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/codeigniter that you can grow independently of the root volume.
  • A development host. Composer stays on the image so you can composer install your own applications. There are no compilers or other build tools.
  • Fully patched Ubuntu 24.04 LTS 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.

Step 1: 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 and your Region, then Launch through EC2. Choose m5.large, your key pair and a security group as above. The AMI brings two volumes: the 8 GiB root volume and the 20 GiB application volume; you can make either larger at launch.

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=codeigniter}]'

First boot takes well under a minute after the instance is running.

Step 2: Connect to your instance

Version SSH user
CodeIgniter 4.7.5 Host on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

Step 3: Check the services

First boot creates the certificate, the passwords, the encryption key and the database, runs the application's migration, and then nginx and PHP-FPM start:

systemctl is-active codeigniter-firstboot nginx php8.3-fpm mariadb
sudo test -f /var/lib/cloudimg/codeigniter-firstboot.done && echo FIRSTBOOT_DONE
active
active
active
active
FIRSTBOOT_DONE

Only SSH and nginx listen on network ports, nginx on 443 only. PHP-FPM (9000) and MariaDB (3306) listen on the loopback address:

sudo ss -Hltn | awk '{print $4}' | sort -u
0.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
[::]:443

Step 4: Get your URL and passwords

sudo cat /root/codeigniter-credentials.txt

The file is readable by root only:

# 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=ci_app
CI_DB_PASSWORD=<CI_DB_PASSWORD>
CI_TLS_CERT=/etc/nginx/tls/codeigniter.crt
CI_CERT_SHA256=50:DE:DD:BE:...:BC:70:6A:67
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 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://www.cloudimg.co.uk/guides/codeigniter-4-7-5-on-ubuntu-24-04-aws/

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:

openssl x509 -noout -subject -ext subjectAltName -enddate -in /etc/nginx/tls/codeigniter.crt
subject=CN = ec2-13-218-49-206.compute-1.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name:
    DNS:ec2-13-218-49-206.compute-1.amazonaws.com, IP Address:13.218.49.206, IP Address:172.31.92.245, IP Address:127.0.0.1, DNS:localhost
notAfter=Jan 22 05:38:38 2029 GMT

Step 5: Open 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 (or install your own, Step 9). The sample application shows the CodeIgniter, PHP and MariaDB versions and a request counter:

The cloudimg CodeIgniter 4 Host landing page with the request counter, CodeIgniter 4.7.5, PHP 8.3.6 and MariaDB 10.11 cards, and the production environment, configured base URL, https scheme and client address

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 the migration ran and the database round trip works. The same facts are available as JSON for monitoring:

The /api/info endpoint returning the CodeIgniter, PHP and database versions, the production environment, the configured base URL and the request counter as JSON

From the instance itself (the password is handed 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": "cloudimg 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-13-218-49-206.compute-1.amazonaws.com/",
  "selfUrl": "https://ec2-13-218-49-206.compute-1.amazonaws.com/",
  "requestHost": "ec2-13-218-49-206.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
ok

Step 6: Add 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-fpm
No syntax errors detected in /srv/codeigniter/app/app/Controllers/Hello.php
No syntax errors detected in /srv/codeigniter/app/app/Config/Routes.php

Use a quoted here-document (<<'PHP') so $routes reaches the file literally; php -l catches a broken Routes.php before you reload. Now 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"; echo
Hello from my own CodeIgniter controller on https://ec2-13-218-49-206.compute-1.amazonaws.com/

The new /hello route served over HTTPS in the browser, showing the controller's greeting and the configured site URL

Note that site_url() returns the configured https:// address: CodeIgniter builds every link from app.baseURL, never from the request.

Step 7: Deploy your own application

Replace the sample with your own code in /srv/codeigniter/app. Keep the existing .env: it holds this instance's database password and encryption key. Run Composer as root (the code is owned by root and read only to PHP), install without development packages, and 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-fpm

Composer is part of the image:

composer --version
Composer version 2.7.1 2024-02-09 15:26:28

Keep writable/ owned by www-data and everything else owned by root. Your migrations go in app/Database/Migrations/ and run as the web user:

cd /srv/codeigniter/app && sudo -u www-data php spark migrate:status
CodeIgniter v4.7.5 Command Line Tool - Server Time: 2026-10-20 05:40:15 UTC+00:00

+-----------+-------------------+---------------------+---------+---------------------+-------+
| Namespace | Version           | Filename            | Group   | Migrated On         | Batch |
+-----------+-------------------+---------------------+---------+---------------------+-------+
| App       | 2026-01-01-000000 | CreateStackCounters | default | 2026-10-20 05:38:42 | 1     |
+-----------+-------------------+---------------------+---------+---------------------+-------+

Apply new migrations with sudo -u www-data php spark migrate. The database is reachable from the instance as root over the local socket:

sudo mysql -t -e 'SELECT counter_key, hits FROM codeigniter.stack_counters;'
+-------------+------+
| counter_key | hits |
+-------------+------+
| requests    |    1 |
+-------------+------+

Your application connects with the database.default.* settings already in .env (user ci_app on 127.0.0.1).

Step 8: Storage layout

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. Grow the volume in EC2 and then run sudo resize2fs /dev/nvme1n1.

Step 9: 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 (keep the key 0600 root) and run sudo systemctl reload nginx. Requests for any other host name are dropped, and allowedHostnames stays empty, so CodeIgniter never builds a link from the Host a client sends.

Step 10: Security checks

A request for a host name the instance does not serve 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 || true
forged 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-13-218-49-206.compute-1.amazonaws.com/",
  "requestHost": "ec2-13-218-49-206.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-firstboot
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

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

The image also ships the check the build runs: sudo /usr/local/sbin/codeigniter-credential-round-trip.sh.

Step 11: Changing the admin password

sudo codeigniter-rotate-password

This writes a new random password for admin (as a bcrypt hash, never on a command line), records it 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.

Step 12: Versions and licences

grep -E '^PINNED_' /opt/cloudimg/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    MIT

Troubleshooting

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

  • Allow port 22 from your admin network only, and port 443 from your users. Never open 3306 or 9000.
  • Use your own DNS name and a certificate your browsers trust (Step 9).
  • Keep CI_ENVIRONMENT = production and keep the .env file private: it holds the encryption key and the database password.
  • 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; cloudimg is not affiliated with or endorsed by the CodeIgniter Foundation. 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