Skip to main content

Docker Deployment

Docker is the recommended deployment method for most users. Codex provides pre-built Docker images that include the web frontend.

Quick Start with Docker Run

The simplest way to run Codex is with a single docker run command using SQLite:

docker run -d \
--name codex \
-p 8080:8080 \
-v /path/to/your/library:/library:ro \
-v codex-data:/app/data \
-e PUID=1000 \
-e PGID=1000 \
-e CODEX_AUTH__JWT_SECRET="$(openssl rand -base64 32)" \
ghcr.io/ashdevfr/codex:latest

Access Codex at http://localhost:8080. On first launch, you'll be guided through a setup wizard to create your admin account.

:::tip Finding Your User ID Run id in your terminal to find your UID and GID. Use these values for PUID and PGID to avoid permission issues with mounted volumes. :::

Replace /path/to/your/library with the path to your comics, manga, or ebooks folder.

Understanding the Volumes

VolumeContainer PathPurpose
Your library/libraryYour comics, manga, and ebooks (read-only)
codex-data/app/dataSQLite database, thumbnails, and uploads

:::tip Library Permissions Mount your media library as read-only (:ro) to prevent accidental modifications. Codex only needs read access to your files. :::

Quick Start with Docker Compose

For a more maintainable setup, use Docker Compose. Create a docker-compose.yml file:

services:
codex:
image: ghcr.io/ashdevfr/codex:latest
container_name: codex
ports:
- "8080:8080"
volumes:
# Your media library (read-only)
- /path/to/your/library:/library:ro
# Codex data: SQLite database, thumbnails, uploads
- codex-data:/app/data
environment:
# User/Group ID for file permissions (run `id` to find yours)
PUID: 1000
PGID: 1000
# Generate with: openssl rand -base64 32
CODEX_AUTH__JWT_SECRET: "your-secure-secret-here"
restart: unless-stopped

volumes:
codex-data:

Then start Codex:

# Start Codex
docker compose up -d

# View logs
docker compose logs -f codex

Access Codex at http://localhost:8080. On first launch, you'll be guided through a setup wizard to create your admin account.

:::tip Finding Your User ID Run id in your terminal to find your UID and GID. Use these values for PUID and PGID to avoid permission issues with mounted volumes. :::

Multiple Libraries

To add multiple library paths, mount each one separately:

services:
codex:
image: ghcr.io/ashdevfr/codex:latest
container_name: codex
ports:
- "8080:8080"
volumes:
# Multiple libraries
- /mnt/comics:/library/comics:ro
- /mnt/manga:/library/manga:ro
- /mnt/ebooks:/library/ebooks:ro
# Codex data
- codex-data:/app/data
environment:
CODEX_AUTH__JWT_SECRET: "your-secure-secret-here"
restart: unless-stopped

volumes:
codex-data:

Then create libraries in the Codex UI pointing to /library/comics, /library/manga, etc.

Using a Bind Mount for Data

If you prefer using a local directory instead of a Docker volume for Codex data:

services:
codex:
image: ghcr.io/ashdevfr/codex:latest
container_name: codex
ports:
- "8080:8080"
volumes:
- /path/to/your/library:/library:ro
# Use a local directory for data
- ./codex-data:/app/data
environment:
CODEX_AUTH__JWT_SECRET: "your-secure-secret-here"
restart: unless-stopped

This makes it easier to backup the database and thumbnails.

PostgreSQL Setup (Optional)

For larger libraries or multi-user setups, you can use PostgreSQL:

services:
postgres:
image: postgres:16-alpine
container_name: codex-postgres
environment:
POSTGRES_USER: codex
POSTGRES_PASSWORD: codex
POSTGRES_DB: codex
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U codex"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped

codex:
image: ghcr.io/ashdevfr/codex:latest
container_name: codex
ports:
- "8080:8080"
depends_on:
postgres:
condition: service_healthy
volumes:
- /path/to/your/library:/library:ro
- codex-data:/app/data
environment:
CODEX_AUTH__JWT_SECRET: "your-secure-secret-here"
CODEX_DATABASE__DB_TYPE: postgres
CODEX_DATABASE__POSTGRES__HOST: postgres
CODEX_DATABASE__POSTGRES__PORT: 5432
CODEX_DATABASE__POSTGRES__USERNAME: codex
CODEX_DATABASE__POSTGRES__PASSWORD: codex
CODEX_DATABASE__POSTGRES__DATABASE_NAME: codex
# CODEX_SCHEDULER__TIMEZONE: America/Los_Angeles # IANA timezone for cron jobs (default: UTC)
restart: unless-stopped

volumes:
postgres-data:
codex-data:

:::tip Timezone for Cron Jobs The Docker TZ environment variable does not affect Codex cron scheduling. To run scheduled library scans in your local timezone, set CODEX_SCHEDULER__TIMEZONE to a valid IANA timezone name (e.g., America/Los_Angeles, Europe/London). Each library can also override this with its own timezone in the scanning configuration. :::

Separate Worker Containers (Advanced)

For high-performance setups or when you need to scale workers independently from the web server, you can run dedicated worker containers. This requires PostgreSQL.

:::info Why Separate Workers? Separating workers from the web server allows you to:

  • Scale background task processing independently
  • Isolate resource-intensive tasks (scanning, thumbnail generation) from API requests
  • Run more workers than web server instances :::

This example uses YAML anchors (x-codex-common) to share configuration between services:

# Shared configuration for all Codex services
x-codex-common: &codex-common
image: ghcr.io/ashdevfr/codex:latest
volumes:
- /path/to/your/library:/library:ro
- codex-data:/app/data
environment: &codex-env
CODEX_AUTH__JWT_SECRET: "your-secure-secret-here"
CODEX_DATABASE__DB_TYPE: postgres
CODEX_DATABASE__POSTGRES__HOST: postgres
CODEX_DATABASE__POSTGRES__PORT: 5432
CODEX_DATABASE__POSTGRES__USERNAME: codex
CODEX_DATABASE__POSTGRES__PASSWORD: codex
CODEX_DATABASE__POSTGRES__DATABASE_NAME: codex
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped

services:
postgres:
image: postgres:16-alpine
container_name: codex-postgres
environment:
POSTGRES_USER: codex
POSTGRES_PASSWORD: codex
POSTGRES_DB: codex
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U codex"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped

# Web server (API only, no workers)
codex:
<<: *codex-common
container_name: codex-web
ports:
- "8080:8080"
environment:
<<: *codex-env
# Workers run in their own container
CODEX_TASK__RUN_IN_PROCESS: "false"
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 10s
timeout: 5s
retries: 5

# Dedicated worker container
codex-worker:
<<: *codex-common
container_name: codex-worker
# Run the worker command instead of serve
command: ["codex", "worker"]
environment:
<<: *codex-env
# Number of parallel task workers
CODEX_TASK__WORKER_COUNT: "4"
# The web container applies migrations; this one waits for them
CODEX_DATABASE__RUN_MIGRATIONS: "false"
depends_on:
postgres:
condition: service_healthy
codex:
condition: service_healthy

volumes:
postgres-data:
codex-data:

Key Configuration Options

VariableDescriptionDefault
CODEX_TASK__RUN_IN_PROCESSRun background workers in this containertrue
CODEX_DATABASE__RUN_MIGRATIONSApply migrations at startup; when false, wait for themtrue
CODEX_TASK__WORKER_COUNTNumber of parallel task workers2

Scaling Workers

To run multiple worker instances:

docker compose up -d --scale codex-worker=3

:::warning SQLite Not Supported Separate worker containers require PostgreSQL. SQLite does not support multiple processes accessing the database simultaneously. :::

Volume Considerations

VolumePurposePermissions
/app/dataDatabase (SQLite), thumbnails, uploadsRead-write
/libraryMedia filesRead-only (recommended)

Health Checks

You can add health checks to your Docker Compose configuration:

healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 10s
timeout: 5s
retries: 5

User/Group Permissions (PUID/PGID)

Codex supports PUID and PGID environment variables to run with specific user/group permissions. This is essential when using bind mounts to ensure the container can read/write to your host directories.

VariableDescriptionDefault
PUIDUser ID to run as1000
PGIDGroup ID to run as1000

To find your user's UID and GID:

id
# Output: uid=1000(youruser) gid=1000(youruser) ...

Example with Custom PUID/PGID

services:
codex:
image: ghcr.io/ashdevfr/codex:latest
environment:
PUID: 1000
PGID: 1000
CODEX_AUTH__JWT_SECRET: "your-secret-here"
volumes:
- ./codex-data:/app/data
- ./codex-config:/app/config
- /mnt/media:/library:ro

:::tip Synology NAS Users Synology DSM often uses different UIDs. Check your user's ID with id via SSH, or use the admin user's UID (usually 1026 or similar). :::

Environment Variables

Common environment variables for Docker:

VariableDescriptionExample
PUIDUser ID for file permissions1000
PGIDGroup ID for file permissions1000
CODEX_AUTH__JWT_SECRETJWT signing secretyour-secret-key
CODEX_DATABASE__DB_TYPEDatabase typepostgres or sqlite
CODEX_DATABASE__POSTGRES__HOSTPostgreSQL hostpostgres
CODEX_DATABASE__POSTGRES__PASSWORDPostgreSQL passwordsecret
CODEX_LOGGING__LEVELLog levelinfo, debug

See Configuration for all options.

Updating

# If using docker run
docker pull ghcr.io/ashdevfr/codex:latest
docker stop codex
docker rm codex
# Then run the docker run command again

# If using Docker Compose
docker compose pull
docker compose up -d

# Check logs for migration status
docker compose logs codex

Troubleshooting

Container Won't Start

# Check logs
docker compose logs codex

# Or for docker run
docker logs codex

Database Connection Issues

# Test PostgreSQL connection
docker compose exec postgres psql -U codex -d codex -c "SELECT 1"

# Check network
docker compose exec codex ping postgres

Permission Issues

If you see Permission denied (os error 13) errors:

  1. Check your host user's UID/GID:

    id
    # uid=1000(user) gid=1000(user) ...
  2. Set PUID/PGID in your docker-compose.yml:

    environment:
    PUID: 1000 # Your UID from step 1
    PGID: 1000 # Your GID from step 1
  3. Verify permissions inside the container:

    docker compose exec codex ls -la /app/data
    docker compose exec codex ls -la /app/config
    docker compose exec codex ls -la /library

The container will automatically adjust the internal user to match your PUID/PGID, ensuring it can write to mounted volumes.