How to Self-Host Omnivore with Docker Compose

What Is Omnivore?

Omnivore is a self-hosted read-later application originally launched as a hosted service with over 500,000 users. After the team was acqui-hired by ElevenLabs in October 2024, the cloud service shut down — making self-hosting the only way to use Omnivore. It saves articles, PDFs, and newsletters with highlights, annotations, labels, and full-text search. Native iOS and Android apps, browser extensions for Chrome/Safari/Firefox/Edge, and integrations with Obsidian and Logseq make it a full-featured Pocket replacement. The project is open source (AGPL-3.0) and maintained by the community.

Prerequisites

  • A Linux server (Ubuntu 22.04+ recommended)
  • Docker and Docker Compose installed (guide)
  • 4 GB of free RAM (10+ container stack)
  • 10 GB of free disk space
  • A domain name (recommended for browser extension compatibility)

Docker Compose Configuration

Omnivore requires a multi-container stack: web frontend, backend API, queue processor, content fetcher, image proxy, database migration service, PostgreSQL with pgvector, Redis, and MinIO for object storage.

Create a docker-compose.yml file:

services:
  postgres:
    image: ankane/pgvector:v0.5.1
    container_name: omnivore-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: omnivore
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: omnivore
    volumes:
      - omnivore-postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U omnivore"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7.2.4
    container_name: omnivore-redis
    restart: unless-stopped
    volumes:
      - omnivore-redis:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  minio:
    image: minio/minio:RELEASE.2024-01-18T22-51-28Z
    container_name: omnivore-minio
    restart: unless-stopped
    environment:
      MINIO_ROOT_USER: ${MINIO_ACCESS_KEY}
      MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY}
    command: server /data --console-address ":9001"
    volumes:
      - omnivore-minio:/data

  migrate:
    image: ghcr.io/omnivore-app/sh-migrate:latest  # Omnivore does not publish versioned Docker tags — :latest is the only option
    container_name: omnivore-migrate
    environment:
      PG_HOST: postgres
      PG_USER: omnivore
      PG_PASSWORD: ${POSTGRES_PASSWORD}
      PG_DB: omnivore
    depends_on:
      postgres:
        condition: service_healthy
    restart: "no"

  backend:
    image: ghcr.io/omnivore-app/sh-backend:latest  # Omnivore does not publish versioned Docker tags — :latest is the only option
    container_name: omnivore-backend
    restart: unless-stopped
    ports:
      - "4000:4000"
    environment:
      API_ENV: production
      PG_HOST: postgres
      PG_USER: omnivore
      PG_PASSWORD: ${POSTGRES_PASSWORD}
      PG_DB: omnivore
      PG_PORT: "5432"
      JWT_SECRET: ${JWT_SECRET}
      SSO_JWT_SECRET: ${SSO_JWT_SECRET}
      CLIENT_URL: ${CLIENT_URL}
      GATEWAY_URL: ${GATEWAY_URL}
      CONTENT_FETCH_URL: http://content-fetch:9090/
      REDIS_URL: redis://redis:6379
      IMAGE_PROXY_URL: ${IMAGE_PROXY_URL}
      MINIO_URL: http://minio:9000
      MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
      MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
      MINIO_UPLOAD_BUCKET: omnivore-uploads
    depends_on:
      migrate:
        condition: service_completed_successfully
      redis:
        condition: service_healthy

  queue-processor:
    image: ghcr.io/omnivore-app/sh-queue-processor:latest  # Omnivore does not publish versioned Docker tags — :latest is the only option
    container_name: omnivore-queue
    restart: unless-stopped
    environment:
      PG_HOST: postgres
      PG_USER: omnivore
      PG_PASSWORD: ${POSTGRES_PASSWORD}
      PG_DB: omnivore
      PG_PORT: "5432"
      REDIS_URL: redis://redis:6379
      JWT_SECRET: ${JWT_SECRET}
      CONTENT_FETCH_URL: http://content-fetch:9090/
    depends_on:
      migrate:
        condition: service_completed_successfully
      redis:
        condition: service_healthy

  content-fetch:
    image: ghcr.io/omnivore-app/sh-content-fetch:latest  # Omnivore does not publish versioned Docker tags — :latest is the only option
    container_name: omnivore-content-fetch
    restart: unless-stopped
    environment:
      REST_BACKEND_ENDPOINT: http://backend:4000/api
    depends_on:
      - backend

  image-proxy:
    image: ghcr.io/omnivore-app/sh-image-proxy:latest  # Omnivore does not publish versioned Docker tags — :latest is the only option
    container_name: omnivore-image-proxy
    restart: unless-stopped
    ports:
      - "7070:7070"
    environment:
      ALLOWED_ORIGINS: "*"

  web:
    image: ghcr.io/omnivore-app/sh-web:latest  # Omnivore does not publish versioned Docker tags — :latest is the only option
    container_name: omnivore-web
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      NEXT_PUBLIC_APP_ENV: production
      NEXT_PUBLIC_BASE_URL: ${CLIENT_URL}
      NEXT_PUBLIC_SERVER_BASE_URL: ${GATEWAY_URL}
      NEXT_PUBLIC_HIGHLIGHTS_BASE_URL: ${CLIENT_URL}
    depends_on:
      - backend

volumes:
  omnivore-postgres:
  omnivore-redis:
  omnivore-minio:

Create a .env file alongside it:

# Database — change POSTGRES_PASSWORD to a strong random value
POSTGRES_PASSWORD=change-me-to-a-strong-password

# JWT secrets — generate with: openssl rand -hex 32
JWT_SECRET=change-me-generate-with-openssl-rand-hex-32
SSO_JWT_SECRET=change-me-generate-another-with-openssl-rand-hex-32

# MinIO (S3-compatible object storage) — change these
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=change-me-to-a-strong-minio-password

# URLs — set to your domain
CLIENT_URL=http://localhost:3000
GATEWAY_URL=http://localhost:4000
IMAGE_PROXY_URL=http://localhost:7070

Start the stack:

docker compose up -d

Initial Setup

After starting the stack, Omnivore creates a demo account automatically:

Log in with the demo credentials, then immediately change the password or create a new account. The demo account is useful for verifying the deployment works before configuring your own account.

To create additional users, use the backend API or configure email-based registration by setting SENDER_EMAIL and SMTP environment variables in the backend service.

Configuration

Public URL Setup

For browser extensions and mobile apps to work, the CLIENT_URL and GATEWAY_URL must be publicly accessible. Update your .env:

CLIENT_URL=https://omnivore.yourdomain.com
GATEWAY_URL=https://api.omnivore.yourdomain.com
IMAGE_PROXY_URL=https://images.omnivore.yourdomain.com

Browser Extensions

Install the Omnivore browser extension for Chrome, Firefox, Safari, or Edge. Point the extension at your self-hosted instance by entering your GATEWAY_URL in the extension settings.

Newsletter Ingestion

Omnivore can receive newsletters via email. To enable this, deploy the sh-local-mail-watcher container and configure an MX record pointing to your server. Newsletters sent to your Omnivore email address are automatically saved as articles.

API Access

Omnivore exposes a GraphQL API at http://your-server:4000/api/graphql. Generate API keys from the web UI under Settings → API Keys. The API enables custom integrations and automation workflows.

Reverse Proxy

You need three reverse proxy entries — one for the web UI, one for the API, and one for the image proxy:

SubdomainTargetPort
omnivore.yourdomain.comweb3000
api.omnivore.yourdomain.combackend4000
images.omnivore.yourdomain.comimage-proxy7070

See our Reverse Proxy Setup guide for full Nginx Proxy Manager or Caddy configuration.

Backup

Back up three volumes:

VolumeContains
omnivore-postgresAll articles, highlights, annotations, user data
omnivore-redisQueue state (can be regenerated)
omnivore-minioUploaded files and PDFs

The PostgreSQL volume is critical. Back it up with pg_dump:

docker exec omnivore-postgres pg_dump -U omnivore omnivore > omnivore-backup.sql

See our Backup Strategy guide for automated backup approaches.

Troubleshooting

Migration Container Keeps Restarting

Symptom: The omnivore-migrate container fails and other services won’t start.

Fix: Check PostgreSQL is healthy first: docker logs omnivore-postgres. Ensure the database credentials in .env match between the postgres and migrate services. The pgvector extension must be available — the ankane/pgvector image includes it by default.

Browser Extension Won’t Connect

Symptom: The browser extension shows a connection error.

Fix: The GATEWAY_URL must be publicly accessible with HTTPS. Self-signed certificates cause issues — use Let’s Encrypt via your reverse proxy. Verify the API is reachable: curl https://api.omnivore.yourdomain.com/api/graphql.

Articles Not Fetching Content

Symptom: Saved articles show only the URL, not the article content.

Fix: Check the content-fetch container logs: docker logs omnivore-content-fetch. This service uses a headless browser to extract article content. It needs sufficient memory (at least 512 MB for this container alone). Ensure the REST_BACKEND_ENDPOINT points to the correct backend URL.

High Memory Usage

Symptom: The stack uses 3-4 GB of RAM.

Fix: This is expected. The 9-container stack includes PostgreSQL, Redis, MinIO, a Node.js backend, a Next.js frontend, and a headless browser for content fetching. Minimum 4 GB RAM recommended. On memory-constrained systems, consider Wallabag or Linkding instead.

Resource Requirements

  • RAM: ~2-3 GB idle across all containers, 4 GB under active use
  • CPU: Medium (content fetching uses headless browser)
  • Disk: ~500 MB for application data, plus storage for saved articles and PDFs

Verdict

Omnivore is the most feature-rich self-hosted read-later app available — highlights, annotations, labels, full-text search, newsletter ingestion, native mobile apps, and integrations with Obsidian and Logseq. The trade-off is complexity: 9+ containers, no pinned version tags (all images use :latest), and a community-maintained project after the original team joined ElevenLabs. If you want a full Pocket replacement with every feature, Omnivore delivers. If you want something simpler to deploy and maintain, Wallabag (3 containers) or Linkding (1 container) are better choices. For a modern, AI-powered alternative, Hoarder offers automatic tagging with a simpler stack.

Note: The Omnivore Docker images use :latest tags with no pinned versions. This means your deployment may change behavior after pulling new images. Pin to specific SHA digests if you need reproducible deployments.

Comments