What the stack needs
Plane is a multi-service application. The Compose file that setup.sh downloads starts web, space, admin and live front ends, a Django API, a Celery worker and beat scheduler, a one-shot migrator, and the data layer next to them on the same host.
| Item | As documented |
|---|---|
| CPU | 2 cores, x64/AMD64 or AArch64/ARM64 (Docker Compose page, Commercial install; the Community notes ask for at least an AWS t3.medium) |
| Memory | 4 GB; 8 GB recommended for production (same Commercial list) |
| Database | PostgreSQL 15 (15.7 or later) or 16.x; the bundled image is 15.7 |
| Cache, queue | Redis or Valkey, plus RabbitMQ for the workers |
| Object storage | MinIO bundled, or any S3-compatible store |
| Search | Runs on the database; OpenSearch advanced search is a paid Commercial feature |
The docs give no per-user figure, so none appears here. Everything above shares one box in the default layout: the database, the queue and the upload store compete with the application for the same memory, which is why the 8 GB line matters more than the 4 GB one.
Community or Commercial
Plane ships two self-hosted codebases, and the edition decides which installer you use. In the project's own words, the Community Edition "Is governed by the AGPL v3.0 license, ensuring free and open usage", while the Commercial Edition "remains closed-source to offer enterprise-grade features".
- Community has feature parity with the Free tier of Plane's cloud and needs no licence key. To buy a paid plan you must first switch to Commercial; edition moves are described as generally one-directional in practice.
- Commercial includes "A bundle of 12 Free user seats per workspace". Paid plans (Pro, Business, Enterprise Grid) are keys from the Prime portal, each bound to one workspace and one machine, and the instance checks its licence with Plane.
- AGPL in practice, per the docs: running or modifying Community for your own team is fine; hosting it as a service for outside customers obliges you to publish your modifications.
Read the installer before it runs
The Commercial page tells you to pipe its installer from prime.plane.so straight into sh. Don't. Piped, the script runs as it arrives: you never see it, and a connection that drops halfway executes whatever part landed. Save it with curl -fsSL -o prime-install.sh https://prime.plane.so/install/, read it, then run bash prime-install.sh, since its first line names bash.
Reading it shows a short wrapper. It sends the machine ID from /etc/machine-id to prime.plane.so in a request header, downloads the prime-cli archive, unpacks it into /bin with sudo and runs prime-cli setup. That binary is closed source, so the reading ends there. Commercial then lives in /opt/plane and is driven by prime-cli for backup, restore and upgrade.
The Community route already downloads to a file. setup.sh is plain bash: it fetches the release's docker-compose.yml and variables.env from GitHub, checks the images exist for your CPU (offering a local build if not), and wraps docker compose for start, stop, upgrade, logs and backup. The rest of this page follows it.
Firewall, secrets and first start
ufw goes on before any container exists, with IPv6 set first so the SSH rule is written for both families. The umask makes the files the scripts create in this shell owner-only; the cron line below sets its own.
# as root on a fresh Ubuntu or Debian box with Docker Engine and the Compose plugin
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH
ufw --force enable
apt-get install -y jq # restore.sh uses it to see whether Plane is still running
umask 077 # everything created from here on is owner-only
mkdir -p /opt/plane-selfhost && cd /opt/plane-selfhost
curl -fsSL -o setup.sh https://github.com/makeplane/plane/releases/latest/download/setup.sh
curl -fsSL -o restore.sh https://raw.githubusercontent.com/makeplane/plane/refs/heads/preview/deployments/cli/community/restore.sh
sha256sum setup.sh restore.sh # note these, then read both files before going on
Open both scripts in less and compare what they do with the description above. Then fill plane.env. The stock file ships plane as both the database and RabbitMQ password, access-key/secret-key as the MinIO login and a placeholder SECRET_KEY. The Compose file falls back to connection URLs with those default passwords whenever DATABASE_URL or AMQP_URL is empty, so set each URL to match its password. Do it before the first start, because Postgres keeps the password it was created with. Hex values avoid the special characters the docs warn against in database passwords. First run bash setup.sh install on its own and let it finish: it writes plane-app/docker-compose.yaml and plane.env and pulls the images, and it stops to ask a question only if no images exist for your CPU. Then paste the rest.
cd /opt/plane-selfhost/plane-app && chmod 600 plane.env
pg=$(openssl rand -hex 24); mq=$(openssl rand -hex 24)
sed -i \
-e "s|^SECRET_KEY=.*|SECRET_KEY=$(openssl rand -hex 32)|" \
-e "s|^LIVE_SERVER_SECRET_KEY=.*|LIVE_SERVER_SECRET_KEY=$(openssl rand -hex 32)|" \
-e "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$pg|" \
-e "s|^DATABASE_URL=.*|DATABASE_URL=postgresql://plane:$pg@plane-db:5432/plane|" \
-e "s|^RABBITMQ_PASSWORD=.*|RABBITMQ_PASSWORD=$mq|" \
-e "s|^AMQP_URL=.*|AMQP_URL=amqp://plane:$mq@plane-mq:5672/plane|" \
-e "s|^AWS_ACCESS_KEY_ID=.*|AWS_ACCESS_KEY_ID=$(openssl rand -hex 12)|" \
-e "s|^AWS_SECRET_ACCESS_KEY=.*|AWS_SECRET_ACCESS_KEY=$(openssl rand -hex 32)|" \
-e "s|^WEB_URL=.*|WEB_URL=http://localhost:8080|" \
-e "s|^CORS_ALLOWED_ORIGINS=.*|CORS_ALLOWED_ORIGINS=http://localhost:8080|" \
plane.env
unset pg mq
# first start on loopback only: add host_ip under each of the proxy's two ports
sed -i 's/^\( *\)mode: host$/&\n\1host_ip: 127.0.0.1/' docker-compose.yaml
cd .. && bash setup.sh start
ss -tlnp | grep -E ':(80|443) ' # expect 127.0.0.1 only
The secrets never reach shell history, but they sit on sed's command line for the instant it runs, visible in ps to anyone else logged in; on a box with one admin that is acceptable, otherwise use an editor. LIVE_SERVER_SECRET_KEY must match between the API and the live service; one variable feeds both.
Only the proxy publishes ports. On IPv4, Docker's own iptables rules are evaluated before ufw's, so ufw status says nothing about 80 and 443 there. On IPv6 it depends on the Docker version and its settings: if Docker is not managing ip6tables, ufw does filter IPv6, so run ufw allow 80,443/tcp before you publish an AAAA record, or Let's Encrypt validation over IPv6 fails. Either way, test 80 and 443 from another network on both families; ss -tlnp shows the listeners.
Claiming God mode, then going public
Whoever completes instance setup first becomes the instance admin, with access to /god-mode. Do that while the proxy only listens on loopback.
- From your own computer,
ssh -L 8080:127.0.0.1:80 you@your-server; the setup screen is now athttp://localhost:8080/god-mode/. Set the admin email and password there. - In God mode → Authentication, switch off sign-up without an invite unless you want open registration; under Workspaces you can stop others creating workspaces. General has the anonymous usage data switch.
- Point the domain's A (and AAAA) record at the server. In plane.env set
APP_DOMAIN,SITE_ADDRESSto the bare domain,WEB_URLandCORS_ALLOWED_ORIGINStohttps://plus the domain, andCERT_EMAIL="email you@example.com", quoted: the variables file says to include the wordemail, because the value is pasted into Caddy's global options, and setup.sh sources plane.env as shell, where the unquoted form would run as a command. - Drop the loopback lines and restart:
sed -i '/host_ip:/d' /opt/plane-selfhost/plane-app/docker-compose.yaml && cd /opt/plane-selfhost && bash setup.sh restart. Caddy now answers on 80 and 443 on every address and requests a Let's Encrypt certificate.
To run your own nginx instead, the Community docs say to remove the proxy service and route the paths yourself: /api, /auth and /static to api:8000, /god-mode to admin, /spaces to space, /live to live, /uploads to plane-minio:9000, everything else to web.
Mail and file storage
SMTP is set in God mode → Email, not in plane.env: host, port, sender address, TLS, SSL or none, and an optional username and password. Only email-and-password SMTP authentication is supported, not OAuth. Without it, invites and password resets never leave the box.
Uploads go to the bundled MinIO by default (USE_MINIO=1, bucket uploads). AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY double as MinIO's root login, and Caddy forwards /uploads to MinIO, which is why the defaults had to go. FILE_SIZE_LIMIT is in bytes, 5 MB by default, and Caddy enforces the same figure. For an external S3-compatible store, set USE_MINIO=0 and the AWS_* values; a MinIO server on its own machine is one option.
Backups, restore and upgrades
bash setup.sh backup (menu option 7) copies the Postgres, MinIO and Valkey data out of the running containers into /opt/plane-selfhost/plane-app/backup/YYYYMMDD-HHMM/ as tar.gz files; the docs list those three, and the current script, as we read it, adds RabbitMQ data too. plane.env is not included: it holds every secret, so copy it separately. For a nightly run, this writes a one-line cron file with its own umask and a dated log:
# writes the cron file /etc/cron.d/plane-backup (one line); run once as root
mkdir -p -m 700 /var/log/plane-backup
printf '%s\n' '30 2 * * * root umask 077; cd /opt/plane-selfhost && bash setup.sh backup > /var/log/plane-backup/run-$(date +\%F).log 2>&1' > /etc/cron.d/plane-backup
cat /etc/cron.d/plane-backup # the % must read \% in the file
Copy each dated folder and plane.env off the machine yourself, to storage you control elsewhere, and prune old folders, since nothing else does. The backup copies the database directory of a running server; our suggestion for a second, consistent copy is a nightly pg_dump run via docker compose exec -T plane-db, written to a file.
Restore. restore.sh deletes and recreates each matching plane-app_* volume from the archives in the folder you name, and refuses while Plane runs (it checks with jq):
cd /opt/plane-selfhost && bash setup.sh stopbash /opt/plane-selfhost/restore.sh /opt/plane-selfhost/plane-app/backup/YYYYMMDD-HHMM, with the dated folder you want in place of the example name.cd /opt/plane-selfhost && bash setup.sh start
Upgrade. Back up first, as the docs ask; versions older than 0.14.0 must pass through that release first. Fetch a fresh setup.sh, read it, then run cd /opt/plane-selfhost && bash setup.sh upgrade on its own, since it asks y/N. The docs describe an older flow in which you compare variables-upgrade.env with plane.env by hand. The current script, as we read it, instead saves your file as plane.env.bak, installs the new template as plane.env and carries your values across. So diff /opt/plane-selfhost/plane-app/plane.env.bak /opt/plane-selfhost/plane-app/plane.env shows the new keys, chmod 600 /opt/plane-selfhost/plane-app/plane.env restores the mode, and bash setup.sh start brings Plane back. The upgrade also replaces the Compose file, which is fine once the proxy is meant to be public.
The machine to run it on
A dedicated server gives this stack every core and all the memory under one root login, with no second tenant on the hardware, so Postgres and MinIO are not contending with a neighbour. Size memory first, as the range page advises: a Moonshot cartridge with 8 GB or more meets the production recommendation for one service per machine, and a single-socket server is the usual base for an application with its database on the same box. The Compose prerequisites list ARM64 too, so the Ampere Altra line qualifies. Racked single-, dual- and quad-socket servers have an out-of-band console, useful when a bad firewall rule locks you out, and a racked machine is handed over in about 30 minutes once payment is approved.
The same pattern runs a self-hosted Gitea for the code your issues point at, or an Outline wiki for the specs.
When this is the wrong setup
- You want the newest features first: they reach Plane's cloud, then Commercial, then Community.
- Nobody owns operations. The docs budget 1 to 3 days of one person's time to stand up a Docker Compose install and an ongoing share of a person afterwards.
- You need high availability. One Compose host is a single machine; Plane's HA guidance is Kubernetes with Helm.
Questions
Can a company use the Community Edition without paying?
Yes. It is AGPL v3.0 with no licence key, and the docs say internal use and internal modification carry no obligations beyond the licence terms. The obligation to publish modifications applies when you host it as a service for external customers. Paid plans are not available on Community at all.
How much RAM should a small team's Plane server have?
For the Commercial install the Docker Compose page lists 4 GB, with 8 GB recommended for production; the Community steps ask for at least an AWS t3.medium, which is 2 vCPU and 4 GB. That memory is shared with the bundled PostgreSQL, Valkey, RabbitMQ and MinIO containers. The docs publish no per-user sizing, so leave headroom and watch usage after the first weeks.
Which address do I use to reach God mode?
It is the /god-mode/ path on the same domain, served by the admin container behind the bundled proxy. Before the server is public, forward a local port over SSH to the loopback-bound proxy and open it there. Once SSO is configured, instance admins can also use it to sign in there.
Does the Commercial Edition need internet access?
Yes for licence activation and checks, which go to Plane's Prime portal; the Free tier runs without a key. The Airgapped Edition exists for networks with no outbound access and is offered with Enterprise Grid only.
Can I move from Community to Commercial later?
Yes, the docs have an upgrade guide for that move, and switching is the only way to reach paid plans. Treat it as one-way: the docs describe edition transitions as generally one-directional in practice. Take a backup with setup.sh first.
Why do invite emails not arrive?
Usually because SMTP was never set. Plane sends invites, password resets and exports through the server you configure in God mode → Email, and it supports username-and-password SMTP authentication only. Check the worker container's logs after a test invite.