Tutorials

How to Self-Host Notion: Complete AFFiNE Setup Guide

Deploy AFFiNE, the best Notion alternative, in 30 minutes. Self-hosted knowledge base with real-time collaboration, offline mode, and complete data ownership.

How to Self-Host Notion: Complete AFFiNE Setup Guide

Notion costs $10/user/month. Your 10-person team pays $1,200/year for notes, docs, and project tracking.

Meanwhile, your data lives on Notion's servers. Search content goes to their AI for training. File uploads are capped at 5MB on free plans. You're locked into their ecosystem.

AFFiNE gives you Notion's block-based editing, databases, and collaboration without the subscription or surveillance. Deploy to your server in 30 minutes. Own your data forever.

This guide walks through deploying AFFiNE with Docker, configuring collaboration features, and migrating from Notion. By the end, you'll have a production-ready knowledge base running on your infrastructure.

If you're new to self-hosting, start with our Docker 101 guide to understand the basics.

What is AFFiNE?

AFFiNE = Notion + Miro + Markdown

AFFiNE combines:

  • Notion's block-based editing (drag-and-drop content blocks)
  • Miro's canvas mode (visual whiteboarding and mind mapping)
  • Markdown's simplicity (keyboard-first shortcuts)
  • Local-first architecture (works offline, syncs when online)

Core Features

Editing & Collaboration:

  • Real-time collaborative editing (multiple users, live cursors)
  • Block-based content (text, images, code, tables, embeds)
  • Databases with views (table, kanban, gallery)
  • Templates for quick starts
  • Version history (restore previous versions)
  • Comments and mentions (@user notifications)

Organization:

  • Nested pages (unlimited hierarchy)
  • Favorites and recent pages
  • Full-text search
  • Tags and labels
  • Workspaces (separate environments)

Unique to AFFiNE:

  • Canvas mode: Visual thinking with flowcharts and diagrams
  • True offline mode: Full functionality without internet
  • Local-first: Data stored locally, synced to server
  • Privacy-focused: No analytics, no tracking, no AI training on your data

AFFiNE vs Notion

| Feature | Notion | AFFiNE (Self-Hosted) | | --------------------------- | ----------------- | -------------------- | | Real-time collaboration | ✅ | ✅ | | Databases | ✅ (advanced) | ✅ (basic) | | Templates | ✅ (1,000s) | ✅ (growing) | | Mobile apps | ✅ | ✅ | | Offline mode | ⚠️ (view only) | ✅ (full editing) | | File uploads | 5MB free/file | Unlimited | | Third-party integrations| ✅ (50+) | ⚠️ (limited) | | AI features | ✅ (paid) | 🚧 (in development) | | Canvas/whiteboard | ❌ | ✅ | | Local-first architecture| ❌ | ✅ | | Data ownership | Notion's servers | Your server | | Cost (10 users/year) | $1,200 | $60-120 |

When to choose AFFiNE:

  • Privacy matters (healthcare, legal, sensitive data)
  • Need true offline editing (field work, poor connectivity)
  • Team size scales (50+ users makes SaaS expensive)
  • Want visual thinking tools (canvas for brainstorming)

When to stick with Notion:

  • Heavy reliance on third-party integrations (Zapier, Slack, etc.)
  • Need advanced database features (complex formulas, rollups)
  • Team is non-technical (managed service reduces friction)

Prerequisites

Before deploying AFFiNE, you'll need:

1. VPS Server

Minimum Requirements:

  • 2 vCPU
  • 2GB RAM
  • 20GB storage
  • Ubuntu 22.04 LTS (or similar)

Recommended Providers:

  • Hetzner: €4.5/month (2 vCPU, 4GB RAM) - Best value
  • DigitalOcean: $12/month (2 vCPU, 2GB RAM) - Great docs
  • Linode: $12/month (2 vCPU, 4GB RAM) - Reliable
  • Vultr: $12/month (2 vCPU, 4GB RAM) - Global locations

2. Domain Name

For production use (HTTPS, custom domain):

  • Register domain ($12/year on Namecheap, Cloudflare)
  • Point A record to your server IP
  • Example: affine.yourdomain.com → 123.45.67.89

3. Basic Tools

On your local machine:

  • SSH client (Terminal on Mac/Linux, PuTTY on Windows)
  • Text editor (for config files)

On your server (install if missing):

# Update system
sudo apt update && sudo apt upgrade -y

# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# Install Docker Compose
sudo apt install docker-compose-plugin -y

# Verify installation
docker --version
docker compose version

Deployment Method 1: Docker Compose (Recommended)

This method deploys AFFiNE with PostgreSQL database and Redis for caching.

Step 1: Create Directory Structure

# Create directory for AFFiNE
mkdir -p ~/affine
cd ~/affine

# Create subdirectories for data persistence
mkdir -p ./postgres-data
mkdir -p ./redis-data
mkdir -p ./affine-data

Step 2: Create docker-compose.yml

Create the configuration file:

nano docker-compose.yml

Paste this configuration:

version: "3.8"

services:
  affine:
    image: ghcr.io/toeverything/affine-graphql:stable
    container_name: affine
    restart: always
    ports:
      - "3010:3010"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://affine:affine_password@postgres:5432/affine
      - REDIS_SERVER_HOST=redis
      - REDIS_SERVER_PORT=6379
      - AFFINE_SERVER_HOST=affine.yourdomain.com
      - AFFINE_SERVER_PORT=3010
      - AFFINE_SERVER_HTTPS=true
    depends_on:
      - postgres
      - redis
    volumes:
      - ./affine-data:/root/.affine/storage

  postgres:
    image: postgres:16-alpine
    container_name: affine_postgres
    restart: always
    environment:
      - POSTGRES_USER=affine
      - POSTGRES_PASSWORD=affine_password
      - POSTGRES_DB=affine
    volumes:
      - ./postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U affine"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: affine_redis
    restart: always
    volumes:
      - ./redis-data:/data
    command: redis-server --appendonly yes

volumes:
  postgres-data:
  redis-data:
  affine-data:

Configuration Explained:

  • POSTGRES_PASSWORD: Change affine_password to strong random password
  • AFFINE_SERVER_HOST: Your domain (or server IP for testing)
  • AFFINE_SERVER_HTTPS: Set to false if testing without HTTPS
  • Ports: AFFiNE runs on 3010 (accessible at http://your-ip:3010)

Step 3: Start AFFiNE

# Start all services
docker compose up -d

# Verify containers are running
docker compose ps

# Should show three containers: affine, affine_postgres, affine_redis
# All should be "Up" status

Step 4: Check Logs

# View AFFiNE logs
docker compose logs -f affine

# You should see:
# "Server started at http://0.0.0.0:3010"
# "Database connected"
# "Redis connected"

Step 5: Access AFFiNE

Open browser and navigate to:

  • Local testing: http://your-server-ip:3010
  • With domain: http://affine.yourdomain.com:3010

You should see the AFFiNE welcome screen.

First-Time Setup:

  1. Create admin account (email + password)
  2. Create your first workspace
  3. Start adding pages

Setting Up HTTPS with Caddy (Production)

For production deployment, use HTTPS with automatic SSL certificates.

Install Caddy

# Create Caddyfile
nano Caddyfile

Paste this configuration:

affine.yourdomain.com {
    reverse_proxy localhost:3010
}

Add Caddy to docker-compose.yml

Edit your docker-compose.yml:

nano docker-compose.yml

Add Caddy service:

services:
  # ... existing services (affine, postgres, redis) ...

  caddy:
    image: caddy:2-alpine
    container_name: affine_caddy
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
      - caddy-config:/config

volumes:
  # ... existing volumes ...
  caddy-data:
  caddy-config:

Update AFFiNE Environment Variables

In docker-compose.yml, update affine service:

affine:
  # ... existing config ...
  environment:
    # ... existing env vars ...
    - AFFINE_SERVER_HOST=affine.yourdomain.com
    - AFFINE_SERVER_HTTPS=true
  # Remove ports (Caddy handles external access)
  # ports:
  #   - "3010:3010"

Restart Stack

# Restart with new configuration
docker compose down
docker compose up -d

# Check Caddy logs (should show certificate obtained)
docker compose logs caddy

Access AFFiNE at https://affine.yourdomain.com (note HTTPS).

Configuring Collaboration Features

Enable User Registration

By default, only the first user (admin) can create an account. To allow team signup:

Edit docker-compose.yml affine service:

environment:
  # ... existing vars ...
  - AFFINE_ENABLE_SIGNUP=true

Restart:

docker compose up -d affine

Now team members can create accounts at https://affine.yourdomain.com/signup.

Invite Users to Workspace

  1. Open your workspace
  2. Click workspace settings (top right)
  3. Go to "Members" tab
  4. Click "Invite members"
  5. Enter email addresses
  6. Set permissions (Owner, Admin, Member, Guest)

Permission Levels:

  • Owner: Full control (delete workspace, manage billing)
  • Admin: Manage members, workspace settings
  • Member: Create/edit pages, databases
  • Guest: View-only or specific page access

Configure Email for Invitations (Optional)

For email invitations, configure SMTP:

affine:
  environment:
    # ... existing vars ...
    - MAILER_HOST=smtp.sendgrid.net
    - MAILER_PORT=587
    - MAILER_USER=apikey
    - MAILER_PASSWORD=your_sendgrid_api_key
    - MAILER_FROM=noreply@yourdomain.com

Free SMTP providers:

  • SendGrid (100 emails/day free)
  • Mailgun (5,000 emails/month free)
  • Amazon SES ($0.10 per 1,000 emails)

Migrating from Notion

Export from Notion

  1. Go to Notion workspace settings
  2. Click "Settings & Members" → "Settings"
  3. Scroll to "Export content"
  4. Select "Export all workspace content"
  5. Choose format: Markdown & CSV
  6. Click "Export"
  7. Download the ZIP file

Import to AFFiNE

Option 1: Manual Import (Simple Pages)

  1. Extract Notion export ZIP
  2. For each page:
    • Create new page in AFFiNE
    • Copy markdown content from Notion export
    • Paste into AFFiNE (formatting mostly preserved)

Option 2: Bulk Import Script (Multiple Pages)

Create import script:

nano import-notion.sh
#!/bin/bash
# Import Notion markdown files to AFFiNE

NOTION_EXPORT_DIR="/path/to/extracted/notion/export"
AFFINE_WORKSPACE_DIR="/path/to/affine-data/workspaces/your-workspace-id"

# Copy all markdown files
find "$NOTION_EXPORT_DIR" -name "*.md" -exec cp {} "$AFFINE_WORKSPACE_DIR/" \;

echo "Import complete. Restart AFFiNE to see imported pages."

Run:

chmod +x import-notion.sh
./import-notion.sh
docker compose restart affine

What Transfers vs. What Needs Rebuilding

Transfers Well:

  • ✅ Text content (headings, paragraphs, lists)
  • ✅ Basic formatting (bold, italic, code)
  • ✅ Tables (structure preserved)
  • ✅ Links (internal converted to markdown links)
  • ✅ Images (included in export)

Needs Manual Rebuilding:

  • ❌ Databases with complex formulas
  • ❌ Linked databases (relations)
  • ❌ Embedded content (Figma, Miro, etc.)
  • ❌ Integrations (Zapier, Slack bots)
  • ❌ Advanced database views (timeline, calendar with filters)

Migration Strategy:

  1. Phase 1: Migrate documentation and simple pages (80% of content)
  2. Phase 2: Rebuild databases in AFFiNE (20% of content, 80% of effort)
  3. Phase 3: Run Notion and AFFiNE in parallel for 2-4 weeks
  4. Phase 4: Cancel Notion subscription once team is comfortable

Backup Strategy

Critical: Set up automated backups before trusting AFFiNE with important data.

Daily Database Backups

Create backup script:

nano backup.sh
#!/bin/bash
# Backup AFFiNE PostgreSQL database to S3

BACKUP_DIR="/tmp/affine-backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="affine_backup_$TIMESTAMP.sql.gz"

# Create backup directory
mkdir -p $BACKUP_DIR

# Dump database and compress
docker compose exec -T postgres pg_dump -U affine affine | gzip > "$BACKUP_DIR/$BACKUP_FILE"

# Upload to S3 (requires AWS CLI configured)
aws s3 cp "$BACKUP_DIR/$BACKUP_FILE" s3://your-backup-bucket/affine/

# Keep local backups for 7 days
find $BACKUP_DIR -name "affine_backup_*.sql.gz" -mtime +7 -delete

echo "Backup completed: $BACKUP_FILE"

Make executable:

chmod +x backup.sh

Schedule with Cron

# Edit crontab
crontab -e

# Add daily backup at 3 AM
0 3 * * * /root/affine/backup.sh >> /var/log/affine-backup.log 2>&1

Restore from Backup

# Stop AFFiNE
docker compose down

# Extract backup
gunzip affine_backup_20260125_030000.sql.gz

# Restore to database
docker compose up -d postgres
cat affine_backup_20260125_030000.sql | docker compose exec -T postgres psql -U affine affine

# Start AFFiNE
docker compose up -d

File Storage Backups

Backup user-uploaded files:

# Backup files to S3
aws s3 sync ./affine-data s3://your-backup-bucket/affine-files/

# Schedule daily file backup
0 4 * * * aws s3 sync /root/affine/affine-data s3://your-backup-bucket/affine-files/ >> /var/log/affine-files-backup.log 2>&1

Monitoring and Maintenance

Health Checks

Create monitoring script:

nano health-check.sh
#!/bin/bash
# Check if AFFiNE is responding

AFFINE_URL="https://affine.yourdomain.com"
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" $AFFINE_URL)

if [ $HTTP_CODE -eq 200 ]; then
  echo "AFFiNE is healthy (HTTP $HTTP_CODE)"
else
  echo "AFFiNE is down (HTTP $HTTP_CODE)"
  # Restart services
  cd /root/affine
  docker compose restart
  # Send alert (optional)
  # curl -X POST https://hooks.slack.com/... -d '{"text":"AFFiNE is down!"}'
fi

Schedule health checks:

crontab -e

# Check every 5 minutes
*/5 * * * * /root/affine/health-check.sh >> /var/log/affine-health.log 2>&1

Update AFFiNE

# Pull latest images
docker compose pull

# Restart with new version
docker compose up -d

# Check logs for errors
docker compose logs -f affine

Schedule monthly updates:

crontab -e

# Update first Sunday of each month at 2 AM
0 2 1-7 * 0 cd /root/affine && docker compose pull && docker compose up -d

Resource Monitoring

Check resource usage:

# Container resource usage
docker stats

# Expected usage for 10-user team:
# affine: 200-500MB RAM, 10-30% CPU
# postgres: 100-200MB RAM, 5-15% CPU
# redis: 50-100MB RAM, 1-5% CPU

If RAM usage exceeds 80%, upgrade VPS or optimize:

# Add resource limits to docker-compose.yml
services:
  affine:
    deploy:
      resources:
        limits:
          memory: 1G
        reservations:
          memory: 512M

Troubleshooting Common Issues

Issue 1: AFFiNE Won't Start

Symptom: Container exits immediately

Debug:

docker compose logs affine

# Common errors:
# "Cannot connect to database" → postgres not ready
# "Port 3010 already in use" → another service using port

Fix database connection:

# Ensure postgres is healthy
docker compose ps postgres
# Should show "Up" and "healthy"

# If postgres is down, check logs
docker compose logs postgres

Fix port conflict:

# Find what's using port 3010
sudo lsof -i :3010

# Either kill that process or change AFFiNE port in docker-compose.yml
ports:
  - "3011:3010"  # Use 3011 instead

Issue 2: Slow Performance

Causes:

  1. Insufficient server resources
  2. Database not optimized
  3. Too many large files

Solutions:

# 1. Check resource usage
docker stats

# If RAM > 80%, upgrade VPS

# 2. Optimize PostgreSQL
# Edit docker-compose.yml postgres service:
postgres:
  command: postgres -c shared_buffers=256MB -c max_connections=200

# 3. Enable Redis caching (already in our config)
# Verify Redis is working:
docker compose exec redis redis-cli ping
# Should return "PONG"

Issue 3: Data Loss After Restart

Cause: Volumes not mounted correctly

Fix:

# Ensure volumes are defined in docker-compose.yml:
volumes:
  - ./postgres-data:/var/lib/postgresql/data  # Must be here
  - ./affine-data:/root/.affine/storage       # And here

# Verify data directories exist:
ls -la postgres-data affine-data

# If empty, data was not persisted (restore from backup)

Issue 4: Can't Access via HTTPS

Cause: Caddy not obtaining certificate

Debug:

docker compose logs caddy

# Common issues:
# "Port 80/443 not accessible" → firewall blocking
# "Domain not pointing to this server" → DNS not configured

Fix firewall:

sudo ufw allow 80
sudo ufw allow 443
sudo ufw status

Fix DNS:

# Check if domain points to your server
dig affine.yourdomain.com +short
# Should return your server IP

# If not, update A record at your DNS provider

Scaling for Larger Teams

10-50 Users: Optimize Current Setup

# Upgrade to better VPS (4 vCPU, 8GB RAM)
# Optimize PostgreSQL:
postgres:
  command: |
    postgres
    -c shared_buffers=512MB
    -c effective_cache_size=2GB
    -c max_connections=300
    -c work_mem=4MB

50-200 Users: Add S3 for File Storage

Store files in S3 instead of local disk:

affine:
  environment:
    # ... existing vars ...
    - AFFINE_STORAGE_PROVIDER=s3
    - AWS_ACCESS_KEY_ID=your_key
    - AWS_SECRET_ACCESS_KEY=your_secret
    - AWS_S3_BUCKET=affine-files
    - AWS_S3_REGION=us-east-1

Cost: ~$0.023/GB/month (much cheaper than VPS storage scaling)

200+ Users: Horizontal Scaling

Run multiple AFFiNE instances behind load balancer:

# docker-compose.yml with 3 AFFiNE instances
services:
  affine-1:
    image: ghcr.io/toeverything/affine-graphql:stable
    # ... config ...

  affine-2:
    image: ghcr.io/toeverything/affine-graphql:stable
    # ... config ...

  affine-3:
    image: ghcr.io/toeverything/affine-graphql:stable
    # ... config ...

  nginx-lb:
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    ports:
      - "3010:3010"

nginx.conf for load balancing:

upstream affine_backend {
    server affine-1:3010;
    server affine-2:3010;
    server affine-3:3010;
}

server {
    listen 3010;
    location / {
        proxy_pass http://affine_backend;
    }
}

Security Hardening

1. Enable Two-Factor Authentication

AFFiNE supports 2FA for user accounts:

  1. User settings → Security
  2. Enable 2FA
  3. Scan QR code with authenticator app (Google Authenticator, Authy)
  4. Enter verification code

2. Implement SSO (Single Sign-On)

For enterprise deployments, use OIDC:

affine:
  environment:
    # ... existing vars ...
    - AFFINE_OIDC_ENABLED=true
    - AFFINE_OIDC_ISSUER=https://your-idp.com
    - AFFINE_OIDC_CLIENT_ID=affine
    - AFFINE_OIDC_CLIENT_SECRET=your_secret

Compatible with: Keycloak, Authentik, Auth0, Okta

3. Rate Limiting

Protect against brute-force attacks:

# In nginx/Caddy configuration
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;

location /api/auth/login {
    limit_req zone=login_limit burst=2 nodelay;
    proxy_pass http://affine:3010;
}

4. Regular Security Updates

# Weekly update routine
docker compose pull
docker compose up -d
docker image prune -f  # Remove old images

Cost Analysis

Notion vs Self-Hosted AFFiNE (3 Years)

Notion Plus (10 users):

  • Monthly: $100
  • Annual: $1,200
  • 3 years: $3,600

Self-Hosted AFFiNE:

  • Initial setup: 10 hours × $0 (your time)
  • VPS (3 years): $10/month × 36 = $360
  • Domain (3 years): $12/year × 3 = $36
  • Maintenance: 2 hours/month × 36 = 72 hours (your time)
  • Total: $396 over 3 years

Savings: $3,204 (89% reduction)

ROI: After 4 months, you've broken even. Everything after is pure savings.

The Exit-Saas Perspective

Notion pioneered the block-based knowledge base. They proved the concept, built the market, and now extract rent from it.

AFFiNE takes that same concept and gives you ownership. The features are 80% identical, but the economics are inverted: Notion's costs scale with your team, AFFiNE's stay flat.

The trade-off is real: Self-hosting requires setup time and ongoing maintenance. You're responsible for backups, security, and uptime. For some teams, that's worth $1,200/year to avoid.

But for teams that value privacy (legal, healthcare), control (data sovereignty), or cost savings (bootstrapped startups), self-hosting is the rational choice.

The best part? You can start with Notion, prove the workflow works for your team, then migrate to AFFiNE once you're confident. The switching cost is one weekend of migration work for years of savings.

Ready to deploy more self-hosted tools? Check out our complete guide to self-hosting for startups under $50/month or browse our tools directory for 20+ alternatives.

The subscription is optional. The ownership is permanent.

Ready to Switch?

Deploy Your Open-Source Stack on DigitalOcean in 1-click

Deploy in under 5 minutes
$200 free credits for 60 days
No credit card required to start
Automatic backups included

Get $200 in Free Credits

New users receive $200 credit valid for 60 days

Trusted by 600,000+ developers worldwide. Cancel anytime.