Skip to content

docs: run Collabora Online (CODE) in the docker-compose example - #1570

Open
DeepDiver1975 wants to merge 10 commits into
masterfrom
docs/docker-compose-collabora-code
Open

docs: run Collabora Online (CODE) in the docker-compose example#1570
DeepDiver1975 wants to merge 10 commits into
masterfrom
docs/docker-compose-collabora-code

Conversation

@DeepDiver1975

Copy link
Copy Markdown
Member

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 + redis over 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 owncloud service moves from a published port to proxy labels.

Changes

  • docker-compose.yml — new always-on collabora (collabora/code, 9980) and proxy (Traefik) services; ownCloud exposed via Traefik labels; new acme volume for certificates.
  • dot.env — real hostnames instead of localhost; new COLLABORA_DOMAIN, OWNCLOUD_DOMAIN_REGEX, COLLABORA_IMAGE, Collabora admin creds, TRAEFIK_IMAGE, LETSENCRYPT_EMAIL; dropped obsolete HTTP_PORT.
  • installing_with_docker.adoc — TLS/two-host explanation, expanded .env settings table, updated docker compose ps output (collabora + proxy), and a new "Enabling Collabora Online (CODE)" section covering app:enable richdocuments and config:app:set richdocuments wopi_url, linking to the existing Collabora / occ pages.

Notes

  • The CODE integration is documented against the Enterprise richdocuments app (what the repo documents today); the repo's own note flags its OC11 availability as "under review". The collabora container itself is edition-independent.

Verification

  • docker compose config validates the file.
  • Full Antora build of the page succeeds; rendered HTML shows the new services, the new section/anchor, and the wopi_url command, with no unresolved AsciiDoc attribute tokens.

🤖 Generated with Claude Code

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>
@DeepDiver1975
DeepDiver1975 requested a review from a team as a code owner July 24, 2026 13:10
…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>
@DeepDiver1975

Copy link
Copy Markdown
Member Author

Follow-up QA commit (a2e8af8): security review, a local test harness, and CI.

Security hardening of the compose example

  • Removed the published host ports on MariaDB (3306) and Redis (6379) — they now live on an internal backend network only. Reframed as an internet-facing setup, an exposed weakly-authenticated DB and an unauthenticated Redis were directly exploitable.
  • Split the topology into frontend (proxy, ownCloud, Collabora) and backend (ownCloud, MariaDB, Redis) so the edge proxy and Collabora cannot reach the data tier.
  • Restricted the Collabora CODE admin console (/browser/dist/admincontrol, /cool/adminws) behind a higher-priority Traefik router + ipallowlist middleware instead of exposing it publicly with admin/admin.
  • Added an HSTS middleware on the ownCloud router only (a framing policy would break the embedded editor iframe).
  • Moved all credentials to .env with CHANGE_ME_* placeholders; defined the previously-undefined ADMIN_*/DB vars; distinct root vs. app DB passwords.
  • Documented the residual caveats next to the compose block (change credentials, keep the data tier private, never expose the admin console, the Docker-socket-as-root trade-off, pin image versions).

Local test harness (tests/docker-compose/, kept out of the Antora examples/ tree)

  • A self-signed-TLS override + runnable test.env boot the actual shipped compose file, then assert: all healthchecks green, status.phpinstalled:true, Collabora /hosting/discovery returns WOPI XML, and 3306/6379 are closed on the host.
  • Run with npm run test:compose. Verified locally: passes end to end and tears down cleanly.

CI + Dependabot

  • New paths-filtered compose-test.yml workflow lints (docker compose config) and runs the smoke test on PRs touching the example/harness. Action SHA-pinned.
  • Added the docker ecosystem to Dependabot.

Note: the compose image tags are .env-interpolated, so the Dependabot docker entry will not find literal tags to bump in the shipped file (the concrete pins live in test.env); the entry is kept to future-proof any literal tags. The test harness pins Traefik v3.6 (vs. the example's v3.3) because Traefik < 3.6 probes the Docker daemon with API v1.24, which Docker Engine >= 29 refuses — routing/TLS behaviour under test is identical.

DeepDiver1975 and others added 2 commits July 27, 2026 10:51
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>
@DeepDiver1975

Copy link
Copy Markdown
Member Author

CI fixes: two root causes debugged and resolved

The new docker-compose example workflow initially failed in CI while passing locally. Both failures were nondeterministic and CI-only; each root cause was confirmed with direct evidence before fixing. Final run: green

1. Traefik multi-network routing nondeterminism (855f76a)

ownCloud is attached to both the frontend and backend networks. With no hint, Traefik registered the container's backend IP (e.g. http://172.19.0.4:8080) as the load-balancer target — an address the proxy cannot reach — so all routing broke.

  • Evidence: the Traefik API (--api.insecure) showed the wrong registered service URL.
  • Fix: add traefik.docker.network=owncloud_frontend on the ownCloud service and give the networks explicit name:s so the label resolves regardless of the compose project name. This ships in the example itself (it's a real correctness fix, not test-only).

2. printf | grep -q SIGPIPE under set -o pipefail (29b4dd6)

The smoke test matched response bodies with printf '%s' "$body" | grep -q …. On a match, grep -q exits immediately and closes the pipe; printf is then killed by SIGPIPE (exit 141), and pipefail turns a successful match into a pipeline failure.

  • Evidence: CI log line line 124: printf: write error: Broken pipe, and the "not found" branch printed the full <wopi-discovery> document — i.e. the match had actually succeeded.
  • Fix: pure-bash matching ([[ "$x" == *needle* ]]) in all three spots, plus a polling loop for Collabora (CODE has no healthcheck and returns 404/502 while starting). The same change closed a latent bug where a real data-tier port exposure would have been silently hidden by the inverted compose config | grep security-guard pipeline.

Branch commits: a2e8af8 (hardening + QA harness) → 855f76a (network routing) → 29b4dd6 (SIGPIPE + CODE startup).

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

Copy link
Copy Markdown
Member Author

Code review

Overall 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 (eb631cc94) fixing the two substantive findings below.

Fixed

  1. Dependabot docker entry was a no-op → removed. The new .github/dependabot.yml docker ecosystem pointed at the example dir, but every image there uses ${VAR} interpolation with the tag living in dot.env:

    image: collabora/code:${COLLABORA_IMAGE}
    image: owncloud/server:${OWNCLOUD_IMAGE}
    image: traefik:${TRAEFIK_IMAGE}
    

    Dependabot's docker/compose parser reads image tags literally — it does not resolve ${VAR} interpolation and does not read dot.env, so the entry produces no PRs. It gave a false impression the tags were being watched, so I removed it. (The only concrete pins in the tree live in tests/docker-compose/test.env, which is outside the monitored directory anyway.)

  2. COLLABORA_IMAGE=latest contradicted the doc's own "pin versions" advice → pinned. The new IMPORTANT box tells readers to pin, but the shipped default floated on latest, meaning silent major CODE bumps. Pinned it to 24.04.13.2.1 (matching the CI test env) with a comment, and reworded the note accordingly.

Not changed — worth a glance

  • Admin allowlist path coverage. The collabora-admin router fences /browser/dist/admincontrol and /cool/adminws. Depending on the CODE release, admin static assets can live under other /browser/dist/admin* paths — worth double-checking the prefixes against the pinned version so the allowlist fences the whole console. The doc's "adjust the ranges to match your network" caveat softens this.
  • Traefik skew (example v3.3 vs test v3.6). Well-documented rationale (API negotiation vs Docker Engine ≥29); acceptable, just means CI runs a slightly different Traefik than the example ships.

Verified good

  • xref anchors (#enabling-collabora-online-code, #quick-evaluation, #upgrading-owncloud, #env-file) all resolve given idprefix: '' / idseparator: '-'.
  • traefik.docker.network=owncloud_frontend matches the explicit network name:.
  • Smoke-test SIGPIPE handling (pure-bash substring match instead of grep -q under set -o pipefail) is correct.
  • GH Action pinned to SHA (actions/checkout@3d3c42e… # v7.0.1).
  • Secrets externalized to .env; MariaDB healthcheck no longer hardcodes the password.
  • Data-tier host ports removed and enforced by the CI regression guard.

docker compose config still validates after the follow-up commit.

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

Copy link
Copy Markdown
Member Author

Correction to the Dependabot fix (commit f49764ac8)

My earlier follow-up (eb631cc94) fixed the no-op Dependabot entry by removing it — that was the wrong call, it just gives up on automated maintenance. This commit does it properly: make the compose file self-describing with literal image tags so Dependabot can actually parse and bump them, and restore the Dependabot docker entry.

What changed:

  • docker-compose.ymlmariadb, redis, collabora/code, traefik now use literal tags (mariadb:10.11, redis:7, collabora/code:24.04.13.2.1, traefik:v3.3) instead of ${VAR} interpolation. These are now maintained by Dependabot.
  • owncloud/server intentionally keeps ${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's bumped deliberately rather than by Dependabot (which can't parse the interpolation and silently skips it).
  • dot.env — dropped the now-unused MARIADB_IMAGE/REDIS_IMAGE/COLLABORA_IMAGE/TRAEFIK_IMAGE; kept OWNCLOUD_IMAGE. Corresponding settings-table rows removed and the "Pin image versions" note reworded.
  • .github/dependabot.yml — restored the docker ecosystem entry (now effective for the four literal tags).
  • Test harness — the smoke test needs Traefik v3.6 (v3.3 fails against Docker Engine ≥ 29), which previously came from ${TRAEFIK_IMAGE} in test.env. Since the base file now hardcodes v3.3, that pin moved to an image: traefik:v3.6 override in docker-compose.override.yml. test.env keeps only OWNCLOUD_IMAGE (still interpolated).

Verified:

  • docker compose config (merged test stack) resolves to traefik:v3.6, mariadb:10.11, redis:7, collabora/code:24.04.13.2.1, owncloud/server:10.15.0.
  • Shipped example standalone with dot.env resolves cleanly (owncloud/server:10.16.2, literal infra tags, no missing-variable warnings).
  • npm run test:compose — full smoke test passes (ownCloud status.php, Collabora WOPI discovery, data-tier port guard).
  • Antora build of the page: compose block renders literal tags, .env block still expands {latest-server-download-version}10.16.2, trimmed settings table has no dangling rows or unresolved {...} tokens.
  • .github/dependabot.yml is valid YAML with all three ecosystems (npm, github-actions, docker).

@dj4oC

dj4oC commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Code review

Found 14 issues:

  1. docker-compose.yml hardcodes container_name and explicit network names (not scoped to the Compose project), so this stack can't run alongside a second instance of itself or the new CI test harness on the same host.

# traefik.docker.network label on the ownCloud service resolves regardless of
# the project/directory name the stack is launched from.
frontend:
name: owncloud_frontend
backend:

  1. The docs now hard-require two publicly DNS-resolvable domains reachable on 80/443 for Let's Encrypt, with no documented fallback for internal-only/offline deployments that the old wording ("hostname(s) and/or IP addresses") explicitly supported.

=== Compose Setup
The following instructions require two DNS names — one for ownCloud (`OWNCLOUD_DOMAIN`) and one for Collabora (`COLLABORA_DOMAIN`) — that both resolve to the host running this stack. Traefik uses them to obtain Let's Encrypt certificates automatically, so the host must be reachable from the internet on ports `80` and `443`. The value of xref:#env-file[OWNCLOUD_DOMAIN] and xref:#env-file[OWNCLOUD_TRUSTED_DOMAINS] must represent the hostname(s) that the server is reachable at.
. Create a new project directory at a location of choice:

  1. The plain ${HTTP_PORT}:8080 host port mapping is removed in favor of Traefik-only routing labels, with no documented way to keep a published port for admins fronting the stack with their own reverse proxy.

<<: *oc-common
container_name: owncloud_server
# No ports are published directly. The Traefik reverse proxy (see the "proxy"
# service below) terminates TLS and forwards HTTPS traffic to port 8080.
labels:

  1. MariaDB's 3306:3306 host port is removed, breaking the existing documented mysqldump -h localhost backup recipe that assumes host-side DB access.

container_name: owncloud_mariadb
restart: always
# No host ports are published. MariaDB is reached only by ownCloud over the
# internal "backend" network. Publishing 3306 to the host would expose the
# database to the internet on the public host this stack is designed for.

  1. The new "ports must not be exposed" smoke-test check uses plain curl http://127.0.0.1:<port> against MariaDB/Redis, which are non-HTTP TCP services — curl never returns exit code 0 against them even when the port is genuinely open, so the check can't detect the exact regression (an accidentally re-published DB port) it exists to catch.

for port in 3306 6379; do
if curl -s --max-time 3 "http://127.0.0.1:${port}" >/dev/null 2>&1; then
fail "port ${port} answered on 127.0.0.1 — it must not be exposed"
exit 1

  1. The CI harness pins Traefik to v3.6 instead of the v3.3 actually shipped in the docs example, per its own comment noting v3.3 fails against modern Docker Engines — meaning CI never exercises the version real users deploy.

# first, so the harness boots on modern local daemons and on CI alike. The
# routing/TLS behaviour under test is identical across these 3.x releases.
image: traefik:v3.6
command:
- --providers.docker=true

  1. collabora-admin's router priority is hardcoded to 100, but the unpinned collabora router's auto-computed priority (based on rule-string length) can exceed it for a sufficiently long COLLABORA_DOMAIN, which would route admin-console/WebSocket requests through the router without the IP allowlist.

- traefik.http.routers.collabora-admin.entrypoints=websecure
- traefik.http.routers.collabora-admin.tls.certresolver=letsencrypt
- traefik.http.routers.collabora-admin.priority=100
- traefik.http.routers.collabora-admin.service=collabora
- traefik.http.routers.collabora-admin.middlewares=collabora-admin-allowlist

  1. smoke-test.sh only tears down via its EXIT trap — there's no compose down -v before up -d — so a prior run that died ungracefully (CI OOM/timeout) leaves a stale MariaDB volume with an old root password, causing the next run's healthcheck to fail for reasons unrelated to the compose example under test.

log "Booting the stack"
compose up -d
# Wait until every container with a healthcheck reports "healthy".

  1. The new dependabot docker-ecosystem entry only watches the shipped example's directory, so it never tracks the CI harness's own hardcoded Traefik v3.6 pin in docker-compose.override.yml.

# Dependabot cannot resolve, so it is intentionally left for manual bumps).
- package-ecosystem: docker
directory: "/modules/admin_manual/examples/installation/docker"
schedule:
interval: weekly

  1. The new .gitignore entries (tests/docker-compose/acme/, tests/docker-compose/.env) don't match anything the harness actually creates — acme is a Docker-managed named volume, and smoke-test.sh always passes an explicit --env-file rather than writing a bare .env.

https://github.com/owncloud/docs-server/blob/f49764ac88a5a9790541cf18facd9ed3b28035f5/.gitignore#L12-L16

  1. The "Validate the merged compose configuration" CI step duplicates a docker compose ... config check that smoke-test.sh already runs as its first action.

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Validate the merged compose configuration
run: |
docker compose \

  1. The override's command: list fully re-lists every Traefik flag from the shipped docker-compose.yml (Compose replaces, not merges, list values), so future flag changes in the production file won't automatically propagate to the CI-tested config.

# routing/TLS behaviour under test is identical across these 3.x releases.
image: traefik:v3.6
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false

  1. Three separate poll-with-deadline loops (container health, status.php, Collabora WOPI discovery) copy-paste identical deadline/sleep/timeout scaffolding instead of sharing one helper.

# Wait until every container with a healthcheck reports "healthy".
log "Waiting for containers to become healthy (timeout ${HEALTH_TIMEOUT}s)"
deadline=$(( $(date +%s) + HEALTH_TIMEOUT ))
while true; do
# IDs of containers that declare a healthcheck but are not yet healthy.

  1. The health-wait loop makes two separate docker inspect calls per container per iteration (status, then name) instead of one combined-format call.

while read -r cid; do
[ -n "${cid}" ] || continue
status=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "${cid}")
name=$(docker inspect -f '{{.Name}}' "${cid}")
if [ "${status}" != "none" ] && [ "${status}" != "healthy" ]; then

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

DeepDiver1975 and others added 4 commits July 27, 2026 19:04
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>
@DeepDiver1975

Copy link
Copy Markdown
Member Author

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 redis:7: exit 52 (empty reply) for an open port, exit 7 (refused) for a closed one — never 0. So if curl ...; then fail; fi passed unconditionally. Replaced with a raw /dev/tcp connect probe and negative-tested it: re-adding 3306:3306 now fails the guard, where the curl version stayed silent.

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 (client version 1.24 is too old. Minimum supported API version is 1.40), so Traefik starts with no routing config at all. Verified: v3.3 fails, v3.6 and v3.7 work. Pinning CI down to v3.3 would have meant testing a version users can't run — so the example moved to traefik:v3.7 and the CI pin is gone. CI now exercises exactly what ships.

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 tls=true and a blanked certresolver serve TRAEFIK DEFAULT CERT and Traefik never places an ACME order (verified HTTP 200 with the ACME flags present). So the override no longer touches command: or image: at all, and the list-replacement drift is gone at the source.

1 — fixed via a top-level name: owncloud instead of per-network name: keys. Worth recording why the hardcoding was there: given a short network name, Traefik logs Could not find network named "frontend" ... Maybe you're missing the project's prefix and then silently falls back to the container's first network — which can be the backend tier the proxy can't reach. Failure mode is broken routing with no hard error. One consequence I've documented rather than hidden: the harness reuses the example's project name (it must, for that label to resolve), so it can't run beside a real deployment on one host.

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 ports: snippet, and a backup-page note giving the container-side docker compose exec -T mariadb mysqldump (with -T, or the redirected dump gets corrupted by a pseudo-TTY).

7 — pinned both Collabora routers, not just raised the admin one, so the outcome no longer depends on COLLABORA_DOMAIN's length at all.

8, 11, 13, 14 — fixed as described: compose down -v before up -d; duplicate CI config step removed; one poll_until helper replacing three copy-pasted loops; single docker inspect per container.

9 — fixed with directories:. To be precise: the harness pins no image of its own anymore (it inherits the example's tags), so this tracks any future pin rather than something currently stale.

10 — confirmed and removed. acme is a Docker-managed named volume, and the script always passes an explicit --env-file, so neither pattern matched anything.

Verification: full smoke test passes end-to-end against the shipped Traefik version (all 6 assertions); the exposed-port guard fails when it should; shellcheck (via koalaman/shellcheck:stable) clean at default severity; Antora build clean — the only xref errors are pre-existing cross-repo refs in the unrelated migrating_to_ocis.adoc.

One deliberate non-change: shellcheck --enable=all flags SC2016 on the poll_until call sites. The single quotes are required there — poll_until evals its condition each iteration, and the timeout diagnostics must read last_body/unhealthy_containers at timeout. Double-quoting would freeze empty values into the failure output, so those are annotated with a scoped disable and a comment explaining why.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants