Self-hosted dev-tools stack: Forgejo (git), Mattermost (chat), Plane
(project mgmt) on one AWS Lightsail host (16 GB plan), Docker Compose, shared
Postgres, provisioned by an AWS CLI script (no OpenTofu). Design spec:
docs/superpowers/specs/2026-06-25-vps-devtools-design.md. Read it before changing
architecture. DECISIONS.md (repo root) is the durable decision ledger — the
why behind every choice and the pivots; check it before reversing anything.
Multi-host group model: one repo deploys several groups, one Lightsail host each. Dev (16 GB: Forgejo, Mattermost, Plane) is built first; Support (8 GB: Planka, Chatwoot) is next; Monitoring (Beszel, to confirm) is on the radar.
Layout: infra/ = cloud/host provisioning (infra/scripts/ = Lightsail script,
user-data.sh, teardown). apps/ = shared per-app contexts, one self-contained
dir each (apps/caddy, apps/postgres, apps/forgejo, apps/mattermost,
apps/plane, apps/postgrest, …). deploy/<group>/docker-compose.yml = one
composition per group → one host, referencing ../../apps/<app> build contexts;
secrets in deploy/<group>/.env. Plane is our fork — apps/plane/upstream/ is a
submodule; log every divergence in apps/plane/CHANGES.md. Each group host is
self-contained (own Caddy + Postgres + reporting); cross-host BI is an open decision
(spec §11) — default to per-group reporting to keep N2.
Operating principles for anyone (human or agent) working in this repo.
-
Always have a working system. Every change keeps the stack bootable. Small, reversible steps over big-bang rewrites. If
docker compose upis red, stop and fix before adding anything. -
Make it work, make it right, make it fast — in that order. Don't optimize the t3.large memory budget before the three apps actually boot and talk to Postgres. Premature tuning is wasted motion.
-
Verify the real thing, not the idea of it. Don't claim "Forgejo is up" from a clean
tofu apply.curl https://git.code42.dev, read the container logs,psqlthe table. Look at the actual bytes. Evidence before assertion. -
The best code is no code; the best service is no service. Every container added costs RAM on an 8 GB box and a thing that can break at 3 a.m. Justify each service. Prefer an app's native feature over a new sidecar.
-
Minimize moving parts and keep them loosely coupled. Apps talk to Postgres as an attached resource, not as a shared schema. The
reportingview layer is the only seam between BI and app internals — keep that boundary clean. -
Tight feedback loops. Lint locally (
docker compose config,tofu validate) before you push a 6-minute boot cycle. Reproduce on the smallest surface that shows the bug. -
Be suspicious of your own setup. Most "it's broken" is a config/env/path mistake, not the upstream software. Check
.env, the Caddyfile, the DNS record, the security group — in that order — before blaming Plane. -
Code (and config) is read far more than written. Match the surrounding style. A Compose file or Dockerfile someone can scan top-to-bottom beats a clever one.
This host behaves like a Heroku-style platform. Honor these:
-
I. Codebase → deploy. One repo, one deployable stack.
infra/scripts/create-lightsail.shbuilds the host;docker compose up -druns the apps. No snowflake hand-clicks in the Lightsail console — if it isn't in a script in the repo, it doesn't exist. -
III. Config in the environment. All secrets/tunables live in
stack/.env(gitignored,chmod 600). Never hardcode a password, key, or hostname in a Dockerfile, Compose file, or committed config. New setting → new env var. -
IV. Backing services are attached resources. Postgres, Redis, MinIO are attached by URL/credentials from
.env. An app must not assume a local socket or a fixed host — swap the resource by changing the env var, nothing else. -
V. Strictly separate build / release / run.
- Build: images are built (Plane from the fork → GHCR; Forgejo/Mattermost
overlays) ahead of time, off-host (local or GitHub Actions). Never build
Plane on the host (OOM risk). Lightsail has no IAM role — host pulls from GHCR
with a token from
.env, not ECR. - Release:
.env+ pinned image tags = the release. Pin tags; no:latestin production paths. - Run:
docker compose up -donly pulls and runs. Run does no compilation.
- Build: images are built (Plane from the fork → GHCR; Forgejo/Mattermost
overlays) ahead of time, off-host (local or GitHub Actions). Never build
Plane on the host (OOM risk). Lightsail has no IAM role — host pulls from GHCR
with a token from
-
VI. Processes are stateless; state lives in backing services. Containers are disposable. All durable data is in the Postgres volume / MinIO / named volumes on the
/dataEBS. Deleting and recreating any app container loses nothing. -
VII. Port binding behind one front door. Apps bind internally; Caddy is the only public ingress (80/443, auto-TLS). Postgres/PostgREST/Studio publish no host port — reach them via
ssh -L. -
IX. Disposability. Fast startup, graceful shutdown. Assume the box can be recreated; the attached Lightsail block disk (
/data) and.envare what must survive. Treat the instance as cattle, the block disk as the pet. -
X. Dev/prod parity. The same Compose stack and pinned images run everywhere. Don't special-case the host with manual
docker runcommands. -
XI. Logs are event streams. Read
docker compose logs -f <svc>; don't write app logs to files inside containers.
- Secrets: never commit
.env,*.tfvars,*.tfstate..gitignoreenforces this — don't weaken it. - No backups exist. Be careful with
docker compose down -v, volume prunes, deleting the Lightsail block disk, anddestroy-lightsail.sh— they are irreversible data loss. The block disk survives instance deletion, but only if you delete the instance, not the disk. Confirm before running any. - Network model is N2: keep Postgres/PostgREST/Studio off public ports. If a change would publish one, stop and flag it.
- 16 GB host. Adding a service? Account for its idle RAM and update the budget in the spec. Plane's stack alone is ~3 GB across ~11 containers. Swap is a safety net, not headroom.
- App tables are upstream-owned. Expose data through
reportingviews and the apps' native APIs — never wire BI directly to an app's internal tables.
bash infra/scripts/create-lightsail.sh # provision host (AWS CLI; per group)
docker compose -f deploy/dev/docker-compose.yml config # lint (group = dev)
docker compose -f deploy/dev/docker-compose.yml up -d # run
docker compose -f deploy/dev/docker-compose.yml logs -f <svc> # logs
ssh -L 5432:127.0.0.1:5432 <host> # reach Postgres/BI (N2)
# Plane fork
git -C apps/plane/upstream fetch upstream # track upstream
# edit on the code42 branch, log in apps/plane/CHANGES.md, build off-host, push GHCR