Plane Project and Issue Tracker on AWS User Guide
Overview
Plane is an open source project and issue tracking tool - a self hosted alternative to Jira and Linear. Teams plan and track work in workspaces and projects, with work items that carry states, priorities, labels, assignees, estimates and sub items. Work items group into cycles for time boxed sprints and modules for longer streams of work, and the same backlog can be viewed as a list, a kanban board, a calendar or a spreadsheet.
This cloudimg AMI delivers Plane Community v1.3.1 fully installed and wired as a twelve container appliance behind a single reverse proxy. The web console answers on port 80 within a few minutes of launch, on infrastructure you control, with no per seat licence fee and no project data leaving your AWS account.
What is in the image
Twelve long running containers, all on a private Docker Compose bridge network. Only the proxy publishes a host port - the database, cache, queue and object store are not reachable from outside the instance at all.
| Component | Image | Role | Host port |
|---|---|---|---|
proxy |
makeplane/plane-proxy:v1.3.1 |
reverse proxy and the single front door | 80, 443 |
web |
makeplane/plane-frontend:v1.3.1 |
the main Plane web application | none |
admin |
makeplane/plane-admin:v1.3.1 |
instance administration console | none |
space |
makeplane/plane-space:v1.3.1 |
public shared views and pages | none |
live |
makeplane/plane-live:v1.3.1 |
live collaborative editing server | none |
api |
makeplane/plane-backend:v1.3.1 |
Django REST API | none |
worker |
makeplane/plane-backend:v1.3.1 |
asynchronous background worker | none |
beat-worker |
makeplane/plane-backend:v1.3.1 |
scheduled job runner | none |
plane-db |
postgres:15.7-alpine |
PostgreSQL database (all projects and work items) | none |
plane-redis |
valkey/valkey:7.2.11-alpine |
Valkey cache | none |
plane-mq |
rabbitmq:3.13.6-management-alpine |
RabbitMQ task queue | none |
plane-minio |
minio/minio (digest pinned) |
S3 compatible object store for attachments | none |
A thirteenth compose service, migrator, runs the database migration set to completion at boot and then exits - it is a job, not a long running component.
A note on names. The container is called
plane-redisfor upstream compatibility, but the engine it runs is Valkey, not Redis. The table above reflects what actually runs.
What cloudimg adds
- No shared secret ever ships. The image is captured with no database, no administrator account and only placeholder secrets, and the stack is never started before capture. On your instance's first boot the database password, the Django
SECRET_KEYthat signs sessions, the object store access and secret keys, the queue password and the live collaboration server key are all generated on your instance, the database initialises against them, and an administrator account plus a REST API key are written to a root only file. - A dedicated gp3 data volume at
/var/lib/planecarries the entire stateful tier: the Docker data-root, the containerd image and snapshot store, the PostgreSQL cluster, the cache, the queue and uploaded attachments. It is mounted by filesystem UUID so it reproduces on every instance, and it can be resized, snapshotted and backed up independently of the operating system disk. - The appliance learns its own address on every boot from EC2 instance metadata (IMDSv2) and rewrites the application URL and allowed origins, so the web console keeps working after a stop and start.
- A ready to use workspace and project are created during that same first boot, so the tracker is usable the moment the instance comes up.
- The base operating system is fully patched with unattended security upgrades enabled.
Licensing
Plane is free software under the GNU Affero General Public License v3.0. Verified at the shipped tag: LICENSE.txt at v1.3.1 is the AGPL v3, and COPYRIGHT.txt reads "Plane Software, Inc. and contributors ... SPDX-License-Identifier: AGPL-3.0-only". The v1.3.1 tree contains no ee/ or enterprise/ directory, so the whole shipped codebase is AGPL-3.0-only and nothing in this image is a paid, Pro or Enterprise feature.
This image ships stock, unmodified upstream Plane images, pinned by tag and by digest, configured externally through an environment file and systemd units. The Corresponding Source for the network hosted appliance, as required by AGPL section 13, is the pinned upstream release: github.com/makeplane/plane/releases/tag/v1.3.1.
The cloudimg charge covers packaging, security patching, image maintenance and support. Plane itself charges nothing.
Launching the instance
Subscribe to the listing in AWS Marketplace and launch the AMI. The recommended instance type is m5.large (2 vCPU, 8 GiB RAM) - Plane's own guidance is at least 4 GiB of RAM for the self hosted stack, and the image adds a swapfile on the data volume for the first boot migration spike.
Your security group needs:
| Port | Protocol | Purpose |
|---|---|---|
| 22 | TCP | SSH administration |
| 80 | TCP | Plane web console and REST API |
| 443 | TCP | HTTPS, once you terminate TLS with your own certificate |
Restrict port 22, and ideally 80 and 443, to your own networks. Two EBS volumes are attached: a 50 GiB operating system root volume, and a 100 GiB gp3 data volume mounted at /var/lib/plane.
Connecting to your instance
SSH in as the login user for the OS variant you launched.
| OS variant | Login user | Example |
|---|---|---|
| Ubuntu 24.04 LTS | ubuntu |
ssh -i your-key.pem ubuntu@<public-ip> |
First boot
First boot generates every secret on your own instance, initialises the database, runs the full Django migration set and creates your administrator account and seed workspace. On an m5.large this takes a few minutes. The appliance writes a sentinel file when it is finished.
sudo test -f /var/lib/cloudimg/plane-firstboot.done && echo "first boot complete"
first boot complete
Confirm the systemd units that own the appliance are all enabled:
sudo systemctl is-enabled docker.service plane-configure.service plane-firstboot.service plane.service cloudimg-plane-swap.service
enabled
enabled
enabled
enabled
enabled
plane-configure.service runs on every boot and resolves the instance's current public address from IMDSv2. plane-firstboot.service is a one shot that only ever runs once - it is gated on the sentinel above. plane.service brings the compose stack up and is gated on that same sentinel, so it is impossible for the stack to start before the per instance secrets exist.
Retrieving your credentials
Every credential is unique to your instance and is written to a root only file at /root/plane-credentials.txt.
The file holds these keys:
sudo grep -oE '^[A-Z_]+=' /root/plane-credentials.txt
PLANE_URL=
PLANE_ADMIN_EMAIL=
PLANE_ADMIN_PASSWORD=
PLANE_API_KEY=
PLANE_WORKSPACE_SLUG=
PLANE_PROJECT_ID=
Read the whole file to get the values:
sudo cat /root/plane-credentials.txt
PLANE_URL=<PLANE_URL>
PLANE_ADMIN_EMAIL=<PLANE_ADMIN_EMAIL>
PLANE_ADMIN_PASSWORD=<PLANE_ADMIN_PASSWORD>
PLANE_API_KEY=<PLANE_API_KEY>
PLANE_WORKSPACE_SLUG=<PLANE_WORKSPACE_SLUG>
PLANE_PROJECT_ID=<PLANE_PROJECT_ID>
Store these somewhere safe and change the administrator password once you have signed in. The file is mode 0600 and owned by root, so only a user who can already become root on the instance can read it.
Verifying the deployment
Every container is up
sudo docker compose -f /opt/plane/docker-compose.yaml --env-file /opt/plane/plane.env ps --format 'table {{.Service}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
SERVICE IMAGE STATUS PORTS
admin makeplane/plane-admin:v1.3.1 Up 11 minutes (healthy) 80/tcp, 3000/tcp
api makeplane/plane-backend:v1.3.1 Up 11 minutes 8000/tcp
beat-worker makeplane/plane-backend:v1.3.1 Up 11 minutes 8000/tcp
live makeplane/plane-live:v1.3.1 Up 11 minutes 3000/tcp
plane-db postgres:15.7-alpine Up 11 minutes 5432/tcp
plane-minio minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e Up 11 minutes 9000/tcp
plane-mq rabbitmq:3.13.6-management-alpine Up 11 minutes 4369/tcp, 5671-5672/tcp, 15671-15672/tcp, 15691-15692/tcp, 25672/tcp
plane-redis valkey/valkey:7.2.11-alpine Up 11 minutes 6379/tcp
proxy makeplane/plane-proxy:v1.3.1 Up 11 minutes 0.0.0.0:80->80/tcp, [::]:80->80/tcp, 0.0.0.0:443->443/tcp, [::]:443->443/tcp, 443/udp, 2019/tcp
space makeplane/plane-space:v1.3.1 Up 11 minutes 3000/tcp
web makeplane/plane-frontend:v1.3.1 Up 11 minutes (healthy) 80/tcp, 3000/tcp
worker makeplane/plane-backend:v1.3.1 Up 11 minutes 8000/tcp
The PORTS column is the security model in one view. Every container except proxy shows only its container port - none of them is bound to a host address, so PostgreSQL, Valkey, RabbitMQ and MinIO cannot be reached from the network even if your security group were wide open.
The API and the web console answer
curl -s -o /dev/null -w 'API /api/instances/ HTTP %{http_code}\n' http://127.0.0.1/api/instances/
curl -s -o /dev/null -w 'web UI / HTTP %{http_code}\n' http://127.0.0.1/
API /api/instances/ HTTP 200
web UI / HTTP 200
Only the proxy is listening on the host
sudo ss -tlnp | grep -E ':(80|443|5432|6379|9000|5672) ' | awk '{print $1, $4, $6}'
LISTEN 0.0.0.0:80 users:(("docker-proxy",pid=5055,fd=8))
LISTEN 0.0.0.0:443 users:(("docker-proxy",pid=5079,fd=8))
LISTEN [::]:80 users:(("docker-proxy",pid=5063,fd=8))
LISTEN [::]:443 users:(("docker-proxy",pid=5088,fd=8))
Ports 5432 (PostgreSQL), 6379 (Valkey), 9000 (MinIO) and 5672 (RabbitMQ) return nothing - they are not bound on the host.
The whole stateful tier is on the dedicated volume
findmnt -no SOURCE,TARGET,FSTYPE /var/lib/plane
findmnt -no SOURCE,TARGET,FSTYPE /var/lib/containerd
/dev/nvme1n1 /var/lib/plane ext4
/dev/nvme1n1[/containerd-store] /var/lib/containerd ext4
Both the Docker data-root and the containerd image store live on the data volume. This matters more than it looks: Docker 29 keeps image layers under /var/lib/containerd, not under the Docker data-root, so relocating only the data-root would leave several gigabytes of Plane images on the operating system disk. Both are bind mounted onto the data volume and both entries are recorded in /etc/fstab, so the layout reproduces on every instance.
grep -E 'plane|containerd' /etc/fstab
UUID=985b4f29-d530-4741-bf37-d250e81c2e33 /var/lib/plane ext4 defaults,nofail 0 2
/var/lib/plane/containerd-store /var/lib/containerd none bind,nofail,x-systemd.requires-mounts-for=/var/lib/plane 0 0
df -h --output=target,size,used,avail /var/lib/plane /
Mounted on Size Used Avail
/var/lib/plane 98G 8.2G 85G
/ 48G 3.4G 45G
The shipped image set is pinned
sudo docker images --format '{{.Repository}}:{{.Tag}}' | sort
makeplane/plane-admin:v1.3.1
makeplane/plane-backend:v1.3.1
makeplane/plane-frontend:v1.3.1
makeplane/plane-live:v1.3.1
makeplane/plane-proxy:v1.3.1
makeplane/plane-space:v1.3.1
minio/minio:latest
postgres:15.7-alpine
rabbitmq:3.13.6-management-alpine
valkey/valkey:7.2.11-alpine
Every image is baked into the AMI, so your instance never pulls from a registry at first boot. The Plane images are pinned by release tag and MinIO is additionally pinned by digest in the compose file.
sudo grep '^APP_RELEASE=' /opt/plane/plane.env
APP_RELEASE=v1.3.1
Signing in to the web console
Open http://<public-ip>/ in a browser. Plane asks for your email first, then your password.

Sign in with the PLANE_ADMIN_EMAIL and PLANE_ADMIN_PASSWORD values from the credentials file. You land straight in the seed workspace - the administrator profile is already marked onboarded, so there is no setup wizard to click through.

Open a project and you are in the work item list, where each item shows its state, priority, dates, assignees and labels inline.

Use the layout switcher, immediately left of Display, to move between List, Board and Calendar. The board groups by state and supports drag and drop between columns.

Workflow states are configurable per project under project settings, grouped into the five Plane state groups - Backlog, Unstarted, Started, Completed and Cancelled.

The instance administration console - where you configure email, authentication providers and instance wide settings - is at http://<public-ip>/god-mode/, and takes the same administrator email and password.
Using the REST API
Every instance is created with an administrative REST API key. Read it into a shell variable rather than printing it, and send it in an X-API-Key header.
List the projects in your workspace:
KEY=$(sudo grep '^PLANE_API_KEY=' /root/plane-credentials.txt | cut -d= -f2-)
SLUG=$(sudo grep '^PLANE_WORKSPACE_SLUG=' /root/plane-credentials.txt | cut -d= -f2-)
curl -s -H "X-API-Key: $KEY" "http://127.0.0.1/api/v1/workspaces/$SLUG/projects/" | jq -r '.results[] | "\(.identifier)\t\(.name)"'
GS Getting Started
Create a work item:
KEY=$(sudo grep '^PLANE_API_KEY=' /root/plane-credentials.txt | cut -d= -f2-)
SLUG=$(sudo grep '^PLANE_WORKSPACE_SLUG=' /root/plane-credentials.txt | cut -d= -f2-)
PROJ=$(sudo grep '^PLANE_PROJECT_ID=' /root/plane-credentials.txt | cut -d= -f2-)
curl -s -X POST "http://127.0.0.1/api/v1/workspaces/$SLUG/projects/$PROJ/issues/" -H "X-API-Key: $KEY" -H 'Content-Type: application/json' -d '{"name":"Check the deployment guide end to end"}' | jq -r '"created \(.sequence_id): \(.name)"'
created 14: Check the deployment guide end to end
An unauthenticated or wrong key is rejected:
SLUG=$(sudo grep '^PLANE_WORKSPACE_SLUG=' /root/plane-credentials.txt | cut -d= -f2-)
curl -s -o /dev/null -w 'bogus key: HTTP %{http_code}\n' -H 'X-API-Key: not-a-real-key' "http://127.0.0.1/api/v1/workspaces/$SLUG/projects/"
bogus key: HTTP 403
Issue additional, scoped API keys per user from Settings > API tokens in the web console rather than sharing the bootstrap key.
Day two operations
Managing the stack
sudo systemctl status plane --no-pager | head -5
plane.service is a one shot that runs docker compose up -d. To restart the whole appliance:
sudo systemctl restart plane.service
To read the logs of a single component:
sudo docker compose -f /opt/plane/docker-compose.yaml --env-file /opt/plane/plane.env logs --tail 20 api
Using your own domain name and TLS
The appliance rewrites APP_DOMAIN, WEB_URL and CORS_ALLOWED_ORIGINS to the instance's current public IPv4 on every boot, which is what makes it work out of the box. Once you have a DNS name you want to use instead, three steps:
- Stop the per boot rewrite so it cannot overwrite your values:
sudo systemctl disable --now plane-configure.service - Edit
/opt/plane/plane.envwith your editor of choice, settingAPP_DOMAIN=tracker.example.com,WEB_URL=https://tracker.example.comandCORS_ALLOWED_ORIGINS=https://tracker.example.com - Apply it:
sudo systemctl restart plane.service
Check the current values at any time:
sudo grep -E '^(APP_DOMAIN|WEB_URL|CORS_ALLOWED_ORIGINS)=' /opt/plane/plane.env
For TLS, the simplest production shape is an Application Load Balancer or CloudFront distribution in front of the instance holding an ACM certificate, forwarding to port 80. The bundled proxy also supports ACME directly through the CERT_EMAIL, CERT_ACME_CA and LISTEN_HTTPS_PORT variables in the same env file if you would rather terminate on the instance.
Outbound email
Plane sends workspace invitations and notifications by email. Configure your SMTP relay in the instance administration console at http://<public-ip>/god-mode/, under Email.
Backups
Everything stateful lives on the /var/lib/plane volume, so an EBS snapshot of that volume captures the database, the queue, the cache and every uploaded attachment together. Schedule it with AWS Backup or a Data Lifecycle Manager policy. For a logical database dump instead:
sudo docker compose -f /opt/plane/docker-compose.yaml --env-file /opt/plane/plane.env exec -T plane-db pg_dump -U plane plane | gzip > /var/tmp/plane-backup.sql.gz && ls -lh /var/tmp/plane-backup.sql.gz
Resizing the data volume
Grow the EBS volume in the console or with the CLI, then extend the filesystem in place - no downtime and no unmount:
sudo resize2fs /dev/nvme1n1 && df -h --output=target,size,avail /var/lib/plane
Patching
Unattended security upgrades are enabled for the operating system. Plane itself is pinned by image tag, so upgrading is a deliberate change: edit APP_RELEASE in /opt/plane/plane.env to the new version, pull, and bring the stack back up. Read the upstream release notes first, take a snapshot of the data volume, and note that the migrator service runs the database migrations on the way up.
Support
cloudimg provides 24/7 technical support for this product by email and live chat.
Contact: support@cloudimg.co.uk
We can help with deployment and initial configuration, creating your first workspaces and projects, inviting users and configuring roles, re-pointing the instance at your own domain, TLS termination, outbound email, attachment storage and retention, backing up the bundled database, resizing the data volume, performance tuning, and patching guidance.