How to Self-Host Outline Wiki on a VPS: Team Knowledge Base with Notion-Like Editing
Outline is an open-source team wiki and knowledge base with a Notion-like block editor, real-time collaborative editing, Slack and Google SSO, full-text search, nested document hierarchies, and a clean reading experience. Outline Cloud costs $10/user/month. Self-hosting on a VPS eliminates per-seat fees entirely — making it cost-effective for teams of any size.
What Outline Provides
- Block editor: Rich text, code blocks, embeds, tables, callouts — Notion-like experience
- Collections: Organize documents into nested hierarchies with team permissions
- Real-time collaboration: Multiple people edit simultaneously, see each other’s cursors
- Full-text search: Instant search across all documents and their content
- SSO integration: Slack, Google, GitHub, OIDC, SAML — employees log in with existing accounts
- Markdown import/export: Migrate from or to any other documentation tool
- API: REST API for integrating with other tools and automations
Architecture
Outline (Node.js) → PostgreSQL (document storage)
→ Redis (real-time collaboration, sessions)
→ S3 / local storage (file attachments, images)
Step 1: Docker Compose Setup
mkdir -p /opt/outline && cd /opt/outline
nano docker-compose.yml
version: '3.8'
services:
outline:
image: outlinewiki/outline:latest
container_name: outline
restart: always
ports:
- "127.0.0.1:3000:3000"
env_file: .env
depends_on:
outline-postgres:
condition: service_healthy
outline-redis:
condition: service_healthy
volumes:
- outline_data:/var/lib/outline/data
outline-postgres:
image: postgres:16-alpine
container_name: outline-postgres
restart: always
environment:
POSTGRES_DB: outline
POSTGRES_USER: outline
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- outline_db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U outline"]
interval: 10s
timeout: 5s
retries: 5
outline-redis:
image: redis:7-alpine
container_name: outline-redis
restart: always
command: redis-server --requirepass ${REDIS_PASSWORD}
healthcheck:
test: ["CMD", "redis-cli", "--pass", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
outline_data:
outline_db:
nano .env
# Security — generate with: openssl rand -hex 32
SECRET_KEY=your_64_char_hex_secret_key_here
UTILS_SECRET=your_second_64_char_hex_key_here
# Database
DATABASE_URL=postgres://outline:${POSTGRES_PASSWORD}@outline-postgres:5432/outline
POSTGRES_PASSWORD=StrongOutlineDbPassword!
# Redis
REDIS_URL=redis://:${REDIS_PASSWORD}@outline-redis:6379
REDIS_PASSWORD=StrongRedisPassword!
# Application URL
URL=https://wiki.yourdomain.com
PORT=3000
# File storage — local (or configure S3 below)
FILE_STORAGE=local
FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data
FILE_STORAGE_UPLOAD_MAX_SIZE=26214400
# Authentication — at least one provider required
# Option A: Slack OAuth
SLACK_CLIENT_ID=your_slack_client_id
SLACK_CLIENT_SECRET=your_slack_client_secret
# Option B: Google OAuth (uncomment to use)
# GOOGLE_CLIENT_ID=your_google_client_id
# GOOGLE_CLIENT_SECRET=your_google_client_secret
# Option C: Generic OIDC (Keycloak, etc.)
# OIDC_CLIENT_ID=outline
# OIDC_CLIENT_SECRET=your_secret
# OIDC_AUTH_URI=https://auth.yourdomain.com/realms/myrealm/protocol/openid-connect/auth
# OIDC_TOKEN_URI=https://auth.yourdomain.com/realms/myrealm/protocol/openid-connect/token
# OIDC_USERINFO_URI=https://auth.yourdomain.com/realms/myrealm/protocol/openid-connect/userinfo
# OIDC_USERNAME_CLAIM=email
# OIDC_DISPLAY_NAME=Keycloak
# OIDC_SCOPES=openid profile email
# Email (optional — for invites and notifications)
SMTP_HOST=smtp.mailgun.org
SMTP_PORT=587
SMTP_USERNAME=postmaster@mg.yourdomain.com
SMTP_PASSWORD=your_smtp_password
SMTP_FROM_EMAIL=wiki@yourdomain.com
SMTP_REPLY_EMAIL=noreply@yourdomain.com
# Node environment
NODE_ENV=production
PGSSLMODE=disable
chmod 600 .env
docker compose up -d
docker compose logs -f outline # Wait for "Listening on port 3000"
Step 2: Nginx Reverse Proxy
sudo nano /etc/nginx/sites-available/outline
server {
listen 80;
server_name wiki.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name wiki.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/wiki.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/wiki.yourdomain.com/privkey.pem;
client_max_body_size 100M;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
}
}
sudo ln -s /etc/nginx/sites-available/outline /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d wiki.yourdomain.com
Step 3: Configure Slack OAuth (Recommended)
- Go to api.slack.com/apps → Create New App → From scratch
- OAuth & Permissions → Redirect URLs:
https://wiki.yourdomain.com/auth/slack.callback - Enable Bot Token Scopes:
users:read,users:read.email - Install to Workspace
- Copy Client ID and Client Secret to your
.envfile
Step 4: Configure S3 File Storage (Production)
For production with multiple users uploading files, S3-compatible storage is more reliable than local storage:
# Add to .env (replace local storage config):
FILE_STORAGE=s3
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_REGION=us-east-1
AWS_S3_UPLOAD_BUCKET_NAME=outline-files-yourdomain
AWS_S3_UPLOAD_BUCKET_URL=https://s3.amazonaws.com
AWS_S3_UPLOAD_MAX_SIZE=26214400
AWS_S3_FORCE_PATH_STYLE=false
# For Cloudflare R2 (free egress):
AWS_ACCESS_KEY_ID=your_r2_key
AWS_SECRET_ACCESS_KEY=your_r2_secret
AWS_REGION=auto
AWS_S3_UPLOAD_BUCKET_NAME=outline-files
AWS_S3_UPLOAD_BUCKET_URL=https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com
AWS_S3_FORCE_PATH_STYLE=true
<code">docker compose restart outline
Step 5: Database Migrations and Updates
<code"># Run database migrations after first start or after updating docker exec outline node ./build/server/index.js db:migrate # Update Outline cd /opt/outline docker compose pull docker compose up -d
Importing Existing Documentation
Outline imports from several formats:
- Notion: Export from Notion as Markdown & CSV → Import in Outline Settings
- Confluence: Export as HTML → use the Confluence importer plugin
- Markdown files: Settings → Import → Upload a .zip of .md files
- Any tool: Export as Markdown and import
Getting Started
Outline needs 1–2 GB RAM for a small team (under 50 users). Ubuntu VPS plans at VPS.DO with NVMe storage handle Outline’s PostgreSQL full-text search and Redis real-time collaboration efficiently. The 2 vCPU / 2 GB RAM plan comfortably runs Outline for teams of 10–50 people alongside other services.
Conclusion
Self-hosted Outline delivers a Notion-quality team wiki experience — block editor, real-time collaboration, full-text search, and SSO — at VPS infrastructure cost instead of $10/user/month. For a 20-person team, that’s $200/month saved against Outline Cloud. The Docker Compose deployment is straightforward, and Outline’s active development means new features ship regularly without additional licensing cost.