feat(meditate,deploy): Meditate beside the instance, and every server's public address from .env
Meditate's deploy step (ADR 0038, step 7; ADR 0039, D14), as Todo's: the server's Dockerfile with the library's check compiled beside it, its production config on its own database and role, compose.meditate.yaml as a member of the register, its site sending /library/* to the web server and the rest to the API server, the operator's library read-only in a compose file of its own, and redeploy.sh, backup.sh and the image replay taking it in. Every server's public address now comes from .env (Serverpod's SERVERPOD_API_SERVER_PUBLIC_HOST and _WEB_SERVER_, which win over the config file): Train, Todo and Meditate from their domains, Learn from LEARN_PUBLIC_HOST, unset learn.halkyon.eu as before. Without it a server on halkyon.li would name halkyon.eu in its SimpleLogin callback, profile pages and privacy page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
29844c7fa6
commit
fd9c2dfe76
15 changed files with 517 additions and 8 deletions
|
|
@ -9,4 +9,5 @@ What holds for every app and package of this repository is in the [root AGENTS.m
|
||||||
- **The bells** (D5) are `assets/bells/*.wav`, synthesised by `apps/meditate/tool/bells.py` (Python's standard library, run from `apps/meditate`; the same bytes every run) and played with `audioplayers` (`AudioplayersBellPlayer`). Only while the app runs: bells with the screen off are for later.
|
- **The bells** (D5) are `assets/bells/*.wav`, synthesised by `apps/meditate/tool/bells.py` (Python's standard library, run from `apps/meditate`; the same bytes every run) and played with `audioplayers` (`AudioplayersBellPlayer`). Only while the app runs: bells with the screen off are for later.
|
||||||
- **The icon** is Lucide's `sprout` from cobalt through pacific into fern (ADR 0031, D5, D7), drawn by the repository's `tool/app_icons.py` (`python3 -I tool/app_icons.py meditate` from the root): `assets/icon/halkyon_meditate_icon.png`, Lucide's licence beside it, every platform's launcher icon.
|
- **The icon** is Lucide's `sprout` from cobalt through pacific into fern (ADR 0031, D5, D7), drawn by the repository's `tool/app_icons.py` (`python3 -I tool/app_icons.py meditate` from the root): `assets/icon/halkyon_meditate_icon.png`, Lucide's licence beside it, every platform's launcher icon.
|
||||||
- **`serverpod start`** runs in `apps/meditate/halkyon_meditate_server` on ports of its own (8280 to 8282, PostgreSQL 8290), so it can run beside Learn's (8080…) and Train's (8180…); its tests need throwaway passwords in its git-ignored `config/passwords.yaml` (`database`, `serviceSecret`, `serverSideSessionKeyHashPepper`, `emailSignInPepper`) or from the environment, as CI gives them.
|
- **`serverpod start`** runs in `apps/meditate/halkyon_meditate_server` on ports of its own (8280 to 8282, PostgreSQL 8290), so it can run beside Learn's (8080…) and Train's (8180…); its tests need throwaway passwords in its git-ignored `config/passwords.yaml` (`database`, `serviceSecret`, `serverSideSessionKeyHashPepper`, `emailSignInPepper`) or from the environment, as CI gives them.
|
||||||
- **Not yet**: releases (`apps/meditate/release.dart`, its key, F-Droid listing, snap), the deploy beside learn.halkyon.eu (`compose.meditate.yaml` with the library's folder mounted read-only, Caddy forwarding `/library/*`, `config/production.yaml`, the `Dockerfile`), and what ADR 0038 lists under *Later*.
|
- **The deploy** (ADR 0038, step 7; ADR 0039, D14, 8 Nov 2026): the server's `Dockerfile` (as Todo's, with `bin/check_library` compiled beside the server) and `config/production.yaml` (its database `halkyon_meditate` as its own role; the public host from `.env`, `MEDITATE_DOMAIN`), `deploy/compose.meditate.yaml` (a member of the register) with `sidecars/meditate.caddy` (`/library/*` to the web server, the rest to the API server), the library in `deploy/compose.meditate-library.yaml` (`MEDITATE_LIBRARY`, read-only), `redeploy.sh` and `backup.sh` taking it in; `test/deploy/meditate_deploy_test.dart`; the steps in `deploy/sidecars/README.md`, *Meditate*. Not deployed yet: halkyon.li is its first instance.
|
||||||
|
- **Not yet**: releases (`apps/meditate/release.dart`, its key, F-Droid listing, snap), and what ADR 0038 lists under *Later*.
|
||||||
|
|
|
||||||
69
apps/meditate/halkyon_meditate_server/Dockerfile
Normal file
69
apps/meditate/halkyon_meditate_server/Dockerfile
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
# Meditate's server image (ADR 0038, step 7). Built from the repository root (the build
|
||||||
|
# context), because the workspace's root pubspec and lockfile live there:
|
||||||
|
#
|
||||||
|
# docker build -f apps/meditate/halkyon_meditate_server/Dockerfile .
|
||||||
|
#
|
||||||
|
# Todo's server image (apps/todo/halkyon_todo_server/Dockerfile) for Meditate: the same
|
||||||
|
# Dart, the same Ubuntu runtime and start command, and beside the server the library's
|
||||||
|
# check (bin/check_library.dart, D16), so an operator checks a folder on the host with
|
||||||
|
# the image the server runs:
|
||||||
|
#
|
||||||
|
# docker compose run --rm --no-deps -v /path/to/library:/check:ro \
|
||||||
|
# --entrypoint ./bin/check_library meditate-server /check
|
||||||
|
|
||||||
|
# ── Build stage: compile the server and the check to native executables ───────────
|
||||||
|
FROM dart:3.13.4 AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY pubspec.yaml pubspec.lock ./
|
||||||
|
COPY tool/workspace_only.sh tool/
|
||||||
|
COPY apps/meditate/halkyon_meditate_server apps/meditate/halkyon_meditate_server
|
||||||
|
COPY packages/halkyon_base_server packages/halkyon_base_server
|
||||||
|
COPY packages/halkyon_base_client packages/halkyon_base_client
|
||||||
|
COPY packages/halkyon_core packages/halkyon_core
|
||||||
|
COPY packages/halkyon_llm packages/halkyon_llm
|
||||||
|
COPY packages/halkyon_oidc_server packages/halkyon_oidc_server
|
||||||
|
COPY packages/halkyon_oidc_client packages/halkyon_oidc_client
|
||||||
|
|
||||||
|
# The workspace root with only the server and the modules it builds on, as Learn's
|
||||||
|
# image has it (the reasons there).
|
||||||
|
RUN sh tool/workspace_only.sh apps/meditate/halkyon_meditate_server packages/halkyon_base_server packages/halkyon_base_client packages/halkyon_core packages/halkyon_llm packages/halkyon_oidc_server packages/halkyon_oidc_client
|
||||||
|
RUN dart pub get
|
||||||
|
WORKDIR /app/apps/meditate/halkyon_meditate_server
|
||||||
|
RUN dart build cli --target bin/main.dart --output build
|
||||||
|
RUN mv build/bundle/bin/main build/bundle/bin/server
|
||||||
|
# The check, built on its own and put beside the server: it reads a folder as the
|
||||||
|
# server does, with the same code.
|
||||||
|
RUN dart build cli --target bin/check_library.dart --output build-check
|
||||||
|
RUN mv build-check/bundle/bin/check_library build/bundle/bin/check_library
|
||||||
|
RUN mkdir -p config migrations
|
||||||
|
|
||||||
|
# ── Runtime stage ─────────────────────────────────────────────────────────────────
|
||||||
|
# Ubuntu 24.04 as Learn's; CA certificates for outgoing TLS (the operator's
|
||||||
|
# assistants, which the module forwards, ADR 0021).
|
||||||
|
FROM ubuntu:24.04
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV runmode=production
|
||||||
|
ENV serverid=default
|
||||||
|
ENV logging=normal
|
||||||
|
ENV role=monolith
|
||||||
|
|
||||||
|
COPY --from=build /app/apps/meditate/halkyon_meditate_server/build/bundle/ ./
|
||||||
|
COPY --from=build /app/apps/meditate/halkyon_meditate_server/config/ config/
|
||||||
|
COPY --from=build /app/apps/meditate/halkyon_meditate_server/migrations/ migrations/
|
||||||
|
COPY --from=build /app/apps/meditate/halkyon_meditate_server/lib/src/generated/protocol.yaml lib/src/generated/protocol.yaml
|
||||||
|
|
||||||
|
# No root rights needed: it listens on 8080 and 8082 and writes nothing; the library is
|
||||||
|
# read-only (compose.meditate-library.yaml).
|
||||||
|
RUN useradd --system --no-create-home --shell /usr/sbin/nologin app
|
||||||
|
USER app
|
||||||
|
|
||||||
|
# The API server, and the web server with the library's files (D16) and the register's
|
||||||
|
# calls about accounts gone (ADR 0016), the latter on the instance's own network alone.
|
||||||
|
EXPOSE 8080 8082
|
||||||
|
|
||||||
|
ENTRYPOINT ["/bin/sh", "-c", "exec ./bin/server --mode=$runmode --server-id=$serverid --logging=$logging --role=$role \"$@\"", "--"]
|
||||||
|
CMD []
|
||||||
66
apps/meditate/halkyon_meditate_server/config/production.yaml
Normal file
66
apps/meditate/halkyon_meditate_server/config/production.yaml
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Meditate's server beside the instance's other servers (ADR 0038, step 7),
|
||||||
|
# behind the instance's Caddy (deploy/compose.meditate.yaml): its API and its web server
|
||||||
|
# on the one domain, as Todo's; its database in the instance's PostgreSQL. The
|
||||||
|
# public host is the instance's, from deploy/.env (MEDITATE_DOMAIN), which overrides
|
||||||
|
# the one below.
|
||||||
|
|
||||||
|
# Configuration for the main API server.
|
||||||
|
apiServer:
|
||||||
|
port: 8080
|
||||||
|
publicHost: meditate.halkyon.eu
|
||||||
|
publicPort: 443
|
||||||
|
publicScheme: https
|
||||||
|
|
||||||
|
# No insightsServer, as Learn's: without the block Serverpod doesn't start Insights.
|
||||||
|
|
||||||
|
# Configuration for the web server.
|
||||||
|
webServer:
|
||||||
|
port: 8082
|
||||||
|
publicHost: meditate.halkyon.eu
|
||||||
|
publicPort: 443
|
||||||
|
publicScheme: https
|
||||||
|
|
||||||
|
# This is the database setup for your servers. The default for the Google Cloud
|
||||||
|
# Engine Terraform configuration is to connect on a private IP address.
|
||||||
|
# If you are connecting on a public IP (e.g. on AWS or Google Cloud Run), you
|
||||||
|
# connect on the public IP of the database e.g. database.examplepod.com.
|
||||||
|
database:
|
||||||
|
host: postgres
|
||||||
|
port: 5432
|
||||||
|
name: halkyon_meditate
|
||||||
|
user: halkyon_meditate
|
||||||
|
# On the instance's own network, never leaving the server.
|
||||||
|
requireSsl: false
|
||||||
|
#isUnixSocket: true # defaults to false
|
||||||
|
#maxConnectionCount: 10 # Defaults to 10, a negative or null value removes the limit
|
||||||
|
|
||||||
|
|
||||||
|
maxRequestSize: 524288 # The maximum size of requests allowed in bytes
|
||||||
|
|
||||||
|
# Session logs stay out of the database, for privacy, as Learn's (never re-enable).
|
||||||
|
sessionLogs:
|
||||||
|
consoleEnabled: false
|
||||||
|
persistentEnabled: false
|
||||||
|
# consoleLogFormat: json # Defaults to "json", options are "text" or "json"
|
||||||
|
|
||||||
|
#futureCall:
|
||||||
|
# enabled: true # Defaults to true, set to false to disable scheduling and running future calls
|
||||||
|
# executionEnabled: true # Defaults to true
|
||||||
|
# concurrencyLimit: 1 # Defaults to 1, a negative or null value removes the limit
|
||||||
|
# scanInterval: 5000 # Unit in milliseconds, defaults to 5000
|
||||||
|
|
||||||
|
# Recommended when serving web clients: keeps auth tokens in httpOnly
|
||||||
|
# cookies. Without this, web tokens live in JavaScript-readable storage,
|
||||||
|
# where any XSS can steal them. Web clients opt in with
|
||||||
|
# `client.cookieAuth = true`. Requires `allowedOrigins`; browsers from
|
||||||
|
# origins not in the list lose cross-origin access, including to public
|
||||||
|
# endpoints.
|
||||||
|
#authCookie:
|
||||||
|
# sameSite: lax # Defaults to "lax", options are "strict" or "none"
|
||||||
|
# #name: serverpod_auth # Defaults to "serverpod_auth"
|
||||||
|
# #refreshName: serverpod_auth_refresh # Defaults to "<name>_refresh"
|
||||||
|
# #domain: examplepod.com # Defaults to host-only; set to share across subdomains
|
||||||
|
# #path: / # Defaults to "/"
|
||||||
|
# #secure: true # Defaults to true; the cookie is only sent over https
|
||||||
|
#allowedOrigins:
|
||||||
|
# - https://app.examplepod.com # The origin serving your Flutter web app
|
||||||
|
|
@ -7,6 +7,10 @@
|
||||||
# DOMAIN until 2 Nov 2026): `http://localhost` for the local rehearsal;
|
# DOMAIN until 2 Nov 2026): `http://localhost` for the local rehearsal;
|
||||||
# `learn.halkyon.eu` on its server.
|
# `learn.halkyon.eu` on its server.
|
||||||
LEARN_DOMAIN=http://localhost
|
LEARN_DOMAIN=http://localhost
|
||||||
|
# Learn's server's public address, a host alone, where it isn't learn.halkyon.eu
|
||||||
|
# (`learn.halkyon.li`): the address its SimpleLogin callback, profile pages and privacy
|
||||||
|
# page are on. Unset: learn.halkyon.eu.
|
||||||
|
#LEARN_PUBLIC_HOST=learn.halkyon.li
|
||||||
|
|
||||||
# The domain people's handles are on (`raisondetredev@halkyon.eu`) and their profile
|
# The domain people's handles are on (`raisondetredev@halkyon.eu`) and their profile
|
||||||
# pages (`https://halkyon.eu/@raisondetredev`, ADR 0011), when it isn't LEARN_DOMAIN's
|
# pages (`https://halkyon.eu/@raisondetredev`, ADR 0011), when it isn't LEARN_DOMAIN's
|
||||||
|
|
@ -100,6 +104,14 @@ COMPOSE_FILE=compose.learn.yaml
|
||||||
#TODO_POSTGRES_PASSWORD=
|
#TODO_POSTGRES_PASSWORD=
|
||||||
#TODO_SESSION_KEY_HASH_PEPPER=
|
#TODO_SESSION_KEY_HASH_PEPPER=
|
||||||
|
|
||||||
|
# With Halkyon Meditate beside the instance (compose.meditate.yaml, sidecars/README.md,
|
||||||
|
# Meditate): as Todo's. Its library (compose.meditate-library.yaml, optional): the
|
||||||
|
# folder on the host with library.yaml, mounted read-only.
|
||||||
|
#MEDITATE_DOMAIN=meditate.example.org
|
||||||
|
#MEDITATE_POSTGRES_PASSWORD=
|
||||||
|
#MEDITATE_SESSION_KEY_HASH_PEPPER=
|
||||||
|
#MEDITATE_LIBRARY=/data/meditate-library
|
||||||
|
|
||||||
# The password of the instance's PostgreSQL superuser, halkyon_learn, which Learn's
|
# The password of the instance's PostgreSQL superuser, halkyon_learn, which Learn's
|
||||||
# server signs in as, and backups and deploys dump as (Train's and Todo's servers have
|
# server signs in as, and backups and deploys dump as (Train's and Todo's servers have
|
||||||
# roles of their own), as TRAIN_POSTGRES_PASSWORD and TODO_POSTGRES_PASSWORD are their
|
# roles of their own), as TRAIN_POSTGRES_PASSWORD and TODO_POSTGRES_PASSWORD are their
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ dump="$HOME/backups/latest.dump"
|
||||||
# (halkyon_<app>) dumped to latest-<app>.dump.
|
# (halkyon_<app>) dumped to latest-<app>.dump.
|
||||||
services=$(docker compose config --services)
|
services=$(docker compose config --services)
|
||||||
apps=
|
apps=
|
||||||
for app in train todo; do
|
for app in train todo meditate; do
|
||||||
if printf '%s\n' "$services" | grep -qx "$app-server"; then
|
if printf '%s\n' "$services" | grep -qx "$app-server"; then
|
||||||
apps="$apps $app"
|
apps="$apps $app"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,12 @@ services:
|
||||||
environment:
|
environment:
|
||||||
SERVERPOD_DATABASE_PASSWORD: ${LEARN_POSTGRES_PASSWORD:?set LEARN_POSTGRES_PASSWORD in deploy/.env}
|
SERVERPOD_DATABASE_PASSWORD: ${LEARN_POSTGRES_PASSWORD:?set LEARN_POSTGRES_PASSWORD in deploy/.env}
|
||||||
SERVERPOD_PASSWORD_serverSideSessionKeyHashPepper: ${LEARN_SESSION_KEY_HASH_PEPPER:?set LEARN_SESSION_KEY_HASH_PEPPER in deploy/.env}
|
SERVERPOD_PASSWORD_serverSideSessionKeyHashPepper: ${LEARN_SESSION_KEY_HASH_PEPPER:?set LEARN_SESSION_KEY_HASH_PEPPER in deploy/.env}
|
||||||
|
# Its public address, which config/production.yaml names otherwise: the host of
|
||||||
|
# LEARN_DOMAIN where the instance isn't learn.halkyon.eu (halkyon.li's
|
||||||
|
# learn.halkyon.li). A host alone, never a URL; unset: learn.halkyon.eu, as the
|
||||||
|
# file has it, which the local rehearsal keeps too.
|
||||||
|
SERVERPOD_API_SERVER_PUBLIC_HOST: ${LEARN_PUBLIC_HOST:-learn.halkyon.eu}
|
||||||
|
SERVERPOD_WEB_SERVER_PUBLIC_HOST: ${LEARN_PUBLIC_HOST:-learn.halkyon.eu}
|
||||||
# Email sign-in (ADR 0010), only with the pepper and an SMTP service; empty: off.
|
# Email sign-in (ADR 0010), only with the pepper and an SMTP service; empty: off.
|
||||||
SERVERPOD_PASSWORD_emailSignInPepper: ${EMAIL_SIGN_IN_PEPPER:-}
|
SERVERPOD_PASSWORD_emailSignInPepper: ${EMAIL_SIGN_IN_PEPPER:-}
|
||||||
SERVERPOD_PASSWORD_emailSmtpHost: ${EMAIL_SMTP_HOST:-}
|
SERVERPOD_PASSWORD_emailSmtpHost: ${EMAIL_SMTP_HOST:-}
|
||||||
|
|
|
||||||
17
deploy/compose.meditate-library.yaml
Normal file
17
deploy/compose.meditate-library.yaml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# The operator's library for Halkyon Meditate (ADR 0038, D16): recordings, pictures and
|
||||||
|
# their library.yaml in a folder on the host, which Meditate's server reads, rereads every
|
||||||
|
# ten minutes and serves to people signed in. After compose.meditate.yaml in .env:
|
||||||
|
#
|
||||||
|
# COMPOSE_FILE=…:compose.meditate.yaml:compose.meditate-library.yaml
|
||||||
|
# MEDITATE_LIBRARY=/data/meditate-library
|
||||||
|
#
|
||||||
|
# A file of its own, as an instance may run Meditate without a library: its app then
|
||||||
|
# shows no library section. Read-only: the server never writes into the folder. Check
|
||||||
|
# a folder before pointing the instance at it (the server's Dockerfile says how).
|
||||||
|
|
||||||
|
services:
|
||||||
|
meditate-server:
|
||||||
|
environment:
|
||||||
|
MEDITATE_LIBRARY_DIR: /library
|
||||||
|
volumes:
|
||||||
|
- ${MEDITATE_LIBRARY:?set MEDITATE_LIBRARY in deploy/.env}:/library:ro
|
||||||
60
deploy/compose.meditate.yaml
Normal file
60
deploy/compose.meditate.yaml
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Halkyon Meditate's server beside the instance (ADR 0038, step 7; ADR 0039, D14).
|
||||||
|
# Switched on in .env, with Meditate's domain and secrets:
|
||||||
|
#
|
||||||
|
# COMPOSE_FILE=compose.learn.yaml:compose.meditate.yaml
|
||||||
|
# MEDITATE_DOMAIN=meditate.halkyon.li
|
||||||
|
#
|
||||||
|
# Meditate is a member of the instance's register, which Learn's server holds (ADR
|
||||||
|
# 0016), as Todo is: one account a person, signing in through the register, the shared
|
||||||
|
# INSTANCE_SECRET on every call between them, which reach each other on the instance's
|
||||||
|
# own networks alone. Its database is `halkyon_meditate` in the instance's PostgreSQL,
|
||||||
|
# under a role of its own that can't connect to the other apps'. Its server reads no
|
||||||
|
# sitting and no dream: every row is sealed on the person's devices (ADR 0038, D7). The
|
||||||
|
# operator's library (D16) is a file of its own, compose.meditate-library.yaml; without
|
||||||
|
# it Meditate has none. Setting it up: sidecars/README.md, Meditate.
|
||||||
|
|
||||||
|
services:
|
||||||
|
meditate-server:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: apps/meditate/halkyon_meditate_server/Dockerfile
|
||||||
|
image: halkyon-meditate-server
|
||||||
|
restart: unless-stopped
|
||||||
|
# As Learn's: migrations at every start (redeploy.sh has applied them before).
|
||||||
|
command: ["--apply-migrations"]
|
||||||
|
environment:
|
||||||
|
SERVERPOD_DATABASE_PASSWORD: ${MEDITATE_POSTGRES_PASSWORD:?set MEDITATE_POSTGRES_PASSWORD in deploy/.env}
|
||||||
|
SERVERPOD_PASSWORD_serverSideSessionKeyHashPepper: ${MEDITATE_SESSION_KEY_HASH_PEPPER:?set MEDITATE_SESSION_KEY_HASH_PEPPER in deploy/.env}
|
||||||
|
# Its public address, which config/production.yaml names otherwise: the
|
||||||
|
# instance's own domain.
|
||||||
|
SERVERPOD_API_SERVER_PUBLIC_HOST: ${MEDITATE_DOMAIN:?set MEDITATE_DOMAIN in deploy/.env}
|
||||||
|
SERVERPOD_WEB_SERVER_PUBLIC_HOST: ${MEDITATE_DOMAIN:?set MEDITATE_DOMAIN in deploy/.env}
|
||||||
|
# The register (ADR 0016): Learn's web server, on the instance's own network,
|
||||||
|
# and the secret every call between them carries.
|
||||||
|
ACCOUNT_REGISTER_URL: http://server:8082/
|
||||||
|
SERVERPOD_PASSWORD_instanceSecret: ${INSTANCE_SECRET:?set INSTANCE_SECRET in deploy/.env}
|
||||||
|
# Caddy, as for Learn's server (compose.learn.yaml).
|
||||||
|
TRUSTED_PROXIES: 172.30.80.2 fd4a:6c6b:7e80::2
|
||||||
|
SOURCE_URL: ${SOURCE_URL:-}
|
||||||
|
DATA_LOCATION: ${DATA_LOCATION:-}
|
||||||
|
BACKUP_LOCATION: ${BACKUP_LOCATION:-}
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- edge
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
logging:
|
||||||
|
driver: journald
|
||||||
|
|
||||||
|
caddy:
|
||||||
|
environment:
|
||||||
|
MEDITATE_DOMAIN: ${MEDITATE_DOMAIN:?set MEDITATE_DOMAIN in deploy/.env}
|
||||||
|
volumes:
|
||||||
|
- ./sidecars/meditate.caddy:/etc/caddy/sidecars/meditate.caddy:ro
|
||||||
|
|
||||||
|
server:
|
||||||
|
environment:
|
||||||
|
# Meditate's web server, told when an account goes (ADR 0016), beside the members
|
||||||
|
# the other compose files and INSTANCE_MEMBERS name.
|
||||||
|
INSTANCE_MEMBER_MEDITATE: http://meditate-server:8082/
|
||||||
|
|
@ -24,6 +24,10 @@ services:
|
||||||
environment:
|
environment:
|
||||||
SERVERPOD_DATABASE_PASSWORD: ${TODO_POSTGRES_PASSWORD:?set TODO_POSTGRES_PASSWORD in deploy/.env}
|
SERVERPOD_DATABASE_PASSWORD: ${TODO_POSTGRES_PASSWORD:?set TODO_POSTGRES_PASSWORD in deploy/.env}
|
||||||
SERVERPOD_PASSWORD_serverSideSessionKeyHashPepper: ${TODO_SESSION_KEY_HASH_PEPPER:?set TODO_SESSION_KEY_HASH_PEPPER in deploy/.env}
|
SERVERPOD_PASSWORD_serverSideSessionKeyHashPepper: ${TODO_SESSION_KEY_HASH_PEPPER:?set TODO_SESSION_KEY_HASH_PEPPER in deploy/.env}
|
||||||
|
# Its public address, which config/production.yaml names otherwise: the
|
||||||
|
# instance's own domain.
|
||||||
|
SERVERPOD_API_SERVER_PUBLIC_HOST: ${TODO_DOMAIN:?set TODO_DOMAIN in deploy/.env}
|
||||||
|
SERVERPOD_WEB_SERVER_PUBLIC_HOST: ${TODO_DOMAIN:?set TODO_DOMAIN in deploy/.env}
|
||||||
# The register (ADR 0016): Learn's web server, on the instance's own network,
|
# The register (ADR 0016): Learn's web server, on the instance's own network,
|
||||||
# and the secret every call between them carries.
|
# and the secret every call between them carries.
|
||||||
ACCOUNT_REGISTER_URL: http://server:8082/
|
ACCOUNT_REGISTER_URL: http://server:8082/
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ services:
|
||||||
environment:
|
environment:
|
||||||
SERVERPOD_DATABASE_PASSWORD: ${TRAIN_POSTGRES_PASSWORD:?set TRAIN_POSTGRES_PASSWORD in deploy/.env}
|
SERVERPOD_DATABASE_PASSWORD: ${TRAIN_POSTGRES_PASSWORD:?set TRAIN_POSTGRES_PASSWORD in deploy/.env}
|
||||||
SERVERPOD_PASSWORD_serverSideSessionKeyHashPepper: ${TRAIN_SESSION_KEY_HASH_PEPPER:?set TRAIN_SESSION_KEY_HASH_PEPPER in deploy/.env}
|
SERVERPOD_PASSWORD_serverSideSessionKeyHashPepper: ${TRAIN_SESSION_KEY_HASH_PEPPER:?set TRAIN_SESSION_KEY_HASH_PEPPER in deploy/.env}
|
||||||
|
# Its public address, which config/production.yaml names otherwise: the
|
||||||
|
# instance's own domain.
|
||||||
|
SERVERPOD_API_SERVER_PUBLIC_HOST: ${TRAIN_DOMAIN:?set TRAIN_DOMAIN in deploy/.env}
|
||||||
|
SERVERPOD_WEB_SERVER_PUBLIC_HOST: ${TRAIN_DOMAIN:?set TRAIN_DOMAIN in deploy/.env}
|
||||||
# The register (ADR 0016): Learn's web server, on the instance's own network,
|
# The register (ADR 0016): Learn's web server, on the instance's own network,
|
||||||
# and the secret every call between them carries.
|
# and the secret every call between them carries.
|
||||||
ACCOUNT_REGISTER_URL: http://server:8082/
|
ACCOUNT_REGISTER_URL: http://server:8082/
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,9 @@
|
||||||
# - as a fast-forward of the deployed commit,
|
# - as a fast-forward of the deployed commit,
|
||||||
# - if every new commit is signed by a key in deploy/allowed-signers, as the deployed
|
# - if every new commit is signed by a key in deploy/allowed-signers, as the deployed
|
||||||
# checkout lists them (so no commit can add its own key),
|
# checkout lists them (so no commit can add its own key),
|
||||||
# dumps the database first (the last 3 dumps stay in ~/backups; Train's and Todo's too where
|
# dumps the database first (the last 3 dumps stay in ~/backups; Train's, Todo's and Meditate's
|
||||||
# .env switches each on, compose.train.yaml, compose.todo.yaml), applies the migrations
|
# too where .env switches each on, compose.train.yaml, compose.todo.yaml,
|
||||||
|
# compose.meditate.yaml), applies the migrations
|
||||||
# before switching to the new server, starts it once more after a migration (see below),
|
# before switching to the new server, starts it once more after a migration (see below),
|
||||||
# and fails unless that server answers afterwards. A failure leaves the checkout at the deployed commit, so
|
# and fails unless that server answers afterwards. A failure leaves the checkout at the deployed commit, so
|
||||||
# running the CI job again retries. By hand, with your own key, which passes the commit as CI does:
|
# running the CI job again retries. By hand, with your own key, which passes the commit as CI does:
|
||||||
|
|
@ -61,10 +62,11 @@ main() {
|
||||||
trap back_out EXIT
|
trap back_out EXIT
|
||||||
cd deploy
|
cd deploy
|
||||||
# The apps' servers beside Learn's, where .env switches each on (compose.train.yaml,
|
# The apps' servers beside Learn's, where .env switches each on (compose.train.yaml,
|
||||||
# compose.todo.yaml): built, dumped, migrated and waited for as Learn's.
|
# compose.todo.yaml, compose.meditate.yaml): built, dumped, migrated and waited for as
|
||||||
|
# Learn's.
|
||||||
services=$(docker compose config --services)
|
services=$(docker compose config --services)
|
||||||
apps=
|
apps=
|
||||||
for app in train todo; do
|
for app in train todo meditate; do
|
||||||
if printf '%s\n' "$services" | grep -qx "$app-server"; then
|
if printf '%s\n' "$services" | grep -qx "$app-server"; then
|
||||||
apps="$apps $app"
|
apps="$apps $app"
|
||||||
fi
|
fi
|
||||||
|
|
@ -136,7 +138,7 @@ main() {
|
||||||
echo "Deployed $commit."
|
echo "Deployed $commit."
|
||||||
}
|
}
|
||||||
|
|
||||||
# Dumps the database $1 (halkyon_learn, halkyon_train, halkyon_todo), as it is before the new server
|
# Dumps the database $1 (halkyon_learn, halkyon_train, halkyon_todo, halkyon_meditate), as it is before the new server
|
||||||
# applies its migrations; as the instance's superuser, halkyon_learn.
|
# applies its migrations; as the instance's superuser, halkyon_learn.
|
||||||
dump() {
|
dump() {
|
||||||
mkdir -p "$HOME/backups"
|
mkdir -p "$HOME/backups"
|
||||||
|
|
@ -153,7 +155,7 @@ dump() {
|
||||||
ls -1t "$HOME"/backups/"$1"-*.dump | tail -n +4 | xargs -r rm --
|
ls -1t "$HOME"/backups/"$1"-*.dump | tail -n +4 | xargs -r rm --
|
||||||
}
|
}
|
||||||
|
|
||||||
# Waits up to a minute for the new server $1 (server, train-server, todo-server) to answer inside the
|
# Waits up to a minute for the new server $1 (server, train-server, todo-server, meditate-server) to answer inside the
|
||||||
# stack's network.
|
# stack's network.
|
||||||
wait_until_answering() {
|
wait_until_answering() {
|
||||||
tries=0
|
tries=0
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ site changes.
|
||||||
| Photon, which finds places for Train's route planner, beside Train | `compose.search.yaml` | none |
|
| Photon, which finds places for Train's route planner, beside Train | `compose.search.yaml` | none |
|
||||||
| Halkyon Todo's server | `compose.todo.yaml` | `todo.caddy` |
|
| Halkyon Todo's server | `compose.todo.yaml` | `todo.caddy` |
|
||||||
| Todo's place search, on Train's Photon | `compose.todo-search.yaml` | none |
|
| Todo's place search, on Train's Photon | `compose.todo-search.yaml` | none |
|
||||||
|
| Halkyon Meditate's server | `compose.meditate.yaml` | `meditate.caddy` |
|
||||||
|
| Meditate's library, the operator's recordings, read-only | `compose.meditate-library.yaml` | none |
|
||||||
|
|
||||||
Both together: `COMPOSE_FILE=compose.learn.yaml:compose.jellyfin.yaml:compose.languagetool.yaml`.
|
Both together: `COMPOSE_FILE=compose.learn.yaml:compose.jellyfin.yaml:compose.languagetool.yaml`.
|
||||||
|
|
||||||
|
|
@ -527,3 +529,70 @@ Steps, in order:
|
||||||
alone. Without it Todo starts as it did, and its app offers no search. Nothing
|
alone. Without it Todo starts as it did, and its app offers no search. Nothing
|
||||||
searched is kept or logged, and a task's location stays sealed on the person's
|
searched is kept or logged, and a task's location stays sealed on the person's
|
||||||
devices.
|
devices.
|
||||||
|
|
||||||
|
## Meditate
|
||||||
|
|
||||||
|
Halkyon Meditate's server beside the instance (Halkyon Meditate's ADR 0038, step 7;
|
||||||
|
ADR 0039, D14), as Todo's: at its own domain, a member of the register Learn's server
|
||||||
|
holds (ADR 0016), its database `halkyon_meditate` in the instance's PostgreSQL. It
|
||||||
|
reads no sitting and no dream: every row is sealed on the person's devices (ADR 0038,
|
||||||
|
D7). `redeploy.sh` builds, dumps, migrates and waits for it once `.env` names its
|
||||||
|
compose file, and `backup.sh` backs its database up beside the others'.
|
||||||
|
|
||||||
|
Steps, in order:
|
||||||
|
|
||||||
|
1. **Its database** (the operator, on the instance's server, as `deploy`, in
|
||||||
|
`~/halkyon/deploy`): a role of Meditate's own, with the password that goes into
|
||||||
|
`.env` as `MEDITATE_POSTGRES_PASSWORD` (`openssl rand -hex 32`), and its database, as
|
||||||
|
Todo's:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose exec -T postgres psql -U halkyon_learn -d postgres -v ON_ERROR_STOP=1 \
|
||||||
|
-v password="<MEDITATE_POSTGRES_PASSWORD>" <<'SQL'
|
||||||
|
CREATE ROLE halkyon_meditate LOGIN PASSWORD :'password';
|
||||||
|
CREATE DATABASE halkyon_meditate OWNER halkyon_meditate;
|
||||||
|
REVOKE CONNECT ON DATABASE halkyon_meditate FROM PUBLIC;
|
||||||
|
SQL
|
||||||
|
```
|
||||||
|
|
||||||
|
The role owns its database alone and connects to no other: the `REVOKE`s of
|
||||||
|
Learn's, Train's and Todo's databases were made with theirs (on an instance without
|
||||||
|
them, also `REVOKE CONNECT ON DATABASE halkyon_learn FROM PUBLIC;`).
|
||||||
|
2. **DNS** (the operator, at the domain's DNS): Meditate's domain pointing at the
|
||||||
|
instance's server (`A`, and `AAAA` if it has an IPv6 address); for halkyon.li, at
|
||||||
|
ramiel (ADR 0039, D7).
|
||||||
|
3. **Switch it on** (the operator, as `deploy`): in `deploy/.env`,
|
||||||
|
`compose.meditate.yaml` added to `COMPOSE_FILE`, and Meditate's settings
|
||||||
|
(`.env.example`, *With Halkyon Meditate*):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
COMPOSE_FILE=compose.learn.yaml:compose.train.yaml:compose.todo.yaml:compose.meditate.yaml
|
||||||
|
MEDITATE_DOMAIN=meditate.halkyon.li
|
||||||
|
MEDITATE_POSTGRES_PASSWORD=<as in step 1>
|
||||||
|
MEDITATE_SESSION_KEY_HASH_PEPPER=<openssl rand -hex 32>
|
||||||
|
```
|
||||||
|
|
||||||
|
`INSTANCE_SECRET` must be set. The compose file names Meditate to Learn's register
|
||||||
|
(`INSTANCE_MEMBER_MEDITATE`).
|
||||||
|
4. **Start it** (the operator, as `deploy`): `docker compose up -d --build`. Caddy and
|
||||||
|
Learn's server are recreated with it (Meditate's site, and the register's member);
|
||||||
|
the instance is away for seconds. Meditate's server applies its migrations as it
|
||||||
|
starts.
|
||||||
|
5. **Check it**: `https://meditate.halkyon.li/` answers; in Meditate, signed in with the
|
||||||
|
instance's account, a sitting kept on one device shows on another.
|
||||||
|
6. **The library** (optional; ADR 0038, D16): recordings, pictures and `library.yaml`
|
||||||
|
(the server's `library.example.yaml` says how) in a folder on the host, readable by
|
||||||
|
all (the server runs as an unprivileged user), such as `/data/meditate-library`.
|
||||||
|
Check it with the server's own image, which reads it as the server will and exits 1
|
||||||
|
on anything it would refuse:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose run --rm --no-deps -v /data/meditate-library:/check:ro \
|
||||||
|
--entrypoint ./bin/check_library meditate-server /check
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, in `deploy/.env`, `compose.meditate-library.yaml` added to `COMPOSE_FILE` after
|
||||||
|
`compose.meditate.yaml`, and `MEDITATE_LIBRARY=/data/meditate-library`, then
|
||||||
|
`docker compose up -d`, which recreates Meditate's server alone. It rereads the
|
||||||
|
folder every ten minutes, so later changes need no restart; check each before it is
|
||||||
|
copied in. Without the file, Meditate starts as before, and its app shows no library.
|
||||||
|
|
|
||||||
20
deploy/sidecars/meditate.caddy
Normal file
20
deploy/sidecars/meditate.caddy
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Halkyon Meditate's site (compose.meditate.yaml, ADR 0038, step 7): its API server, and
|
||||||
|
# the operator's library's files from its web server (D16), which serves them only with
|
||||||
|
# an access its catalogue gave a person signed in. Never the register's calls between
|
||||||
|
# the instance's services (`/halkyon-base/…`, ADR 0016), which its web server takes on
|
||||||
|
# the instance's own network alone: only /library/* goes there.
|
||||||
|
{$MEDITATE_DOMAIN} {
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
# Recordings and pictures, served by ranges (encode leaves audio and pictures
|
||||||
|
# as they are).
|
||||||
|
handle /library/* {
|
||||||
|
reverse_proxy meditate-server:8082
|
||||||
|
}
|
||||||
|
|
||||||
|
# Serverpod's streaming methods use WebSockets; reverse_proxy upgrades them as-is.
|
||||||
|
# The visitor's address comes last in X-Forwarded-For, as for Learn's server.
|
||||||
|
handle {
|
||||||
|
reverse_proxy meditate-server:8080
|
||||||
|
}
|
||||||
|
}
|
||||||
178
test/deploy/meditate_deploy_test.dart
Normal file
178
test/deploy/meditate_deploy_test.dart
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:yaml/yaml.dart';
|
||||||
|
|
||||||
|
import '../../tool/src/images.dart';
|
||||||
|
|
||||||
|
/// Meditate's server beside the instance (Halkyon Meditate's ADR 0038, step
|
||||||
|
/// 7; ADR 0039, D14), as Todo's: a member of the register, its own database
|
||||||
|
/// and role, its site, and the operator's library read-only (D16).
|
||||||
|
void main() {
|
||||||
|
YamlMap services(String path) =>
|
||||||
|
(loadYaml(File(path).readAsStringSync()) as YamlMap)['services']
|
||||||
|
as YamlMap;
|
||||||
|
String read(String path) => File(path).readAsStringSync();
|
||||||
|
|
||||||
|
group('its server', () {
|
||||||
|
final server =
|
||||||
|
services('deploy/compose.meditate.yaml')['meditate-server'] as YamlMap;
|
||||||
|
final environment = server['environment'] as YamlMap;
|
||||||
|
|
||||||
|
test('signs in through the register, with the instance secret', () {
|
||||||
|
expect(environment['ACCOUNT_REGISTER_URL'], 'http://server:8082/');
|
||||||
|
expect(
|
||||||
|
environment['SERVERPOD_PASSWORD_instanceSecret'],
|
||||||
|
r'${INSTANCE_SECRET:?set INSTANCE_SECRET in deploy/.env}',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('has a database password and pepper of its own', () {
|
||||||
|
expect(
|
||||||
|
environment['SERVERPOD_DATABASE_PASSWORD'],
|
||||||
|
startsWith(r'${MEDITATE_POSTGRES_PASSWORD:?'),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
environment['SERVERPOD_PASSWORD_serverSideSessionKeyHashPepper'],
|
||||||
|
startsWith(r'${MEDITATE_SESSION_KEY_HASH_PEPPER:?'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('names its own domain as its public address', () {
|
||||||
|
for (final variable in [
|
||||||
|
'SERVERPOD_API_SERVER_PUBLIC_HOST',
|
||||||
|
'SERVERPOD_WEB_SERVER_PUBLIC_HOST',
|
||||||
|
]) {
|
||||||
|
expect(environment[variable], startsWith(r'${MEDITATE_DOMAIN:?'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('has no library unless its own file mounts one', () {
|
||||||
|
expect(environment.keys, isNot(contains('MEDITATE_LIBRARY_DIR')));
|
||||||
|
expect(server['volumes'], isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("is told to Learn's register as a member", () {
|
||||||
|
final learn =
|
||||||
|
services('deploy/compose.meditate.yaml')['server'] as YamlMap;
|
||||||
|
expect(
|
||||||
|
(learn['environment'] as YamlMap)['INSTANCE_MEMBER_MEDITATE'],
|
||||||
|
'http://meditate-server:8082/',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('connects to its own database as its own role', () {
|
||||||
|
final config = loadYaml(
|
||||||
|
read('apps/meditate/halkyon_meditate_server/config/production.yaml'),
|
||||||
|
) as YamlMap;
|
||||||
|
final database = config['database'] as YamlMap;
|
||||||
|
expect(database['name'], 'halkyon_meditate');
|
||||||
|
expect(database['user'], 'halkyon_meditate');
|
||||||
|
expect(database['host'], 'postgres');
|
||||||
|
expect((config['sessionLogs'] as YamlMap)['persistentEnabled'], isFalse);
|
||||||
|
expect(config.keys, isNot(contains('insightsServer')));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group("the operator's library (D16)", () {
|
||||||
|
final server =
|
||||||
|
services('deploy/compose.meditate-library.yaml')['meditate-server']
|
||||||
|
as YamlMap;
|
||||||
|
|
||||||
|
test('is mounted read-only where the server reads it', () {
|
||||||
|
expect(server['volumes'], [
|
||||||
|
r'${MEDITATE_LIBRARY:?set MEDITATE_LIBRARY in deploy/.env}:/library:ro',
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
(server['environment'] as YamlMap)['MEDITATE_LIBRARY_DIR'],
|
||||||
|
'/library',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('is checked by a program in the image', () {
|
||||||
|
final dockerfile = read(
|
||||||
|
'apps/meditate/halkyon_meditate_server/Dockerfile',
|
||||||
|
);
|
||||||
|
expect(dockerfile, contains('--target bin/check_library.dart'));
|
||||||
|
expect(dockerfile, contains('bin/check_library'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('its site', () {
|
||||||
|
// The site's directives, without its comments.
|
||||||
|
final site = read('deploy/sidecars/meditate.caddy')
|
||||||
|
.split('\n')
|
||||||
|
.where((line) => !line.trim().startsWith('#'))
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
test("sends the library's files to the web server, everything else to "
|
||||||
|
'the API server', () {
|
||||||
|
expect(site, contains('handle /library/* {'));
|
||||||
|
expect(site, contains('reverse_proxy meditate-server:8082'));
|
||||||
|
expect(site, contains('reverse_proxy meditate-server:8080'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("never passes on the calls between the instance's services", () {
|
||||||
|
expect(site, isNot(contains('/halkyon-base')));
|
||||||
|
expect(
|
||||||
|
RegExp('reverse_proxy meditate-server:8082').allMatches(site),
|
||||||
|
hasLength(1),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('is mounted into Caddy by the compose file', () {
|
||||||
|
final caddy =
|
||||||
|
services('deploy/compose.meditate.yaml')['caddy'] as YamlMap;
|
||||||
|
expect(
|
||||||
|
caddy['volumes'],
|
||||||
|
contains(
|
||||||
|
'./sidecars/meditate.caddy:/etc/caddy/sidecars/meditate.caddy:ro',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('the deploy and the backups', () {
|
||||||
|
test('build, dump, migrate and wait for it as for Todo', () {
|
||||||
|
expect(
|
||||||
|
read('deploy/redeploy.sh'),
|
||||||
|
contains('for app in train todo meditate; do'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('back its database up beside the others', () {
|
||||||
|
expect(
|
||||||
|
read('deploy/backup.sh'),
|
||||||
|
contains('for app in train todo meditate; do'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("have tool/check.dart replay its image's resolution", () {
|
||||||
|
expect(
|
||||||
|
dartImages,
|
||||||
|
contains('apps/meditate/halkyon_meditate_server/Dockerfile'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group("every app's server names the instance's domain, not halkyon.eu's", () {
|
||||||
|
for (final (file, service, variable) in [
|
||||||
|
('compose.learn.yaml', 'server', 'LEARN_PUBLIC_HOST'),
|
||||||
|
('compose.train.yaml', 'train-server', 'TRAIN_DOMAIN'),
|
||||||
|
('compose.todo.yaml', 'todo-server', 'TODO_DOMAIN'),
|
||||||
|
('compose.meditate.yaml', 'meditate-server', 'MEDITATE_DOMAIN'),
|
||||||
|
]) {
|
||||||
|
test(service, () {
|
||||||
|
final environment =
|
||||||
|
(services('deploy/$file')[service] as YamlMap)['environment']
|
||||||
|
as YamlMap;
|
||||||
|
for (final key in [
|
||||||
|
'SERVERPOD_API_SERVER_PUBLIC_HOST',
|
||||||
|
'SERVERPOD_WEB_SERVER_PUBLIC_HOST',
|
||||||
|
]) {
|
||||||
|
expect(environment[key], startsWith('\${$variable'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ const dartImages = [
|
||||||
'apps/learn/halkyon_learn_curator/Dockerfile',
|
'apps/learn/halkyon_learn_curator/Dockerfile',
|
||||||
'apps/train/halkyon_train_server/Dockerfile',
|
'apps/train/halkyon_train_server/Dockerfile',
|
||||||
'apps/todo/halkyon_todo_server/Dockerfile',
|
'apps/todo/halkyon_todo_server/Dockerfile',
|
||||||
|
'apps/meditate/halkyon_meditate_server/Dockerfile',
|
||||||
];
|
];
|
||||||
|
|
||||||
/// One instruction of a Dockerfile's build stage that the replay follows.
|
/// One instruction of a Dockerfile's build stage that the replay follows.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue