|
| 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