NeoDB logo

NeoDB

Self-hosted server for tracking books, movies, music, games, and podcasts

Alternative to: goodreads, letterboxd, myanimelist, storygraph


NeoDB is a self-hosted platform for cataloging and reviewing books, movies, TV shows, music, games, and podcasts. It federates via ActivityPub and ATProto, so reviews and ratings can be shared and discovered across the fediverse instead of staying locked in a single commercial platform. It supports multi-user accounts, custom lists, and cross-instance following.

NeoDB Docker Compose example

Self-host NeoDB on your own server, homelab, or VPS starting from this Docker Compose example. It runs NeoDB in Docker containers using the official neodb/neodb:latest, redis:alpine, typesense/typesense:30.2, postgres:14-alpine, postgres:14-alpine images, with persistent volumes and automatic restarts preconfigured. Review the environment variables and adjust them to your setup, save the file as compose.yml (or docker-compose.yml), and start the stack with docker compose up -d.

x-neodb-common: &neodb-common
  image: neodb/neodb:latest
  restart: unless-stopped
  environment:
    # Set to False in production.
    NEODB_DEBUG: "False"
    # Encryption key for session data and tokens. Use a long random string.
    NEODB_SECRET_KEY: "changeme"
    # Display name of your instance.
    NEODB_SITE_NAME: "NeoDB"
    # Public domain name of your site, without scheme or trailing slash (e.g. db.example.org).
    NEODB_SITE_DOMAIN: "changeme"
    # PostgreSQL connection URL for the main NeoDB database (internal service).
    NEODB_DB_URL: "postgres://neodb:aubergine@neodb-db/neodb"
    # PostgreSQL connection URL for the bundled Takahe (ActivityPub) database (internal service).
    TAKAHE_DB_URL: "postgres://takahe:aubergine@takahe-db/takahe"
    # Redis connection URL (internal service).
    NEODB_REDIS_URL: "redis://redis:6379/0"
    # Typesense search connection URL (internal service).
    NEODB_SEARCH_URL: "typesense://user:eggplant@typesense:8108/catalog"
    # Address used as the From: header for outgoing mail.
    NEODB_EMAIL_FROM: "no-reply@changeme"
    # Where NeoDB stores uploaded media inside the container.
    NEODB_MEDIA_ROOT: "/www/m"
    # Public path NeoDB media is served from.
    NEODB_MEDIA_URL: "/m/"
    # Path to the Python virtualenv inside the image. Do not change.
    NEODB_VENV: "/neodb-venv"
    # Takahe environment (production or development).
    TAKAHE_ENVIRONMENT: "production"
    # Takahe secret key. Kept identical to NEODB_SECRET_KEY.
    TAKAHE_SECRET_KEY: "changeme"
    # Primary domain Takahe federates under. Kept identical to NEODB_SITE_DOMAIN.
    TAKAHE_MAIN_DOMAIN: "changeme"
    # From: address for Takahe mail.
    TAKAHE_EMAIL_FROM: "no-reply@changeme"
    # PostgreSQL connection URL Takahe uses (internal service).
    TAKAHE_DATABASE_SERVER: "postgres://takahe:aubergine@takahe-db/takahe"
    # Redis cache URL Takahe uses (internal service).
    TAKAHE_CACHES_DEFAULT: "redis://redis:6379/0"
    # Takahe media storage backend.
    TAKAHE_MEDIA_BACKEND: "local://"
    # Where Takahe stores media inside the container.
    TAKAHE_MEDIA_ROOT: "/www/media"
    # Public URL Takahe media is served from.
    TAKAHE_MEDIA_URL: "https://changeme/media/"
    # Trust X-Forwarded-* headers from the reverse proxy.
    TAKAHE_USE_PROXY_HEADERS: "true"
    # Number of Takahe stator (background task) workers.
    TAKAHE_STATOR_CONCURRENCY: "4"
    # Per-model stator concurrency.
    TAKAHE_STATOR_CONCURRENCY_PER_MODEL: "2"
    # Set to False in production.
    TAKAHE_DEBUG: "False"
    # Path to the Python virtualenv inside the image. Do not change.
    TAKAHE_VENV: "/neodb-venv"
  volumes:
    - neodb_media:/www/m
    - takahe_media:/www/media
    - takahe_cache:/www/cache
    - www_root:/www/root

services:
  redis:
    image: redis:alpine
    command: redis-server --save 60 1 --loglevel warning
    restart: unless-stopped
    volumes:
      - redis_data:/data

  typesense:
    image: typesense/typesense:30.2
    restart: unless-stopped
    environment:
      GLOG_minloglevel: "2"
    command: "--data-dir /data --api-key=eggplant"
    volumes:
      - typesense_data:/data

  neodb-db:
    image: postgres:14-alpine
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "neodb"]
    environment:
      POSTGRES_DB: "neodb"
      POSTGRES_USER: "neodb"
      # Internal-only database password; not reachable outside the compose network.
      POSTGRES_PASSWORD: "aubergine"
    volumes:
      - neodb_db:/var/lib/postgresql/data

  takahe-db:
    image: postgres:14-alpine
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "takahe"]
    environment:
      POSTGRES_DB: "takahe"
      POSTGRES_USER: "takahe"
      # Internal-only database password; not reachable outside the compose network.
      POSTGRES_PASSWORD: "aubergine"
    volumes:
      - takahe_db:/var/lib/postgresql/data

  # One-shot init: runs database migrations and search-index setup, then exits.
  migration:
    <<: *neodb-common
    restart: "no"
    command: /bin/neodb-init
    depends_on:
      neodb-db:
        condition: service_healthy
      takahe-db:
        condition: service_healthy
      typesense:
        condition: service_started
      redis:
        condition: service_started

  neodb-web:
    <<: *neodb-common
    command: /neodb-venv/bin/gunicorn boofilsic.wsgi -w 8 --preload --max-requests 2000 --timeout 60 -b 0.0.0.0:8000
    healthcheck:
      test: ["CMD", "wget", "-qO/tmp/test", "--header", "X-Forwarded-Proto: https", "http://127.0.0.1:8000/nodeinfo/2.0/"]
    depends_on:
      migration:
        condition: service_completed_successfully

  neodb-web-api:
    <<: *neodb-common
    command: /neodb-venv/bin/gunicorn boofilsic.wsgi -w 4 --preload --max-requests 2000 --timeout 30 -b 0.0.0.0:8000
    healthcheck:
      test: ["CMD", "wget", "-qO/tmp/test", "--header", "X-Forwarded-Proto: https", "http://127.0.0.1:8000/nodeinfo/2.0/"]
    depends_on:
      migration:
        condition: service_completed_successfully

  neodb-worker:
    <<: *neodb-common
    command: neodb-manage rqworker-pool --num-workers 4 mastodon fetch ap crawl import export cron
    depends_on:
      migration:
        condition: service_completed_successfully

  neodb-worker-extra:
    <<: *neodb-common
    command: neodb-manage rqworker-pool --num-workers 4 mastodon fetch ap crawl
    depends_on:
      migration:
        condition: service_completed_successfully

  takahe-web:
    <<: *neodb-common
    command: /neodb-venv/bin/gunicorn --chdir /takahe takahe.wsgi -w 8 --max-requests 2000 --timeout 60 --preload -b 0.0.0.0:8000
    healthcheck:
      test: ["CMD", "wget", "-qO/tmp/test", "--header", "X-Forwarded-Proto: https", "http://127.0.0.1:8000/api/v1/instance"]
    depends_on:
      migration:
        condition: service_completed_successfully

  takahe-stator:
    <<: *neodb-common
    command: takahe-manage runstator
    stop_signal: SIGINT
    depends_on:
      migration:
        condition: service_completed_successfully

  # Bundled nginx: the single entry point. Routes / to neodb-web, the API to
  # neodb-web-api and /media (ActivityPub) to takahe-web, and serves static/media
  # files. Its nginx config is baked into the image.
  nginx:
    <<: *neodb-common
    user: "root:root"
    command: nginx-start
    environment:
      NEODB_WEB_SERVER: "neodb-web:8000"
      NEODB_API_SERVER: "neodb-web-api:8000"
      TAKAHE_WEB_SERVER: "takahe-web:8000"
      NGINX_CONF: "/neodb/misc/nginx.conf.d/neodb.conf"
    depends_on:
      neodb-web:
        condition: service_started
      takahe-web:
        condition: service_started

volumes:
  redis_data:
  typesense_data:
  neodb_db:
  takahe_db:
  neodb_media:
  takahe_media:
  takahe_cache:
  www_root:

Values set to changeme are required — replace them with your own values before starting NeoDB.

Prefer a managed setup? WinterFlow installs, configures, and updates NeoDB for you using this same Docker Compose configuration.