Codex on Ubuntu 24.04 LTS on Azure
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
:80reverse proxying Codex, including the WebSocket used for live library updates - A dedicated 64 GiB data disk mounted at
/var/lib/codexholding 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

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

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

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.

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.

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.

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

| 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
adminaccount is shipped with an unusable password, so upstream's documentedadmin/admindefault 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; only22and80are 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/tcpto 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.