Media & Entertainment Azure

Codex on Ubuntu 24.04 LTS on Azure

| Product: Codex on Ubuntu 24.04 LTS

Overview

Codex is an open source, self hosted comic archive server and reader. Point it at folders of comic and ebook archives you already own, and it indexes them into a browsable library with covers, metadata, full text search and a reader that works in any browser. It reads CBZ, CBR, CB7, CBT and PDF, imports embedded ComicInfo and comicbox metadata, watches your folders for changes, and publishes an OPDS feed so e readers and reader apps can pull from the same library.

Codex is a library manager for files you supply. It does not search for, download or acquire content, and nothing of that kind is present in this image.

The cloudimg image runs Codex 2.2.6 from PyPI in a dedicated Python 3.12 virtual environment on a hardened, fully patched Ubuntu 24.04 LTS base. Codex serves from its Granian application server bound to loopback only, with nginx in front on port 80 handling both HTTP and the WebSocket Codex uses for live library updates.

The image ships unprovisioned and unreachable. Codex documents a default administrator of admin with the password admin, so that account is shipped with an unusable password: there is no working credential of any kind inside the image. On the first boot of every VM a one shot service generates this VM's administrator password, regenerates this VM's application secret keys, starts Codex, and only then publishes the site on port 80. Until that finishes nothing is bound to port 80, so an instance is never reachable in an unprovisioned state. Backed by 24/7 cloudimg support.

What is included:

  • Codex 2.2.6 in a dedicated Python 3.12 virtual environment, managed by systemd
  • nginx on :80 reverse proxying Codex, including the WebSocket used for live library updates
  • A dedicated 64 GiB data disk mounted at /var/lib/codex holding the database, the covers cache and your comic library
  • A per VM administrator password, Django secret key and field encryption key generated on first boot and recorded in a root only file
  • Anonymous browsing and self registration both disabled, so a fresh VM is private until you decide otherwise
  • A six page cloudimg sample comic so the library and reader work the moment you sign in
  • Nothing bound to port 80 until first boot has provisioned and secured the instance

Key facts

Item Value
Default SSH user azureuser
Web interface http://<vm-public-ip>/
OPDS feed http://<vm-public-ip>/opds
Application port 127.0.0.1:9810 (loopback only, reached through nginx)
Credentials file /root/codex-credentials.txt (mode 600, root only)
Data disk /var/lib/codex (config, database, covers cache, library)
Recommended size Standard_B2s

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a sensible starting point for a personal or small team library; size up if you import very large collections, because the first scan of a big library is CPU and IO heavy. NSG inbound: allow 22/tcp from your management network and 80/tcp (plus 443/tcp once you terminate TLS on the VM) so your browser and reader apps can reach Codex.

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Codex by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Then Review + create and Create.

The image carries its own 64 GiB data disk. It is attached and mounted automatically at /var/lib/codex, and you do not need to add or format anything.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name codex \
  --image <marketplace-image-urn> \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Then open port 80 so your browser and reader apps can reach Codex:

az vm open-port --resource-group <your-rg> --name codex --port 80

Step 3 - Confirm the services are running

SSH in as azureuser. First boot takes well under a minute; once it has finished, nginx binds port 80 and Codex is ready.

systemctl is-active codex nginx codex-firstboot

Confirm the versions and that Codex answers its own health endpoint through nginx:

/opt/codex/venv/bin/python -c "import importlib.metadata as m; print('Codex', m.version('codex'))"; nginx -v 2>&1; curl -s -o /dev/null -w 'GET /health -> HTTP %{http_code}\n' http://127.0.0.1/health

The codex and nginx units reporting active, the first boot unit complete, Codex 2.2.6 with its nginx version, and the health endpoint answering HTTP 200 through the reverse proxy

If port 80 is not bound, first boot has not completed. That is deliberate and fail closed: Codex is never published before it has been provisioned and secured. Check with systemctl status codex-firstboot.service and journalctl -u codex-firstboot.service.

Step 4 - Retrieve this VM's administrator credentials

Every VM generates its own administrator password on first boot and writes it, with the sign in address, to a root only credentials file:

sudo cat /root/codex-credentials.txt

The same first boot regenerated this VM's Django secret key and field encryption key. You never need those values, but you can confirm they are unique to this machine without printing them, and confirm the file permissions:

stat -c '%a %U:%G %n' /root/codex-credentials.txt /var/lib/codex/config/secret_key /var/lib/codex/config/field_encryption_key

The per VM credentials file with the administrator password masked, and the credentials file, Django secret key and field encryption key all reported at mode 600 owned by root or the codex service account

You can prove the generated password really authenticates, straight from the command line. The bundled probe signs in with the password from the credentials file, retries with a deliberately wrong one, checks that an anonymous caller is refused, and then reads an actual comic page image from the reader API:

sudo /usr/local/sbin/codex-cred-roundtrip.sh

Confirm for yourself that nothing is readable without signing in, and that Codex itself is only listening on loopback:

ss -tlnH | awk '{print $1, $4}' | sort -u; for p in /api/v4/auth/profile /opds/v1.2/ /api/v4/comics/1/pages/0; do printf '%-30s anonymous -> HTTP ' "$p"; curl -s -o /dev/null -w '%{http_code}\n' "http://127.0.0.1$p"; done

Only ports 22 and 80 reachable from the network with Codex bound to 127.0.0.1 port 9810, every anonymous API and OPDS request refused with 401 or 403, and the credential probe confirming the per VM password authenticates and the reader serves a real page image

Step 5 - Sign in to Codex

Browse to http://<vm-public-ip>/. Because anonymous browsing is switched off in this image, Codex opens on an Unauthorized screen with a Login button rather than exposing your library to the internet. Select Login and enter admin with the codex.admin.pass value from Step 4.

The Codex sign in dialog with the per VM administrator account entered, the only credential that exists on this instance

Change the password after your first sign in from the hamburger menu, then Admin Panel, then the Users tab.

Step 6 - Browse the library

Once signed in you land in the browser view. A cloudimg sample comic is already imported so you can see covers, metadata and the reader working before you add anything of your own.

The Codex library showing the imported sample comic with its generated cover thumbnail, issue number, year and title

Use top collection to switch between publishers, imprints, series, volumes, issues and folders, filter by to narrow by reading status or metadata, and the magnifier for full text search across your library.

Step 7 - Read a comic

Select a cover to open the reader. Click or tap the middle of the page to show the toolbars, the page slider and the reading controls; the arrows or your keyboard turn pages, and Codex remembers where you were.

The Codex reader displaying a page of the sample comic, with the title bar, the reading order control and the page slider showing progress through the issue

Reading settings live behind the hamburger menu inside the reader: fit to width, height or screen, one page or two page spreads, and left to right or right to left reading direction for manga.

Step 8 - Add your own comic library

Your own comics go on the dedicated data disk, which is where the pre registered library already points. Copy your archives up from your workstation with any file transfer tool, for example scp -r ./my-comics azureuser@<vm-ip>:/tmp/, then move them into place and hand ownership to the service account:

sudo install -d -o codex -g codex -m 0755 /var/lib/codex/library/My\ Comics; sudo chown -R codex:codex /var/lib/codex/library; ls -ld /var/lib/codex/library/My\ Comics

Codex watches the library folder for file events and also polls it hourly, so anything you drop in is picked up automatically. To index immediately, open the hamburger menu and choose Poll All Libraries, or add another folder entirely from Admin Panel, then the Libraries tab, then + Library.

The Codex Admin Panel Libraries tab showing the pre registered library on the data disk, its comic count, file event watching and hourly polling, with the controls to add further library folders

The Admin Panel also holds Users and Groups for granting people access, Auth for optional single sign on, Settings for library defaults and rate limits, and Restore for the user data sidecar described in Step 10.

Delete the bundled sample whenever you like: remove the Cloudimg Samples folder from /var/lib/codex/library and poll the library again.

Step 9 - Read on an e reader with OPDS

Codex publishes a standard OPDS catalogue, so any OPDS capable reader app can browse and download from your library using the same credentials:

printf 'anonymous OPDS:      HTTP %s\n' "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/opds/v1.2/)"; printf 'authenticated OPDS:  HTTP %s\n' "$(curl -s -o /dev/null -w '%{http_code}' -u "admin:<CODEX_ADMIN_PASSWORD>" http://127.0.0.1/opds/v1.2/)"

A 401 for the anonymous request and a 200 for the authenticated one is exactly what you want to see. Point your reader app at http://<vm-public-ip>/opds and sign in with admin and this VM's password.

Step 10 - Storage, persistence and backups

Everything that matters lives on the dedicated data disk, separate from the operating system disk, so you can resize or snapshot it independently:

findmnt -no SOURCE,TARGET,FSTYPE,OPTIONS /var/lib/codex; df -h /var/lib/codex | tail -1; ls -l /var/lib/codex/config/codex.sqlite3

The 64 GiB ext4 data disk mounted at /var/lib/codex with its free space, the Codex database file on that disk, and the bundled sample comic archive in the library folder

Path Holds
/var/lib/codex/config codex.sqlite3, codex.toml, secret keys, covers cache, logs
/var/lib/codex/library Your comic archives
/opt/codex/venv The Codex Python virtual environment
/etc/codex/codex.env Service environment: config directory and the loopback bind
/etc/nginx/sites-available/codex The public reverse proxy vhost

Codex writes a user_data.sqlite sidecar next to its database holding the things a library rescan cannot rebuild: accounts, groups, permissions, bookmarks, reading progress and preferences. Snapshot the data disk for a full backup, and keep a copy of that sidecar if you ever plan to rebuild the library from scratch. The Restore tab in the Admin Panel replays it.

Step 11 - Change the service configuration

Ports and bind address come from /etc/codex/codex.env, which takes precedence over codex.toml:

cat /etc/codex/codex.env

Codex is bound to 127.0.0.1 on purpose so nginx is the only public face. If you change that binding, remember to add a matching NSG rule, and prefer putting TLS in front rather than exposing the application port directly. After any change:

sudo systemctl restart codex.service && sleep 8 && systemctl is-active codex nginx

Step 12 - Enable HTTPS

Codex handles authentication over whatever transport you give it, so put TLS in front before exposing an instance to the internet. Point a DNS record at the VM, open 443/tcp in the NSG, then install a certificate with certbot for nginx:

sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.example -m you@your-domain.example --agree-tos --redirect -n

certbot edits the shipped vhost in place and adds the redirect. The proxy already forwards the real scheme to Codex, so sessions and CSRF continue to work once you are on HTTPS with no further changes.

Step 13 - Keep the VM patched

Unattended upgrades are enabled, so the base OS keeps taking security updates on its own. To apply everything immediately:

sudo apt-get update && sudo apt-get -y dist-upgrade && sudo reboot

To move to a newer Codex release, upgrade inside the virtual environment and restart the service:

sudo /opt/codex/venv/bin/pip install --upgrade codex
sudo systemctl restart codex.service

Security notes

  • No credential ships in the image. The built in admin account is shipped with an unusable password, so upstream's documented admin / admin default never exists on a cloudimg VM. First boot sets a random password for this VM only.
  • Nothing listens on port 80 until first boot completes. The public vhost is staged but not enabled in the image, and the first boot service enables it only after the administrator password is set and Codex answers on loopback.
  • The application port is never exposed. Codex binds 127.0.0.1:9810; only 22 and 80 are reachable from the network.
  • Anonymous browsing and self registration are switched off. A fresh VM is private. Turn either back on in Admin Panel, then Settings, if you want a public library or open sign ups.
  • Per VM cryptographic material. The Django secret key and field encryption key are regenerated on every VM's first boot, so no session token or encrypted field is portable between instances.
  • Restrict 22/tcp to your management network, put TLS in front before exposing the instance, and create individual accounts in the Admin Panel rather than sharing the administrator login.

Architecture summary

Component Detail
Codex 2.2.6 from PyPI, Python 3.12 virtual environment at /opt/codex/venv
Application server Granian, bound to 127.0.0.1:9810
Reverse proxy nginx on :80, HTTP plus the /api/v4/ws WebSocket
Database SQLite at /var/lib/codex/config/codex.sqlite3
Archive support CBZ, CB7, CBT and PDF natively, CBR and RAR through unar
Services codex.service, nginx.service, codex-firstboot.service (one shot)
Base image Ubuntu 24.04 LTS, fully patched at capture

Support

Codex is licensed under the GNU General Public License version 3. The licence text and cloudimg's attribution notice ship in the image at /usr/share/doc/cloudimg/. cloudimg packages unmodified upstream Codex; the additions are packaging only.

cloudimg provides 24/7 support for this image at support@cloudimg.co.uk. For questions about Codex itself, see the Codex documentation and the upstream project.