How to Self-Host Plane (Community Edition) on Coolify

How to Self-Host Plane (Community Edition) on Coolify

 

If your team is paying $5,000–$6,000 a year for project management software and you're not using half its features, this guide is for you.

Plane is an open-source project management tool that looks and feels like a modern SaaS product — boards, cycles, modules, timelines, dashboards — but runs entirely on your own server, for free. No per-seat pricing. No vendor lock-in. Your data stays yours.

This tutorial walks you through deploying Plane's Community Edition on a server running Coolify — a self-hosted platform that makes deploying Docker-based apps straightforward, even if you've never touched a terminal before.

What you'll end up with: A fully working Plane instance accessible at your own domain (e.g. plane.yourdomain.com), with SSL, running completely free on your own infrastructure.


Prerequisites

Before you start, make sure you have the following in place:

  • A server running Coolify. This guide assumes Coolify is already installed and accessible. If not, follow the official Coolify installation guide first.
  • A domain name with access to your DNS settings. You'll need to create an A record pointing a subdomain (e.g. plane.yourdomain.com) to your server's IP address. If you're unsure how to do this, refer to Coolify's DNS configuration guide.
  • SSH access to your server. You'll need to run a few commands directly on the server. Coolify's built-in terminal (under Servers → Terminal) works fine if you don't have a separate SSH client.
  • Server specs: Minimum 2 CPU cores and 4 GB RAM.

Step 1 — Point Your Domain to Your Server

Before anything else, create a DNS A record for the subdomain you want to use.

  1. Log in to your domain registrar or DNS provider (e.g. Cloudflare, Namecheap, GoDaddy).
  2. Create an A record:
    • Name: plane (or whatever subdomain you want)
    • Value: Your server's public IP address
    • TTL: Auto or 300

To find your server's public IP, SSH into your server and run:

curl -4 ifconfig.me

The output is your server's public IP. Use that as the value for your A record.

DNS changes can take a few minutes to an hour to propagate. You can continue with the rest of the setup while you wait.


Step 2 — Gather Your Server's Docker Network Information

Later in this guide, you'll need to connect Coolify's built-in proxy (Traefik) to Plane's Docker network. To do this, you need two pieces of information from your server: your Docker gateway IP and the ability to identify Plane's Docker network ID after deployment.

Run this command to find the Docker gateway IP for the Coolify network:

sudo docker network inspect coolify | grep Gateway

You'll see output like:

"Gateway": "10.0.2.1"

Write this IP down. You'll need it later when configuring the Traefik proxy rule.


Step 3 — Create the Docker Compose File

In Coolify, navigate to your Project → New Resource → Docker Compose Empty.

Paste the following compose file exactly as shown. This is a production-ready configuration with Plane's proxy service binding to port 8090 on the host (instead of the default 80/443), so it doesn't conflict with Coolify's own proxy which already owns those ports.

x-db-env: &db-env
  PGHOST: ${PGHOST:-plane-db}
  PGDATABASE: ${PGDATABASE:-plane}
  POSTGRES_USER: ${POSTGRES_USER:-plane}
  POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-plane}
  POSTGRES_DB: ${POSTGRES_DB:-plane}
  POSTGRES_PORT: ${POSTGRES_PORT:-5432}
  PGDATA: ${PGDATA:-/var/lib/postgresql/data}

x-redis-env: &redis-env
  REDIS_HOST: ${REDIS_HOST:-plane-redis}
  REDIS_PORT: ${REDIS_PORT:-6379}
  REDIS_URL: ${REDIS_URL:-redis://plane-redis:6379/}

x-minio-env: &minio-env
  MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID:-access-key}
  MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY:-secret-key}

x-aws-s3-env: &aws-s3-env
  AWS_REGION: ${AWS_REGION:-}
  AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-access-key}
  AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-secret-key}
  AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
  AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}

x-proxy-env: &proxy-env
  APP_DOMAIN: ${APP_DOMAIN:-plane.yourdomain.com}
  FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
  CERT_EMAIL: ${CERT_EMAIL:-}
  CERT_ACME_CA: ${CERT_ACME_CA:-}
  CERT_ACME_DNS: ${CERT_ACME_DNS:-}
  LISTEN_HTTP_PORT: ${LISTEN_HTTP_PORT:-8090}
  LISTEN_HTTPS_PORT: ${LISTEN_HTTPS_PORT:-443}
  BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
  SITE_ADDRESS: ${SITE_ADDRESS:-:80}

x-mq-env: &mq-env
  RABBITMQ_HOST: ${RABBITMQ_HOST:-plane-mq}
  RABBITMQ_PORT: ${RABBITMQ_PORT:-5672}
  RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-plane}
  RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-plane}
  RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST:-plane}
  RABBITMQ_VHOST: ${RABBITMQ_VHOST:-plane}

x-live-env: &live-env
  API_BASE_URL: ${API_BASE_URL:-http://api:8000}
  LIVE_SERVER_SECRET_KEY: ${LIVE_SERVER_SECRET_KEY}

x-app-env: &app-env
  WEB_URL: ${WEB_URL:-https://plane.yourdomain.com}
  DEBUG: ${DEBUG:-0}
  CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-https://plane.yourdomain.com}
  GUNICORN_WORKERS: 1
  USE_MINIO: ${USE_MINIO:-1}
  DATABASE_URL: ${DATABASE_URL:-postgresql://plane:plane@plane-db/plane}
  SECRET_KEY: ${SECRET_KEY}
  AMQP_URL: ${AMQP_URL:-amqp://plane:plane@plane-mq:5672/plane}
  API_KEY_RATE_LIMIT: ${API_KEY_RATE_LIMIT:-60/minute}
  MINIO_ENDPOINT_SSL: ${MINIO_ENDPOINT_SSL:-0}
  LIVE_SERVER_SECRET_KEY: ${LIVE_SERVER_SECRET_KEY}

services:
  web:
    image: makeplane/plane-frontend:${APP_RELEASE:-stable}
    deploy:
      replicas: ${WEB_REPLICAS:-1}
      restart_policy:
        condition: any
    depends_on:
      - api
      - worker

  space:
    image: makeplane/plane-space:${APP_RELEASE:-stable}
    deploy:
      replicas: ${SPACE_REPLICAS:-1}
      restart_policy:
        condition: any
    depends_on:
      - api
      - worker
      - web

  admin:
    image: makeplane/plane-admin:${APP_RELEASE:-stable}
    deploy:
      replicas: ${ADMIN_REPLICAS:-1}
      restart_policy:
        condition: any
    depends_on:
      - api
      - web

  live:
    image: makeplane/plane-live:${APP_RELEASE:-stable}
    environment:
      <<: [*live-env, *redis-env]
    deploy:
      replicas: ${LIVE_REPLICAS:-1}
      restart_policy:
        condition: any
    depends_on:
      - api
      - web

  api:
    image: makeplane/plane-backend:${APP_RELEASE:-stable}
    command: ./bin/docker-entrypoint-api.sh
    deploy:
      replicas: ${API_REPLICAS:-1}
      restart_policy:
        condition: any
    volumes:
      - logs_api:/code/plane/logs
    environment:
      <<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
    depends_on:
      - plane-db
      - plane-redis
      - plane-mq

  worker:
    image: makeplane/plane-backend:${APP_RELEASE:-stable}
    command: ./bin/docker-entrypoint-worker.sh
    deploy:
      replicas: ${WORKER_REPLICAS:-1}
      restart_policy:
        condition: any
    volumes:
      - logs_worker:/code/plane/logs
    environment:
      <<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
    depends_on:
      - api
      - plane-db
      - plane-redis
      - plane-mq

  beat-worker:
    image: makeplane/plane-backend:${APP_RELEASE:-stable}
    command: ./bin/docker-entrypoint-beat.sh
    deploy:
      replicas: ${BEAT_WORKER_REPLICAS:-1}
      restart_policy:
        condition: any
    volumes:
      - logs_beat-worker:/code/plane/logs
    environment:
      <<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
    depends_on:
      - api
      - plane-db
      - plane-redis
      - plane-mq

  migrator:
    image: makeplane/plane-backend:${APP_RELEASE:-stable}
    command: ./bin/docker-entrypoint-migrator.sh
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
    volumes:
      - logs_migrator:/code/plane/logs
    environment:
      <<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
    depends_on:
      - plane-db
      - plane-redis

  plane-db:
    image: postgres:15.7-alpine
    command: postgres -c 'max_connections=1000'
    deploy:
      replicas: 1
      restart_policy:
        condition: any
    environment:
      <<: *db-env
    volumes:
      - pgdata:/var/lib/postgresql/data

  plane-redis:
    image: valkey/valkey:7.2.11-alpine
    deploy:
      replicas: 1
      restart_policy:
        condition: any
    volumes:
      - redisdata:/data

  plane-mq:
    image: rabbitmq:3.13.6-management-alpine
    deploy:
      replicas: 1
      restart_policy:
        condition: any
    environment:
      <<: *mq-env
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq

  plane-minio:
    image: minio/minio:latest
    command: server /export --console-address ":9090"
    deploy:
      replicas: 1
      restart_policy:
        condition: any
    environment:
      <<: *minio-env
    volumes:
      - uploads:/export

  proxy:
    image: makeplane/plane-proxy:${APP_RELEASE:-stable}
    deploy:
      replicas: 1
      restart_policy:
        condition: any
    environment:
      <<: *proxy-env
    ports:
      - target: 80
        published: 8090
        protocol: tcp
        mode: host
    volumes:
      - proxy_config:/config
      - proxy_data:/data
    depends_on:
      - web
      - api
      - space
      - admin
      - live

volumes:
  pgdata:
  redisdata:
  uploads:
  logs_api:
  logs_worker:
  logs_beat-worker:
  logs_migrator:
  rabbitmq_data:
  proxy_config:
  proxy_data:

Why port 8090? Coolify's Traefik proxy already listens on ports 80 and 443. If Plane's internal proxy tried to bind to those same ports, the deployment would fail with a port conflict. We use 8090 as a free internal port, and Traefik handles the public-facing traffic on 80/443 and forwards it to Plane.


Step 4 — Configure the Environment Variables

In Coolify, go to your service's Environment Variables tab and add the following. Replace all values marked with YOUR_... with your actual values.

# ── Domain ────────────────────────────────────────────────
APP_DOMAIN=plane.yourdomain.com
APP_RELEASE=stable
WEB_URL=https://plane.yourdomain.com
CORS_ALLOWED_ORIGINS=https://plane.yourdomain.com

# ── Proxy ─────────────────────────────────────────────────
LISTEN_HTTP_PORT=8090
LISTEN_HTTPS_PORT=443
SITE_ADDRESS=:80
MINIO_ENDPOINT_SSL=0

# ── Security (generate your own random strings) ───────────
SECRET_KEY=YOUR_RANDOM_64_CHARACTER_STRING
LIVE_SERVER_SECRET_KEY=YOUR_RANDOM_32_CHARACTER_STRING

# ── Database ──────────────────────────────────────────────
PGHOST=plane-db
PGDATABASE=plane
POSTGRES_USER=plane
POSTGRES_PASSWORD=plane
POSTGRES_DB=plane
POSTGRES_PORT=5432
PGDATA=/var/lib/postgresql/data
DATABASE_URL=postgresql://plane:plane@plane-db/plane

# ── Redis ─────────────────────────────────────────────────
REDIS_HOST=plane-redis
REDIS_PORT=6379
REDIS_URL=redis://plane-redis:6379/

# ── RabbitMQ ──────────────────────────────────────────────
RABBITMQ_HOST=plane-mq
RABBITMQ_PORT=5672
RABBITMQ_USER=plane
RABBITMQ_PASSWORD=plane
RABBITMQ_VHOST=plane
AMQP_URL=amqp://plane:plane@plane-mq:5672/plane

# ── Storage ───────────────────────────────────────────────
USE_MINIO=1
AWS_ACCESS_KEY_ID=access-key
AWS_SECRET_ACCESS_KEY=secret-key
AWS_REGION=
AWS_S3_ENDPOINT_URL=http://plane-minio:9000
AWS_S3_BUCKET_NAME=uploads
FILE_SIZE_LIMIT=5242880

# ── Other ─────────────────────────────────────────────────
DEBUG=0
API_BASE_URL=http://api:8000
API_KEY_RATE_LIMIT=60/minute

Generating secret keys: You need two secure random strings. Run this command on your server to generate each one:

openssl rand -hex 32

Run it twice — once for SECRET_KEY and once for LIVE_SERVER_SECRET_KEY. Copy each output and paste it as the value.

Important: Never share these keys publicly. They are used to sign sessions and authenticate internal services.


Step 5 — Deploy

Click Deploy in Coolify. The first deployment pulls all Docker images, which may take 3–5 minutes depending on your server's internet speed.

Once deployed, you'll see all services listed in the Logs tab. Don't worry if Coolify shows the service as "Degraded (unhealthy)" — this is expected. Coolify cannot health-check manually deployed Docker Compose stacks the same way it does its native resources. What matters is that the individual containers are running, which you can verify in the Logs tab.

Look for these indicators that everything is healthy:

  • Migrator — should show No migrations to apply and then exit. This is normal — it's a one-time job.
  • API — should show Starting gunicorn and Listening at: http://0.0.0.0:8000
  • Web, Admin, Space — should show health check requests every 30 seconds
  • Worker / Beat Worker — should show Connected to amqp://
  • Live — should show Express server has started at port 3000
  • Plane DB — should show database system is ready to accept connections
  • Plane Redis — should show Ready to accept connections tcp

Step 6 — Configure Traefik to Route Your Domain to Plane

This is the most important step. Coolify uses Traefik as its reverse proxy — the service that receives all traffic on ports 80 and 443 and routes it to the right application. You need to tell Traefik to forward traffic for plane.yourdomain.com to Plane's internal proxy container.

6a — Find Plane's Docker Network

SSH into your server and run:

sudo docker network ls

You'll see output like:

NETWORK ID     NAME                        DRIVER    SCOPE
1ca43bdda6db   bridge                      bridge    local
7bebd2e6a992   sok0sgggc88kss0sos8k4w0s   bridge    local

The network with a random-looking name (not bridge, host, or none) is Plane's Docker network. Note the Network ID (e.g. 7bebd2e6a992).

To confirm it's Plane's network, run:

sudo docker inspect YOUR_NETWORK_ID | grep -i "name\|container"

You should see container names like proxy-..., api-..., web-..., etc. That confirms it's the right network.

Also note the exact name of Plane's proxy container from this output — it will look like proxy-sok0sgggc88kss0sos8k4w0s. You'll need this in the next step.

6b — Connect Traefik to Plane's Network

Traefik runs inside Docker and by default can only reach containers on its own network. Run this command to connect it to Plane's network (replace YOUR_NETWORK_ID with the ID you found above):

sudo docker network connect YOUR_NETWORK_ID coolify-proxy

6c — Create the Traefik Routing Rule

Now create a dynamic configuration file that tells Traefik how to route traffic for your domain. Replace plane.yourdomain.com with your actual domain, and proxy-sok0sgggc88kss0sos8k4w0s with your actual proxy container name from Step 6a.

sudo tee /data/coolify/proxy/dynamic/plane.yaml << 'EOF'
http:
  routers:
    plane-http:
      rule: "Host(`plane.yourdomain.com`)"
      entryPoints:
        - http
      middlewares:
        - redirect-to-https
      service: plane

    plane-https:
      rule: "Host(`plane.yourdomain.com`)"
      entryPoints:
        - https
      service: plane
      tls:
        certResolver: letsencrypt

  middlewares:
    redirect-to-https:
      redirectScheme:
        scheme: https
        permanent: true

  services:
    plane:
      loadBalancer:
        servers:
          - url: "http://proxy-sok0sgggc88kss0sos8k4w0s:80"
EOF

What this does: It creates two Traefik routers — one that catches HTTP traffic and redirects it to HTTPS, and one that handles HTTPS traffic with an automatically issued Let's Encrypt SSL certificate, forwarding everything to Plane's internal proxy container by name.

6d — Set Correct File Permissions and Reload Traefik

Traefik runs as a non-root user (UID 9999). The config file needs to be owned by that user, otherwise Traefik silently ignores it:

sudo chown 9999:9999 /data/coolify/proxy/dynamic/plane.yaml
sudo docker kill --signal=SIGHUP coolify-proxy

The SIGHUP signal tells Traefik to reload its configuration without restarting.


Step 7 — Verify It's Working

Open your browser and go to https://plane.yourdomain.com.

You should see the Plane sign-up screen. Create your admin account and your first workspace.

If you see a Bad Gateway error, wait 30–60 seconds and refresh. Traefik may still be issuing the SSL certificate from Let's Encrypt. If the error persists, check the troubleshooting section below.


Step 8 — Access the Instance Admin Panel

Plane has a separate admin panel called God Mode for managing instance-level settings like authentication methods, email configuration, and sign-up restrictions.

Access it at:

https://plane.yourdomain.com/god-mode/

Log in with the same credentials you used to create your first account. From here you can:

  • Enable or disable public sign-ups
  • Configure SMTP for email notifications
  • Set up OAuth (Google, GitHub)
  • View instance health and telemetry settings

Troubleshooting

Bad Gateway after setup

This usually means Traefik can't reach Plane's proxy container. The most common causes are:

  1. The plane.yaml file has wrong permissions — re-run the chown command from Step 6d.
  2. Traefik isn't connected to Plane's Docker network — re-run the docker network connect command from Step 6b.
  3. The container name in plane.yaml is wrong — double-check it against the output of sudo docker inspect YOUR_NETWORK_ID | grep Name.

Migrator container keeps restarting

This is normal on the first run. The migrator is a one-time job — once it finishes applying database migrations, it exits. Coolify may show it as "Exited" which is the correct final state.

Coolify shows "Degraded (unhealthy)"

This is expected and can be ignored. Coolify can't perform health checks on manually managed Docker Compose stacks. As long as your containers are running and the site is accessible, everything is fine.

SSL certificate not issuing

Make sure your domain's A record is pointing to the correct server IP and has fully propagated. You can check propagation at dnschecker.org. Traefik will automatically retry certificate issuance — give it a few minutes.

Port 8090 already in use

Run sudo ss -tlnp | grep 8090 to check. If it's taken, choose another free port (e.g. 8091), update LISTEN_HTTP_PORT in your environment variables, redeploy, and update the published port in the compose file's proxy service accordingly.


Summary

You now have a fully self-hosted Plane instance running on your own infrastructure at zero licensing cost. Here's what you set up:

  • A full Plane Community Edition stack with PostgreSQL, Redis, RabbitMQ, and MinIO storage
  • Coolify managing the Docker Compose deployment
  • Traefik handling HTTPS with automatic SSL from Let's Encrypt
  • A clean routing setup that doesn't interfere with other apps on the same server

The Community Edition includes unlimited projects, unlimited users, kanban boards, timelines, cycles, modules, dashboards, pages, and a full REST API. For most small to medium teams migrating off Monday.com, it covers everything you need.


This guide was written by Batin Studio — a digital product design and development agency. If you'd like help setting this up for your team or need a custom deployment, get in touch.

Subscribe to our newsletter

Be the first to know the latest