Codex 2.5.7 on AWS User Guide
Overview
This guide covers launching and verifying Codex 2.5.7 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened SRS Streaming Media Server & Media Servers AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other media and streaming products in the listing are described on the listing's guide hub.
Codex is a free, self-hosted comic and e-book library server and web reader (GPL-3.0-only). It indexes the CBZ, CBR, CB7, CBT and PDF archives you already own, reads their ComicInfo metadata, and lets you browse them by publisher, series, volume, issue or folder, search them, and read them in the browser. It also publishes an OPDS catalogue for e-reader apps, and remembers each reader's progress and bookmarks.
What is on the image:
- Codex 2.5.7, the unmodified release wheel from PyPI, in a Python 3.12 virtual environment under
/opt/codex/venv - All 118 Python packages installed from a cloudimg lock that pins every version and file hash exactly as upstream resolves them (
pip --require-hashes), with pip 26.3 - Codex on
127.0.0.1:9810only, behind nginx serving HTTPS on port 443 with a certificate created on your instance; port 80 redirects to HTTPS - A per-instance administrator account created on first boot; the upstream
admin/adminaccount is never created - A separate 64 GiB data volume mounted at
/var/lib/codexfor the database and your comics unarfrom Ubuntu, so Codex can read CBR (RAR) archives without the non-freeunrar- A generated six-page sample comic (cloudimg Sample), so the library is not empty on first sign-in
codex-set-hostname, a helper that serves Codex under your own DNS name, andcodex-selftest, an end-to-end check
Default user: ubuntu · Architecture: x86_64 · Root volume: 12 GiB gp3 · Data volume: 64 GiB gp3
Security posture
- No default account. Upstream Codex creates an administrator
adminwith the passwordadminwhenever no superuser exists. This image creates the administratoradminon first boot with a 32-character password generated on your instance, before Codex first starts, so the upstream account is never made. The password is handed to the provisioning step through a temporary root-only file, never on a command line or in a logged environment, and is written to a root-only file. - Private by default. Anonymous browsing ("Non Users") and self-registration are off, so every page, comic and OPDS feed needs a sign-in.
- No outbound calls from Codex's own switches. Upstream turns on Auto Update and Send Telemetry outside its Docker image. Both are off here. Auto Update could not work on this image anyway: the virtual environment is read-only to Codex, so you move to a newer release by launching a newer image. Codex still asks PyPI (
pypi.org) for the latest version number, so the web UI can show an "upgrade available" note. - Per-instance secrets. Django's secret key, the field-encryption key, the administrator password, the API key and the TLS key are all generated on your instance. The image ships without a database: first boot creates it, so no two instances share a secret or a session.
- Fails closed. Codex (
codex.service) starts only after first boot has written its keys, database and administrator, and nginx only after first boot has finished. If first boot cannot finish, nothing is served. - Only nginx faces the network. Exactly ports 22, 80 and 443 listen beyond loopback; Codex (the Granian server) listens on
127.0.0.1:9810. Codex runs as the unprivilegedcodexuser under a systemd sandbox (read-only system, no home directories, no new privileges, its own/tmp). - HTTPS from first boot. nginx serves TLS 1.2 and 1.3 with a self-signed certificate created on your instance (TLS 1.1 is refused), marks every Codex cookie
Secure, and sendsStrict-Transport-Security. - The host name comes from configuration, never from a request. First boot reads the instance's public DNS name (else its public or private IPv4 address) from EC2 instance metadata (IMDSv2) and pins nginx to it. Requests for any other host name get their connection closed; requests to the instance's IP addresses are redirected to that name. nginx always hands Codex the configured name and drops client-sent
X-Forwarded-Host, so Django's CSRF origin check compares against your real address. - Client address from the connection. nginx replaces
X-Forwarded-ForandX-Real-IPwith the real peer address and dropsForwarded. At build time a failed sign-in carrying forged forwarding headers was logged with the real peer address. - The app cannot read instance metadata. Codex, the first-boot unit and nginx are blocked from the instance metadata service (169.254.169.254, fd00:ec2::254) by systemd
IPAddressDeny; only the small unit that looks up the host name at first boot may use it. - Cross-site protection. nginx refuses state-changing requests and WebSocket upgrades (
/api/v4/ws) that come from other sites, in addition to Django's own CSRF protection. - Dependencies. Every installed Python package was checked against the OSV vulnerability database at build time with no advisory found; the lock and the notes are in
/opt/codex/requirements.lock.txtand/opt/codex/SECURITY-NOTES.txt. The venv's pip is 26.3 rather than the pip 24.0 that Ubuntu'spython3.12-venvbundles. - Residual: unar updates.
unar(and the GNUstep libraries it needs) comes from Ubuntu universe, where some security fixes are delivered only through Ubuntu Pro (ESM). Attaching the instance to Ubuntu Pro delivers them;pro security-status --esm-appslists them. Codex usesunaronly to read the CBR archives in your own library. - Hardened SSH and system. SSH allows keys only, no root login and no X11 forwarding. The AMI requires IMDSv2. SSH host keys and the machine ID are generated fresh on every instance.
Prerequisites
- An AWS account and permission to launch EC2 instances
- An EC2 key pair in the Region you launch in
- Your comic archives (CBZ, CBR, CB7, CBT or PDF), which you copy onto the data volume
Recommended instance type: m5.large. Codex uses a few hundred MiB of memory at rest; importing a large library and generating cover thumbnails is the main CPU load. Comics live on the 64 GiB data volume: increase it at launch, or grow it later, to fit your library.
Connecting to your instance
| Version | Operating system | SSH user |
|---|---|---|
| Codex 2.5.7 on Ubuntu 24.04 | Ubuntu 24.04 LTS | ubuntu |
Step 1: Launch the instance
Option A: AWS Marketplace console
- Open the cloudimg Hardened SRS Streaming Media Server & Media Servers AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
- Once the subscription is active, choose Launch your software.
- Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose Codex 2.5.7 on Ubuntu 24.04, then choose your Region.
- Pick an instance type (
m5.largerecommended; the software charge is $0.04 per vCPU per hour) and your key pair. - Security group: the listing's default group opens SSH (22), HTTP (80) and HTTPS (443). Restrict SSH to your own IP address, and restrict 80 and 443 to the networks your readers use.
- Choose your VPC and subnet, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.
Option B: AWS CLI
Run this on your own workstation. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and CIDRs with your own:
SG=$(aws ec2 create-security-group --group-name codex \
--description "Codex comic library" --vpc-id vpc-0123456789abcdef0 \
--query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 443 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 80 --cidr <your-mgmt-cidr>
aws ec2 run-instances \
--image-id ami-0123456789abcdef0 \
--instance-type m5.large \
--key-name my-key-pair \
--security-group-ids "$SG" \
--subnet-id subnet-0123456789abcdef0 \
--block-device-mappings 'DeviceName=/dev/sdf,Ebs={VolumeSize=200,VolumeType=gp3}' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=codex}]'
The block device mapping grows the data volume (/dev/sdf, mounted at /var/lib/codex) to 200 GiB for a larger library; leave it out to keep 64 GiB. You do not need --metadata-options: the AMI already makes IMDSv2 required. Associate an Elastic IP if you want the address to survive a stop and start.
Step 2: Connect and find your login
Connect as ubuntu with the key pair you chose:
ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>
First boot takes about a minute after the instance starts. It sets the name Codex is served as (the instance's public DNS name when it has one), creates the TLS certificate, Django's keys, the database and the administrator, imports the sample comic, and writes /var/lib/cloudimg/codex-credentials.txt (mode 0600, root). These are the entries in that file (the password is not shown here):
sudo grep -E '^(CODEX_URL|ADMIN_USERNAME)=' /var/lib/cloudimg/codex-credentials.txt
sudo grep -oE '^[A-Z_]+=' /var/lib/cloudimg/codex-credentials.txt
sudo stat -c '%a %U:%G %n' /var/lib/cloudimg/codex-credentials.txt
CODEX_URL=https://ec2-107-22-99-191.compute-1.amazonaws.com/
ADMIN_USERNAME=admin
CODEX_URL=
ADMIN_USERNAME=
ADMIN_PASSWORD=
600 root:root /var/lib/cloudimg/codex-credentials.txt
| Value | What it is for |
|---|---|
CODEX_URL |
The address of this instance's Codex |
ADMIN_USERNAME |
The administrator's user name (admin) |
ADMIN_PASSWORD |
Administrator password, generated on this instance |
To read the password, run sudo cat /var/lib/cloudimg/codex-credentials.txt in your own session and store it in your password manager. Codex keeps only a PBKDF2 hash of it.
Step 3: Confirm Codex is running
The two first-boot units have finished, Codex and nginx are active, and only sshd and nginx listen beyond loopback (Codex is on 127.0.0.1:9810, the resolver on 127.0.0.53):
systemctl is-active codex-host.service codex-firstboot.service codex.service nginx.service
sudo ss -Htlnp | awk '{print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u
cat /opt/codex/VERSION
active
active
active
active
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
127.0.0.1:9810 users:(("codex"))
127.0.0.53%lo:53 users:(("systemd-resolve"))
127.0.0.54:53 users:(("systemd-resolve"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))
codex_version=2.5.7
codex_source=https://github.com/ajslater/codex/tree/v2.5.7 (PyPI: https://pypi.org/project/codex/2.5.7/)
python=3.12.3
pip=26.3
requirements_sha256=3792a203a7951b6d973803125ddca52084e4a0024dd88a69ecac16f59a0367db
Step 4: Prove the login and the web defaults
The web app and its health check answer, a comic page and the OPDS catalogue refuse a signed-out request, plain HTTP redirects to HTTPS, an unknown Host header gets no answer and TLS 1.1 is not offered. The last command is the image's own round-trip check: it signs in to the API with this instance's administrator password, proves a wrong password is refused, reads a page of the sample comic, proves a signed-out read is refused, and signs out:
H=$(sed -n 's/^set \$cx_host "\(.*\)";$/\1/p' /etc/nginx/codex-hostvar.conf)
C=(--connect-to ::127.0.0.1:)
curl -sk "${C[@]}" -o /dev/null -w 'web app -> %{http_code}\n' "https://$H/"
curl -sk "${C[@]}" -o /dev/null -w 'health -> %{http_code}\n' "https://$H/health"
curl -sk "${C[@]}" -o /dev/null -w 'comic page, signed out -> %{http_code}\n' "https://$H/api/v4/comics/1/pages/0"
curl -sk "${C[@]}" -o /dev/null -w 'OPDS, signed out -> %{http_code}\n' "https://$H/opds/v1.2/"
curl -s "${C[@]}" -o /dev/null -w 'http -> %{http_code}\n' "http://$H/"
curl -sk -o /dev/null -w 'unknown Host -> %{http_code}\n' -H 'Host: evil.example' https://127.0.0.1/ || true
curl -sk "${C[@]}" -o /dev/null -w 'TLS 1.1 -> %{http_code}\n' --tlsv1.1 --tls-max 1.1 "https://$H/" || true
sudo codex-selftest
web app -> 200
health -> 200
comic page, signed out -> 403
OPDS, signed out -> 401
http -> 301
unknown Host -> 000
TLS 1.1 -> 000
round-trip OK: admin signed in over https://ec2-107-22-99-191.compute-1.amazonaws.com/ (wrong password refused 400), comic 1 page 0 served 187016 bytes of image, anonymous read refused (403), logout ended the session (403)
--connect-to ::127.0.0.1: sends each request to this instance while keeping the real host name in the request, exactly as a browser would send it. 000 means the server closed the connection without an answer.
Step 5: Sign in to Codex
Browse to CODEX_URL (https://<public-dns-name>/). The certificate is self-signed, so your browser warns once; accept it for this host, or install your own certificate (Step 9). Codex opens on its sign-in screen (registration is disabled); choose Login and sign in with ADMIN_USERNAME and ADMIN_PASSWORD.

Step 6: Browse and read
Once signed in you land in the library. The cloudimg sample comic is already imported, so you can see covers, metadata and the reader working before you add anything of your own. Use top collection to switch between publishers, series, volumes, issues and folders, filter by to narrow by reading status or metadata, and the magnifier to search.

Select a cover to open the reader. Click the middle of the page to show or hide the title bar and the page slider; the arrow keys or the page edges turn pages, and Codex remembers where you stopped. The menu at the top right of the reader holds the display settings: fit to screen, width or height, two-page spreads, and reading direction (right to left for manga).

Step 7: Add your comics
The library /var/lib/codex/library is on the data volume and is already registered in Codex, which watches it for new files and also polls it every hour. Copy your archives up from your workstation, move them into the library and give them to the codex user:
scp -r ~/Comics ubuntu@<public-ip>:/tmp/
sudo cp -r /tmp/Comics /var/lib/codex/library/
sudo chown -R codex:codex /var/lib/codex/library
To index straight away, open the menu (top right) and choose Poll All Libraries. To add another folder, open Admin Panel, then Libraries, then + Library; a second EBS volume works well for a large collection.

The Admin Panel also holds Users and Groups (accounts for other readers, and which libraries each group may see), Auth, and Settings, where the server switches live (among them anonymous access, registration, Auto Update and Send Telemetry). The library as it ships, on its own volume:
findmnt -no TARGET,FSTYPE,SIZE /var/lib/codex
findmnt --fstab -no TARGET,FSTYPE,OPTIONS /var/lib/codex
sudo ls -R /var/lib/codex/library
/var/lib/codex ext4 62.4G
/var/lib/codex ext4 defaults,nofail
/var/lib/codex/library:
cloudimg Samples
/var/lib/codex/library/cloudimg Samples:
cloudimg Sample - Getting Started with Codex (2026).cbz
Delete the cloudimg Samples folder whenever you like and poll the library again.
Step 8: Check the security controls
Codex, its first-boot unit and nginx carry IPAddressDeny for the metadata addresses, first boot recorded that it could not reach the metadata service from inside its own unit, Codex runs sandboxed as codex, and SSH runs with the cloudimg pins:
systemctl show -p IPAddressDeny codex.service codex-firstboot.service nginx.service
systemctl show -p User -p ProtectSystem -p ProtectHome -p NoNewPrivileges codex.service
sudo grep -o 'IMDS-REFUSED-IN-FIRSTBOOT.*' /var/log/cloudimg-firstboot.log
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|x11forwarding) '
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
User=codex
ProtectHome=yes
ProtectSystem=strict
NoNewPrivileges=yes
IMDS-REFUSED-IN-FIRSTBOOT (http 000; IPAddressDeny in effect)
permitrootlogin no
passwordauthentication no
x11forwarding no
Step 9: Serve Codex under your own DNS name
Point a DNS record (for example comics.example.com) at the instance's public or Elastic IP address, then tell the image to serve that name. It creates a new self-signed certificate for the name, pins nginx and the cross-site guard to it, updates CODEX_URL in the credentials file and keeps the name across reboots:
sudo codex-set-hostname comics.example.com
For a trusted certificate, obtain one for that name (for example with certbot's certonly mode using its own temporary web server, or from your organisation's CA), copy the certificate chain and key over /etc/ssl/codex/codex.crt and /etc/ssl/codex/codex.key (key mode 0600, root), and run sudo systemctl reload nginx.
Step 10: Accounts, passwords and e-readers
- Administrator: change the password from Profile in the menu. The credentials file is not updated; remove the old password from it (
sudo nano /var/lib/cloudimg/codex-credentials.txt) once you have stored the new one. - Other readers: add them under Admin Panel, then Users; give administrator rights only to people who should manage libraries and users. Keep at least one administrator: Codex recreates
admin/adminwhen no superuser exists. - E-reader apps (OPDS): point an OPDS app at
https://<your-name>/opds/v1.2/(or/opds/v2.0/for OPDS 2 apps) and sign in with a Codex user name and password; the catalogue uses HTTP Basic authentication over HTTPS. Apps generally need a certificate the device trusts, so set up your DNS name and certificate first (Step 9).
Maintenance
- Operating system updates: unattended security upgrades are on. Reboot when
/var/run/reboot-requiredexists; Codex and nginx start again by themselves. - Codex updates: Codex is installed from a pinned lock into
/opt/codex/venv, and its Auto Update switch is off. To move to a newer release, launch the newer version of this image when the listing offers it and move the data volume (or/var/lib/codex) across. - Backups: everything Codex keeps is on the data volume: the database, keys and settings in
/var/lib/codex/config, and your comics in/var/lib/codex/library. Take an EBS snapshot of that volume, or stop Codex (sudo systemctl stop codex) and copy the directory. - Logs:
sudo journalctl -u codexfor Codex,/var/lib/codex/config/logs/for Codex's own logs (including failed sign-ins),/var/log/nginx/for web requests,/var/log/cloudimg-firstboot.logfor first boot. - What the instance contacts: with Auto Update and Send Telemetry off, Codex's only outbound request is its version check against
pypi.org. Everything else comes from the operating system: the AWS Systems Manager agent, snapd, unattended upgrades and the time service. Disable the SSM agent if you do not use Session Manager.
Troubleshooting
- The web page does not load right after launch: first boot is still running.
sudo journalctl -u codex-host.service -u codex-firstboot.service -fshows its progress. - Browser shows "connection closed": nginx answers only the configured name and redirects the instance's own IP addresses to it. Use
CODEX_URL, or runsudo codex-set-hostname <name>for a new name. - New comics do not appear: check the files are owned by
codex(Step 7) and choose Poll All Libraries;sudo journalctl -u codex -n 50shows the import. - A CBR archive does not open: Codex reads RAR archives with
unar; checklsar <file>lists it, and that the archive is not password protected. - The end-to-end check:
sudo codex-selftestsigns in to the API, reads a comic page and printsround-trip OKwhen everything works.
Licensing
Codex is licensed under the GNU General Public License version 3 only (GPL-3.0-only). The licence text is on the instance in /opt/codex/LICENSE, and /opt/codex/SOURCE.txt points to the complete corresponding source of this release at github.com/ajslater/codex/releases/tag/v2.5.7. cloudimg is not affiliated with, endorsed by or sponsored by the Codex project or its author.
Support
cloudimg provides 24/7 technical support for this image by email and live chat: deployment, first login, HTTPS and DNS setup, upgrades and troubleshooting. Contact support@cloudimg.co.uk.