Skip to content

Configuration ​

All configuration is done through environment variables. Every variable has a sensible default, so SnapOtter works out of the box without setting any of them.

Environment variables ​

Server ​

VariableDefaultDescription
PORT1349Port the server listens on.
BASE_PATH(empty)Public deployment path, e.g. /snapotter. Empty or / serves at the domain root. Applied at runtime; no image rebuild required.
RATE_LIMIT_PER_MIN1000Maximum requests per minute per IP. Set to 0 to disable rate limiting.
CORS_ORIGIN(empty)Comma-separated allowed origins for CORS, or empty for same-origin only.
LOG_LEVELinfoLog verbosity. One of: fatal, error, warn, info, debug, trace.
TRUST_PROXYloopback,linklocal,uniquelocalWhich peers may set the client IP through X-Forwarded-For. The default believes only a private-network peer, so a reverse proxy on a Docker network or a LAN is trusted and a public client's forged header is not. Set true only when a proxy you control sits in front on a public address.

Authentication ​

The two booleans below accept only true and false. Anything else, 1 or yes or on, fails validation and the server exits before it starts listening.

VariableDefaultDescription
AUTH_ENABLEDtrueRequire a login. Set to false to run with no accounts at all, which grants every request admin rights, so keep that to a trusted network.
DEFAULT_USERNAMEadminUsername for the initial admin account. Only used on first run.
DEFAULT_PASSWORDadminPassword for the initial admin account. Change this after first login.
MAX_USERS0 (unlimited)Maximum number of registered user accounts. Set to 0 for unlimited.
SESSION_DURATION_HOURS168Login session lifetime in hours (default is 7 days).
SKIP_MUST_CHANGE_PASSWORDfalseSet to true to skip the forced password-change prompt on first login.

Storage ​

VariableDefaultDescription
STORAGE_MODElocallocal or s3. S3 and MinIO need a license with the s3_storage feature plus the S3_* variables below.
DATABASE_URLpostgres://snapotter:snapotter@localhost:5432/snapotterPostgreSQL connection string, and the connection every request is served on. On 2.3.0 and later, set DATABASE_MIGRATION_URL too and this becomes the least-privilege runtime role, holding DML on the app's tables and nothing else. The Compose stack points it at its postgres service; leave it unset (together with REDIS_URL) to get embedded mode.
DATABASE_MIGRATION_URL(empty)Available from 2.3.0; the 2.2.0 image ignores it and needs DATABASE_URL to name the database owner. Privileged connection, used only during boot to run migrations and grant the runtime role, then closed before the first request. Leave it empty and SnapOtter runs single-role, with DATABASE_URL doing both jobs. See Least-privilege roles.
REDIS_URLredis://localhost:6379Redis connection string (used for BullMQ job queues). Compose points this at its redis service.
WORKSPACE_PATH./tmp/workspaceDirectory for temporary files during processing. Cleaned up automatically. The image sets /tmp/workspace.
FILES_STORAGE_PATH./data/filesDirectory for persistent user files (uploaded images, saved results). The image sets /data/files.

S3 object storage ​

Only read when STORAGE_MODE=s3. Miss any of the three required ones and startup fails with the name of the variable you left out.

VariableDefaultDescription
S3_BUCKET(empty)Bucket that holds uploads and outputs. Required.
S3_ACCESS_KEY_ID(empty)Access key. Required. In the container you can mount it instead, via S3_ACCESS_KEY_ID_FILE.
S3_SECRET_ACCESS_KEY(empty)Secret key. Required. Same file convention: S3_SECRET_ACCESS_KEY_FILE.
S3_REGIONus-east-1Bucket region.
S3_ENDPOINT(empty)Custom endpoint for MinIO, R2, Backblaze, and other S3-compatible stores. Empty means AWS.
S3_FORCE_PATH_STYLEfalseSet to true for MinIO and anything else that wants endpoint/bucket/key instead of virtual-host addressing.
S3_PREFIX(empty)Key prefix, so one bucket can hold several instances.

Encryption at rest ​

VariableDefaultDescription
DATA_ENCRYPTION_KEY(empty)64 hex characters (32 bytes). Encrypts sensitive settings stored in the database. Anything that is not 64 hex characters is rejected at startup.
DATA_ENCRYPTION_KEY_PREVIOUS(empty)The key you are rotating away from, same format. Set both during a rotation so existing rows still decrypt, then drop this one.

Embedded mode ​

Run the image with no DATABASE_URL and no REDIS_URL and it starts its own PostgreSQL 17 and Redis inside the container, bound to loopback, with all data on the /data volume. This restores the single-command docker run experience for quick start, homelab, and upgrades from 1.x. It is a convenience path, not a production deployment: for production, run the 3-container Compose stack with separate PostgreSQL and Redis. Embedded mode requires running the container as root and is incompatible with arbitrary-UID runtimes (OpenShift, Kubernetes runAsNonRoot); use Compose there.

VariableDefaultDescription
EMBEDDEDautoAuto-enabled when both DATABASE_URL and REDIS_URL are unset. Set to 0 to disable it (the app then fails fast if no external DATABASE_URL/REDIS_URL is set, rather than silently starting an in-container database).
REDIS_MAXMEMORY512mbMemory cap for the embedded Redis (embedded mode only). Lower it on memory-constrained hosts such as a Raspberry Pi.
EMBEDDED_REDIS_TIMEOUT_S180Seconds the boot waits for the embedded Redis to answer PING before giving up. On timeout the container stops and logs why, instead of hanging. Whole seconds from 1 to 999999; there is no unlimited value, because waiting forever is the failure this bound exists to prevent. Raise it only if a slow host trips it.
EMBEDDED_POSTGRES_TIMEOUT_S600Same, for the embedded PostgreSQL. The clock starts after first-boot initdb finishes, so it covers server start and crash recovery rather than the initial database creation.

Upgrading from 1.x: put your old snapotter.db at /data/snapotter.db in the volume and embedded mode imports it into the embedded PostgreSQL on first boot. The import runs once; later boots skip it.

Telemetry note: embedded mode inherits the image's analytics default like any other configuration. The published image ships with analytics on; build with --build-arg SNAPOTTER_ANALYTICS=off, or use the in-app admin opt-out, to disable it.

Processing limits ​

VariableDefaultDescription
MAX_UPLOAD_SIZE_MB0 (unlimited)Maximum file size per upload in megabytes. Set to 0 for unlimited. The published image ships 0; a source build starts at 100.
MAX_BATCH_SIZE0 (unlimited)Maximum number of files in a single batch request. Set to 0 for unlimited. The published image ships 0; a source build starts at 100.
CONCURRENT_JOBS0 (auto)Number of batch jobs that run in parallel. Set to 0 to auto-detect based on available CPU cores.
MAX_MEGAPIXELS0 (unlimited)Maximum image resolution allowed in megapixels. Set to 0 for unlimited.
MAX_WORKER_THREADS0 (auto)Maximum worker threads for image processing. Set to 0 to auto-detect based on available CPU cores.
PROCESSING_TIMEOUT_S0 (no limit)Maximum processing time per request in seconds. Set to 0 for no timeout.
MAX_PIPELINE_STEPS20Maximum number of steps in a pipeline. Set to 0 for no limit.
MAX_CANVAS_PIXELS0 (no limit)Maximum canvas size in pixels for output images. Set to 0 for no limit.
MAX_SVG_SIZE_MB50Largest SVG accepted before sanitizing, in megabytes. 0 behaves differently here than in the rows around it. It removes the pre-parse size cap entirely rather than raising it, so leave this one set.
MAX_PDF_PAGES0 (unlimited)Maximum number of PDF pages for PDF-to-image conversion. Set to 0 for unlimited.

Engines ​

SnapOtter looks up each processing binary on $PATH inside the container, so these stay empty unless you mount a build of your own.

VariableDefaultDescription
FFMPEG_PATH(empty)Path to the ffmpeg binary behind the video and audio tools.
FFPROBE_PATH(empty)Path to ffprobe, which reads media files before they're processed. Point it at the same build as FFMPEG_PATH.
QPDF_PATH(empty)Path to qpdf, used by the PDF tools and to check uploaded PDFs.
SOFFICE_PATH(empty)Path to LibreOffice's soffice, used for office document conversions.
PDFCPU_PATH(empty)Path to pdfcpu, used by some PDF tools.
SNAPOTTER_HW_ACCEL(empty)Hardware video encoder family, nvenc or vaapi. Empty encodes in software.

SNAPOTTER_HW_ACCEL needs an ffmpeg built with that encoder family, and the ffmpeg in the image has neither. With the image's own binary the setting falls back to software encoding and says so in the startup log. To use it, mount your own ffmpeg build, point FFMPEG_PATH and FFPROBE_PATH at it, and give the container the GPU device. For nvenc, also set NVIDIA_DRIVER_CAPABILITIES=compute,utility,video: the image asks for compute,utility only, and without video the NVIDIA runtime doesn't mount the encode library, so the encoder is listed but every encode fails. A path override that doesn't exist or isn't executable is also named in the startup log, and every job that needs that binary fails until you fix the path or unset it.

The published Compose files pass all six through from .env, so setting them there is enough.

Cleanup ​

VariableDefaultDescription
FILE_MAX_AGE_HOURS72How long unsaved processing results (raw uploads and tool outputs) are kept before automatic deletion. Files you explicitly save to the Files library are not affected and persist until you delete them.
CLEANUP_INTERVAL_MINUTES60How often the cleanup job runs.

Appearance ​

VariableDefaultDescription
DEFAULT_THEMElightDefault theme for new sessions. light, dark, or system.
DEFAULT_LOCALEenDefault interface language.
DEFAULT_TOOL_VIEWsidebarDefault tool layout. sidebar or fullscreen.

Docker permissions ​

VariableDefaultDescription
PUID999Run the container process as this UID. Set to match your host user for bind mounts (id -u).
PGID999Run the container process as this GID. Set to match your host group for bind mounts (id -g).

Docker example ​

yaml
services:
  SnapOtter:
    image: snapotter/snapotter:latest
    ports:
      - "1349:1349"
    volumes:
      - SnapOtter-data:/data
      - SnapOtter-workspace:/tmp/workspace
    environment:
      - AUTH_ENABLED=true
      - DEFAULT_USERNAME=admin
      - DEFAULT_PASSWORD=changeme
      - DATABASE_URL=postgres://snapotter:snapotter@postgres:5432/snapotter
      - REDIS_URL=redis://redis:6379
      - MAX_UPLOAD_SIZE_MB=200
      - CONCURRENT_JOBS=4
      - FILE_MAX_AGE_HOURS=12
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: snapotter
      POSTGRES_PASSWORD: snapotter     # Change this for non-local deployments
      POSTGRES_DB: snapotter
    volumes:
      - SnapOtter-pgdata:/var/lib/postgresql/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U snapotter -d snapotter"]
      interval: 10s
      timeout: 5s
      retries: 12

  redis:
    image: redis:8-alpine
    command: ["redis-server", "--maxmemory-policy", "noeviction", "--appendonly", "yes"]
    volumes:
      - SnapOtter-redisdata:/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 12

volumes:
  SnapOtter-data:
  SnapOtter-workspace:
  SnapOtter-pgdata:
  SnapOtter-redisdata:

Volumes ​

The Docker Compose stack uses four volumes:

  • /data (app) - AI models, Python venv, and user files. Mount this to keep uploaded files and installed AI bundles across restarts.
  • /tmp/workspace (app) - Temporary storage for files being processed. This can be ephemeral, but mounting it avoids filling up the container's writable layer.
  • SnapOtter-pgdata (postgres) - PostgreSQL data directory. This holds all relational data (users, settings, pipelines, jobs, audit log). Back up via pg_dump or volume snapshot.
  • SnapOtter-redisdata (redis) - Redis append-only file for durable job queues.