diff --git a/productivity/project-management/plane/.env.example b/productivity/project-management/plane/.env.example new file mode 100644 index 0000000..49e1353 --- /dev/null +++ b/productivity/project-management/plane/.env.example @@ -0,0 +1,32 @@ +# ── Public URL ──────────────────────────────────────────────────────────────── +DOMAIN=plane.whitney.rip + +# ── Plane image tag ─────────────────────────────────────────────────────────── +# Pin to a release (e.g. v1.3.0) or use `stable` to follow latest. +# See: https://github.com/makeplane/plane/releases +APP_RELEASE=v1.3.0 + +# ── Django / backend ────────────────────────────────────────────────────────── +# Generate a fresh 50-char random string, e.g.: +# openssl rand -hex 25 +SECRET_KEY=replace-me-with-a-long-random-string + +# Gunicorn workers for the API container +GUNICORN_WORKERS=2 + +# Max upload size in bytes (default 5 MiB) +FILE_SIZE_LIMIT=5242880 + +# ── Postgres ────────────────────────────────────────────────────────────────── +POSTGRES_DB=plane +POSTGRES_USER=plane +POSTGRES_PASSWORD=change-me + +# ── RabbitMQ ────────────────────────────────────────────────────────────────── +RABBITMQ_USER=plane +RABBITMQ_PASSWORD=change-me + +# ── MinIO (object storage for uploads) ──────────────────────────────────────── +MINIO_ROOT_USER=change-me-access-key +MINIO_ROOT_PASSWORD=change-me-secret-key +MINIO_BUCKET_NAME=uploads diff --git a/productivity/project-management/plane/.gitignore b/productivity/project-management/plane/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/productivity/project-management/plane/.gitignore @@ -0,0 +1 @@ +.env diff --git a/productivity/project-management/plane/README.md b/productivity/project-management/plane/README.md new file mode 100644 index 0000000..a793f4c --- /dev/null +++ b/productivity/project-management/plane/README.md @@ -0,0 +1,173 @@ +# Plane + +[Plane](https://plane.so) is an open-source project management tool — issues, +cycles (sprints), modules, pages, and views. Think of it as a self-hosted +alternative to Jira / Linear / Asana. + +- Website: +- Source: +- Docs: +- Self-hosting reference: + +This stack runs the community edition behind Traefik at +. + +## Architecture + +The upstream release ships a bundled `plane-proxy` (Caddy) that routes +subpaths to the right internal service. We keep that proxy in place and only +expose **it** to the external `traefik` network — Traefik terminates TLS and +forwards to `plane-proxy:80`, which fans out inside the `plane` bridge +network: + +| Path | Service | What it is | +|---------------|-----------------|---------------------------------------------| +| `/` | `web` | Main Next.js application | +| `/spaces/*` | `space` | Public-shared views (read-only share links) | +| `/god-mode/*` | `admin` | Instance admin UI (see below) | +| `/live/*` | `live` | Real-time collaboration server | +| `/api/*` | `api` | Django/DRF backend | +| `/auth/*` | `api` | Auth endpoints | +| `/uploads/*` | `plane-minio` | Object storage (attachments, avatars) | + +Supporting services: Postgres (`plane-db`), Valkey (`plane-redis`), RabbitMQ +(`plane-mq`), MinIO (`plane-minio`), plus a Celery `worker` + `beat-worker` +and a one-shot `migrator`. + +## The `/god-mode` admin page + +Plane splits its UI across two Next.js apps: + +- `web` serves the normal app at `/` — this is what end-users see. +- `admin` serves the **instance-admin UI** at `/god-mode`. + +`/god-mode` is where you, the operator, configure the Plane instance itself: +initial superuser, SMTP, SSO/OAuth providers (Google, GitHub, GitLab, Gitea), +sign-up toggles, and license keys. It is **not** a workspace admin — each +workspace has its own settings accessible from within the `web` app. + +On first launch, hit before anything +else to create the instance admin. If you forget and create a normal user +first, you can promote them later via `docker compose exec api python +manage.py shell`. + +## First-time setup + +1. **Copy the env template and fill it in.** + + ```bash + cp .env.example .env + $EDITOR .env + ``` + + At minimum, set: + - `DOMAIN=plane.whitney.rip` + - `SECRET_KEY` — generate with `openssl rand -hex 25` + - `POSTGRES_PASSWORD`, `RABBITMQ_PASSWORD` + - `MINIO_ROOT_USER`, `MINIO_ROOT_PASSWORD` + + `.env` is gitignored; only `.env.example` is committed. + +2. **Ensure the external Traefik network exists.** + + ```bash + docker network inspect traefik >/dev/null \ + || docker network create traefik + ``` + +3. **Create the data directories.** All persistent state lives on the host + under `/pwspool/software/plane/`: + + ```bash + sudo mkdir -p /pwspool/software/plane/{db,redis,rabbitmq,minio,logs/{api,worker,beat}} + ``` + +4. **Pull images and start the stack.** + + ```bash + docker compose pull + docker compose up -d + ``` + + The `migrator` and `plane-createbuckets` containers run once and exit — + that is expected. Tail logs until things settle: + + ```bash + docker compose logs -f api web proxy + ``` + +5. **Create the instance admin.** Browse to + , set your admin email/password, + then configure auth providers and SMTP. + +6. **Create your first workspace.** Visit + , sign in with the admin account, and create + a workspace. Invite users from within the workspace settings. + +## Normal use + +```bash +# Start / stop +docker compose up -d +docker compose down # stop; data persists under /pwspool +docker compose restart api worker # restart a subset + +# Status & logs +docker compose ps +docker compose logs -f api +docker compose logs -f proxy + +# Pull latest images for the pinned APP_RELEASE and re-up +docker compose pull && docker compose up -d + +# Upgrade to a newer Plane release +# 1. Check https://github.com/makeplane/plane/releases +# 2. Bump APP_RELEASE in .env (e.g. v1.3.0 -> v1.4.0) +# 3. Pull + up — migrator re-runs automatically +docker compose pull && docker compose up -d + +# Shell into a running service +docker compose exec api bash +docker compose exec plane-db psql -U "$POSTGRES_USER" "$POSTGRES_DB" + +# Run a Django management command +docker compose exec api python manage.py shell +docker compose exec api python manage.py reset_password +``` + +## Data layout + +``` +/pwspool/software/plane/ +├── db/ # Postgres PGDATA +├── redis/ # Valkey persistence +├── rabbitmq/ # RabbitMQ mnesia +├── minio/ # MinIO object store (uploads bucket) +└── logs/ + ├── api/ + ├── worker/ + └── beat/ +``` + +For backups, at minimum capture: +- `pg_dump` of the `plane` database (cleaner than tar'ing `db/`) +- `minio/` (attachments, avatars) + +## Notes & gotchas + +- **Password strength is hardcoded.** Plane requires a zxcvbn score of 3 + (strong) plus 8-character minimum. There is no env var to relax this — + use a passphrase or a password manager. +- **`plane-proxy` is Caddy**, not nginx (despite the historical + `NGINX_BASE_DOMAIN` variable in older docs). It needs `SITE_ADDRESS`, + `BUCKET_NAME`, and `FILE_SIZE_LIMIT`. In our setup Caddy listens on plain + HTTP :80 and Traefik terminates TLS in front. +- **`certresolver=lets-encrypt`** in the Traefik labels matches the + convention used by the sibling stacks (`taiga`, `planka`). Adjust if your + Traefik uses a different resolver name. +- **Uploads live in MinIO** inside the stack (`USE_MINIO=1`). To use + external S3, flip `USE_MINIO` to `0` in `docker-compose.yml`'s + `x-backend-env` block and override the `AWS_*` vars. +- Upstream docker-compose / variables files are published per release at + — useful as a + reference when new knobs appear. diff --git a/productivity/project-management/plane/docker-compose.yml b/productivity/project-management/plane/docker-compose.yml new file mode 100644 index 0000000..646368a --- /dev/null +++ b/productivity/project-management/plane/docker-compose.yml @@ -0,0 +1,299 @@ +networks: + traefik: + external: true + plane: + driver: bridge + +# Shared environment for all plane-backend services +x-backend-env: + &backend-env + SECRET_KEY: ${SECRET_KEY} + DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@plane-db/${POSTGRES_DB} + REDIS_URL: redis://plane-redis:6379/ + AMQP_URL: amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@plane-mq:5672/ + WEB_URL: https://${DOMAIN} + CORS_ALLOWED_ORIGINS: https://${DOMAIN} + DOCKERIZED: 1 + GUNICORN_WORKERS: ${GUNICORN_WORKERS:-2} + USE_MINIO: 1 + AWS_REGION: us-east-1 + AWS_ACCESS_KEY_ID: ${MINIO_ROOT_USER} + AWS_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD} + AWS_S3_ENDPOINT_URL: http://plane-minio:9000 + AWS_S3_BUCKET_NAME: ${MINIO_BUCKET_NAME:-uploads} + FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880} + +services: + + # ── Database ───────────────────────────────────────────────────────────────── + + plane-db: + image: postgres:15.7-alpine + container_name: plane-db + restart: always + networks: + - plane + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + PGDATA: /var/lib/postgresql/data + volumes: + - /pwspool/software/plane/db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 5s + timeout: 5s + retries: 5 + + # ── Cache ───────────────────────────────────────────────────────────────────── + + plane-redis: + image: valkey/valkey:7.2.11-alpine + container_name: plane-redis + restart: always + networks: + - plane + volumes: + - /pwspool/software/plane/redis:/data + healthcheck: + test: ["CMD-SHELL", "redis-cli ping"] + interval: 5s + timeout: 5s + retries: 5 + + # ── Message Queue ───────────────────────────────────────────────────────────── + + plane-mq: + image: rabbitmq:3.13.6-management-alpine + container_name: plane-mq + restart: always + networks: + - plane + environment: + RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD} + RABBITMQ_DEFAULT_VHOST: / + volumes: + - /pwspool/software/plane/rabbitmq:/var/lib/rabbitmq + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "check_running", "-q"] + interval: 30s + timeout: 10s + retries: 5 + + # ── Object Storage (MinIO) ──────────────────────────────────────────────────── + + plane-minio: + image: minio/minio:latest + container_name: plane-minio + restart: always + networks: + - plane + command: server /export --console-address ":9090" + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + volumes: + - /pwspool/software/plane/minio:/export + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 30s + timeout: 20s + retries: 3 + + plane-createbuckets: + image: minio/mc:latest + container_name: plane-createbuckets + restart: "no" + networks: + - plane + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + MINIO_BUCKET_NAME: ${MINIO_BUCKET_NAME:-uploads} + depends_on: + plane-minio: + condition: service_healthy + entrypoint: > + /bin/sh -c " + mc alias set myminio http://plane-minio:9000 $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD && + mc mb myminio/$$MINIO_BUCKET_NAME --ignore-existing" + + # ── Migrations (one-shot) ───────────────────────────────────────────────────── + + migrator: + image: makeplane/plane-backend:${APP_RELEASE:-stable} + container_name: plane-migrator + restart: "no" + networks: + - plane + command: ./bin/docker-entrypoint-migrator.sh + environment: + <<: *backend-env + depends_on: + plane-db: + condition: service_healthy + plane-redis: + condition: service_healthy + + # ── API ─────────────────────────────────────────────────────────────────────── + + api: + image: makeplane/plane-backend:${APP_RELEASE:-stable} + container_name: plane-api + restart: always + networks: + - plane + command: ./bin/docker-entrypoint-api.sh + environment: + <<: *backend-env + volumes: + - /pwspool/software/plane/logs/api:/logs + depends_on: + plane-db: + condition: service_healthy + plane-redis: + condition: service_healthy + plane-mq: + condition: service_healthy + migrator: + condition: service_completed_successfully + + # ── Celery Worker ───────────────────────────────────────────────────────────── + + worker: + image: makeplane/plane-backend:${APP_RELEASE:-stable} + container_name: plane-worker + restart: always + networks: + - plane + command: ./bin/docker-entrypoint-worker.sh + environment: + <<: *backend-env + volumes: + - /pwspool/software/plane/logs/worker:/logs + depends_on: + plane-db: + condition: service_healthy + plane-redis: + condition: service_healthy + plane-mq: + condition: service_healthy + migrator: + condition: service_completed_successfully + + # ── Celery Beat ─────────────────────────────────────────────────────────────── + + beat-worker: + image: makeplane/plane-backend:${APP_RELEASE:-stable} + container_name: plane-beat-worker + restart: always + networks: + - plane + command: ./bin/docker-entrypoint-beat.sh + environment: + <<: *backend-env + volumes: + - /pwspool/software/plane/logs/beat:/logs + depends_on: + plane-db: + condition: service_healthy + plane-redis: + condition: service_healthy + plane-mq: + condition: service_healthy + migrator: + condition: service_completed_successfully + + # ── Frontend ────────────────────────────────────────────────────────────────── + + web: + image: makeplane/plane-frontend:${APP_RELEASE:-stable} + container_name: plane-web + restart: always + networks: + - plane + environment: + NEXT_PUBLIC_API_BASE_URL: https://${DOMAIN} + NEXT_PUBLIC_SPACE_BASE_URL: https://${DOMAIN}/spaces + NEXT_PUBLIC_ADMIN_BASE_URL: https://${DOMAIN}/god-mode + NEXT_PUBLIC_LIVE_BASE_URL: https://${DOMAIN}/live + depends_on: + - api + + # ── Admin Dashboard ─────────────────────────────────────────────────────────── + + admin: + image: makeplane/plane-admin:${APP_RELEASE:-stable} + container_name: plane-admin + restart: always + networks: + - plane + environment: + NEXT_PUBLIC_API_BASE_URL: https://${DOMAIN} + NEXT_PUBLIC_ADMIN_BASE_URL: https://${DOMAIN}/god-mode + depends_on: + - api + + # ── Public Space ────────────────────────────────────────────────────────────── + + space: + image: makeplane/plane-space:${APP_RELEASE:-stable} + container_name: plane-space + restart: always + networks: + - plane + environment: + NEXT_PUBLIC_API_BASE_URL: https://${DOMAIN} + NEXT_PUBLIC_SPACE_BASE_URL: https://${DOMAIN}/spaces + depends_on: + - api + + # ── Live (real-time collaboration) ──────────────────────────────────────────── + + live: + image: makeplane/plane-live:${APP_RELEASE:-stable} + container_name: plane-live + restart: always + networks: + - plane + environment: + LIVE_BASE_PATH: /live + SECRET_KEY: ${SECRET_KEY} + API_BASE_URL: http://api:8000 + depends_on: + - api + + # ── Proxy (gateway – only service on the Traefik network) ───────────────────── + + proxy: + image: makeplane/plane-proxy:${APP_RELEASE:-stable} + container_name: plane-proxy + restart: always + networks: + - plane + - traefik + environment: + # plane-proxy is Caddy — it needs SITE_ADDRESS (listen spec), + # BUCKET_NAME, and FILE_SIZE_LIMIT. Traefik terminates TLS in front + # of it, so Caddy listens plain-HTTP on :80 and issues no certs. + SITE_ADDRESS: ":80" + BUCKET_NAME: ${MINIO_BUCKET_NAME:-uploads} + FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880} + CERT_EMAIL: "" + CERT_ACME_DNS: "" + TRUSTED_PROXIES: "0.0.0.0/0" + depends_on: + - web + - api + - admin + - space + - live + labels: + - traefik.enable=true + - traefik.http.routers.plane.rule=Host(`${DOMAIN}`) + - traefik.http.routers.plane.tls=true + - traefik.http.routers.plane.tls.certresolver=lets-encrypt + - traefik.http.services.plane.loadbalancer.server.port=80 + - traefik.docker.network=traefik