phase-1: deploy via deploy.sh, correct the container count, and note that stripped host ports leave no path for the Phase 5 scrape. phase-2: explain why Grafana is not on the Ledgerline server and why Cloudflare's proxy never forwards :3000 or the other platform ports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
86 lines
5.0 KiB
Markdown
86 lines
5.0 KiB
Markdown
# Phase 1 — Deployment: Ledgerline on a server behind Traefik
|
||
|
||
*Part of `docs/roadmap/` — see [`intro.md`](intro.md) for platform/background context and [`summary.md`](summary.md) for how every phase fits together. Prev: [`phase-0.md`](phase-0.md) · Next: [`phase-2.md`](phase-2.md)*
|
||
|
||
**Status:** Done
|
||
|
||
**Message:** Ledgerline is running on a remote server and served over HTTPS at
|
||
**https://ledgerline.vione.cloud**. Verified against the live deployment, not
|
||
just the config: the frontend loads, `/api/health/all` reports every service
|
||
`ok`, and the origin serves a real Let's Encrypt certificate (valid until
|
||
2027-01-07, auto-renewed by Traefik).
|
||
|
||
- **Target:** the throwaway server `czy` (Ubuntu 24.04, 1 vCPU, 1 GB RAM), repo
|
||
at `/opt/apps/LedgerLine`. Docker 29.9 and Compose v5.6 installed from
|
||
Docker's apt repo. A 2 GB swapfile was added (persisted in `/etc/fstab`) —
|
||
ten containers (nine Ledgerline + Traefik) do not fit in 1 GB of RAM without
|
||
it, and steady state still uses roughly 600–700 MB plus swap. There is no
|
||
headroom for the observability platform on this host.
|
||
- **How it's wired:** `docker-compose.traefik.yml` is an *override*, not an
|
||
edit of `docker-compose.yml`, so local development is unchanged. It adds
|
||
Traefik v3.6 on ports 80/443 (Docker provider, `exposedByDefault=false`,
|
||
HTTP→HTTPS redirect, Let's Encrypt via HTTP-01), routes
|
||
``Host(`ledgerline.vione.cloud`)`` to the gateway on port 8000, and strips
|
||
the published host ports from every other service (`ports: !reset []`) so
|
||
Postgres, Redis and the services are reachable only on the Compose network.
|
||
- **No application or `tests/` changes.** The Dockerfile and base compose file
|
||
are untouched; schema applied and data seeded on the server with the same
|
||
steps as `make seed` (800 accounts, 250,000 transactions).
|
||
- **Deploy / redeploy:** run `./deploy.sh` from your own machine. It SSHes to
|
||
`czy` (override with `DEPLOY_HOST` / `DEPLOY_DIR`), does `git pull --ff-only`
|
||
in `/opt/apps/LedgerLine`, and runs the compose command below. The server
|
||
pulls from the remote, so push first; the script warns about unpushed
|
||
commits. The underlying command, if run by hand on the server (always both
|
||
files — a bare `docker compose up` would use the base file alone and
|
||
publish every port):
|
||
|
||
```bash
|
||
docker compose -f docker-compose.yml -f docker-compose.traefik.yml up -d --build
|
||
```
|
||
|
||
- **Cloudflare gotchas hit on the way:**
|
||
- The first certificate attempt failed with an ACME 404 because the
|
||
Cloudflare DNS record did not yet point at the server — a packet capture
|
||
on the server showed no inbound traffic from Cloudflare at all. Traefik
|
||
only retries issuance on restart, so after fixing the record it needed
|
||
`docker compose ... restart traefik`.
|
||
- With the orange-cloud proxy on, the SSL/TLS mode must be **Full** —
|
||
**Flexible** combined with Traefik's HTTPS redirect causes a redirect loop.
|
||
- **Caveats to remember:** the deployment is public, including the
|
||
`/api/admin/chaos` endpoints (anyone who finds them can flip flags);
|
||
internet scanners probed the site within seconds of it going live. The
|
||
server is disposable by design, and Traefik's state lives in the
|
||
`letsencrypt` volume, which is lost on an OS reinstall (a new certificate is
|
||
simply issued on the next run).
|
||
- **Consequence for later phases:** because `!reset` strips every service's
|
||
host port, nothing outside `czy` can reach a service's `/metrics` directly.
|
||
The Phase 5 scrape (Prometheus on the observability host →
|
||
`prometheus_extra_scrape_jobs`) needs a deliberate path in — e.g. a
|
||
firewalled port or a Traefik route — decided before Phase 5.
|
||
|
||
**Time-box:** 1 evening.
|
||
|
||
**Deliverable:** the unmodified-behaviour Ledgerline system reachable at a real
|
||
domain over HTTPS, reproducible with one compose command.
|
||
|
||
### Concepts
|
||
|
||
- **Traefik's model:** *entrypoints* (the ports it listens on), *routers*
|
||
(rule → service), *services* (where to send traffic), *middlewares*, and
|
||
*providers* (where config comes from — here, Docker labels on containers).
|
||
Compare with nginx, where the same information lives in one static config
|
||
file.
|
||
- **Compose override files.** `-f base -f override` merges the files;
|
||
`!reset` clears an inherited key (here, `ports`) instead of appending to it.
|
||
- **ACME HTTP-01 behind a CDN.** Let's Encrypt must be able to reach port 80 on
|
||
the *hostname's actual origin*. With Cloudflare proxying, that only works
|
||
once the DNS record points at the right server.
|
||
- **Why this phase exists before observability.** The telemetry work in
|
||
Phases 2–11 is more convincing when the system under observation is a real
|
||
deployment rather than a laptop, and the platform (`observability-ansible`)
|
||
is already a remote deployment — Ledgerline now sits on the same footing.
|
||
|
||
**Done when:** `https://ledgerline.vione.cloud` shows the frontend with a valid
|
||
certificate; `/api/health/all` is all `ok`; only ports 22, 80 and 443 are
|
||
reachable on the server; and the deploy is one documented compose command.
|