Skip to content

Commit df9cf03

Browse files
rubenvdlindeConduction Release Bot
andauthored
docs: add a local demo environment (#480)
Adds `keepiq-compose.yaml` and a setup page describing it. The compose brings up Postgres and Nextcloud, installs openregister (required), thematiq and integriq (optional) and keepiq from release tarballs, and enables them in dependency order. Nothing is bind-mounted: Nextcloud installs an app by deleting its directory and extracting an archive over it, so pointing that at a checkout deletes the working tree — measured on a development machine on 2026-08-27, where an app-store update fired on a container restart and removed every top-level file including .git. Release tarballs rather than a clone for a second reason: a tarball is a complete app carrying vendor/ and the built js/, and an app with no vendor/ does not fail loudly — it warns once and keeps loading, so it looks installed while every service needing a dependency is absent. The openregister dependency is not declared in appinfo/info.xml — no app in the fleet declares an <app> dependency — so the compose encodes what the manifest does not. Verified: docker compose config parses and interpolates; the same generated file was booted end to end for portaliq, which produced 17 registers, 86 schemas and 13 magic tables for its own register, with the portal content API returning a real site rather than an empty shell. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
1 parent 7694771 commit df9cf03

2 files changed

Lines changed: 381 additions & 0 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Run a local demo
2+
3+
This page gets a working Keepiq running on your own machine in two commands. You end with archiving and retention over registry objects.
4+
5+
It is a **demo**, not a development environment. Nothing is mounted from a checkout, and that is deliberate — see [What this is not](#what-this-is-not).
6+
7+
## What you need
8+
9+
Docker, with Compose v2.23 or newer. Nothing else — no PHP, no Node, no Nextcloud.
10+
11+
```bash
12+
docker --version
13+
docker compose version
14+
```
15+
16+
If `docker compose version` prints v2.22 or older, upgrade first. The compose file declares its scripts inline via `configs`, and older versions ignore the `content:` field **silently** — which produces an instance with no apps installed and nothing in the logs to explain why.
17+
18+
## Step 1 — get the compose file
19+
20+
```bash
21+
curl -fsSLO https://raw.githubusercontent.com/ConductionNL/keepiq/development/keepiq-compose.yaml
22+
```
23+
24+
A single self-contained file. There is nothing else to fetch and nothing to edit.
25+
26+
## Step 2 — start it
27+
28+
```bash
29+
docker compose -f keepiq-compose.yaml up -d
30+
```
31+
32+
The first run takes a few minutes: it pulls three images and downloads the application archives. Watch it work if you like:
33+
34+
```bash
35+
docker compose -f keepiq-compose.yaml logs -f app-installer
36+
```
37+
38+
You are looking for:
39+
40+
```
41+
==> installing openregister <version>
42+
==> installing thematiq <version>
43+
==> installing integriq <version>
44+
==> installing keepiq <version>
45+
==> apps present: integriq keepiq openregister thematiq
46+
```
47+
48+
Then Nextcloud installs itself and enables the apps **in dependency order**. OpenRegister goes first: it owns the registers and schemas the others declare against, and a leaf app enabled before it finds no register to attach to.
49+
50+
That is done when this returns `"installed":true`:
51+
52+
```bash
53+
curl -s http://localhost:8616/status.php
54+
```
55+
56+
## Step 3 — open the demo
57+
58+
| What | Where |
59+
| --- | --- |
60+
| **Keepiq** | [http://localhost:8616/apps/keepiq/](http://localhost:8616/apps/keepiq/) |
61+
| Admin interface | [http://localhost:8616](http://localhost:8616)`admin` / `admin` |
62+
63+
## What gets installed, and why more than one app
64+
65+
| App | Why |
66+
| --- | --- |
67+
| `openregister` | **Required.** Every Connext app declares its registers and schemas against OpenRegister. |
68+
| `thematiq` | Optional. Government theming. Absent, the UI renders unthemed rather than wrong. |
69+
| `integriq` | Optional. The connector, for feeding in data from systems you do not control. |
70+
| `keepiq` | The app this page is about. |
71+
72+
That OpenRegister dependency is **not declared** in `appinfo/info.xml` — no app in the fleet declares an `<app>` dependency — so nothing stops the App Store from installing keepiq without it. It would then load, find no register to attach to, and show you an empty app rather than an error. The compose file encodes the dependency the manifest does not.
73+
74+
## Verifying it actually worked
75+
76+
A page loading is not the same as a page working. Nextcloud serves its shell before the app decides whether it has anything to render, so an app URL returns HTTP 200 even when it resolves to nothing at all. A smoke test that checks for a 200 would call that a success.
77+
78+
Check content instead:
79+
80+
```bash
81+
# The app answers, rather than 404 or an empty shell
82+
curl -s -o /dev/null -w '%{http_code}\n' "http://localhost:8616/apps/keepiq/"
83+
84+
# OpenRegister has registers — an empty list means the configuration
85+
# was never imported, which is not the same as "nothing configured yet"
86+
curl -s -u admin:admin "http://localhost:8616/apps/openregister/api/registers" | head -c 300
87+
```
88+
89+
## Changing the defaults
90+
91+
The port and every version are overridable:
92+
93+
```bash
94+
DEMO_PORT=9000 \
95+
KEEPIQ_VERSION=1.2.3 \
96+
docker compose -f keepiq-compose.yaml up -d
97+
```
98+
99+
Leaving a version empty resolves the newest release for that app, pre-releases included — which is what most Connext apps still ship, so that is the default.
100+
101+
## Tearing it down
102+
103+
```bash
104+
# Stop, keep the data
105+
docker compose -f keepiq-compose.yaml down
106+
107+
# Stop and delete everything, including the database
108+
docker compose -f keepiq-compose.yaml down -v
109+
```
110+
111+
## What this is not
112+
113+
**It is not a development environment, and it cannot be turned into one by adding a bind mount.**
114+
115+
Nextcloud installs and updates an app by deleting the app directory and extracting a fresh archive over it. Point that at a checkout and an app-store update will delete your working tree — measured on a development machine on 27 August 2026, where `\OC\Updater::upgradeAppStoreApp` fired on a container restart and removed every top-level file from a bind-mounted checkout, including its `.git` directory. Only the subdirectories it lacked permission to unlink survived.
116+
117+
So this compose keeps its apps in a named volume and installs them from release archives. That also happens to be the only thing that works: a release archive is a **complete** app carrying `vendor/` and the built `js/` bundle, while a `git clone` carries neither — and a Nextcloud app with no `vendor/` does not fail loudly. It warns once and keeps loading, so the app appears installed while every service that needs a dependency is quietly absent.
118+
119+
To work *on* these apps rather than *with* them, use the development environment instead.
120+
121+
## Troubleshooting
122+
123+
**`app-installer` exits non-zero.** It could not download an archive. Check the log for the URL it tried; the most common cause is a pinned version with no matching release.
124+
125+
**It stops with `openregister missing; aborting`.** Deliberate. Every other app declares registers against OpenRegister, so a stack without it would start and then fail in a dozen confusing ways instead of one clear one.
126+
127+
**The UI renders unthemed.** Thematiq is not installed or not enabled. Expected, and cosmetic — the theme resolver renders unthemed rather than wrong when it is absent.
128+
129+
**Everything returns 404 or a maintenance page after a restart.** Nextcloud is waiting for an upgrade. Run `docker compose -f keepiq-compose.yaml exec -u www-data nextcloud php occ upgrade`.

keepiq-compose.yaml

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# Keepiq — local demo environment
2+
#
3+
# docker compose -f keepiq-compose.yaml up -d
4+
#
5+
# Then open http://localhost:8616/apps/keepiq/
6+
# Admin UI: http://localhost:8616 (admin / admin)
7+
#
8+
# Tear down, including all data:
9+
# docker compose -f keepiq-compose.yaml down -v
10+
#
11+
# ---------------------------------------------------------------------------
12+
# THIS IS A DEMO ENVIRONMENT, NOT A DEVELOPMENT ENVIRONMENT.
13+
#
14+
# Nothing here is bind-mounted from your working copy, and that is deliberate
15+
# rather than merely simpler. Nextcloud installs and updates an app by DELETING
16+
# its directory and extracting a fresh archive over it. Measured 2026-08-27 on
17+
# a development machine: `\OC\Updater::upgradeAppStoreApp` fired on a container
18+
# restart and removed every top-level file from a bind-mounted checkout —
19+
# including its `.git` directory — leaving only the subdirectories it lacked
20+
# permission to unlink. A demo rig has no business pointing at a checkout, so
21+
# this one owns its apps in a named volume.
22+
#
23+
# If you want to work ON these apps rather than WITH them, use the development
24+
# environment instead. This file cannot serve that purpose and should not try.
25+
#
26+
# ---------------------------------------------------------------------------
27+
# WHY RELEASE TARBALLS RATHER THAN `git clone`
28+
#
29+
# A release tarball is a COMPLETE app: it carries `vendor/` and the built `js/`
30+
# bundle. A git checkout carries neither, and a Nextcloud app whose `vendor/`
31+
# is missing does not fail loudly — `include_once` warns and the app keeps
32+
# loading, so the app appears installed while every service that needs a
33+
# dependency is absent.
34+
#
35+
# ---------------------------------------------------------------------------
36+
# WHAT IS INSTALLED, AND WHY MORE THAN ONE APP
37+
#
38+
# openregister REQUIRED. Every Connext app declares its registers and
39+
# schemas against OpenRegister. Note that this dependency is
40+
# NOT declared in appinfo/info.xml — no app in the fleet
41+
# declares an <app> dependency — so nothing stops the App
42+
# Store installing keepiq without it. It would then load
43+
# and find no register to attach to.
44+
# thematiq Optional. Government theming. Absent, the UI renders
45+
# unthemed rather than wrong.
46+
# integriq Optional. The connector, for feeding data in from systems
47+
# you do not control.
48+
# keepiq the app this file is for.
49+
#
50+
# Versions float to the newest release by default, pre-releases included,
51+
# because most Connext apps do not yet publish a stable one. Pin any of them:
52+
#
53+
# KEEPIQ_VERSION=1.2.3 docker compose -f keepiq-compose.yaml up -d
54+
#
55+
# The Nextcloud image is pinned to a MAJOR tag rather than `:latest`. A demo
56+
# that is stopped and restarted weeks later would otherwise boot a drifted
57+
# Nextcloud over its existing data volume, which lands the instance in
58+
# maintenance mode with its apps disabled.
59+
60+
name: keepiq-demo
61+
62+
volumes:
63+
db:
64+
nextcloud:
65+
apps:
66+
67+
services:
68+
db:
69+
image: postgres:16-alpine
70+
restart: unless-stopped
71+
environment:
72+
POSTGRES_USER: nextcloud
73+
POSTGRES_PASSWORD: nextcloud
74+
POSTGRES_DB: nextcloud
75+
volumes:
76+
- db:/var/lib/postgresql/data
77+
healthcheck:
78+
test: ["CMD-SHELL", "pg_isready -U nextcloud -d nextcloud"]
79+
interval: 5s
80+
timeout: 3s
81+
retries: 20
82+
83+
# Downloads each app's release tarball into the shared `apps` volume before
84+
# Nextcloud starts. It runs to completion and exits; Nextcloud waits for that
85+
# exit via `condition: service_completed_successfully`, so there is no window
86+
# in which Nextcloud boots against a half-populated app directory.
87+
#
88+
# Idempotent: an app whose appinfo/info.xml is already present is skipped, so
89+
# `up` on an existing demo does not re-download 100MB per app.
90+
app-installer:
91+
image: alpine:3.20
92+
restart: "no"
93+
environment:
94+
OPENREGISTER_VERSION: ${OPENREGISTER_VERSION:-}
95+
THEMATIQ_VERSION: ${THEMATIQ_VERSION:-}
96+
INTEGRIQ_VERSION: ${INTEGRIQ_VERSION:-}
97+
KEEPIQ_VERSION: ${KEEPIQ_VERSION:-}
98+
volumes:
99+
- apps:/apps
100+
configs:
101+
- source: install-apps
102+
target: /install-apps.sh
103+
mode: "0755"
104+
command: ["/bin/sh", "/install-apps.sh"]
105+
106+
nextcloud:
107+
image: nextcloud:34-apache
108+
restart: unless-stopped
109+
ports:
110+
- "${DEMO_PORT:-8616}:80"
111+
depends_on:
112+
db:
113+
condition: service_healthy
114+
app-installer:
115+
condition: service_completed_successfully
116+
environment:
117+
POSTGRES_HOST: db
118+
POSTGRES_USER: nextcloud
119+
POSTGRES_PASSWORD: nextcloud
120+
POSTGRES_DB: nextcloud
121+
NEXTCLOUD_ADMIN_USER: admin
122+
NEXTCLOUD_ADMIN_PASSWORD: admin
123+
# The port has to appear here as well as in `ports:`. Nextcloud rejects a
124+
# request whose Host header names a domain it does not trust, and
125+
# "localhost" and "localhost:8616" are different entries.
126+
NEXTCLOUD_TRUSTED_DOMAINS: "localhost localhost:${DEMO_PORT:-8616} 127.0.0.1 127.0.0.1:${DEMO_PORT:-8616}"
127+
# OVERWRITECLIURL IS LOAD-BEARING, NOT COSMETIC. Apps that federate
128+
# advertise this address to peers. It is a local address here, and a
129+
# local address is refused rather than broadcast — which is what keeps a
130+
# demo on somebody's laptop out of the national directory.
131+
OVERWRITECLIURL: "http://localhost:${DEMO_PORT:-8616}"
132+
OVERWRITEPROTOCOL: http
133+
volumes:
134+
- nextcloud:/var/www/html
135+
- apps:/var/www/html/custom_apps
136+
configs:
137+
- source: enable-apps
138+
target: /docker-entrypoint-hooks.d/post-installation/10-enable-connext-apps.sh
139+
mode: "0755"
140+
141+
configs:
142+
# EVERY SHELL VARIABLE BELOW IS WRITTEN `$$name`, NOT `$name`.
143+
#
144+
# Compose interpolates `$name` inside `configs.content` before the file is
145+
# written, so an un-escaped shell variable arrives as an EMPTY STRING and the
146+
# script runs on silently. Measured while building this file: `$version` was
147+
# blanked, which made the "no version pinned" branch look true for an app
148+
# whose version WAS pinned, and the resulting error named no repository —
149+
# `could not resolve a release for ` — because `$repo` had been blanked too.
150+
#
151+
# `$$` is the escape that survives interpolation and reaches /bin/sh as `$`.
152+
# `${DEMO_PORT:-8616}` is deliberately NOT escaped: that one is Compose's
153+
# to substitute.
154+
install-apps:
155+
content: |
156+
#!/bin/sh
157+
set -eu
158+
apk add --no-cache curl tar jq >/dev/null
159+
160+
# RESOLVING "NEWEST" TAKES TWO CORRECTIONS, NOT ONE.
161+
#
162+
# 1. The GitHub "latest release" endpoint EXCLUDES prereleases and answers
163+
# 404 for a repository that has only ever shipped them — which reads
164+
# exactly like "no such app". Ask the releases LIST instead.
165+
#
166+
# 2. THE LIST IS NOT ORDERED BY CREATION DATE. Taking the first entry looks
167+
# correct and is not. Measured 2026-08-27 on openregister:
168+
#
169+
# v1.1.6 created 10:17:45 <- returned first
170+
# v1.1.6-unstable.20260827110807 created 11:09:37 <- actually newest
171+
#
172+
# Taking the first entry installs an OLDER build than intended, and the
173+
# failure surfaces far from the cause — as a missing CLASS in a
174+
# different app, not as a version complaint.
175+
#
176+
# Sorting by created_at explicitly is the fix; jq is here for that.
177+
resolve_latest() {
178+
curl -fsSL "https://api.github.com/repos/ConductionNL/$$1/releases?per_page=50" \
179+
| jq -r '[.[] | select(.draft | not)] | sort_by(.created_at) | last | .tag_name // empty' \
180+
| sed 's/^v//'
181+
}
182+
183+
install_app() {
184+
repo="$$1"; appid="$$2"; version="$$3"; asset="$$4"
185+
186+
if [ -f "/apps/$$appid/appinfo/info.xml" ]; then
187+
echo "==> $$appid already present, skipping"
188+
return 0
189+
fi
190+
191+
if [ -z "$$version" ]; then
192+
version="$$(resolve_latest "$$repo")"
193+
[ -n "$$version" ] || { echo "!! could not resolve a release for $$repo"; return 1; }
194+
echo "==> $$appid: no version pinned, resolved $$version"
195+
fi
196+
197+
url="https://github.com/ConductionNL/$$repo/releases/download/v$$version/$$asset-$$version.tar.gz"
198+
echo "==> installing $$appid $$version"
199+
200+
# Extract into a staging directory rather than straight into /apps.
201+
# The archive's own top-level directory name is not guaranteed to equal
202+
# the Nextcloud app id — Nextcloud resolves an app by its DIRECTORY
203+
# name, so an archive that unpacks under the old name after a rename
204+
# yields an app that is silently never loaded. Several Connext apps were
205+
# renamed in August 2026, so this is a live concern, not a hypothetical.
206+
rm -rf /tmp/stage && mkdir -p /tmp/stage
207+
curl -fsSL "$$url" | tar -xz -C /tmp/stage
208+
209+
top="$$(ls /tmp/stage | head -n1)"
210+
if [ "$$top" != "$$appid" ]; then
211+
echo " archive unpacked as '$$top', installing it as '$$appid'"
212+
fi
213+
mv "/tmp/stage/$$top" "/apps/$$appid"
214+
rm -rf /tmp/stage
215+
}
216+
217+
# OpenRegister is not optional. If it could not be fetched, stop here
218+
# rather than let Nextcloud boot into a dozen confusing downstream
219+
# failures instead of one clear one.
220+
install_app openregister openregister "$$OPENREGISTER_VERSION" openregister
221+
install_app thematiq thematiq "$$THEMATIQ_VERSION" thematiq
222+
install_app integriq integriq "$$INTEGRIQ_VERSION" integriq
223+
install_app keepiq keepiq "$$KEEPIQ_VERSION" keepiq
224+
225+
[ -f /apps/openregister/appinfo/info.xml ] || { echo "!! openregister missing; aborting"; exit 1; }
226+
227+
# 33 is www-data inside the Nextcloud image.
228+
chown -R 33:33 /apps
229+
echo "==> apps present: $$(ls /apps | tr '\n' ' ')"
230+
231+
enable-apps:
232+
content: |
233+
#!/bin/sh
234+
set -eu
235+
# Runs once, after Nextcloud has installed itself.
236+
#
237+
# ORDER IS NOT ARBITRARY. OpenRegister owns the registers and schemas the
238+
# other apps declare against, and a leaf app enabled before it finds no
239+
# register to attach to. Enabling them in dependency order is what makes
240+
# a first boot produce a working instance instead of an empty one.
241+
for app in openregister thematiq integriq keepiq; do
242+
echo "==> enabling $$app"
243+
php /var/www/html/occ app:enable "$$app" || echo "!! failed to enable $$app"
244+
done
245+
246+
# Several apps ship a schema whose slug is not unique across the
247+
# instance. OpenRegister resolves a duplicate slug by tie-break and warns,
248+
# which means a leaf app can silently read another app's schema. This is a
249+
# no-op on a clean install and a repair on one that has drifted.
250+
php /var/www/html/occ openregister:schemas:dedup || true
251+
252+
echo "==> Keepiq demo: http://localhost:${DEMO_PORT:-8616}/apps/keepiq/"

0 commit comments

Comments
 (0)