1<!DOCTYPE html><html lang=en data-bs-theme=dark> <head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width, initial-scale=1.0"><meta name=description content="Codex Comicbook Reader Admin Guide"><meta name=author content=AJ><link rel="shortcut icon" href=img/favicon.ico><title>Codex</title><link href=css/bootstrap.min.css rel=stylesheet><link href=css/fontawesome.min.css rel=stylesheet><link href=css/brands.min.css rel=stylesheet><link href=css/solid.min.css rel=stylesheet><link href=css/v4-font-face.min.css rel=stylesheet><link href=css/base.css rel=stylesheet><link id=hljs-light rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css disabled><link id=hljs-dark rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css><link href=style.7bc0d0.min.css rel=stylesheet>
1<script src=https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js></script>
1<script>hljs.highlightAll();</script>
1<script async type="text/javascript" src="/_/static/javascript/readthedocs-addons.js"></script>
1<meta name="readthedocs-project-slug" content="codex-comic-reader" /><meta name="readthedocs-version-slug" content="latest" /><meta name="readthedocs-resolver-filename" content="/" /><meta name="readthedocs-http-status" content="200" /></head> <body class=homepage> <div class="navbar fixed-top navbar-expand-lg navbar-dark bg-primary"> <div class=container> <a class=navbar-brand href=.>Codex</a> <button type=button class=navbar-toggler data-bs-toggle=collapse data-bs-target=#navbar-collapse aria-controls=navbar-collapse aria-expanded=false aria-label="Toggle navigation"> <span class=navbar-toggler-icon></span> </button> <div id=navbar-collapse class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li class=nav-item> <a href=NEWS/ class=nav-link>ð Codex News</a> </li> <li class=nav-item> <a href=DOCKER/ class=nav-link>ð³ Codex Docker Image</a> </li> </ul> <ul class="nav navbar-nav ms-md-auto"> <li class=nav-item> <a href=# class=nav-link data-bs-toggle=modal data-bs-target=#mkdocs_search_modal> <i class="fa fa-search"></i> Search </a> </li> </ul> </div> </div> </div>
1<script src=js/darkmode.js></script>
1 <div class=container> <div class=row> <div class=col-md-3><div class="navbar-expand-md bs-sidebar hidden-print affix" role=complementary> <div class=navbar-header> <button type=button class="navbar-toggler collapsed" data-bs-toggle=collapse data-bs-target=#toc-collapse title="Table of Contents"> <span class="fa fa-angle-down"></span> </button> </div> <div id=toc-collapse class="navbar-collapse collapse card bg-body-tertiary"> <ul class="nav flex-column"> <li class=nav-item data-bs-level=1><a href=#codex class=nav-link>Codex</a> <ul class="nav flex-column"> <li class=nav-item data-bs-level=2><a href=#features class=nav-link>⨠Features</a> <ul class="nav flex-column"> </ul> </li> <li class=nav-item data-bs-level=2><a href=#demonstration class=nav-link>ð Demonstration</a> <ul class="nav flex-column"> </ul> </li> <li class=nav-item data-bs-level=2><a href=#news class=nav-link>ð News</a> <ul class="nav flex-column"> </ul> </li> <li class=nav-item data-bs-level=2><a href=#html-docs class=nav-link>ð¸ï¸ HTML Docs</a> <ul class="nav flex-column"> </ul> </li> <li class=nav-item data-bs-level=2><a href=#installation class=nav-link>ð¦ Installation</a> <ul class="nav flex-column"> </ul> </li> <li class=nav-item data-bs-level=2><a href=#administration class=nav-link>ð Administration</a> <ul class="nav flex-column"> </ul> </li> <li class=nav-item data-bs-level=2><a href=#configuration class=nav-link>ðï¸ Configuration</a> <ul class="nav flex-column"> </ul> </li> <li class=nav-item data-bs-level=2><a href=#use class=nav-link>ð Use</a> <ul class="nav flex-column"> </ul> </li> <li class=nav-item data-bs-level=2><a href=#troubleshooting class=nav-link>𩺠Troubleshooting</a> <ul class="nav flex-column"> </ul> </li> <li class=nav-item data-bs-level=2><a href=#alternatives-to-codex class=nav-link>ðAlternatives to Codex</a> <ul class="nav flex-column"> </ul> </li> <li class=nav-item data-bs-level=2><a href=#popular-comic-utilities class=nav-link>ð§ Popular comic utilities</a> <ul class="nav flex-column"> </ul> </li> <li class=nav-item data-bs-level=2><a href=#contributing class=nav-link>ð¤ Contributing</a> <ul class="nav flex-column"> </ul> </li> <li class=nav-item data-bs-level=2><a href=#support class=nav-link>ð¬ Support</a> <ul class="nav flex-column"> </ul> </li> <li class=nav-item data-bs-level=2><a href=#links class=nav-link>ð Links</a> <ul class="nav flex-column"> </ul> </li> <li class=nav-item data-bs-level=2><a href=#thanks class=nav-link>ðð» Thanks</a> <ul class="nav flex-column"> </ul> </li> <li class=nav-item data-bs-level=2><a href=#enjoy class=nav-link>ð Enjoy</a> <ul class="nav flex-column"> </ul> </li> </ul> </li> </ul> </div> </div></div> <div class=col-md-9 role=main> <h1 id=codex>Codex<a class=headerlink href=#codex title="Permanent link">¶</a></h1> <p>A comic archive browser and reader.</p> <p><img src=/img/logo.svg alt="Codex logo" style=" 2height: 128px; 3width: 128px; 4border-radius: 128px; 5"></p> <blockquote> <p><strong>Note:</strong> The Docker image has moved to <a href=https://github.com/ajslater/codex/pkgs/container/codex>ghcr.io/ajslater/codex</a>. The <code>docker.io/ajslater/codex</code> image is deprecated. It is republished from ghcr.io only so that Codex can warn you to switch. See <a href=https://codex-comic-reader.readthedocs.io/DOCKER/#migrating-from-docker-hub>Migrating from Docker Hub</a>.</p> </blockquote> <h2 id=features>⨠Features<a class=headerlink href=#features title="Permanent link">¶</a></h2> <h3 id=library>ð Library<a class=headerlink href=#library title="Permanent link">¶</a></h3> <ul> <li>Reads <strong>CBZ, CBR, CB7, CBT and PDF</strong> comics.</li> <li><strong>Fastest bulk importer</strong> of any comic server.</li> <li><strong>Watches the filesystem</strong> and auto-imports new or changed comics.</li> <li><strong>Custom covers</strong> for Folders, Publishers, Imprints, Series, and Story Arcs.</li> </ul> <h3 id=browse-search>ð Browse & Search<a class=headerlink href=#browse-search title="Permanent link">¶</a></h3> <ul> <li>Browse a tree of <strong>Publishers, Imprints, Series, Volumes</strong>, your <strong>folder hierarchy</strong>, or by tagged <strong>Story Arc</strong>.</li> <li><strong>Full-text search</strong> across comic metadata and bookmarks.</li> <li><strong>Filter and sort</strong> on any metadata field, including per-user unread status.</li> <li>A <strong>multi-sortable metadata table</strong> view for power browsing.</li> <li><strong>Save and load</strong> named views and searches.</li> <li><strong>Favorites</strong> at the Publisher, Series, Volume, Folder, Story Arc, or Issue level â filterable per user.</li> </ul> <h3 id=edit-tag>ð·ï¸ Edit & Tag<a class=headerlink href=#edit-tag title="Permanent link">¶</a></h3> <ul> <li><strong>Edit tags</strong> on one or many comics right in the browser â credits, story arcs, identifiers, and more â written back to your comic files. Requires the comics directory mounted <strong>writable</strong> with filesystem write permission.</li> <li><strong>Online tagging</strong> looks up and applies metadata from online sources, with interactive match prompts and per-source credentials in the Admin Tagging tab.</li> <li><strong>Rename comic files</strong> to the comicbox naming scheme derived from their tags, with a filename preview and confirmation, when editing tags or tagging online.</li> </ul> <h3 id=read>ð Read<a class=headerlink href=#read title="Permanent link">¶</a></h3> <ul> <li>Adapts to any screen with multiple <strong>aspect ratios and reading directions</strong>.</li> <li><strong>Per-user bookmarks and reading settings</strong>, preserved even without an account.</li> </ul> <h3 id=users-access>ð¥ Users & Access<a class=headerlink href=#users-access title="Permanent link">¶</a></h3> <ul> <li><strong>Anonymous browsing</strong> or registration-required mode â your choice.</li> <li><strong>Private libraries</strong> restricted to specific groups of users.</li> <li>Optional <strong>age restrictions</strong> for age-tagged comics.</li> <li>Optional <strong>self-service password reset</strong> by email (requires SMTP config).</li> </ul> <h3 id=integrations>ð Integrations<a class=headerlink href=#integrations title="Permanent link">¶</a></h3> <ul> <li><strong>OPDS 1 & 2</strong> syndication with streaming, search, and authentication.</li> <li><strong>OIDC single sign-on</strong> with identity providers like Authentik and Authelia.</li> <li><strong>Remote-User HTTP header SSO</strong> for reverse-proxy single sign-on (tinyauth, Authelia, Authentik proxy mode).</li> <li><strong>Fail2Ban</strong> log for IP-banning failed login attempts.</li> </ul> <h3 id=operations>𪶠Operations<a class=headerlink href=#operations title="Permanent link">¶</a></h3> <ul> <li>Runs in <strong>1 GB of RAM</strong> (faster with more).</li> <li><strong>GPLv3</strong> licensed.</li> </ul> <h3 id=examples>Examples<a class=headerlink href=#examples title="Permanent link">¶</a></h3> <ul> <li><em>Filter by</em> Story Arc and Unread, <em>Order by</em> Publish Date to create an event reading list.</li> <li><em>Filter by</em> Unread and <em>Order by</em> Added Time to see your latest unread comics.</li> <li><em>Search by</em> your favorite character to find their appearances across different c
5omics.</li> </ul> <h2 id=demonstration>ð Demonstration<a class=headerlink href=#demonstration title="Permanent link">¶</a></h2> <p>You may browse a <a href=https://demo.codex-reader.app/ >live demo server</a> to get a feel for Codex.</p> <h2 id=news>ð News<a class=headerlink href=#news title="Permanent link">¶</a></h2> <p>Codex has a <a href=NEWS/ >NEWS file</a> to summarize changes that affect users.</p> <h2 id=html-docs>ð¸ï¸ HTML Docs<a class=headerlink href=#html-docs title="Permanent link">¶</a></h2> <p><a href=https://codex-comic-reader.readthedocs.io>HTML formatted docs are available here</a></p> <h2 id=installation>ð¦ Installation<a class=headerlink href=#installation title="Permanent link">¶</a></h2> <h3 id=install-run-with-docker>Install & Run with Docker<a class=headerlink href=#install-run-with-docker title="Permanent link">¶</a></h3> <p>Run the official <a href=https://github.com/ajslater/codex/pkgs/container/codex>Docker Image</a> at ghcr.io/ajslater/codex.</p> <p>Read the <a href=docs/DOCKER/ >Docker instructions</a></p> <p>You’ll then want to read the <a href=#administration>Administration</a> section of this document.</p> <h3 id=install-run-on-homeassistant-server>Install & Run on HomeAssistant server<a class=headerlink href=#install-run-on-homeassistant-server title="Permanent link">¶</a></h3> <p>If you have a <a href=https://www.home-assistant.io/ >HomeAssistant</a> server, Codex can be installed with the following steps :</p> <ul> <li>Add the <code>https://github.com/alexbelgium/hassio-addons</code> repository by <a href="https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Falexbelgium%2Fhassio-addons">clicking here</a></li> <li>Install the addon : <a href=https://my.home-assistant.io/redirect/supervisor>click here to automatically open the addon store, then install the addon</a></li> <li>Customize addon options, then then start the add-on.</li> </ul> <h3 id=install-run-as-a-native-application>Install & Run as a Native Application<a class=headerlink href=#install-run-as-a-native-application title="Permanent link">¶</a></h3> <p>You can also run Codex as a natively installed python application with pip. Codex requires Python 3.12 or later.</p> <h4 id=binary-dependencies>Binary Dependencies<a class=headerlink href=#binary-dependencies title="Permanent link">¶</a></h4> <p>You’ll need to install the appropriate system dependencies for your platform before installing Codex.</p> <h5 id=linux-dependencies>Linux Dependencies<a class=headerlink href=#linux-dependencies title="Permanent link">¶</a></h5> <h6 id=debian-dependencies>Debian Dependencies<a class=headerlink href=#debian-dependencies title="Permanent link">¶</a></h6> <p>…and Ubuntu, Mint, MX, Window Subsystem for Linux, and others.</p> <pre><code class=language-sh>apt install build-essential libimagequant0 libjpeg-turbo8 libopenjp2-7 libssl3 libyaml-0-2 libtiff6 libwebp7 python3-dev python3-pip sqlite3 unrar zlib1g 6</code></pre> <p>Versions of packages like libjpeg, libssl, libtiff may differ between flavors and versions of your distribution. If the package versions listed in the example above are not available, try searching for ones that are with <code>apt-cache</code> or <code>aptitude</code>.</p> <pre><code class=language-sh>apt-cache search libjpeg-turbo 7</code></pre> <h6 id=alpine-dependencies>Alpine Dependencies<a class=headerlink href=#alpine-dependencies title="Permanent link">¶</a></h6> <pre><code class=language-sh>apk add bsd-compat-headers build-base jpeg-dev libffi-dev libwebp openssl-dev sqlite yaml-dev zlib-dev 8</code></pre> <h5 id=install-unrar-runtime-dependency-on-non-debian-linux>Install unrar Runtime Dependency on non-debian Linux<a class=headerlink href=#install-unrar-runtime-dependency-on-non-debian-linux title="Permanent link">¶</a></h5> <p>Codex requires <a href=https://www.rarlab.com>RARLAB’s unrar</a> to read CBR comic archives; the bsdtar and 7-Zip fallbacks cannot extract them. Unrar is often not packaged for Linux, but here are some instructions: <a href=https://www.unixtutorial.org/how-to-install-unrar-in-linux/ >How to install unrar in Linux</a></p> <p>Unrar as packaged for Alpine Linux v3.14 seems to work on Alpine v3.15+</p> <h5 id=macos-dependencies>macOS Dependencies<a class=headerlink href=#macos-dependencies title="Permanent link">¶</a></h5> <p>Using <a href=https://brew.sh/ >Homebrew</a>:</p> <pre><code class=language-sh>brew install jpeg libffi libyaml libzip openssl python sqlite unrar webp 9</code></pre> <h4 id=windows-installation>Windows Installation<a class=headerlink href=#windows-installation title="Permanent link">¶</a></h4> <p>Windows users are encouraged to use Docker to run Codex, but it will also run natively on the Windows Subsystem for Linux.</p> <p>Installation instructions are in the <a href=docs/WINDOWS/ >Native Windows Dependencies Installation Document</a>.</p> <h4 id=run-codex-natively>Run Codex Natively<a class=headerlink href=#run-codex-natively title="Permanent link">¶</a></h4> <p>Once you have installed codex, the codex binary should be on your path. To start codex, run:</p> <pre><code class=language-sh>codex 10</code></pre> <h3 id=use-codex>Use Codex<a class=headerlink href=#use-codex title="Permanent link">¶</a></h3> <p>
10Once installed and running you may navigate to <a href=http://localhost:9810/ >http://localhost:9810/</a></p> <p><a name=administration></a></p> <h2 id=administration>ð Administration<a class=headerlink href=#administration title="Permanent link">¶</a></h2> <h3 id=navigate-to-the-admin-panel>Navigate to the Admin Panel<a class=headerlink href=#navigate-to-the-admin-panel title="Permanent link">¶</a></h3> <ul> <li>Click the hamburger menu â° to open the browser settings drawer.</li> <li>Log in as the ‘admin’ user. The default administrator password is also ‘admin’.</li> <li>Navigate to the Admin Panel by clicking on its link in the browser settings drawer after you have logged in.</li> </ul> <h3 id=change-the-admin-password>Change the Admin password<a class=headerlink href=#change-the-admin-password title="Permanent link">¶</a></h3> <p>The first thing you should do is log in as the admin user and change the admin password.</p> <ul> <li>Navigate to the Admin Panel as described above.</li> <li>Select the Users tab.</li> <li>Change the admin user’s password using the small lock button.</li> <li>You may also change the admin user’s name with the edit button.</li> <li>You may create other users and grant them admin privileges by making them staff.</li> </ul> <h3 id=add-comic-libraries>Add Comic Libraries<a class=headerlink href=#add-comic-libraries title="Permanent link">¶</a></h3> <p>The second thing you will want to do is log in as an Administrator and add one or more comic libraries.</p> <ul> <li>Navigate to the Admin Panel as described above.</li> <li>Select the Libraries tab in the Admin Panel</li> <li>Add a Library with the “+ LIBRARY” button in the upper left.</li> </ul> <h3 id=reset-the-admin-password>Reset the admin password<a class=headerlink href=#reset-the-admin-password title="Permanent link">¶</a></h3> <p>If you forget all your superuser passwords, you may restore the original default admin account by running codex with the <code>CODEX_RESET_ADMIN</code> environment variable set.</p> <pre><code class=language-sh>CODEX_RESET_ADMIN=1 codex 11</code></pre> <p>or, if using Docker:</p> <pre><code class=language-sh>docker run -e CODEX_RESET_ADMIN=1 -v host-parent-dir/config:/config ghcr.io/ajslater/codex 12</code></pre> <h3 id=default-settings-for-new-sessions>ðï¸ Default Settings for New Sessions<a class=headerlink href=#default-settings-for-new-sessions title="Permanent link">¶</a></h3> <p>The Admin Panel’s Defaults tab sets the browser and reader settings new sessions start with: the top collection and which collections to show, the sort order, cover or table view and the table columns for each collection, the time and filename display, the bookmark filter, and the reader’s fit, reading direction, two pages, page turn animation and caching. It replaces the old Default View setting.</p> <ul> <li>Defaults apply to new sessions and to browser and reader resets. A new user keeps the settings their anonymous session started with.</li> <li>Visitors who already have settings keep them. To move existing anonymous sessions that still hold the old default, tick “Also apply to existing anonymous sessions” before you save. Settings a visitor changed are kept.</li> <li>A collection’s default table columns apply to every visitor who hasn’t picked their own columns for that collection.</li> <li>Folders can only be the default top collection while Folder View is on.</li> <li>OPDS keeps the factory defaults, except its top collection.</li> </ul> <p><a name=backup-restore-user-data></a></p> <h3 id=backup-restore-user-data>ð¾ Backup & Restore User Data<a class=headerlink href=#backup-restore-user-data title="Permanent link">¶</a></h3> <p>Codex’s main SQLite database (<code>codex.sqlite3</code>) holds two very different kinds of data: comic metadata, which can always be rebuilt by re-scanning your library, and <em>user</em> data â accounts, bookmarks, favorites, browser settings, library definitions, admin flags â which cannot. To make the second kind survive a database loss or rebuild, Co
12dex snapshots every user-bound row into a separate SQLite file: the <strong>user data sidecar</strong>.</p> <h4 id=where-it-lives>Where it lives<a class=headerlink href=#where-it-lives title="Permanent link">¶</a></h4> <pre><code class=language-text><CODEX_CONFIG_DIR>/user_data.sqlite 13</code></pre> <p>â right next to <code>codex.toml</code>.</p> <h4 id=what-it-contains>What it contains<a class=headerlink href=#what-it-contains title="Permanent link">¶</a></h4> <p>Users (with hashed passwords), groups & permissions, group memberships, libraries and their access lists, bookmarks, favorites, per-user browser settings, admin flags, timestamps, online-tagging defaults, and the default settings for new sessions.</p> <p>It deliberately does <strong>not</strong> mirror anything derivable from a filesystem re-scan: comics, publishers, series, volumes, folders, story arcs, tags, credits, the full-text search index. Those rebuild themselves when the librarian re-imports your library.</p> <h4 id=taking-a-snapshot>Taking a snapshot<a class=headerlink href=#taking-a-snapshot title="Permanent link">¶</a></h4> <p>The sidecar is <strong>not</strong> continuously updated. A snapshot is taken:</p> <ul> <li>automatically every night, as part of the existing Janitor sweep (right after the database backup); and</li> <li>on demand, whenever you click <strong>Snapshot Now</strong> on the Admin Panel’s <strong>Restore</strong> tab, or run <code>Snapshot User Data Sidecar</code> from the Jobs tab.</li> </ul> <p>Each snapshot <strong>replaces</strong> the previous contents â the file is always a true point-in-time mirror of the main DB, never a partial log.</p> <h4 id=backing-it-up-offsite>Backing it up offsite<a class=headerlink href=#backing-it-up-offsite title="Permanent link">¶</a></h4> <p>Copy <code>user_data.sqlite</code> somewhere safe. The file is a single self-contained SQLite database; no companion files are required to restore from it (the <code>-wal</code>/<code>-shm</code> siblings, if present, can be left behind).</p> <p>You can copy it while Codex is running, but for the cleanest backup, take a fresh snapshot first (Restore tab â Snapshot Now) and then copy the file.</p> <h4 id=restoring>Restoring<a class=headerlink href=#restoring title="Permanent link">¶</a></h4> <p>Two equivalent paths:</p> <p><strong>From the Admin Panel:</strong> open the <strong>Restore</strong> tab and click <em>Restore Now</em>. A dry-run option reports what <em>would</em> happen without writing.</p> <p><strong>From the command line:</strong></p> <pre><code class=language-sh>codex restore_user_data # default sidecar in CODEX_CONFIG_DIR 14codex restore_user_data --dry-run # report only 15codex restore_user_data --from /path/to/another/user_data.sqlite 16</code></pre> <p>Both paths are idempotent: re-running a restore on top of an already-restored database is safe. Rows whose targets can’t be resolved (a deleted comic, a renamed tag) are logged to <code>restore_user_data.log</code> in your config directory and skipped â the operation never aborts.</p> <h4 id=migrating-to-a-new-host>Migrating to a new host<a class=headerlink href=#migrating-to-a-new-host title="Permanent link">¶</a></h4> <ol> <li>On the old host, take a fresh snapshot (Restore tab â Snapshot Now).</li> <li>Stop the old Codex.</li> <li>Copy <code>user_data.sqlite</code> to the new host’s config directory.</li> <li>Start Codex on the new host with your comics mounted at the same library paths.</li> <li>Let the librarian finish its initial filesystem scan, then run <code>codex restore_user_data</code> (or click <em>Restore Now</em> in the admin panel).</li> </ol> <p>Bookmarks reattach by comic path, favorites by collection name-chain (e.g. publisher â imprint â series), and tag filters by tag name. As long as your library paths and tag names match, everything reattaches.</p> <h3 id=private-libraries>Private Libraries<a class=headerlink href=#private-libraries title="Permanent link">¶</a></h3> <p>In the Admin Panel you may configure private libraries that are only accessible to specific groups.</p> <p>A library with <em>no</em> groups is accessible to every user including anonymous users.</p> <p>A library with <em>any</em>
16 groups is accessible only to users who are in those groups.</p> <p>Use the Groups admin panel to create groups and the Users admin panel to add and remove users to groups.</p> <h4 id=include-and-exclude-groups>Include and Exclude Groups<a class=headerlink href=#include-and-exclude-groups title="Permanent link">¶</a></h4> <p>Codex can make groups for libraries that exclude groups of users or exclude everyone and include only certain groups of users.</p> <h3 id=pdf-metadata>PDF Metadata<a class=headerlink href=#pdf-metadata title="Permanent link">¶</a></h3> <p>Codex reads PDF metadata from the filename, PDF metadata fields and also many formats of common complex comic metadata if they are embedded in the PDF <code>keywords</code> field.</p> <p>If you decide to include PDFs in your comic library, I recommend taking time to rename your files so Codex can find some metadata. Codex recognizes several file naming schemes. This one has good results:</p> <p><code>{series} v{volume} #{issue} {title} ({year}) {ignored}.pdf</code></p> <p>Complex comic metadata, such as ComicInfo.xml, can be also be embedded in the keywords field by using the <a href=https://github.com/ajslater/comicbox>comicbox</a> command line tool. Codex will read this data because it relies on comicbox internally. Not many people use comicbox or embedded metadata in PDFs in this fashion, so you likely won’t find it unless you’ve added it yourself.</p> <h3 id=api-with-key-access>ðï¸ API with Key Access<a class=headerlink href=#api-with-key-access title="Permanent link">¶</a></h3> <p>Codex has a limited number of API endpoints available with API Key Access. The API Key is available on the admin/stats tab.</p> <h3 id=email-password-reset>ð§ Email & Password Reset<a class=headerlink href=#email-password-reset title="Permanent link">¶</a></h3> <p>By default Codex has no outbound email and the “Forgot password?” link is hidden. To enable self-service password reset, configure SMTP in the Admin Panel’s <strong>Email</strong> tab â editable without a restart. You must set at least a host and either a from-address or user; until then the feature stays off and the reset endpoints return 404.</p> <p>The <code>[email]</code> section in <code>codex.toml</code> (and the <code>CODEX_EMAIL_*</code> environment variables) is <strong>deprecated</strong>: those values are imported once on upgrade and otherwise only seed the initial Admin settings. See the <a href=#full-codextoml-reference>Full <code>codex.toml</code> Reference</a> below.</p> <p>Provider-specific notes:</p> <ul> <li><strong>Gmail</strong>: requires a 16-character App Password (regular passwords are rejected by Google’s SMTP). Account â Security â App Passwords. Use <code>port = 587</code>, <code>use_tls = true</code>, and set <code>from_address</code> to the same address as <code>user</code>.</li> <li><strong>Amazon SES</strong>: <code>from_address</code> MUST be a verified SES identity (the domain or address). Use the SMTP credentials from the SES console, not IAM keys directly.</li> <li><strong>Mailgun / SendGrid / generic SMTP relays</strong>: usually accept <code>user</code> as the authenticated sender; <code>from_address</code> defaults to that when blank.</li> </ul> <p>If <code>register_verification</code> is enabled (Admin â Flags â “Verify New User Email”), new sign-ups receive an activation email and stay inactive until they click the link. Has no effect until email (SMTP) is configured.</p> <p>Existing users created before this feature won’t have an email on file and can’t request a reset; admins can backfill addresses on the Users admin tab, or users can set their own via Profile (the user menu).</p> <h2 id=configuration>ðï¸ Configuration<a class=headerlink href=#configuration title="Permanent link">¶</a></h2> <h3 id=config-dir>Config Dir<a class=headerlink href=#config-dir title="Permanent link">¶</a></h3> <p>The default config directory is <code>config/</code> directly under the working directory you run codex from. You may specify an alternate config directory with the environment variable <code>CODEX_CONFIG_DIR</code>.</p> <p>The config directory contains a file named <code>codex.toml</code> where you can specify ports and bind addresses. If no <code>codex.toml</code> is present Codex copies a default one to that directory on startup. e.g.</p> <pre><code class=language-toml>[server] 17host = "0.0.0.0" 18port = 9810 19url_path_prefix = "" 20</code></pre> <p>The config directory also holds the main sqlite database, the <code>user_data.sqlite</code> sidecar (see <a href=#backup-restore-user-data>Backup & Restore User Data</a>), a Django cache, and comic book cover thumbnails.</p> <h3 id=full-codextoml-reference>
20Full <code>codex.toml</code> Reference<a class=headerlink href=#full-codextoml-reference title="Permanent link">¶</a></h3> <p>All available options with their defaults. Uncomment to override. Codex writes this file to the config directory on first startup if one is not already present.</p> <blockquote> <p><strong>Deprecated since v2.0.0:</strong> the <code>[browser]</code>, <code>[throttle]</code>, and <code>[email]</code> sections (and their matching <code>CODEX_*</code> environment variables) are now managed in the Admin Panel â browser page size and rate limits on the <strong>Settings</strong> tab, SMTP on the <strong>Email</strong> tab â and are editable without a restart. Values here are imported once on upgrade and otherwise only seed the initial Admin settings. The <code>[server]</code>, <code>[logging]</code>, <code>[cache]</code>, and <code>[auth]</code> sections are still configured here.</p> </blockquote> <pre><code class=language-toml># Codex Configuration File 21# Copy to config/codex.toml and edit as needed. 22# Environment variables override values in this file. 23# See README.md for full documentation. 24 25# [server] 26# Granian ASGI server settings 27# host = "0.0.0.0" 28# port = 9810 29# Number of worker processes. 1 is recommended for containerized environments. 30# workers = 1 31# HTTP version: "auto", "1", or "2" 32# http = "auto" 33# Enable websockets (required for Codex live updates) 34# websockets = true 35# HTTP path prefix for codex (e.g. "/codex" for reverse proxy sub-path) 36# url_path_prefix = "" 37 38# [logging] 39# Log level: TRACE, DEBUG, INFO, SUCCESS, WARNING, ERROR, CRITICAL 40# loglevel = "INFO" 41# log_retention = "6 months" 42# log_to_console = true 43# log_to_file = true 44# Directory for log files. Defaults to <config_dir>/logs. 45# log_dir = "" 46 47# [cache] 48# Directory for the file-based cache (covers, query results, etc). 49# Defaults to <config_dir>/cache. 50# dir = "" 51 52# [browser] 53# Deprecated: now set on the Admin Settings tab (seeds the initial default). 54# max_obj_per_page = 100 55 56# [throttle] 57# Deprecated: now set on the Admin Settings tab (seeds initial defaults). 58# Rate limiting (requests per minute). 0 = disabled. 59# anon = 0 60# user = 0 61# opds = 0 62# opensearch = 0 63# Password reset requests per hour, per IP. Defaults to 5. 64# reset_password = 5 65 66# [email] 67# Deprecated: now set on the Admin Email tab (seeds initial defaults). 68# SMTP configuration for outbound email. When unset, password-reset and 69# email-verification features stay disabled and the related endpoints 70# respond 404. Set `host` AND `from_address` (or `user`) to enable. 71# 72# IMPORTANT: codex.toml stores credentials in plain text. Restrict file 73# permissions (chmod 600) and consider environment variable overrides 74# (CODEX_EMAIL_PASSWORD) for secrets management systems. 75# 76# host = "" 77# port = 587 78# user = "" 79# password = "" 80# use_tls = true 81# use_ssl = false 82# timeout = 10 83# Sender address. If blank, `user` is used as the From address. Many 84# providers accept the auth user as sender; SES and similar require an 85# explicit verified identity here. 86# from_address = "" 87# subject_prefix = "[Codex] " 88 89# [auth] 90# Allows authentication without authorization via the Remote-User header. 91# Only enable if you have authorization in front of Codex. Dangerous. 92# remote_user = false 93# Log failed login attempts to a separate file. Useful as input for 94# banning tools like fail2ban, CrowdSec, or sshguard. 95# Line format: "<ISO timestamp> | Failed login from <ip> user=<username>" 96# Example fail2ban failregex: 97# ^\s*\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \| Failed login from <HOST> user=.*$ 98# failed_login_log = false 99# Path to the failed-login log. Defaults to <log_dir>/failed_logins.log. 100# failed_login_log_path = "" 101# When behind a reverse proxy, trust X-Forwarded-For for the client IP. 102# Disable if Codex is exposed directly (otherwise clients can forge their IP). 103# failed_login_log_trust_forwarded_for = true 104</code></pre> <h3 id=environment-variables>Environment Variables<a class=headerlink href=#environment-variables title="Permanent link">¶</a></h3> <p>Environment variables override values set in the TOML config file.</p> <h4 id=general>General<a class=headerlink href=#general title="Permanent link">¶</a></h4> <ul> <li><code>TIMEZONE</code> or <code>TZ</code> will explicitly set the timezone in long format (e.g. <code>"America/Los Angeles"</code>). This is useful inside Docker because codex cannot automatically detect the host machine’s timezone.</li> <li><code>DEBUG_TRANSFORM</code> will show verbose information about how the comicbox library reads all archive metadata sources and transforms it into a the comicbox schema.</li> <li><code>CODEX_CONFIG_DIR</code> will set the path to codex config directory. Defaults to <code>$CWD/config</code></li> </ul> <h5 id=server>Server<a class=headerlink href=#server title="Permanent link">¶</a></h5> <ul> <li><code>GRANIAN_HOST</code> the IP or hostname to serve Codex from. Defaults to “0.0.0.0”, all interfaces.</li> <li><code>GRANIAN_PORT</code> the port to serve Codex from. Defaults to 9810.</li> <li><code>GRANIAN_WORKERS</code>
104 Number of worker processes. 1 recommended for containerized environments.</li> <li><code>GRANIAN_HTTP</code> HTTP protocol to use. “auto”, “1” or “2”. Defaults to “auto”. Generally you want to serve codex from behind nginx or traefik which will handle the protocol, even HTTP 3, so this should stay on “auto”.</li> <li><code>GRANIAN_WEBSOCKETS</code> Enable websockets. Required for codex live updates. Default true.</li> <li><code>GRANIAN_URL_PATH_PREFIX</code> HTTP path prefix for codex (e.g. “/codex” for reverse proxy sub-path). Defaults to “”.</li> </ul> <h5 id=repair>Repair<a class=headerlink href=#repair title="Permanent link">¶</a></h5> <ul> <li><code>CODEX_RESET_ADMIN=1</code> will reset the admin user and its password to defaults when codex starts.</li> <li><code>CODEX_FIX_FOREIGN_KEYS=1</code> will check for and try to repair illegal foreign keys on startup.</li> <li><code>CODEX_INTEGRITY_CHECK=1</code> will perform database integrity check on startup.</li> <li><code>CODEX_FTS_INTEGRITY_CHECK=1</code> will perform an integrity check on the full text search index.</li> <li><code>CODEX_FTS_REBUILD=1</code> will rebuild the full text search index.</li> </ul> <h4 id=logging>Logging<a class=headerlink href=#logging title="Permanent link">¶</a></h4> <ul> <li><code>LOGLEVEL</code> will change how verbose codex’s logging is. Valid values are <code>CRITICAL</code>, <code>ERROR</code>, <code>WARNING</code>, <code>SUCCESS</code>, <code>INFO</code>, <code>DEBUG</code>, and the overly noisy <code>TRACE</code>. The default is <code>INFO</code>.</li> <li><code>CODEX_LOG_DIR</code> sets a custom directory for saving logfiles. Defaults to <code>$CODEX_CONFIG_DIR/logs</code></li> <li><code>CODEX_LOG_RETENTION</code> how long to keep logs. Defaults to “6 months”.</li> <li><code>CODEX_LOG_TO_FILE=0</code> will not log to files.</li> <li><code>CODEX_LOG_TO_CONSOLE=0</code> will not log to the console.</li> </ul> <h4 id=cache>Cache<a class=headerlink href=#cache title="Permanent link">¶</a></h4> <ul> <li><code>CODEX_CACHE_DIR</code> sets a custom directory for the file-based cache (Django cache entries and comic book cover thumbnails). Defaults to <code>$CODEX_CONFIG_DIR/cache</code>. Useful for placing the cache on a separate (e.g. faster or ephemeral) volume from the rest of the config directory.</li> </ul> <h5 id=browser>Browser<a class=headerlink href=#browser title="Permanent link">¶</a></h5> <blockquote> <p><strong>Deprecated:</strong> set Browser Page Size on the Admin <strong>Settings</strong> tab instead; this only seeds the initial default.</p> </blockquote> <ul> <li><code>CODEX_BROWSER_MAX_OBJ_PER_PAGE</code> the maximum number of objects per page. Defaults to 100.</li> </ul> <h4 id=throttling>Throttling<a class=headerlink href=#throttling title="Permanent link">¶</a></h4> <blockquote> <p><strong>Deprecated:</strong> set rate limits on the Admin <strong>Settings</strong> tab instead; these variables only seed the initial defaults.</p> </blockquote> <p>Codex contains some experimental throttling controls. The value supplied to these variables will be interpreted as the maximum number of allowed requests per minute. For example, the following settings would limit each described group to 2 queries per second.</p> <ul> <li><code>CODEX_THROTTLE_ANON=30</code> Anonymous users</li> <li><code>CODEX_THROTTLE_USER=30</code> Authenticated users</li> <li><code>CODEX_THROTTLE_OPDS=30</code> The OPDS v1 & v2 APIs (Panels uses this for search)</li> <li><code>CODEX_THROTTLE_OPENSEARCH=30</code> The OPDS v1 Opensearch API</li> </ul> <h4 id=authentication>Authentication<a class=headerlink href=#authentication title="Permanent link">¶</a></h4> <ul> <li><code>CODEX_AUTH_REMOTE_USER</code> will allow unauthenticated logins with the Remote-User HTTP header. This can be very insecure if not configured properly. Please read the Remote-User docs devoted to it below.</li> <li><code>CODEX_AUTH_FAILED_LOGIN_LOG=1</code> will append every failed login attempt (form login and OPDS Basic auth) to a separate log file for consumption by banning tools like fail2ban, CrowdSec, or sshguard. Disabled by default. See the <a href=#failed-login-log>Failed-Login Log</a> section below.</li> <li><code>CODEX_AUTH_FAILED_LOGIN_LOG_PATH</code> overrides the failed-login log path. Defaults to <code>$CODEX_LOG_DIR/failed_logins.log</code>.</li> <li><code>CODEX_AUTH_FAILED_LOGIN_LOG_TRUST_FORWARDED_FOR=0</code> makes the failed-login log use <code>REMOTE_ADDR</code> instead of the leftmost <code>X-Forwarded-For</code> entry. Default is <code>1</code> (trust XFF), which is correct when Codex sits behind a reverse proxy. Set to <code>0</code> when Codex is exposed directly so that clients can’t forge <code>X-Forwarded-For</code> to poison the log.</li> <li>OIDC single sign-on has no environment variables or TOML keys â it is configured in the Admin UI under the <strong>Auth</strong> tab. See the <a href=#oauth-oidc>OIDC Single Sign On</a> section below.</li> </ul> <h3 id=reverse-proxy>Reverse Proxy<a class=headerlink href=#reverse-proxy title="Permanent link">¶</a></h3> <p><a href=https://nginx.org/ >nginx</a> is often used as a TLS terminator and subpath proxy.</p> <p>Here’s an example nginx config with a subpath named ‘/codex’.</p> <pre><code class=language-nginx># Only send "Connection: upgrade" for real WebSocket requests; plain HTTP 105# requests get "Connection: close". This map must live in the http {} block. 106map $http_upgrade $connection_upgrade { 107 default upgrade; 108 '' close; 109} 110 111# Use "^~" so this prefix wins over any regex location (e.g. a shared 112# "location ~* \.(js|css|...)$" or "/static" block common in multi-app / 113# Organizr / SWAG setups). Without "^~", such a regex hijacks requests like 114# /codex/static/assets/*.js and returns 404 even though Codex serves them 115# correctly, giving a blank page with a working favicon. See GitHub #784. 116location ^~ /codex { 117 proxy_pass http://codex:9810; 118 # HTTP 119 proxy_set_header Host $http_host; 120 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 121 proxy_set_header X-Forwarded-Host $server_name; 122 proxy_set_header X-Forwarded-Port $server_port; 123 proxy_set_header X-Forwarded-Proto $scheme; 124 proxy_set_header X-Real-IP $remote_addr; 125 proxy_set_header X-Scheme $scheme; 126 # Websockets (Codex live updates)
127 proxy_http_version 1.1; 128 proxy_set_header Upgrade $http_upgrade; 129 proxy_set_header Connection $connection_upgrade; 130 # Codex reads http basic authentication. 131 # If the nginx credentials are different than codex credentials use this line to 132 # not forward the authorization. 133 proxy_set_header Authorization ""; 134 # Let clients see "Server: codex/<version>" instead of "Server: nginx". 135 # nginx hides the upstream Server header by default. See below. 136 proxy_pass_header Server; 137} 138</code></pre> <p>Specify a reverse proxy sub path (if you have one) in <code>config/codex.toml</code></p> <pre><code class=language-toml>[server] 139url_path_prefix = "/codex" 140</code></pre> <p>If you put an authenticating reverse proxy (Organizr, Authelia, etc.) in front of the <code>/codex</code> location with <code>auth_request</code>, exempt the API the way Codex’s own examples do (<code>location /codex/api { auth_request off; ... }</code>); the <code>^~</code> note above is what keeps the static assets reachable.</p> <h4 id=preserving-the-codex-server-header>Preserving the Codex Server Header<a class=headerlink href=#preserving-the-codex-server-header title="Permanent link">¶</a></h4> <p>Codex identifies itself with a <code>Server: codex/<version></code> response header, which is how OPDS clients, uptime monitors and support requests tell a Codex install apart from whatever is proxying it. nginx does <strong>not</strong> pass that header through: by default it hides <code>Date</code>, <code>Server</code>, <code>X-Accel-...</code> and <code>X-Pad</code> from the upstream response and substitutes its own <code>Server: nginx/<version></code>. Add <a href=https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass_header><code>proxy_pass_header</code></a> to the Codex location (as in the example above) to un-hide it:</p> <pre><code class=language-nginx>proxy_pass_header Server; 141</code></pre> <pre><code class=language-console>$ curl -sI https://example.com/codex/ | grep -i '^server' 142server: codex/2.2.8 143</code></pre> <p>Notes:</p> <ul> <li><code>server_tokens off;</code> is not a substitute. It only shortens nginx’s own header to <code>Server: nginx</code>; the Codex header is still discarded.</li> <li>Like <code>proxy_set_header</code>, <code>proxy_pass_header</code> is inherited from the enclosing <code>http {}</code> / <code>server {}</code> block <strong>only if no <code>proxy_pass_header</code> is defined in the location itself</strong>. Keep it in the same block as the rest of the Codex proxy headers.</li> <li>Other reverse proxies have their own rules for the <code>Server</code> header; if <code>curl -sI</code> shows something other than <code>codex/<version></code>, check your proxy’s documentation for passing upstream response headers through.</li> </ul> <h4 id=nginx-reverse-proxy-502-when-container-refreshes>Nginx Reverse Proxy 502 when container refreshes<a class=headerlink href=#nginx-reverse-proxy-502-when-container-refreshes title="Permanent link">¶</a></h4> <p>Nginx requires a special trick to refresh dns when linked Docker containers recreate. See this <a href=https://tenzer.dk/nginx-with-dynamic-upstreams/ >nginx with dynamix upstreams</a> article.</p> <h4 id=single-sign-on-and-third-party-authentication>Single Sign On and Third Party Authentication<a class=headerlink href=#single-sign-on-and-third-party-authentication title="Permanent link">¶</a></h4> <p><a name=oauth-oidc></a></p> <h5 id=oauth-oidc>OAuth & OIDC<a class=headerlink href=#oauth-oidc title="Permanent link">¶</a></h5> <p>Codex is a native OIDC client. Point it at an identity provider like <a href=https://goauthentik.io/ >Authentik</a> or <a href=https://www.authelia.com/ >Authelia</a> and a “Login with ⦔ button appears on the login dialog and the unauthorized screen. (tinyauth is not an OIDC provider â tinyauth users should use the <a href=#remote-user-authentication>Remote-User</a> method below instead.)</p> <p>Configure it in the Admin UI under the <strong>Auth</strong> tab â there are no TOML keys or environment variables for OIDC. Set at minimum the <strong>Provider Name</strong> (the login button label), the issuer <strong>Server URL</strong> (discovery is fetched from <code><server-url>/.well-known/openid-configuration</code>), and the <strong>Client ID</strong>, then flip the enable switch; the client secret is stored encrypted at rest. A <strong>Test Connection</strong> button fetches the provider’s discovery document and reports the endpoints it advertises before you commit to the config. Every change takes effect on the next request â no restart. The tab also carries the user-mapping knobs (username claim, auto-provisioning, email linking, group sync, admin group) and RP-initiated logout, all documented inline.</p> <p>Register this redirect URI with your identity provider (including your <code>url_path_prefix</code> if you use one):</p> <pre><code class=language-text>https://your-host[/url_path_prefix]/sso/oidc/login/callback/ 144</code></pre> <p><strong>Identity mapping.</strong> Users are keyed on the stable OIDC <code>sub</code> claim. On first login, Codex links the login to an existing local user with the same username (and by email if <strong>Link by Email</strong> is on); otherwise it creates a new user when <strong>Create Users on First Login</strong> is on. With <strong>Sync Groups</strong> on, the identity provider’s groups claim replaces the user’s Codex groups on every login â only existing Codex groups are matched, never created â so IdP groups can drive Codex library access controls. <strong>Admin Group</strong> members are granted (and, when absent from the claim, revoked) Codex admin rights.</p> <p>â ï¸ Username linking includes admin accounts: an identity-provider user named <code>admin</code> links to Codex’s built-in <code>admin</code> account. Only enable OIDC against an identity provider whose username namespace you control, and change the default admin password regardless. â ï¸</p> <p><strong>Provider notes.</strong></p> <ul> <li><strong>Authentik:</strong> create an OAuth2/OpenID provider + application. The default <code>openid profile email</code> scope works as-is; <code>groups</code> is included in the profile scope. RP-initiated logout works.</li> <li><strong>Authelia:</strong> register Codex as a client in <code>identity_providers.oidc</code> with a hashed client secret. Keep <strong>Fetch Userinfo</strong> on (Authelia serves username and email claims only from the userinfo endpoint) and add <code>groups</code> to the scope for group sync. Authelia does not implement RP-initiated logout; leave it off.</li> </ul> <p><strong>Good to know.</strong></p> <ul> <li>The client secret is entered in the Admin UI and stored encrypted at rest â it never appears in <code>codex.toml</code> or the API responses.</li> <li>Codex sessions live 60 days; disabling a user at the identity provider does not end their existing Codex session. Deactivate the user in Codex too, or lower <code>SESSION_COOKIE_AGE</code>-equivalent exposure by logging them out.</li> <li>OIDC-created users have no local password, so OPDS readers can’t use HTTP Basic auth for them. Use the per-user API token (User Profile in the sidebar) for OPDS, or set a local password.</li> <li>On first start after upgrading, Codex applies a few small new database tables for the OIDC account linkage and the settings singleton.</li> <li>OIDC login failures are recorded in the <a href=#failed-login-log>Failed-Login Log</a> when it is enabled.</li> </ul> <h5 id=remote-user-authentication>Remote-User Authentication<a class=headerlink href=#remote-user-authentication title="Permanent link">¶</a></h5> <p>Remote-User authentication tells Codex to accept a username from the webserver and assume that authentication has already been done. This is very insecure if you haven’t configured an authenticating reverse proxy in front of Codex. This is the right method for forward-auth gateways like <a href=https://tinyauth.app/ >tinyauth</a>, and also works with Authelia’s <code>auth_request</code> mode and Authentik’s proxy outpost.</p> <p>Here’s a complete nginx location for the tinyauth case, assuming the tinyauth server itself is already configured and reachable at <code>tinyauth:3000</code>:</p> <pre><code class=language-nginx># In the http {} block (shared with the WebSocket map above). 145# The internal auth endpoint nginx consults for every request. 146location = /tinyauth { 147 internal; 148 proxy_pass http://tinyauth:3000/api/auth/nginx; 149 proxy_set_header Host $host; 150 proxy_set_header X-Original-URL $scheme://$http_host$request_uri; 151 proxy_pass_request_body off; 152 proxy_set_header Content-Length ""; 153} 154 155location ^~ /codex { 156 auth_request /tinyauth; 157 # Pass the authenticated username to Codex, OVERRIDING anything the 158 # client sent. Never pass the client's own Remote-User through. 159 auth_request_set $remote_user $upstream_http_remote_user; 160 proxy_set_header Remote-User $remote_user; 161 proxy_pass http://codex:9810; 162 # ... the same proxy_set_header / WebSocket lines as the 163 # Reverse Proxy example above ... 164} 165</code></pre> <p>The equivalent Traefik middleware (<code>forwardAuth</code> with <code>authResponseHeaders: [Remote-User]</code>) or Caddy directive (<code>forward_auth</code> with <code>copy_headers Remote-User</code>) works the same way: the proxy authenticates, then injects <code>Remote-User</code> toward Codex.</p> <p>Then enable it in Codex with <code>CODEX_AUTH_REMOTE_USER=1</code> or:</p> <pre><code class=language-toml>[auth] 166remote_user = true 167</code></pre> <p>â ï¸ Only turn on the <code>CODEX_AUTH_REMOTE_USER</code> environment variable if your webserver sets the <code>Remote-User</code> header itself every time for the Codex location, overriding any malicious client that might set it themselves. â ï¸</p> <p>Checklist for a forward-auth deployment:</p> <ul> <li>The auth gate covers the <strong>whole</strong> Codex location â including <code>/opds/</code> and the WebSocket path <code>/api/v4/ws</code> â not just the HTML pages. If you exempt OPDS for reader apps, those requests fall back to Codex’s own Basic/token auth, which is fine.</li> <li>A request with a client-forged <code>Remote-User</code>
167 header and no proxy auth must be rejected or have the header overwritten (test it with <code>curl -H "Remote-User: admin"</code>).</li> <li>Remote-User and OIDC can be enabled at the same time; they are independent paths to a normal Codex session.</li> </ul> <h5 id=http-token-authentication>HTTP Token Authentication<a class=headerlink href=#http-token-authentication title="Permanent link">¶</a></h5> <p>You can also configure your proxy to add token authentication to the headers. Codex will read âBearerâ prefixed authorization tokens. The token is unique for each user and may be found in the User Profile accessible from the sidebar. You must configure your proxy or single sign on software to send this token.</p> <pre><code class=language-nginx>set user_token 'user-token-taken-from-web-ui'; 168proxy_set_header Authorization "Bearer $user_token"; 169</code></pre> <h3 id=failed-login-log>Failed-Login Log<a class=headerlink href=#failed-login-log title="Permanent link">¶</a></h3> <p>Codex can append every failed login attempt to a dedicated log file in a format easy for IP-banning tools (fail2ban, CrowdSec, sshguard, etc.) to parse. The feature is off by default. Enable it by setting <code>CODEX_AUTH_FAILED_LOGIN_LOG=1</code> or in <code>codex.toml</code>:</p> <pre><code class=language-toml>[auth] 170failed_login_log = true 171# failed_login_log_path = "" # defaults to <log_dir>/failed_logins.log 172# failed_login_log_trust_forwarded_for = true # set false if exposed directly 173</code></pre> <p>A single signal receiver covers both the form login at <code>/api/v4/auth/login</code> and OPDS HTTP Basic auth â no separate setup per endpoint. The IP-bearing line is written <strong>only</strong> to <code>failed_logins.log</code>; the main <code>codex.log</code> still records Django’s standard <code>"Unauthorized: /api/v4/auth/login"</code> (or <code>"Forbidden: ..."</code>) WARNING for the same request, so the failure is visible in the main log without the client IP. This keeps PII (IP + username) concentrated in one file that you can chmod, forward to a SIEM, or retain on its own schedule.</p> <p>Each line looks like:</p> <pre><code class=language-log>2026-05-10 12:34:56 | Failed login from 192.168.1.42 user=alice 174</code></pre> <h4 id=x-forwarded-for-trust>X-Forwarded-For trust<a class=headerlink href=#x-forwarded-for-trust title="Permanent link">¶</a></h4> <p>The client IP is taken from the leftmost <code>X-Forwarded-For</code> entry when <code>failed_login_log_trust_forwarded_for = true</code> (the default), falling back to <code>REMOTE_ADDR</code>. This is correct when Codex sits behind a reverse proxy that sets the header (the typical Docker deployment).</p> <p>If Codex is exposed directly on its port, set <code>failed_login_log_trust_forwarded_for = false</code> â otherwise a client can set their own <code>X-Forwarded-For: 8.8.8.8</code> and your banning tool will ban that address instead of the real attacker.</p> <h4 id=example-fail2ban-filter>Example fail2ban filter<a class=headerlink href=#example-fail2ban-filter title="Permanent link">¶</a></h4> <p><code>/etc/fail2ban/filter.d/codex.conf</code>:</p> <pre><code class=language-ini>[Definition] 175failregex = ^\s*\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \| Failed login from <HOST> user=.*$ 176ignoreregex = 177</code></pre> <p><code>/etc/fail2ban/jail.d/codex.conf</code>:</p> <pre><code class=language-ini>[codex] 178enabled = true 179filter = codex 180logpath = /path/to/codex/config/logs/failed_logins.log 181maxretry = 5 182findtime = 10m 183bantime = 1h 184</code></pre> <p>Validate the filter against a real log with <code>fail2ban-regex</code> before enabling the jail:</p> <pre><code class=language-sh>fail2ban-regex /path/to/codex/config/logs/failed_logins.log /etc/fail2ban/filter.d/codex.conf 185</code></pre> <h3 id=restricted-memory-environments>Restricted Memory Environments<a class=headerlink href=#restricted-memory-environments title="Permanent link">¶</a></h3> <p>Codex can run with as little as 1GB available RAM. Large batch jobs âlike importing and indexing tens of thousands of comics at onceâ will run faster the more memory is available to Codex. The biggest gains in speed happen when you increase memory up to about 6GB. Codex batch jobs do get faster the more memory it has above 6GB, but with diminishing returns.</p> <p>If you must run Codex in an admin restricted memory environment you might want to temporarily give Codex a lot of memory to run a very large import job and then restrict it for normal operation.</p> <h2 id=use>ð Use<a class=headerlink href=#use title="Permanent link">¶</a></h2> <h3 id=sessions-accounts>ð¤ Sessions & Accounts<a class=headerlink href=#sessions-accounts title="Permanent link">¶</a></h3> <p>Once your administrator has added some comic libraries, you may browse and read comics. Codex will remember your preferences, bookmarks and progress in the browser session. A new session starts with the default settings your administrator chose. Codex destroys anonymous sessions and bookmarks after 60 days. To preserve these settings across browsers and after sessions expire, you may register an account with a username and password. You will have to contact your administrator to reset your password if you forget it.</p> <h3 id=opds>ᯤ OPDS<a class=headerlink href=#opds title="Permanent link">¶</a></h3> <p>Codex supports OPDS syndication and OPDS streaming. You may find the OPDS url in the side drawer. It should take the form:</p> <p><code>http(s)://host.tld(:9810)(/path_prefix)/opds/v1.2/</code></p> <p>or</p> <p><code>http(s)://host.tld(:9810)(/path_prefix)/opds/v2.0/</code></p> <h4 id=opds-v1-clients>OPDS v1 Clients<a class=headerlink href=#opds-v1-clients title="Permanent link">¶</a></h4> <ul> <li>iOS<ul> <li><a href=https://panels.app/ >Panels</a></li> <li><a href=https://pocketbook.ch/ >PocketBooks</a></li> <li><a href=http://kybook-reader.com/ >KYBook 3</a></li> <li><a href=https://apps.apple.com/us/app/chunky-comic-reader/id663567628>Chunky Comic Reader</a></li> </ul> </li> <li>Android<ul> <li><a href="https://play.google.com/store/apps/details?id=com.flyersoft.moonreader">Moon+</a></li> <li><a href="https://play.google.com/store/apps/details?id=com.foobnix.pdf.reader">Librera</a></li> </ul> </li> </ul> <p>
185Kybook 3 does not seem to support http basic authentication, so Codex users are not supported.</p> <h4 id=opds-v2-clients>OPDS v2 Clients<a class=headerlink href=#opds-v2-clients title="Permanent link">¶</a></h4> <ul> <li> <p>iOS & macOS</p> <ul> <li><a href=https://www.stumpapp.dev/guides/mobile/app>Stump</a> - Technically an Alpha, but stable and featureful.</li> </ul> </li> <li> <p>Multi Platform Mobile & Desktop</p> <ul> <li><a href=https://readest.com/ >Readest</a></li> </ul> </li> <li> <p>Desktop</p> <ul> <li><a href=https://github.com/comiccatcher/comiccatcher>ComicCatcher</a></li> </ul> </li> </ul> <h4 id=opds-authentication>OPDS Authentication<a class=headerlink href=#opds-authentication title="Permanent link">¶</a></h4> <h5 id=opds-login>OPDS Login<a class=headerlink href=#opds-login title="Permanent link">¶</a></h5> <p>The few clients that implement the OPDS 1.0 Authentication spec present the user with a login screen for interactive authentication.</p> <h5 id=http-basic>HTTP Basic<a class=headerlink href=#http-basic title="Permanent link">¶</a></h5> <p>Some OPDS clients allow configuring HTTP Basic authentication in their OPDS server settings. If the don’t, you will have to add your username and password to the URL. In that case the OPDS url will look like:</p> <p><code>http(s)://username:[email protected](:9810)(/path_prefix)/opds/v1.2/</code></p> <h5 id=http-token>HTTP Token<a class=headerlink href=#http-token title="Permanent link">¶</a></h5> <p>Some clients allow adding a unique login token to the HTTP headers. Codex will read “Bearer” prefixed authorization tokens. The token is unique for each user and may be found in the Web UI sidebar.</p> <h4 id=reading-positions>Reading Positions<a class=headerlink href=#reading-positions title="Permanent link">¶</a></h4> <p>OPDS v2 clients sync reading positions with <code>GET</code>/<code>PUT /opds/v2.0/comics/<id>/position</code>, an <a href=https://drafts.opds.io/opds-progression-1.0.html>OPDS Progression 1.0</a> document. Two Codex behaviors are worth knowing before you write a client against it:</p> <ul> <li><strong>An empty <code>200</code> means “no position recorded yet”, not an error.</strong> The <a href=https://drafts.opds.io/opds-progression-1.0.html>OPDS Progression 1.0</a> draft answers a book that has never been opened with <code>200 OK</code> and an empty payload, and Codex does the same. An empty body is distinct from a document with <code>progression: 0.0</code> (a book open on its first page), which is the distinction a Start vs. Continue button needs. Treat an empty <code>200</code> as “no position” and <code>404</code> as “no such book, or not yours.” (Releases before v2.4.3 answered <code>204 No Content</code> here.) Errors other than <code>401</code> carry an <a href=https://www.rfc-editor.org/rfc/rfc7807>RFC 7807</a> problem document; a successful <code>PUT</code> returns the stored Progression Document.</li> <li><strong>A position belongs to whoever asks for it.</strong> Codex keys a reading position to the authenticated user, or, for anonymous use, to the session cookie. A client that browses feeds with HTTP Basic auth but fetches positions without it â or that discards cookies between requests â is asking as a different reader each time and will see an empty <code>200</code> where it expects its own position. Send the same credentials on position requests that you send on feed requests.</li> <li><strong>“Keep Reading” lists issues in progress.</strong> The OPDS <em>Keep Reading</em> group lists the issues you have started and not finished, most recently read first, so every entry there has a position. It is absent when nothing is in progress. The <em>Latest Unread</em> and <em>Oldest Unread</em> groups include issues you have never opened, and their position is the empty <code>200</code> above.</li> </ul> <p>A comic whose file has temporarily vanished from disk is held for a retention window rather than deleted outright. For that window it is hidden from every feed and its position reads <code>404</code>, even for the reader who was in the middle of it. Position writes are still accepted, so nothing a client syncs during the outage is lost – it becomes readable again when the file returns.</p> <h4 id=supported-opds-specifications>Supported OPDS Specifications<a class=headerlink href=#supported-opds-specifications title="Permanent link">¶</a></h4> <h5 id=opds-v1>OPDS v1<a class=headerlink href=#opds-v1 title="Permanent link">¶</a></h5> <ul> <li><a href=https://specs.opds.io/opds-1.2.html>OPDS 1.2</a></li> <li>
185<a href=https://github.com/anansi-project/opds-pse/blob/master/v1.2.md>OPDS-PSE 1.2</a></li> <li><a href=https://drafts.opds.io/authentication-for-opds-1.0.html>OPDS Authentication 1.0 (draft)</a></li> </ul> <h5 id=opds-v2>OPDS v2<a class=headerlink href=#opds-v2 title="Permanent link">¶</a></h5> <ul> <li> <p><a href=https://specs.opds.io/opds-2.0.html>OPDS Catalog 2.0</a></p> </li> <li> <p><a href=https://github.com/readium/webpub-manifest/blob/master/profiles/divina.md>OPDS 2.0 Digital Visual Narratives Profile (DiViNa)</a></p> </li> <li> <p><a href=https://drafts.opds.io/authentication-for-opds-1.0.html>OPDS Authentication 1.0 (draft)</a></p> </li> <li> <p><a href=https://drafts.opds.io/opds-progression-1.0.html>OPDS Progression 1.0 (draft)</a></p> </li> <li> <p><a href=https://github.com/opds-community/drafts/discussions/43>OPDS 2.0 Authentication (proposal)</a></p> </li> </ul> <h5 id=opensearch-v1>OpenSearch v1<a class=headerlink href=#opensearch-v1 title="Permanent link">¶</a></h5> <ul> <li><a href=https://github.com/dewitt/opensearch>OpenSearch 1.1 (draft)</a></li> </ul> <h2 id=troubleshooting>𩺠Troubleshooting<a class=headerlink href=#troubleshooting title="Permanent link">¶</a></h2> <h3 id=logs>ð Logs<a class=headerlink href=#logs title="Permanent link">¶</a></h3> <p>Codex collects its logs in the <code>config/logs</code> directory. Take a look to see what th e server is doing.</p> <p>You can change how much codex logs by setting the <code>LOGLEVEL</code> environment variable. By default this level is <code>INFO</code>. To see more verbose messages, run codex like:</p> <pre><code class=language-sh>LOGLEVEL=DEBUG codex 186</code></pre> <h3 id=doctor>𩺠Doctor<a class=headerlink href=#doctor title="Permanent link">¶</a></h3> <p>The Admin panel’s Doctor tab reports whether this install can read each comic archive format, has the image codecs cover matching needs, whether comicbox’s config and package versions are in order, and whether the database, config directory, library folders, filesystem watcher and stored credentials are usable, with a fix for each problem. Codex logs the problems when it starts, too.</p> <p>The same report runs from the command line, which helps when Codex will not start. In Codex’s Python environment:</p> <pre><code class=language-sh>comicbox doctor 187</code></pre> <p>In Docker:</p> <pre><code class=language-sh>docker exec codex comicbox doctor 188</code></pre> <h3 id=watching-filesystem-events-with-docker>Watching Filesystem Events with Docker<a class=headerlink href=#watching-filesystem-events-with-docker title="Permanent link">¶</a></h3> <p>Codex tries to watch for filesystem events to instantly update your comic libraries when they change on disk. But these native filesystem events are not translated between macOS & Windows Docker hosts and the Docker Linux container. If you find that your installation is not updating to filesystem changes instantly, you might try enabling polling for the affected libraries and decreasing the <code>poll_every</code> value in the Admin console to a frequency that suits you.</p> <h3 id=emergency-database-repair>Emergency Database Repair<a class=headerlink href=#emergency-database-repair title="Permanent link">¶</a></h3> <p>If the database becomes corrupt, Codex includes a facility to rebuild the database. Place a file named <code>rebuild_db</code> in your Codex config directory like so:</p> <pre><code class=language-sh>touch config/rebuild_db 189</code></pre> <p>Shut down and restart Codex.</p> <p>The next time Codex starts it will back up the existing database and try to rebuild it. The database lives in the config directory as the file <code>config/codex.sqlite3</code>. If this procedure goes kablooey, you may recover the original database at <code>config/backups/codex.sqlite3.before-rebuild.bak</code>. Codex will remove the <code>rebuild_db</code> file.</p> <h3 id=warnings-to-ignore>Warnings to Ignore<a class=headerlink href=#warnings-to-ignore title="Permanent link">¶</a></h3> <h4 id=streaminghttpresponse-iterator-warning>StreamingHttpResponse Iterator Warning<a class=headerlink href=#streaminghttpresponse-iterator-warning title="Permanent link">¶</a></h4> <pre><code class=language-pycon>packages/django/http/response.py:517: Warning: StreamingHttpResponse must consume synchronous iterators in order to serve them asynchronously. Use an asynchronous iterator instead. 190</code></pre> <p>This is a known warning and does not represent anything bad happening. It’s an artifact of the Django framework slowly supporting asynchronous server endpoints and unfortunately isn’t practical to remove yet.</p> <h2 id=alternatives-to-codex>ðAlternatives to Codex<a class=headerlink href=#alternatives-to-codex title="Permanent link">¶</a></h2> <ul> <li><a href=https://www.kavitareader.com/ >Kavita</a> has light metadata filtering/editing, supports comics, eBooks, and features for manga.</li> <li><a href=https://komga.org/ >Komga</a> has light metadata editing and duplicate page elimination.</li> <li><a href=https://vaemendis.net/ubooquity/ >Ubooquity</a> reads both comics and eBooks.</li> </ul> <h2 id=popular-comic-utilities>ð§ Popular comic utilities<a class=headerlink href=#popular-comic-utilities title="Permanent link">¶</a></h2> <ul> <li><a href=https://mylar.nerdfirehurricane.com/ >Mylar</a> is the best comic book manager which also has a built in reader.</li> <li><a href=https://comicbox.readthedocs.io/ >Comicbox</a> is a powerful command line comic metadata editor, multi source online tagger, and multi metadata format synthesizer. It is what
190Codex uses under the hood to read comic metadata.</li> <li><a href=https://github.com/Metron-Project/metron-tagger>Metron Tagger</a> is a command line comic metadata editor. It will tag identified comics from online database sources.</li> <li><a href=https://github.com/comictagger/comictagger>Comictagger</a> is a comic metadata editor. It comes with a command line and desktop GUI. It will tag identified comics from online database sources.</li> </ul> <h2 id=contributing>ð¤ Contributing<a class=headerlink href=#contributing title="Permanent link">¶</a></h2> <h3 id=bug-reports>ð Bug Reports<a class=headerlink href=#bug-reports title="Permanent link">¶</a></h3> <p>Issues and feature requests are best filed on the <a href=https://github.com/ajslater/codex/issues>Github issue tracker</a>.</p> <h2 id=support>ð¬ Support<a class=headerlink href=#support title="Permanent link">¶</a></h2> <p>I and other Codex users answer questions on the <a href=https://discord.gg/CU5kKxv7kg>Codex Comic Server Discord</a></p> <h3 id=develop>ð Develop<a class=headerlink href=#develop title="Permanent link">¶</a></h3> <p>Codex’s git repo is mirrored on <a href=https://github.com/ajslater/codex/ >Github</a></p> <p>Codex is a Django Python webserver with a VueJS front end.</p> <p><code>/codex/codex/</code> is the main django app which provides the webserver and database.</p> <p><code>/codex/frontend/</code> is where the vuejs frontend lives.</p> <p>Most of Codex development is now controlled through the Makefile. Type <code>make</code> for a list of commands.</p> <h2 id=links>ð Links<a class=headerlink href=#links title="Permanent link">¶</a></h2> <ul> <li><a href=https://github.com/ajslater/codex/pkgs/container/codex>Docker Image</a></li> <li><a href=https://pypi.org/project/codex/ >PyPi Package</a></li> <li><a href=https://github.com/ajslater/codex/ >GitHub Project</a></li> </ul> <h2 id=thanks>ðð» Thanks<a class=headerlink href=#thanks title="Permanent link">¶</a></h2> <ul> <li>Thanks to <a href=https://pypi.org/user/ajmazurie/ >Aurélien Mazurie</a> for allowing me to use the PyPi name ‘codex’.</li> <li>To <a href=https://github.com/professionaltart>ProfessionalTart</a> for providing native Windows installation instructions.</li> <li>Thanks to the good people of <a href=https://mylar.nerdfirehurricane.com/ >Mylar</a> continuous feedback and comic ecosystem education.</li> </ul> <h2 id=enjoy>ð Enjoy<a class=headerlink href=#enjoy title="Permanent link">¶</a></h2> <p><img alt="These simple people have managed to tap into the spiritual forces that mystics and yogis spend literal lifetimes seeking. I feel... ...I feel..." src=docs/strange.jpg></p></div> </div> </div> <footer class=col-md-12> <hr> <p>Documentation built with <a href=https://www.mkdocs.org/ >MkDocs</a>.</p> </footer>
190<script src=js/bootstrap.bundle.min.js></script>
190
190<script> 191 var base_url = ".", 192 shortcuts = {"help": 191, "next": 78, "previous": 80, "search": 83}; 193 </script>
193
193<script src=js/base.js></script>
193
193<script src=search/main.js></script>
193 <div class=modal id=mkdocs_search_modal tabindex=-1 role=dialog aria-labelledby=searchModalLabel aria-hidden=true> <div class="modal-dialog modal-lg"> <div class=modal-content> <div class=modal-header> <h4 class=modal-title id=searchModalLabel>Search</h4> <button type=button class=btn-close data-bs-dismiss=modal aria-label=Close></button> </div> <div class=modal-body> <p>From here you can search these documents. Enter your search terms below.</p> <form> <div class=form-group> <input type=search class=form-control placeholder=Search... id=mkdocs-search-query title="Type search term here"> </div> </form> <div id=mkdocs-search-results data-no-results-text="No results found"></div> </div> <div class=modal-footer> </div> </div> </div> </div><div class=modal id=mkdocs_keyboard_modal tabindex=-1 role=dialog aria-labelledby=keyboardModalLabel aria-hidden=true> <div class=modal-dialog> <div class=modal-content> <div class=modal-header> <h4 class=modal-title id=keyboardModalLabel>Keyboard Shortcuts</h4> <button type=button class=btn-close data-bs-dismiss=modal aria-label=Close></button> </div> <div class=modal-body> <table class=table> <thead> <tr> <th style="width: 20%;">Keys</th> <th>Action</th> </tr> </thead> <tbody> <tr> <td class="help shortcut"><kbd>?</kbd></td> <td>Open this help</td> </tr> <tr> <td class="next shortcut"><kbd>n</kbd></td> <td>Next page</td> </tr> <tr> <td class="prev shortcut"><kbd>p</kbd></td> <td>Previous page</td> </tr> <tr> <td class="search shortcut"><kbd>s</kbd></td> <td>Search</td> </tr> </tbody> </table> </div> <div class=modal-footer> </div> </div> </div> </div> </body> </html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.