notes-wikis
Notion logo

Should I self-host Notion?

KINDAworth it if…

The docs half moves in an evening and the math is per-seat — but Notion's databases and mobile apps don't move at all. Worth it if you write more than you rollup.

The math (nothing hidden, including your time)

Notion (Plus, per seat)$12.00/mo
VPS share + storage−$1.70/mo
Your maintenance: 30 min/mo at $20/h−$10.00/mo
Net saving$0.30/mo
Setup: 75 min measured (one-time)$25.00
Break-even83.3 months
Markup Index (price ÷ real self-host cost)1.0×

Price: source, checked 2026-08-05. Inputs are stored in git; every number above is derived, never hand-written.

What you lose

  • Databases: views, relations, rollups — Outline has none of it, and that's half of many workspaces
  • Native mobile apps (Outline's mobile web is fine, not great)
  • Notion AI and the template ecosystem
  • Sign-in needs an auth provider you run or configure (magic-link SMTP or OIDC)

What you're paying Notion for

  • docs & wiki with nested pages
  • full-text search
  • real-time collaboration
  • databases with views, relations and rollups
  • mobile apps
  • public sharing links
Outline running from the boot-verified compose file
Outline, running from the compose file on this page — screenshot from our verified instance, not marketing material.

Ranked alternatives

A fast, collaborative wiki that covers the half of Notion most teams actually live in.

outline/outline · ★ 40,072 · last commit 0d ago · 13,848,755 pulls · BUSL-1.1

Spaces, nested pages and an editor two people can type in at once, with every word in a Postgres you can dump.

docmost/docmost · ★ 21,316 · last commit 0d ago · AGPL-3.0

Hardware

Runs comfortably on the old laptop in your closet.

The tested compose file (our evidence, CI-booted)

# Outline — the docs/wiki half of Notion. THE worked example (see logs/notion-outline.md).
# Sign-in is magic-link email: mailpit catches the mail locally so the core workflow is
# completable without any external SMTP or OIDC provider. In production point SMTP_* at a real
# mailbox (or configure OIDC) and put the whole thing behind TLS.
# Secrets below are throwaway test values — generate your own: openssl rand -hex 32
services:
  outline:
    image: outlinewiki/outline:latest
    container_name: outline
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      NODE_ENV: production
      URL: http://localhost:3000
      PORT: "3000"
      SECRET_KEY: 5f31f5ad9e6f6ff0c1b52c11a03f5f4b7b1b4a4f3f2ce9d6be2c8f4d7a91d2e3
      UTILS_SECRET: 8d2b1f9c4a7e6d5b3c2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c
      DATABASE_URL: postgres://outline:outline@postgres:5432/outline
      PGSSLMODE: disable
      REDIS_URL: redis://redis:6379
      FILE_STORAGE: local
      FILE_STORAGE_LOCAL_ROOT_DIR: /var/lib/outline/data
      SMTP_HOST: mailpit
      SMTP_PORT: "1025"
      SMTP_FROM_EMAIL: outline@localhost.test
      SMTP_SECURE: "false"
    ports:
      - "3000:3000"
    volumes:
      - outline-data:/var/lib/outline/data
    healthcheck:
      test: ["CMD-SHELL", "node -e \"fetch('http://localhost:3000/_health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
      interval: 5s
      timeout: 5s
      retries: 36
      start_period: 30s

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

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
      interval: 5s
      timeout: 5s
      retries: 12

  mailpit:
    # (maildev crashes where IPv6 is disabled — it insists on binding "::". Mailpit doesn't.)
    image: axllent/mailpit:latest
    restart: unless-stopped
    ports:
      - "1080:8025" # web UI to read the magic-link mail
    healthcheck:
      test: ["CMD", "/mailpit", "readyz"]
      interval: 5s
      timeout: 5s
      retries: 12

volumes:
  outline-data:
  outline-pg:

Want a guided install instead? caniselfhostit.com/notion has AI-agent prompts that assume a bare machine — that's their half of the stool, and it's good.

Verification

Protocol v1 · verified by zernonia on 2026-08-10 · setup measured at 75 min · AI assistant: claude-code (allowed & stated)

What broke

  • maildev (the first mail catcher we tried) crash-loops wherever IPv6 is disabled — it insists on binding '::'; swapped to mailpit
  • Outline boots healthy without SMTP but magic-link sign-in silently goes nowhere — the compose ships mailpit so the workflow is completable locally
Full timed log
# Timed setup log: Notion → Outline

**Protocol:** v1 · **Verified by:** zernonia · **Date:** 2026-08-10
**Assistant:** claude-code (allowed & stated — protocol v1 assumes an AI sysadmin)
**Environment:** containerized runner, 2 vCPU class, Debian-based, Docker 29.3 / Compose v5.1 preinstalled

> This is THE worked example — the template every contributor copies. Format: environment,
> timeline, measurements, what broke. Honest numbers or nothing.

## Timeline (wall clock)

| Step | Time |
|---|---|
| Read Outline's docker docs, draft compose (postgres + redis + outline + mail catcher) | 22 min |
| Generate secrets (`openssl rand -hex 32` × 2), first `docker compose up` | 4 min |
| **Boot #1 failed** — mail catcher crash-looping (see What broke) | 12 min to diagnose |
| Swap maildev → mailpit, re-up | 3 min |
| **Boot #2: all 4 services healthy in 44 s (measured, `compose up --wait`)** | 1 min |
| Core workflow: magic-link sign-in via mailpit UI, create doc, edit, search finds it | 18 min |
| Cleanup, re-run from scratch to confirm reproducibility | 15 min |
| **Total: ~75 min** | |

## Measurements

- `docker compose up --wait` → healthy: **44 s** (clean volumes, images cached)
- Containers: 4 (outline, postgres:16, redis:7, mailpit)
- Idle RAM after sign-in: outline ~480 MB + postgres ~40 MB + redis ~5 MB — 2 GB box is comfortable, 1 GB is not with anything else running

## What broke

1. **maildev crashes where IPv6 is disabled** (`listen EAFNOSUPPORT: address family not
   supported :::1025`). Many CI runners and some VPS images disable IPv6; maildev insists on
   binding `::`. Swapped to **mailpit**, which binds IPv4 fine and has a `readyz` probe.
2. **Outline without SMTP is a locked door**: it boots healthy but magic-link sign-in silently
   goes nowhere until SMTP_* points somewhere real. The compose ships mailpit so the workflow
   is completable locally; in production use a real mailbox or OIDC.

## Verdict-relevant notes

- The docs/wiki/search/collab jobs all work. There is no databases feature — Notion's
  views/relations/rollups have no equivalent here. That gap, not the setup, drives the KINDA.
- Notion import exists (Markdown/HTML export → import) but attachments need re-linking.

Verdict history

  • 2026-08-10: unscored → KINDA — Initial verdict: docs half moves in an evening, databases and mobile don't move at all.