Deployment guide

Codex 2.5.7

Overview

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 and reads their ComicInfo metadata. You can browse by publisher, series, volume, issue or folder, search, and read in the browser. Codex also publishes an OPDS catalogue for e-reader apps and remembers each reader's progress and bookmarks.

This guide covers launching and verifying Codex 2.5.7 on Ubuntu 24.04 from SecureAnyCloud on Amazon EC2. The image is one version in the SecureAnyCloud Hardened SRS Streaming Media Server & Media Servers AMIs listing on AWS Marketplace. Choose 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.

What you get:

  • 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 SecureAnyCloud 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:9810 only, 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 / admin account is never created
  • A separate 64 GiB data volume mounted at /var/lib/codex for the database and your comics
  • unar from Ubuntu, so Codex can read CBR (RAR) archives without the non-free unrar
  • A generated six-page sample comic (SecureAnyCloud Sample), so the library is not empty on first sign-in
  • codex-set-hostname, a helper that serves Codex under your own DNS name, and codex-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 named admin with the password admin whenever no superuser exists. This image creates the administrator admin on first boot, before Codex first starts, with a 32-character password generated on your instance. The upstream account is therefore never made. The password reaches the provisioning step through a temporary root-only file, never on a command line or in a logged environment, and is stored in a root-only file.
  • Private by default. Anonymous browsing ("Non Users") and self-registration are off, so every page, comic and OPDS feed requires 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 unprivileged codex user 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 sends Strict-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-For and X-Real-IP with the real peer address and drops Forwarded. 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.txt and /opt/codex/SECURITY-NOTES.txt. The venv's pip is 26.3 rather than the pip 24.0 that Ubuntu's python3.12-venv bundles.
  • 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-apps lists them. Codex uses unar only 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

1

Launch the instance

Option A: AWS Marketplace console

  • Open the SecureAnyCloud 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.large recommended; the software charge is $0.04 per vCPU per hour) and your key pair.
  • Set the 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 and leave the metadata defaults, because the image already requires IMDSv2. Then choose Launch.

Option B: AWS CLI

Run the following on your own workstation. Replace the AMI ID with the one shown for your Region on the Marketplace launch page. Replace 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, because the AMI already requires IMDSv2. Associate an Elastic IP if you want the address to survive a stop and start.

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). It creates the TLS certificate, Django's keys, the database and the administrator, and imports the sample comic.

It also writes /var/lib/sac/data/codex-credentials.txt (mode 0600, root). The following commands list the entries in that file. The password is not shown:

sudo grep -E '^(CODEX_URL|ADMIN_USERNAME)=' /var/lib/sac/data/codex-credentials.txt
sudo grep -oE '^[A-Z_]+=' /var/lib/sac/data/codex-credentials.txt
sudo stat -c '%a %U:%G %n' /var/lib/sac/data/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/sac/data/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/sac/data/codex-credentials.txt in your own session. Store the password in your password manager. Codex keeps only a PBKDF2 hash of it.

3

Confirm Codex is running

Check that the two first-boot units have finished and that Codex and nginx are active. Only sshd and nginx listen beyond loopback. Codex is on 127.0.0.1:9810 and the resolver is 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
4

Prove the login and the web defaults

Run the following checks. 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 and proves that a wrong password is refused. It then reads a page of the sample comic, proves that 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 and keeps the real host name in the request, exactly as a browser would send it. A result of 000 means the server closed the connection without an answer.

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.

6

Browse and read

After you sign in, you land in the library. The SecureAnyCloud 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. Use 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. Use the arrow keys or the page edges to turn pages. 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).

7

Add your comics

The library /var/lib/codex/library is on the data volume and is already registered in Codex. Codex 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. The server switches live under Settings, 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: sac Samples /var/lib/codex/library/sac Samples: sac Sample - Getting Started with Codex (2026).cbz

Delete the SecureAnyCloud Samples folder whenever you like and poll the library again.

8

Check the security controls

Confirm the security controls with the following checks. 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 SecureAnyCloud 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/sac/sac-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
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.

The helper creates a new self-signed certificate for the name and pins nginx and the cross-site guard to it. It also 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, use certbot's certonly mode with its own temporary web server, or request one 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). Then run sudo systemctl reload nginx.

10

Accounts, passwords and e-readers

  • Administrator: change the password from Profile in the menu. The credentials file is not updated. Once you have stored the new password, remove the old one from the file (sudo nano /var/lib/sac/data/codex-credentials.txt).
  • Other readers: add them under Admin Panel, then Users. Grant administrator rights only to people who should manage libraries and users. Keep at least one administrator, because Codex recreates the admin / admin account when 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-required exists. 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 codex for Codex, /var/lib/codex/config/logs/ for Codex's own logs (including failed sign-ins), /var/log/nginx/ for web requests, /var/log/sac/sac-firstboot.log for 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 -f shows 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 run sudo 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 50 shows the import.
  • A CBR archive does not open: Codex reads RAR archives with unar; check lsar <file> lists it, and that the archive is not password protected.
  • The end-to-end check: sudo codex-selftest signs in to the API, reads a comic page and prints round-trip OK when 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. SecureAnyCloud is not affiliated with, endorsed by or sponsored by the Codex project or its author.

Support

SecureAnyCloud provides 24/7 technical support for this image by email and live chat: deployment, first login, HTTPS and DNS setup, upgrades and troubleshooting. Contact sales@secureanycloud.com.

Deploy on AWS

Launch Codex 2.5.7 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