I track personal work in Linear, including ilo-lang, and wanted it on the arm64 box that already carries analytics, error tracking and DMARC reporting. The two open-source trackers worth running in place of Linear or Jira are Plane and Huly.
Here we will explore what separated them:
- the advertised one-line installer, which installs a different edition to the one people mean by self-hosted Plane
- where the Community compose file went, since it is no longer at the path most write-ups give
- what each stack costs in resident memory, measured rather than estimated
- what the free edition does not include, from Plane’s own pricing and edition docs
- the changes needed to put it behind a reverse proxy that already owns ports 80 and 443
The box is a Hetzner CAX, 4 vCPU and 8 GB, arm64, running 54 containers behind Dokploy. Before any of this, free -m reported 3,679 MB available.
The advertised installer is the Commercial edition
Plane’s install page offers a single curl-into-shell line. Reading the script it serves before running it, two things stand out, what it downloads and what it sends:
MACHINE_ID=$(cat /etc/machine-id)
curl -sL -H "x-machine-signature: ${MACHINE_ID}" "$PRIME_HOST/api/v2/downloads/cli?arch=..."
sudo tar -xzf ~/prime-cli.tar.gz -C /bin
sudo prime-cli setup --host="$PRIME_HOST" --behind-proxy="$BEHIND_PROXY"
It sends the host’s /etc/machine-id to the vendor, installs a prime-cli binary into /bin, and hands the rest of the install to that binary. Its own error text names the edition: “Plane Commercial only works with some flavors of Linux and macOS.”
prime-cli then stands up its own compose stack with its own proxy bound to 80 and 443. On a box where Traefik already holds those ports that is a collision, and the result would sit outside Dokploy, managed by a second tool. There is a --behind-proxy flag, so the port clash is avoidable. Having a second tool own the stack is the part I did not want.
The Community compose moved
Most write-ups point at deploy/selfhost/docker-compose.yml. That path returns 404 on the current tags, and deploy/ does not exist at master at all. The Community stack is now under a different tree:
curl -s https://api.github.com/repos/makeplane/plane/contents/deployments?ref=v1.4.1 \
| jq -r '.[].name'
# aio
# cli
# kubernetes
# swarm
The file is deployments/cli/community/docker-compose.yml, with variables.env beside it. It is maintained, it pulls published images, and it needed about a dozen edits to fit the existing setup rather than a rewrite.
Check the manifest before pinning a tag
The compose file defaults to ${APP_RELEASE:-stable}. On arm64 that default is fine and the obvious upgrade is not:
curl -s "https://hub.docker.com/v2/repositories/makeplane/plane-backend/tags?page_size=6" \
| jq -r '.results[] | "\(.name) \([.images[].architecture] | unique | join(","))"'
v1.4.2-rc1 amd64
preview amd64
stable amd64,arm64,unknown
v1.4.1 amd64,arm64,unknown
v1.4.1-rc2 amd64,unknown
stable and the released version tags carry arm64. preview and the release candidates are amd64 only, and an amd64 image on this box loads, starts, then dies with exec format error. I pinned APP_RELEASE=v1.4.1 rather than tracking stable, so an upstream retag cannot move the running version underneath me.
Huly’s images are fine here too. hardcoreeng/front, transactor, account, collaborator, fulltext and rekoni-service all publish amd64 and arm64 on their current tags. Architecture was not what ruled Huly out.
What each stack costs to run
Plane Community is thirteen services:
- four front ends:
web,space,admin,live - four processes off one backend image:
api,worker,beat-worker,migrator plane-db(Postgres 15.7),plane-redis(Valkey 7.2),plane-mq(RabbitMQ 3.13),plane-minioproxy, a Caddy that routes between them
Measured on the box once it had settled, against the mem_limit I set on each:
worker-1 401.6MiB / 640MiB
api-1 168.2MiB / 768MiB
live-1 157.5MiB / 512MiB
beat-worker-1 150.6MiB / 384MiB
plane-mq-1 132.3MiB / 512MiB
space-1 102.1MiB / 384MiB
plane-minio-1 78.4MiB / 384MiB
plane-db-1 70.5MiB / 512MiB
proxy-1 22.1MiB / 128MiB
plane-redis-1 8.8MiB / 128MiB
admin-1 6.7MiB / 384MiB
web-1 5.8MiB / 512MiB
That is 1,305 MiB across twelve running containers, and about 3 GB of images on disk. Available memory on the box went from 3,679 MB to 2,644 MB.
Huly’s compose.yml defines fourteen services, and four of them are CockroachDB, Redpanda, Elasticsearch and MinIO underneath the application containers. Its README opens the System Requirements section with “Huly is resource-heavy”, asks for 2 vCPU and 8 GB as a minimum, recommends 4 vCPU and 16 GB, and warns that “servers below the minimum may stop responding or fail”. The same file says the supplied CockroachDB and Redpanda configurations “might not be production-ready”.
I did not deploy Huly. An 8 GB minimum against 3.6 GB free is not a close call, and I am not going to describe how a product behaves when I have not run it. Fitting it here would mean a bigger instance or a second machine, which is more than I wanted to run for a personal tracker.
What the free edition does not include
Plane’s editions page is direct about the ceiling: the Community Edition “is at par with the Free tier of the Cloud edition in its feature availability”, and “to upgrade to paid plans, you must first switch to the Commercial Edition”. Community is AGPL v3.0 and needs no licence key. Commercial is closed source.
Reading that against the pricing page shows what sits above the line. Integrations and the marketplace, work item types and custom properties, worklogs and templates start at Pro. Intake email and forms, project templates and a single workflow start at Business. Approvals, group sync and audit logs are Enterprise Grid. The editions page is also explicit about order: Cloud is the test bed for new features, Commercial innovates quickly, and changes to Community are “intentful and deliberate”.
Seat limits are the part I could not pin down. There is an open issue documenting four inconsistent statements across Plane’s marketing page, blog, billing docs and a maintainer reply. The editions page describes the 12-seat allowance as a bundle inside Commercial rather than a cap on Community, which is the reading I went with. For one user it does not matter, and I would want it settled before putting a team on it.
Fitting it behind a proxy that already exists
The published compose is written for Swarm and for owning the machine. Three changes made it a Dokploy stack.
Every service uses deploy.replicas with a restart_policy, which plain compose ignores, so those became restart: always and restart: on-failure on the migrator. The proxy publishes 80 and 443 on the host, which became an internal expose plus membership of the shared Traefik network. And the bundled Caddy reads its listen address from an environment variable:
proxy:
environment:
SITE_ADDRESS: ":80"
expose:
- "80"
networks:
- default
- dokploy-network
A bare :80 makes Caddy serve plain HTTP and skip ACME entirely, because it only requests certificates for named hosts. Traefik terminates TLS at the edge and the inner proxy does no certificate work. I also dropped Postgres from the shipped max_connections=1000 to 200, which is closer to what four workers need.
Keep the bundled Caddy
The obvious simplification is to delete the proxy container and point the domain at web. Its Caddyfile is why that breaks:
reverse_proxy /spaces/* space:3000
reverse_proxy /god-mode/* admin:3000
reverse_proxy /live/* live:3000
reverse_proxy /api/* api:8000
reverse_proxy /auth/* api:8000
reverse_proxy /{$BUCKET_NAME}/* plane-minio:9000
reverse_proxy /* web:3000
Seven path prefixes across five containers. Point the domain straight at web and sign-in, file uploads and the admin console all stop working, because /auth, /uploads and /god-mode never reach anything. At 22 MB resident it is the cheapest container in the stack to keep.
The 502 after everything reports healthy
migrator runs the Django migrations and exits 0, and api waits on service_completed_successfully before it starts. Twelve containers reported Up while /api/instances/ was still returning 502 through Traefik, and inside the proxy container the API was refusing connections outright.
That lasted about a minute and then cleared on its own. First deploy was roughly four minutes end to end on this box, most of it pulling images, so the gap between containers being up and the API answering is easy to read as a failure when it is the backend still loading. The web, admin and spaces front ends answer before the API does, so the site looks up while sign-in still fails.
Two things to change on first run
Instance setup happens at /god-mode/, and doing it over the API is more particular than the docs suggest. It is a form-encoded POST to /api/instances/admins/sign-up/, and it wants a token from /auth/get-csrf-token/ sent back both as the csrfmiddlewaretoken field and as an X-CSRFToken header, plus a Referer. The path that reads like the right one, under /auth/, returns 404.
Open signup ships enabled, which is the change worth making first:
{ "ENABLE_SIGNUP": "1", "ENABLE_EMAIL_PASSWORD": "1" }
On a host with a public DNS record that means anyone who finds it can create an account. A PATCH to /api/instances/configurations/ setting ENABLE_SIGNUP to "0" closes it, and the same call took the SMTP settings, which live in the database rather than in the compose environment and so survive a redeploy.
Where this leaves the migration
Plane is running with an admin account and a workspace on it. The issues are all still in Linear.
Community has no Linear importer. That is part of the Commercial edition’s integration set, which the pricing page puts at Pro and above, so moving the existing work items means a script against Linear’s GraphQL API and Plane’s REST API: teams to projects, Linear projects to modules, states and labels created to match rather than accepting the five defaults, then work items with their comments and parent relations. Attachments are the awkward part, since Linear’s file URLs need auth, so they have to be downloaded and re-uploaded rather than rewritten.
The other gap is the same one every service on this box has. Four named volumes now hold the only copy of anything I put in there, and nothing snapshots them.