How to Self-Host Plane — Open Source Jira and Linear Alternative 2026
What Is Plane?
Plane is an open source project management tool designed to feel like Linear — clean, fast, and opinionated. It supports issues, cycles (sprints), modules (epic-like grouping), pages (wiki), and analytics. Unlike Jira, which buries you in configuration, Plane gets out of the way.
Jira Software costs $8.15/seat/month (up to $16.18 for enterprise). Linear starts at $8/seat/month and doesn't have a self-hosted option. Plane is free when self-hosted — just your server costs.
Key features:
- Issues with states, priorities, labels, cycles
- Cycles (sprint management with burndown)
- Modules (feature groupings like Epics)
- Pages (wiki/documentation built-in)
- Analytics and velocity tracking
- GitHub, GitLab, Slack integrations
- REST API
- OIDC/SAML SSO
Prerequisites
- VPS with 2 vCPU, 4GB RAM (Hetzner CX32 ~€5.49/mo works)
- Docker + Docker Compose v2+
- Domain name + DNS configured
- SMTP credentials for email notifications
Deploy Plane with Docker Compose
1. Clone the Repository
git clone https://github.com/makeplane/plane.git
cd plane/deploy/selfhost
2. Configure Environment
cp .env.example .env
Edit .env:
# .env
# Domain
WEB_URL=https://pm.yourdomain.com
# Secret key — generate a strong random string
SECRET_KEY=$(openssl rand -hex 32)
# Database
POSTGRES_PASSWORD=your-secure-postgres-password
# Redis
REDIS_URL=redis://plane-redis:6379/
# Email
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST=smtp.yourdomain.com
EMAIL_HOST_USER=your@email.com
EMAIL_HOST_PASSWORD=your-email-password
EMAIL_PORT=587
EMAIL_USE_TLS=True
DEFAULT_FROM_EMAIL=noreply@yourdomain.com
# Storage (local by default)
USE_MINIO=1
# Disable analytics/telemetry
POSTHOG_API_KEY=
ANALYTICS_BASE_API=
3. The docker-compose.yaml (Official)
version: "3.8"
networks:
plane-network:
driver: bridge
volumes:
pgdata:
redisdata:
uploads:
minio_data:
services:
# Backend API
api:
image: makeplane/plane-backend:stable
restart: always
networks: [plane-network]
env_file: .env
depends_on:
- db
- redis
- minio
command: ./bin/docker-entrypoint-api.sh
# Background worker
worker:
image: makeplane/plane-backend:stable
restart: always
networks: [plane-network]
env_file: .env
depends_on:
- api
command: ./bin/docker-entrypoint-worker.sh
# Beat scheduler
beat-worker:
image: makeplane/plane-backend:stable
restart: always
networks: [plane-network]
env_file: .env
depends_on:
- api
command: ./bin/docker-entrypoint-beat.sh
# Next.js frontend
web:
image: makeplane/plane-frontend:stable
restart: always
networks: [plane-network]
env_file: .env
depends_on:
- api
# Space app (public-facing project pages)
space:
image: makeplane/plane-space:stable
restart: always
networks: [plane-network]
env_file: .env
depends_on:
- api
# Admin panel
admin:
image: makeplane/plane-admin:stable
restart: always
networks: [plane-network]
env_file: .env
depends_on:
- api
# PostgreSQL
db:
image: postgres:15-alpine
restart: always
volumes:
- pgdata:/var/lib/postgresql/data
networks: [plane-network]
environment:
POSTGRES_USER: plane
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: plane
# Redis
redis:
image: redis:7-alpine
restart: always
volumes:
- redisdata:/data
networks: [plane-network]
# MinIO (S3-compatible storage for uploads)
minio:
image: minio/minio:latest
restart: always
volumes:
- minio_data:/data
networks: [plane-network]
environment:
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
command: server /data --console-address ":9090"
# Nginx reverse proxy (internal)
proxy:
image: makeplane/plane-proxy:stable
restart: always
networks: [plane-network]
ports:
- "80:80"
env_file: .env
depends_on:
- web
- api
- space
- admin
4. Start the Stack
docker compose up -d
# Watch the startup logs
docker compose logs -f api
Wait for the api container to print: Starting development server at http://0.0.0.0:8000/
Configure Caddy Reverse Proxy
# /etc/caddy/Caddyfile
pm.yourdomain.com {
reverse_proxy localhost:80
# Allow large file uploads
request_body {
max_size 25MB
}
}
systemctl reload caddy
Initial Setup
Visit https://pm.yourdomain.com and complete the setup wizard:
1. Create your admin account (first registration is automatically admin)
2. Create your first workspace (e.g., "Acme Corp")
3. Invite team members
4. Create your first project
Create Admin via CLI (If Email Isn't Working)
docker compose exec api python manage.py createsuperuser
Key Plane Concepts
Understanding Plane's structure before inviting your team:
Workspace
└── Project (one per product/team)
├── Issues (tasks, bugs, stories)
│ ├── States (Todo, In Progress, Done, etc.)
│ ├── Cycles (sprints — time-boxed iteration)
│ └── Modules (epics — feature groups)
└── Pages (wiki — meeting notes, specs)
Import from Jira
# Plane supports CSV import from Jira
# In Jira: Issues → Export → CSV
# In Plane: Settings → Import → Jira CSV
# Or use GitHub Issues import:
# Settings → Import → GitHub Issues
Import from Linear
Linear has an export function (Settings → Export → CSV). Plane's import handles it via the generic CSV importer with field mapping.
Enable GitHub Integration
1. In Plane: Settings → Integrations → GitHub
2. Create a GitHub App in your GitHub org
3. Paste App ID and private key into Plane
4. Install the app on your repositories
Once connected:
- Mention
closes PROJ-123in PR descriptions to auto-close issues - Link commits to Plane issues
- See PR status directly in issues
Enable SSO (OIDC)
# .env
OIDC_PROVIDER=authentik # or google, okta, keycloak
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY=your-client-id # for Google SSO
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET=your-secret
For Authentik/Keycloak OIDC:
OIDC_PROVIDER=custom
OIDC_CLIENT_ID=plane
OIDC_CLIENT_SECRET=your-secret
OIDC_ISSUER_URL=https://auth.yourdomain.com/application/o/plane/
Use S3/MinIO for File Storage
Plane uses MinIO by default (bundled in docker-compose). For production, you can point it at external S3:
# .env
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret
AWS_S3_BUCKET_NAME=plane-uploads
AWS_S3_ENDPOINT_URL= # leave blank for AWS; set for MinIO/R2/Hetzner
Plane vs Jira vs Linear
| Feature | Plane (self-hosted) | Jira | Linear |
|---|---|---|---|
| Price (10 seats) | ~$10/mo server | $82/mo | $80/mo |
| Self-hosted | ✅ | ❌ | ❌ |
| Speed | ✅ Fast | ⚠️ Slow | ✅ Fast |
| Setup time | 30 min | N/A (cloud) | N/A (cloud) |
| Cycles (sprints) | ✅ | ✅ | ✅ |
| Roadmaps | ✅ | ✅ | ✅ |
| Custom workflows | ✅ | ✅ Advanced | ⚠️ Limited |
| GitHub integration | ✅ | ✅ | ✅ |
| API | ✅ REST | ✅ REST | ✅ |
| Data ownership | ✅ Full | ❌ | ❌ |
Backup and Restore
#!/bin/bash
# backup-plane.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups/plane"
mkdir -p $BACKUP_DIR
# Database backup
docker compose exec -T db pg_dump -U plane plane \
| gzip > $BACKUP_DIR/db_$DATE.sql.gz
# MinIO data
tar -czf $BACKUP_DIR/uploads_$DATE.tar.gz \
$(docker volume inspect selfhost_minio_data --format '{{.Mountpoint}}')
# Rotate 14 days
find $BACKUP_DIR -mtime +14 -delete
echo "Plane backup complete: $DATE"
Restore
# Restore database
gunzip < /backups/plane/db_20260101.sql.gz | \
docker compose exec -T db psql -U plane plane
# Restart to pick up restored state
docker compose restart api worker
Monitoring Plane Health
# Check API health
curl https://pm.yourdomain.com/api/health/
# Watch all service logs
docker compose logs -f --tail=50
# Check resource usage
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
Troubleshooting
API crashes on startup:
docker compose logs api | grep "Error"
# Usually a database migration issue — run:
docker compose exec api python manage.py migrate
Frontend shows blank page:
docker compose logs web
# Check WEB_URL matches your domain exactly (no trailing slash)
File uploads fail:
docker compose logs minio
# Check MINIO_ROOT_USER and MINIO_ROOT_PASSWORD are set in .env
Plane is one of the top open source Jira alternatives on OSSAlt — compare all self-hosted project management tools.