docs: run Collabora Online (CODE) in the docker-compose example - #1570
docs: run Collabora Online (CODE) in the docker-compose example#1570DeepDiver1975 wants to merge 10 commits into
Conversation
Extend the Server v11 docker-compose example to start a Collabora Online Development Edition (CODE) container so documents can be edited in the browser, wired to ownCloud end-to-end. Because the Collabora editor is embedded via an iframe and a WebSocket, both ownCloud and Collabora must be served over HTTPS on their own hostnames. A Traefik reverse proxy is added to terminate TLS (Let's Encrypt) and route the two domains; the ownCloud service moves from a published port to proxy labels. The page gains a TLS/two-host explanation, an expanded .env settings table, the collabora/proxy containers in the `docker compose ps` output, and an "Enabling Collabora Online (CODE)" section covering the richdocuments app and `wopi_url` occ wiring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
…arness Security review of the internet-facing compose example surfaced several remotely exploitable defaults now that it is framed as a production setup: - Remove the published host ports from MariaDB (3306) and Redis (6379); they are reached only over a new internal "backend" network. An exposed, weakly-authenticated database and an unauthenticated Redis on a public host were directly exploitable. - Split the topology into "frontend" (proxy, ownCloud, Collabora) and "backend" (ownCloud, MariaDB, Redis) networks so the edge proxy and Collabora cannot address the data tier. - Restrict the Collabora CODE admin console (/browser/dist/admincontrol, /cool/adminws) with a higher-priority Traefik router guarded by an ipallowlist middleware, instead of exposing it publicly with admin/admin. - Add an HSTS headers middleware on the ownCloud router only (a framing policy would break the embedded editor iframe). - Move all credentials to the .env file with CHANGE_ME_* placeholders, define the previously-undefined ADMIN_* and DB password variables, and use distinct root vs. app database passwords. Document the residual caveats (change credentials, keep the data tier private, never expose the admin console, the Docker-socket-as-root trade-off, pin image versions) next to the compose block, and correct the "docker compose ps" note. Add a QA harness under tests/docker-compose/ that boots the shipped example (via a self-signed-TLS override + runnable test env) and smoke-tests it: status.php reports installed, Collabora serves WOPI discovery, and 3306/6379 are confirmed closed. Wire it up as "npm run test:compose" and a paths-filtered GitHub Actions workflow, and add the docker ecosystem to Dependabot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
|
Follow-up QA commit ( Security hardening of the compose example
Local test harness (
CI + Dependabot
Note: the compose image tags are |
The security hardening put the ownCloud service on two networks (frontend + backend). With no `traefik.docker.network` label, Traefik picks one of the container's IPs at random (per its Docker provider docs) and may register the `backend` IP, which the proxy — attached only to `frontend` — cannot reach. Routing to ownCloud then fails silently: `status.php` returns an empty body and the compose smoke test times out. This was nondeterministic, so it passed locally but failed in CI where Docker ordered the networks the other way. Add `traefik.docker.network=owncloud_frontend` on the ownCloud service and give the networks explicit names (`owncloud_frontend` / `owncloud_backend`) so the label resolves regardless of the compose project/directory name. Verified via the Traefik API that the frontend IP is now registered deterministically, even under a different project name and with the subnets assigned in reverse order. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
The smoke test failed intermittently in CI (passed locally). Two root causes,
both in tests/docker-compose/smoke-test.sh:
1. `printf '%s' "$body" | grep -q PATTERN` under `set -o pipefail`. When grep -q
matches it closes the pipe immediately; printf, still writing, is killed by
SIGPIPE, and pipefail then reports the whole pipeline as failed even though
the match succeeded. The 32 KB Collabora discovery document lost this race in
CI ("printf: write error: Broken pipe" in the logs) and the check reported
"not found" while printing the document it had just found. The same idiom
guarded status.php (survived only because its body fits the pipe buffer) and
the port-exposure regression check — where the inverted status would have
silently hidden a real violation. Replace all three with pure-bash matching
(`[[ ... == *needle* ]]` / `[[ ... =~ ... ]]`), which uses no pipe.
2. Collabora CODE has no healthcheck, so the health-wait loop never covered it,
and the proxy returns 404/502 for /hosting/discovery while CODE is still
starting. The single-shot discovery check raced startup. Poll it until the
document is served or INSTALL_TIMEOUT elapses, like the status.php check.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
CI fixes: two root causes debugged and resolvedThe new 1. Traefik multi-network routing nondeterminism (
|
The Dependabot `docker` ecosystem entry pointed at the compose example
cannot resolve the `${COLLABORA_IMAGE}` / `${OWNCLOUD_IMAGE}` variable
interpolation (tags live in the .env file, which Dependabot does not
read), so it produced no PRs. Remove it to avoid implying the image
tags are watched automatically.
Pin `COLLABORA_IMAGE` in the shipped dot.env to a concrete version so
the example follows its own "pin image versions" hardening advice
instead of tracking the floating `latest` tag, and reword the note to
match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
Code reviewOverall a high-quality, security-conscious change. The compose-file comments and the doc admonitions do an excellent job explaining the why (network split, Docker-socket exposure, admin allowlist, HSTS-only framing), and the smoke test even guards the data-tier hardening as a regression invariant. I pushed a follow-up commit ( Fixed
Not changed — worth a glance
Verified good
|
Move the MariaDB, Redis, Collabora and Traefik image tags out of the .env
indirection and into literal tags in docker-compose.yml, then restore the
Dependabot `docker` ecosystem entry. Dependabot's compose parser reads image
tags literally and cannot resolve `${VAR}` interpolation or read the .env
file, so the previous env-var indirection meant Dependabot produced no update
PRs. Literal tags make the four infrastructure images automatically
maintained.
ownCloud keeps `owncloud/server:${OWNCLOUD_IMAGE}`: its version is a single
source of truth driven by the {latest-server-download-version} docs attribute
and reused by the Quick Evaluation example, so it is bumped deliberately
rather than by Dependabot.
Drop the now-unused MARIADB_IMAGE/REDIS_IMAGE/COLLABORA_IMAGE/TRAEFIK_IMAGE
variables from dot.env and their rows from the settings table, and reword the
"Pin image versions" note to match.
The smoke-test harness pinned Traefik to v3.6 (v3.3 fails against Docker
Engine >= 29) via ${TRAEFIK_IMAGE} in test.env; since the base file now hard-
codes v3.3, relocate that pin to an `image:` override in
docker-compose.override.yml so CI keeps booting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
Correction to the Dependabot fix (commit
|
Code reviewFound 14 issues:
docs-server/tests/docker-compose/smoke-test.sh Lines 157 to 161 in f49764a
docs-server/tests/docker-compose/docker-compose.override.yml Lines 41 to 45 in f49764a
docs-server/tests/docker-compose/smoke-test.sh Lines 70 to 74 in f49764a
docs-server/.github/dependabot.yml Lines 33 to 37 in f49764a
docs-server/.github/workflows/compose-test.yml Lines 27 to 31 in f49764a
docs-server/tests/docker-compose/docker-compose.override.yml Lines 42 to 46 in f49764a
docs-server/tests/docker-compose/smoke-test.sh Lines 74 to 78 in f49764a
docs-server/tests/docker-compose/smoke-test.sh Lines 80 to 84 in f49764a 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
The example pinned traefik:v3.3, whose Docker provider probes the daemon
with API version 1.24 before negotiating. Docker Engine >= 29 rejects
that ("client version 1.24 is too old. Minimum supported API version is
1.40"), so Traefik starts but never receives any routing configuration —
the whole stack is unreachable on a current Engine. Verified against
Engine 29.6.2: v3.3 fails, v3.6 and v3.7 work.
Also pin the "collabora" router's priority. Traefik derives an unpinned
router's priority from its rule-string length, so a sufficiently long
COLLABORA_DOMAIN could out-rank the fixed priority=100 on the
"collabora-admin" router and steal the admin-console and WebSocket paths
— serving them without the IP allowlist. Pinning both routers removes
the dependency on the domain length.
Finally, set the Compose project name explicitly instead of hardcoding
"name:" on each network. Traefik's traefik.docker.network label must name
the real Docker network; given a short name it logs a warning and then
silently falls back to the container's first network, which may be the
"backend" tier the proxy cannot reach. Fixing the project name keeps the
generated names stable without opting the networks out of project
scoping.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
The data-tier guard probed MariaDB and Redis with "curl http://127.0.0.1:<port>". Neither speaks HTTP, so curl never exits 0 against them even when the port is wide open — verified against a published redis:7 on Engine 29.6.2: exit 52 ("empty reply") for an open port versus exit 7 ("refused") for a closed one. The check therefore passed unconditionally and could never catch the accidental re-publishing of a database port it exists to catch. Replace it with a raw /dev/tcp connect probe, which succeeds on connect alone. Verified by negative test: re-adding "3306:3306" to mariadb now fails the guard, where the curl version stayed silent. Test the version the docs ship. The harness pinned Traefik to v3.6 and re-listed the whole command: array, so CI exercised neither the shipped image tag nor the shipped flags — and because Compose replaces rather than merges list values, future flag changes in the example would not have reached the tested config. Both overrides are now dropped: a certresolver that no router references is never used (routers with tls=true serve TRAEFIK DEFAULT CERT and Traefik places no ACME order, verified HTTP 200), so blanking the per-router certresolver labels is sufficient to disable Let's Encrypt. Also tear down before boot, so a run whose EXIT trap never fired (CI OOM or timeout) cannot leave a stale MariaDB volume holding an old root password and fail the next run for unrelated reasons; and factor the three copy-pasted deadline/sleep loops into one poll_until helper, collapsing the two docker inspect calls per container per iteration into one. shellcheck (koalaman/shellcheck:stable) is clean at default severity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
Switch the docker ecosystem entry to "directories:" so the smoke-test harness is covered too. It currently pins no image of its own — it inherits the example's tags — but any future pin there is now tracked rather than silently going stale. Remove the "Validate the merged compose configuration" job step: smoke-test.sh already runs "docker compose config" as its first action. Drop the two .gitignore entries added with the harness. Neither matches anything it creates: "acme" is a Docker-managed named volume, not a bind-mounted directory, and the script always passes an explicit --env-file rather than writing a bare .env. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
Moving the example behind Traefik dropped three things admins relied on, with nothing documented in their place. The setup now hard-requires two publicly DNS-resolvable domains reachable on 80/443 for Let's Encrypt, where the previous wording explicitly allowed "hostname(s) and/or IP addresses". Add a note covering internal-only and offline deployments: how to swap ACME for your own certificate, and why a bare IP address cannot work together with Collabora (no certificate can be issued for one, and the editor needs two distinct HTTPS host names). The plain published port was replaced by proxy-only routing labels, so admins fronting the stack with their own nginx/HAProxy/load balancer had no documented path. Add a tip showing how to drop the bundled proxy and publish ownCloud's port instead, including binding to a specific interface and the reverse-proxy configuration requirement. Removing MariaDB's 3306 host port broke the documented "mysqldump -h localhost" recipe, which assumes host-side database access. Note the Docker case on the backup page and give the container-side equivalent, with -T so the redirected dump is not corrupted by a pseudo-TTY, plus a warning not to republish the port just to back up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
|
Went through all 14 items and verified each against the code and a real Docker Engine (29.6.2) before changing anything. Fixed in c90b1b3, df33308, e6fce5b, d25ba36. Three items needed empirical evidence, and two turned out to be framed backwards. 5 — confirmed, and the guard was worse than described. It's not just that curl can't detect an open non-HTTP port; it can never fail the check. Measured against a published 6 — correct that CI tested the wrong version, but the shipped file was the broken one. Traefik v3.3's Docker provider probes the daemon with API 1.24 before negotiating; Engine >= 29 rejects it ( 12 — fixed by removing the override rather than restructuring it. I checked whether a certresolver must be absent to be inactive: it doesn't. Routers with 1 — fixed via a top-level 2, 3, 4 — agreed, these were real regressions: capabilities removed with no replacement. Added an internal-only/offline TLS note (incl. why a bare IP can't work with Collabora — no certificate can be issued for one, and the editor needs two distinct HTTPS names), a "front it with your own reverse proxy" tip with a 7 — pinned both Collabora routers, not just raised the admin one, so the outcome no longer depends on 8, 11, 13, 14 — fixed as described: 9 — fixed with 10 — confirmed and removed. Verification: full smoke test passes end-to-end against the shipped Traefik version (all 6 assertions); the exposed-port guard fails when it should; One deliberate non-change: |
Summary
Extends the Server v11 docker-compose example so it also starts a Collabora Online Development Edition (CODE) container, wired to ownCloud end-to-end. Previously the example ran only
owncloud/server+mariadb+redisover plain HTTP.Because the Collabora editor is embedded via an iframe and a WebSocket, both ownCloud and Collabora must be served over HTTPS on their own hostnames — browsers block the mixed HTTP/HTTPS content a plain-HTTP setup produces. A Traefik reverse proxy is therefore added to terminate TLS (Let's Encrypt), redirect HTTP→HTTPS, and route the two domains; the
owncloudservice moves from a published port to proxy labels.Changes
docker-compose.yml— new always-oncollabora(collabora/code, 9980) andproxy(Traefik) services; ownCloud exposed via Traefik labels; newacmevolume for certificates.dot.env— real hostnames instead oflocalhost; newCOLLABORA_DOMAIN,OWNCLOUD_DOMAIN_REGEX,COLLABORA_IMAGE, Collabora admin creds,TRAEFIK_IMAGE,LETSENCRYPT_EMAIL; dropped obsoleteHTTP_PORT.installing_with_docker.adoc— TLS/two-host explanation, expanded.envsettings table, updateddocker compose psoutput (collabora + proxy), and a new "Enabling Collabora Online (CODE)" section coveringapp:enable richdocumentsandconfig:app:set richdocuments wopi_url, linking to the existing Collabora / occ pages.Notes
richdocumentsapp (what the repo documents today); the repo's own note flags its OC11 availability as "under review". Thecollaboracontainer itself is edition-independent.Verification
docker compose configvalidates the file.wopi_urlcommand, with no unresolved AsciiDoc attribute tokens.🤖 Generated with Claude Code