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.