Skip to main content

How to Self-Host Plane — Open Source Jira and Linear Alternative 2026

·OSSAlt Team
planejira-alternativelinear-alternativeself-hostingproject-managementdocker2026

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-123 in 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

FeaturePlane (self-hosted)JiraLinear
Price (10 seats)~$10/mo server$82/mo$80/mo
Self-hosted
Speed✅ Fast⚠️ Slow✅ Fast
Setup time30 minN/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.

Comments