Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ jobs:
- name: Check links and paths
run: docs/check-links.sh

# The realm writer of the deploy guide (Story 10.3); needs jq, which the runner has.
- name: Test the realm writer
run: deploy/realm/test.sh

# The Compose reference example parses with the placeholder settings, and each required
# variable stops `config` when it is missing (deploy/compose/test.sh). Nothing is started.
- name: Validate the Compose example
run: |
docker compose -f deploy/compose/compose.yaml --env-file deploy/compose/.env.example config --quiet
deploy/compose/test.sh

commands:
name: Commands
runs-on: ubuntu-24.04
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and deployment all live in this repository.

- [Developer quickstart](docs/quickstart.md): run the local stack and every test suite.
- [Build guide](docs/build/README.md): build, flash and add your own Node and Hub.
- [Deploy guide](docs/deploy/README.md): run Coldframe on your home server, then
[build your own apps](docs/build/apps.md).
- [Hardware design](hardware/README.md): schematics, BOM, wiring, enclosure and probe sealing.
- [Hub radio coexistence spike](docs/spikes/hub-radio-coexistence.md): what the Hub's radio can do at once.

Expand All @@ -22,7 +24,7 @@ and deployment all live in this repository.
| [`packages/`](packages) | Contracts, shared libraries and generated clients the runtimes reference |
| [`tests/`](tests) | All tests, split by language |
| [`aspire/`](aspire) | The local development stack, which is also the integration-test host |
| [`deploy/`](deploy) | Helm charts, Fleet bundles and a Compose example |
| [`deploy/`](deploy) | Helm charts, Fleet bundles and a Compose reference example ([`deploy/compose`](deploy/compose/README.md)); deploy at home with [`docs/deploy`](docs/deploy/README.md) |
| [`hardware/`](hardware) | The reference Node and Hub design: schematics, BOM, wiring, enclosure |
| [`docs/`](docs) | Guides and references |

Expand Down

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Everything needed to run Coldframe outside a developer machine.
| Epic 2, Story 2.5a | The Fleet bundles: a `fleet.yaml` in every chart folder, the operator bundles and the reference GitRepo in [`fleet/`](fleet), and the `rke2-traefik` bundle in [`rke2/`](rke2/README.md); the GitOps install runbook [`docs/operations/install.md`](../docs/operations/install.md). |
| Epic 2, Story 2.5b | The Fleet smoke [`fleet/smoke.sh`](fleet/smoke.sh): Fleet installs the bundles on a disposable cluster in dependency order, then an upgrade and three restarts lose no committed data |
| Epic 10, Story 10.1 | One release per tag `vX.Y.Z`: images, Hub and Node firmware, chart packages and `SHA256SUMS` on a GitHub Release, the wire gate and the dry run: see [`release/`](release) and the runbook [`docs/operations/release.md`](../docs/operations/release.md). [`charts/set-version.sh`](charts/set-version.sh) sets the charts to the version before tagging |
| Epic 10 | The Compose file, as a reference example only |
| Epic 10, Story 10.3 | The adopter deploy guide [`docs/deploy/`](../docs/deploy/README.md), the realm writer [`realm/prepare-realm.sh`](realm/prepare-realm.sh), and the Compose file in [`compose/`](compose/README.md), as a reference example only (not the supported deployment) |

No secret value is ever committed here. Charts reference Secrets by fixed names and keys, listed in
[`SECRETS.md`](SECRETS.md), and never create one (AD-15).
Expand Down
4 changes: 3 additions & 1 deletion deploy/charts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ In the namespace of the releases (the defaults assume one namespace):
database Secrets (each `username` equal to its role name) and `coldframe-backup-s3`.
2. An S3 bucket off the node for the backups, and its endpoint.
3. For Keycloak, a ConfigMap holding the `coldframe` realm file, with the redirect URIs of your
host names (the realm in `aspire/keycloak/realms` is for localhost and is not packaged).
host names (the realm in `aspire/keycloak/realms` is for localhost and is not packaged): write
it with [`../realm/prepare-realm.sh`](../realm/prepare-realm.sh) `--web-origin https://<domain>`
([`docs/deploy/`](../../docs/deploy/README.md#4-keycloak-realm-and-phase-two)).
4. A domain with its DNS zone at Cloudflare, the Secret `coldframe-dns01` holding an API token for
that zone, and split DNS for the three hosts
([`docs/operations/split-dns.md`](../../docs/operations/split-dns.md)).
Expand Down
48 changes: 48 additions & 0 deletions deploy/compose/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# The settings of the Compose reference example (deploy/compose/README.md): REFERENCE EXAMPLE ONLY,
# not the supported deployment. Copy this file to .env (git ignores it) and replace every value.
# The values here are placeholders that only make `docker compose config` pass.
#
# Compose interpolates `$` in .env values: use the suggested openssl commands, and single-quote any
# value that contains `$`. Never commit a filled .env.

# The release to run, without the leading v (a tag vX.Y.Z publishes the images as X.Y.Z). There
# is no default and no `latest`.
COLDFRAME_VERSION=X.Y.Z

# The LAN name or IP address of the Docker host. The browser and the containers both use it, so
# the Keycloak issuer is the same string for both (web http://<host>:3000, Keycloak <host>:8180).
COLDFRAME_HOST=192.168.1.10

# PostgreSQL: the superuser, and the passwords of the roles coldframe, temporal and keycloak.
# The coldframe password must not contain ';' (it goes into a connection string).
POSTGRES_PASSWORD=replace-me-postgres
COLDFRAME_DB_PASSWORD=replace-me-coldframe-db
TEMPORAL_DB_PASSWORD=replace-me-temporal-db
KEYCLOAK_DB_PASSWORD=replace-me-keycloak-db

# The temporary admin of Keycloak's master realm, created on the first start.
KEYCLOAK_ADMIN_USERNAME=admin
KEYCLOAK_ADMIN_PASSWORD=replace-me-keycloak-admin

# The secrets of the coldframe-web and coldframe-server clients, substituted into the realm at
# the first import (openssl rand -hex 32).
WEB_CLIENT_SECRET=replace-me-web-client-secret
SERVER_CLIENT_SECRET=replace-me-server-client-secret

# The SMTP server for registration and invitation emails. SMTP_USERNAME and SMTP_PASSWORD may stay
# empty with SMTP_AUTH=false.
SMTP_HOST=smtp.example.org
SMTP_PORT=587
SMTP_FROM=coldframe@example.org
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_AUTH=true
SMTP_STARTTLS=true
SMTP_SSL=false

# Device enrolment: the X25519 private key as PKCS#8 PEM, on one line with \n between the lines
# and in double quotes (deploy/compose/README.md shows the command), and the key-encryption key
# of the stored Device keys, at least 32 characters (openssl rand -base64 32). Record the KEK
# outside this machine: without it no enrolled Device works.
ENROLMENT_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----\nreplace-me\n-----END PRIVATE KEY-----\n"
DEVICE_KEK=replace-me-with-at-least-32-characters
1 change: 1 addition & 0 deletions deploy/compose/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
coldframe-realm.json
85 changes: 85 additions & 0 deletions deploy/compose/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Compose reference example

> **Reference example only, not the supported deployment (NFR7).** The supported path is
> single-node RKE2 with Fleet: [`docs/deploy/`](../../docs/deploy/README.md). Nothing here is
> tested beyond `docker compose config`, and no upgrade, backup or restore path exists for it.

[`compose.yaml`](compose.yaml) runs the released images the way the Helm charts in
[`deploy/charts`](../charts/README.md) do, on one Docker host: PostgreSQL 18.6 with the databases
`coldframe`, `temporal`, `temporal_visibility` and `keycloak` ([`postgres-init.sh`](postgres-init.sh)),
NATS 2.15.0 with JetStream, the Temporal schema and namespace one-shots and Temporal Server 1.31.2,
Phase Two Keycloak, the migration one-shot, the Server and the web app. It is useful to read how the
pieces fit together, or to try the web app on a LAN before you set up the real thing.

What it does not have:

- **No TLS.** Everything is plain HTTP on the LAN: the Server trusts an `http://` issuer
(`Identity__RequireHttpsMetadata=false`), the web app talks to Keycloak over HTTP
(`KEYCLOAK_ALLOW_INSECURE_HTTP=true`) and its session cookie is not `Secure`. Never expose these
ports to the internet.
- **No backups.** The data lives in the Docker volumes `postgres` and `nats`; `docker compose down -v`
deletes it.
- **The web app only.** Release builds of the mobile apps need HTTPS with a public certificate, and
no push credentials are wired in ([`docs/build/apps.md`](../../docs/build/apps.md)).

## Ports

| Port on the Docker host | Service |
| --- | --- |
| 3000 | the web app: `http://<COLDFRAME_HOST>:3000` |
| 8080 | the Server |
| 8180 | Keycloak: `http://<COLDFRAME_HOST>:8180`, issuer `http://<COLDFRAME_HOST>:8180/realms/coldframe` |

`COLDFRAME_HOST` is one LAN name or IP address of the Docker host. The browser and the containers
both use it, so the issuer in a token is the same string the Server and the web app expect; the
containers must be able to reach the host's published ports under that name.

## Start it

From the repository root, at the release tag you run (`git checkout vX.Y.Z`), with Docker Compose
v2.23 or later and `jq`:

1. Write the realm for your origin. The origin must be exactly `http://<COLDFRAME_HOST>:3000`,
with the same `COLDFRAME_HOST` as in `.env` (step 2); a mismatch only shows as a `redirect_uri`
error at the first sign-in, after the one-time import. `--allow-http` exists for this example
only:

```sh
deploy/realm/prepare-realm.sh --allow-http --web-origin http://<COLDFRAME_HOST>:3000 \
--out deploy/compose/coldframe-realm.json
```

2. Copy the settings and replace every value (git ignores `.env`; never commit it):

```sh
cp deploy/compose/.env.example deploy/compose/.env
$EDITOR deploy/compose/.env
```

`COLDFRAME_VERSION` is the release without the `v` (there is no default and no `latest`).
Generate the secrets with `openssl rand -hex 32` and the Device key-encryption key with
`openssl rand -base64 32`; record that key apart from this machine, since no enrolled Device works
without it. The enrolment key goes on one line with `\n` between its lines:

```sh
printf 'ENROLMENT_PRIVATE_KEY_PEM="%s"\n' "$(openssl genpkey -algorithm X25519 | awk '{printf "%s\\n", $0}')"
```

3. Start it:

```sh
docker compose -f deploy/compose/compose.yaml --env-file deploy/compose/.env up -d
docker compose -f deploy/compose/compose.yaml --env-file deploy/compose/.env ps -a
```

`temporal-schema`, `temporal-namespace` and `migrations` run once and exit 0; the rest keep
running. On a host with SELinux (Fedora, RHEL) the containers cannot read the bind-mounted
`postgres-init.sh` and `coldframe-realm.json` until you relabel them
(`chcon -t container_file_t deploy/compose/postgres-init.sh deploy/compose/coldframe-realm.json`).

4. Open `http://<COLDFRAME_HOST>:3000`, Register, verify the email (the SMTP values of `.env`) and
create a Site, as in [section 6 of the deploy guide](../../docs/deploy/README.md#6-first-sign-in).

The realm is imported on the first start only; to change it later use the Keycloak admin console
(`http://<COLDFRAME_HOST>:8180/admin`, the bootstrap admin of `.env`), or start over with
`docker compose ... down -v`, which deletes every volume.
Loading