From 1155e9185a3577aedd7796c4edf00d7d864fdd8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:09:26 +0200 Subject: [PATCH 01/10] docs: run Collabora Online (CODE) in the docker-compose example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- .../installation/docker/docker-compose.yml | 68 +++++++++++++- .../examples/installation/docker/dot.env | 22 ++++- .../installation/installing_with_docker.adoc | 90 ++++++++++++++++--- 3 files changed, 163 insertions(+), 17 deletions(-) diff --git a/modules/admin_manual/examples/installation/docker/docker-compose.yml b/modules/admin_manual/examples/installation/docker/docker-compose.yml index a43762511..3c49dbbdf 100644 --- a/modules/admin_manual/examples/installation/docker/docker-compose.yml +++ b/modules/admin_manual/examples/installation/docker/docker-compose.yml @@ -34,8 +34,14 @@ services: # Start the main ownCloud instance <<: *oc-common container_name: owncloud_server - ports: - - ${HTTP_PORT}:8080 + # No ports are published directly. The Traefik reverse proxy (see the "proxy" + # service below) terminates TLS and forwards HTTPS traffic to port 8080. + labels: + - traefik.enable=true + - traefik.http.routers.owncloud.rule=Host(`${OWNCLOUD_DOMAIN}`) + - traefik.http.routers.owncloud.entrypoints=websecure + - traefik.http.routers.owncloud.tls.certresolver=letsencrypt + - traefik.http.services.owncloud.loadbalancer.server.port=8080 # owncloud-wnd-1: # # Enterprise only: uncomment and configure it accordingly if WND is used ! @@ -45,6 +51,61 @@ services: # container_name: owncloud-wnd-1 # command: ["/usr/bin/owncloud", "occ", "wnd:listen", "myhost", "myshare", "workgroup\myuser", "password"] + collabora: + # Collabora Online Development Edition (CODE): the self-hosted office server + # that ownCloud connects to as a WOPI client. See the "Enabling Collabora + # Online (CODE)" section of the documentation for the required occ wiring. + image: collabora/code:${COLLABORA_IMAGE} + container_name: owncloud_collabora + restart: always + environment: + # "domain" is a regex of the ownCloud host(s) CODE accepts as WOPI host. + # Dots must be escaped for the regex (e.g. owncloud\.example\.com). + - domain=${OWNCLOUD_DOMAIN_REGEX} + - aliasgroup1=https://${OWNCLOUD_DOMAIN}:443 + - username=${COLLABORA_ADMIN_USERNAME} + - password=${COLLABORA_ADMIN_PASSWORD} + # TLS is terminated at the reverse proxy, so CODE itself speaks plain HTTP. + - extra_params=--o:ssl.enable=false --o:ssl.termination=true + cap_add: + - MKNOD + labels: + - traefik.enable=true + - traefik.http.routers.collabora.rule=Host(`${COLLABORA_DOMAIN}`) + - traefik.http.routers.collabora.entrypoints=websecure + - traefik.http.routers.collabora.tls.certresolver=letsencrypt + - traefik.http.services.collabora.loadbalancer.server.port=9980 + networks: + - oc_network + + proxy: + # Traefik terminates TLS (Let's Encrypt) and routes the two hostnames to the + # ownCloud and Collabora containers. Routing/TLS is driven by the labels on + # each service above; WebSocket upgrades (needed by CODE) are handled + # transparently by Traefik. + image: traefik:${TRAEFIK_IMAGE} + container_name: owncloud_proxy + restart: always + command: + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + # Redirect all plain HTTP traffic to HTTPS. + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https + - --certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL} + - --certificatesresolvers.letsencrypt.acme.storage=/acme/acme.json + - --certificatesresolvers.letsencrypt.acme.tlschallenge=true + ports: + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - acme:/acme + networks: + - oc_network + mariadb: image: mariadb:${MARIADB_IMAGE} container_name: owncloud_mariadb @@ -110,6 +171,9 @@ volumes: # Use the above options when configuring bind mounts redis: driver: local + acme: + # Stores the Let's Encrypt certificates issued for the reverse proxy. + driver: local networks: oc_network: diff --git a/modules/admin_manual/examples/installation/docker/dot.env b/modules/admin_manual/examples/installation/docker/dot.env index 3e0ed5b46..58837e1d9 100644 --- a/modules/admin_manual/examples/installation/docker/dot.env +++ b/modules/admin_manual/examples/installation/docker/dot.env @@ -1,7 +1,21 @@ OWNCLOUD_IMAGE={latest-server-download-version} -OWNCLOUD_DOMAIN=localhost -OWNCLOUD_TRUSTED_DOMAINS=localhost -OWNCLOUD_OVERWRITE_CLI_URL=http://localhost -HTTP_PORT={std-port-http} +# The public hostnames must resolve (DNS) to the host running this stack, so the +# reverse proxy can obtain Let's Encrypt certificates for them. +OWNCLOUD_DOMAIN=owncloud.example.com +OWNCLOUD_TRUSTED_DOMAINS=owncloud.example.com +OWNCLOUD_OVERWRITE_CLI_URL=https://owncloud.example.com MARIADB_IMAGE=10.11 REDIS_IMAGE=7 + +# Collabora Online Development Edition (CODE) +COLLABORA_DOMAIN=collabora.example.com +# Regex of the ownCloud host CODE accepts as WOPI host. Escape every dot with a +# backslash so it is not treated as a regex wildcard. +OWNCLOUD_DOMAIN_REGEX=owncloud\.example\.com +COLLABORA_IMAGE=latest +COLLABORA_ADMIN_USERNAME=admin +COLLABORA_ADMIN_PASSWORD=admin + +# Reverse proxy (Traefik) and Let's Encrypt +TRAEFIK_IMAGE=v3.3 +LETSENCRYPT_EMAIL=admin@example.com diff --git a/modules/admin_manual/pages/installation/installing_with_docker.adoc b/modules/admin_manual/pages/installation/installing_with_docker.adoc index 4a4bcc118..0565d66bc 100644 --- a/modules/admin_manual/pages/installation/installing_with_docker.adoc +++ b/modules/admin_manual/pages/installation/installing_with_docker.adoc @@ -110,13 +110,19 @@ When running `docker ps` again, the entry for `oc-eval` should be gone. The example configuration: -* Exposes ports {std-port-http}, allowing for HTTP connections. +* Runs behind a https://traefik.io/traefik[Traefik] reverse proxy that terminates TLS and exposes ports `80` and `443`, redirecting HTTP to HTTPS. * Uses separate _MariaDB_ and _Redis_ containers. * Provides mounts plus a volume bind configuration example. +* Includes a https://www.collaboraonline.com/code/[Collabora Online Development Edition (CODE)] container so documents can be edited in the browser. See xref:#enabling-collabora-online-code[Enabling Collabora Online (CODE)] below for the ownCloud-side wiring. + +[NOTE] +==== +Because the Collabora editor is embedded into the ownCloud web UI in an iframe and communicates over a WebSocket, both ownCloud and Collabora *must* be served over HTTPS on their own hostnames. Browsers block the mixed HTTP/HTTPS content that a plain-HTTP setup would produce. For this reason the example is designed for a production-style deployment with a reverse proxy and real, publicly resolvable domains, rather than the `localhost` HTTP flow used in the xref:#quick-evaluation[Quick Evaluation] section. +==== === Compose Setup -The following instructions assume you install locally. For remote access, the value of xref:#env-file[OWNCLOUD_DOMAIN] and xref:#env-file[OWNCLOUD_TRUSTED_DOMAINS] must be updated to represent the hostname(s) and/or IP addresses that the server is reachable at. +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: + @@ -175,8 +181,8 @@ See the config description for more details. | `my_host,my_ip,my_domain` a| xref:{config-sample}#override-cli-url[OWNCLOUD_OVERWRITE_CLI_URL] -| See the config description for more details. -| `\http://localhost` +| See the config description for more details. With the reverse proxy this must be the public HTTPS URL. +| `\https://owncloud.example.com` | `ADMIN_USERNAME` | The webUI admin username @@ -186,10 +192,6 @@ a| xref:{config-sample}#override-cli-url[OWNCLOUD_OVERWRITE_CLI_URL] | The webUI admin user’s password | `admin` -| `HTTP_PORT` -| The HTTP port to bind to -| `{std-port-http}` - | `MARIADB_IMAGE` | The mariadb version | `10.11` @@ -197,6 +199,35 @@ a| xref:{config-sample}#override-cli-url[OWNCLOUD_OVERWRITE_CLI_URL] | `REDIS_IMAGE` | The redis version | `7` + +| `COLLABORA_DOMAIN` +| The public hostname Collabora Online (CODE) is served at +| `collabora.example.com` + +| `OWNCLOUD_DOMAIN_REGEX` +a| The ownCloud host that CODE accepts as WOPI host, as a regular expression. + +Escape every dot with a backslash. +| `owncloud\.example\.com` + +| `COLLABORA_IMAGE` +| The Collabora Online (CODE) version +| `latest` + +| `COLLABORA_ADMIN_USERNAME` +| The Collabora admin console username +| `admin` + +| `COLLABORA_ADMIN_PASSWORD` +| The Collabora admin console password +| `admin` + +| `TRAEFIK_IMAGE` +| The Traefik reverse proxy version +| `v3.3` + +| `LETSENCRYPT_EMAIL` +| The email address used for Let's Encrypt certificate registration +| `admin@example.com` |=== + [NOTE] @@ -250,10 +281,20 @@ Check that all the containers have successfully started, by running `docker comp | owncloud_server | /usr/bin/entrypoint /usr/b ... | Up (healthy) -| 0.0.0.0:{std-port-http}->{std-port-http}/tcp +| 8080/tcp + +| owncloud_collabora +| /start-collabora-online.sh ... +| Up +| 9980/tcp + +| owncloud_proxy +| /entrypoint.sh --provider ... +| Up +| 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp |=== -Here you can see, that the database, ownCloud and Redis containers are running, and that ownCloud is accessible via port {std-port-http} on the host machine. +Here you can see that the database, ownCloud, Redis and Collabora containers are running, and that the Traefik `proxy` container publishes ports `80` and `443`. ownCloud and Collabora are reachable over HTTPS at their respective domains through the proxy. [TIP] ==== @@ -275,7 +316,7 @@ IMPORTANT: Any apps that came with the image will reappear when the container is === Logging In -To log in to the ownCloud UI, open `pass:a[http://localhost:{std-port-http}]` (or the URL defined) in your browser of choice, where you see the standard ownCloud login screen as in the image below. +To log in to the ownCloud UI, open `pass:a[https://owncloud.example.com]` (the `OWNCLOUD_DOMAIN` you defined) in your browser of choice, where you see the standard ownCloud login screen as in the image below. image::docker/owncloud-ui-login.png[The ownCloud UI via Docker,width=300] @@ -306,6 +347,33 @@ docker compose exec owncloud occ IMPORTANT: Don't use the `php` command prefix, this leads to several errors and is not intended to run in docker environments. +=== Enabling Collabora Online (CODE) + +The `collabora` service in the compose file runs a self-hosted Collabora Online Development Edition (CODE) server. This is the office server that ownCloud connects to; once the containers are up, ownCloud still needs to be told where to reach it. + +ownCloud integrates with Collabora through the `richdocuments` app, acting as the WOPI host while CODE acts as the WOPI client. To wire them together: + +. Enable the `richdocuments` app: ++ +[source,docker,subs="attributes+"] +---- +{occ-command-example-prefix-docker} app:enable richdocuments +---- + +. Point the app at the public HTTPS URL of the CODE server (`COLLABORA_DOMAIN`): ++ +[source,docker,subs="attributes+"] +---- +{occ-command-example-prefix-docker} config:app:set richdocuments wopi_url --value='https://collabora.example.com' +---- + +After this, opening a supported document (for example a `.docx`) in the ownCloud web UI loads the Collabora editor in an iframe over HTTPS. + +[NOTE] +==== +The `richdocuments` app and its Secure View capabilities are an Enterprise feature and its OC11 availability is subject to change. The `collabora` (CODE) container itself is edition-independent, but the ownCloud-side integration depends on the app being available in your installation. For the full list of `richdocuments` settings and the Secure View feature, see xref:enterprise/collaboration/collabora_secure_view.adoc[Collabora Online / Secure View] and the xref:configuration/server/occ_command.adoc#collabora-online-secure-view[Collabora related occ command set]. +==== + === Configuration For more configuration options, please refer to the xref:installation/configuration_notes.adoc[Configuration Notes] documentation. From a2e8af8ae8e88d01c55cca9b75eb2255cda315c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Sat, 25 Jul 2026 08:41:28 +0200 Subject: [PATCH 02/10] fix(docker): harden ownCloud + Collabora compose example and add QA harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- .github/dependabot.yml | 14 ++ .github/workflows/compose-test.yml | 38 +++++ .gitignore | 4 + .../installation/docker/docker-compose.yml | 63 ++++++-- .../examples/installation/docker/dot.env | 12 +- .../installation/installing_with_docker.adoc | 25 ++- package.json | 3 +- tests/docker-compose/README.md | 47 ++++++ .../docker-compose.override.yml | 44 ++++++ tests/docker-compose/smoke-test.sh | 148 ++++++++++++++++++ tests/docker-compose/test.env | 43 +++++ 11 files changed, 419 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/compose-test.yml create mode 100644 tests/docker-compose/README.md create mode 100644 tests/docker-compose/docker-compose.override.yml create mode 100755 tests/docker-compose/smoke-test.sh create mode 100644 tests/docker-compose/test.env diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b1646f161..0f667fd88 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -27,3 +27,17 @@ updates: update-types: - minor - patch + + - package-ecosystem: docker + directory: "/modules/admin_manual/examples/installation/docker" + schedule: + interval: weekly + day: sunday + time: '22:00' + timezone: Etc/UTC + open-pull-requests-limit: 5 + groups: + minor-and-patch: + update-types: + - minor + - patch diff --git a/.github/workflows/compose-test.yml b/.github/workflows/compose-test.yml new file mode 100644 index 000000000..89e9edc3a --- /dev/null +++ b/.github/workflows/compose-test.yml @@ -0,0 +1,38 @@ +name: docker-compose example + +# Lints and smoke-tests the ownCloud + Collabora CODE docker-compose example +# that ships in the admin manual. Runs only when the example, the test harness, +# or this workflow change. +on: + push: + branches: + - master + paths: + - 'modules/admin_manual/examples/installation/docker/**' + - 'tests/docker-compose/**' + - '.github/workflows/compose-test.yml' + pull_request: + branches: + - master + paths: + - 'modules/admin_manual/examples/installation/docker/**' + - 'tests/docker-compose/**' + - '.github/workflows/compose-test.yml' + +jobs: + smoke-test: + name: Lint and smoke-test the compose example + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Validate the merged compose configuration + run: | + docker compose \ + --env-file tests/docker-compose/test.env \ + -f modules/admin_manual/examples/installation/docker/docker-compose.yml \ + -f tests/docker-compose/docker-compose.override.yml \ + config + + - name: Boot the stack and run smoke tests + run: tests/docker-compose/smoke-test.sh diff --git a/.gitignore b/.gitignore index 38cb90749..6bef151f4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ tmp/ .DS_Store .vscode/ .idea/ + +# docker-compose smoke-test artifacts +tests/docker-compose/acme/ +tests/docker-compose/.env diff --git a/modules/admin_manual/examples/installation/docker/docker-compose.yml b/modules/admin_manual/examples/installation/docker/docker-compose.yml index 3c49dbbdf..439ee2ef2 100644 --- a/modules/admin_manual/examples/installation/docker/docker-compose.yml +++ b/modules/admin_manual/examples/installation/docker/docker-compose.yml @@ -14,7 +14,7 @@ x-owncloud: &oc-common - OWNCLOUD_DB_TYPE=mysql - OWNCLOUD_DB_NAME=owncloud - OWNCLOUD_DB_USERNAME=owncloud - - OWNCLOUD_DB_PASSWORD=owncloud + - OWNCLOUD_DB_PASSWORD=${MYSQL_PASSWORD} - OWNCLOUD_DB_HOST=mariadb - OWNCLOUD_ADMIN_USERNAME=${ADMIN_USERNAME} - OWNCLOUD_ADMIN_PASSWORD=${ADMIN_PASSWORD} @@ -26,7 +26,10 @@ x-owncloud: &oc-common source: oc_files target: /mnt/data networks: - - oc_network + # ownCloud needs both tiers: the proxy reaches it on "frontend", and it + # reaches MariaDB/Redis on "backend". + - frontend + - backend services: @@ -42,6 +45,12 @@ services: - traefik.http.routers.owncloud.entrypoints=websecure - traefik.http.routers.owncloud.tls.certresolver=letsencrypt - traefik.http.services.owncloud.loadbalancer.server.port=8080 + # Enable HSTS on ownCloud only. Do NOT add frame-ancestors/X-Frame-Options + # here or on Collabora: ownCloud embeds the Collabora editor in an iframe and + # a restrictive framing policy would break document editing. + - traefik.http.routers.owncloud.middlewares=owncloud-hsts + - traefik.http.middlewares.owncloud-hsts.headers.stsSeconds=15552000 + - traefik.http.middlewares.owncloud-hsts.headers.stsIncludeSubdomains=true # owncloud-wnd-1: # # Enterprise only: uncomment and configure it accordingly if WND is used ! @@ -71,12 +80,23 @@ services: - MKNOD labels: - traefik.enable=true + # Public WOPI router: serves the document editor to ownCloud users. - traefik.http.routers.collabora.rule=Host(`${COLLABORA_DOMAIN}`) - traefik.http.routers.collabora.entrypoints=websecure - traefik.http.routers.collabora.tls.certresolver=letsencrypt - traefik.http.services.collabora.loadbalancer.server.port=9980 + # Admin console router (higher priority): the CODE admin console and its + # WebSocket must NOT be reachable from the internet. Restrict them to + # trusted source ranges. WOPI clients never need these paths. + - traefik.http.routers.collabora-admin.rule=Host(`${COLLABORA_DOMAIN}`) && (PathPrefix(`/browser/dist/admincontrol`) || PathPrefix(`/cool/adminws`)) + - 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 + - traefik.http.middlewares.collabora-admin-allowlist.ipallowlist.sourcerange=127.0.0.1/32,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 networks: - - oc_network + - frontend proxy: # Traefik terminates TLS (Let's Encrypt) and routes the two hostnames to the @@ -101,40 +121,47 @@ services: - "80:80" - "443:443" volumes: + # NOTE: mounting the Docker socket gives Traefik full control of the Docker + # daemon, which is equivalent to root on the host. The ":ro" flag only makes + # the socket file read-only; it does NOT restrict the Docker API. For a + # hardened setup, put a scoped docker-socket-proxy in front of it instead. - /var/run/docker.sock:/var/run/docker.sock:ro - acme:/acme networks: - - oc_network + # The proxy only needs the frontend tier; it must not reach MariaDB/Redis. + - frontend mariadb: image: mariadb:${MARIADB_IMAGE} container_name: owncloud_mariadb restart: always - ports: - - "3306:3306" + # 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. environment: - - MYSQL_ROOT_PASSWORD=owncloud + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} - MYSQL_USER=owncloud - - MYSQL_PASSWORD=owncloud + - MYSQL_PASSWORD=${MYSQL_PASSWORD} - MYSQL_DATABASE=owncloud - MARIADB_AUTO_UPGRADE=1 command: ["--max-allowed-packet=128M", "--innodb-log-file-size=64M"] healthcheck: - test: ["CMD", "mysqladmin", "ping", "-u", "root", "--password=owncloud"] + test: ["CMD", "mysqladmin", "ping", "-u", "root", "--password=${MYSQL_ROOT_PASSWORD}"] interval: 10s timeout: 5s retries: 5 volumes: - mysql:/var/lib/mysql networks: - - oc_network + - backend redis: image: redis:${REDIS_IMAGE} container_name: owncloud_redis restart: always - ports: - - "6379:6379" + # No host ports are published. Redis is reached only by ownCloud over the + # internal "backend" network. An unauthenticated Redis published to the host + # would be directly exploitable on a public server. command: ["--databases", "1"] healthcheck: test: ["CMD", "redis-cli", "ping"] @@ -144,7 +171,7 @@ services: volumes: - redis:/data networks: - - oc_network + - backend # As alternative to Redis, you can enable Memcached. # See the caching configuration documentation for more details. @@ -153,10 +180,8 @@ services: # image: memcached:latest # container_name: owncloud_memcached # restart: always -# ports: -# - "11211:11211" # networks: -# - oc_network +# - backend volumes: oc_files: @@ -176,4 +201,8 @@ volumes: driver: local networks: - oc_network: + # "frontend" carries public traffic (proxy <-> ownCloud/Collabora). + # "backend" is internal only (ownCloud <-> MariaDB/Redis) so the internet-facing + # proxy and Collabora cannot reach the data tier directly. + frontend: + backend: diff --git a/modules/admin_manual/examples/installation/docker/dot.env b/modules/admin_manual/examples/installation/docker/dot.env index 58837e1d9..fee46ef02 100644 --- a/modules/admin_manual/examples/installation/docker/dot.env +++ b/modules/admin_manual/examples/installation/docker/dot.env @@ -1,3 +1,5 @@ +# IMPORTANT: every credential below is a placeholder. Replace every CHANGE_ME_* +# value with a strong, unique secret before exposing this stack to the internet. OWNCLOUD_IMAGE={latest-server-download-version} # The public hostnames must resolve (DNS) to the host running this stack, so the # reverse proxy can obtain Let's Encrypt certificates for them. @@ -7,6 +9,14 @@ OWNCLOUD_OVERWRITE_CLI_URL=https://owncloud.example.com MARIADB_IMAGE=10.11 REDIS_IMAGE=7 +# Initial ownCloud web UI admin account (used for first setup only). +ADMIN_USERNAME=admin +ADMIN_PASSWORD=CHANGE_ME_ADMIN + +# Database credentials. Use distinct, strong values for the root and app users. +MYSQL_ROOT_PASSWORD=CHANGE_ME_DB_ROOT +MYSQL_PASSWORD=CHANGE_ME_DB_APP + # Collabora Online Development Edition (CODE) COLLABORA_DOMAIN=collabora.example.com # Regex of the ownCloud host CODE accepts as WOPI host. Escape every dot with a @@ -14,7 +24,7 @@ COLLABORA_DOMAIN=collabora.example.com OWNCLOUD_DOMAIN_REGEX=owncloud\.example\.com COLLABORA_IMAGE=latest COLLABORA_ADMIN_USERNAME=admin -COLLABORA_ADMIN_PASSWORD=admin +COLLABORA_ADMIN_PASSWORD=CHANGE_ME_COLLABORA # Reverse proxy (Traefik) and Let's Encrypt TRAEFIK_IMAGE=v3.3 diff --git a/modules/admin_manual/pages/installation/installing_with_docker.adoc b/modules/admin_manual/pages/installation/installing_with_docker.adoc index 0565d66bc..85bd77fdd 100644 --- a/modules/admin_manual/pages/installation/installing_with_docker.adoc +++ b/modules/admin_manual/pages/installation/installing_with_docker.adoc @@ -140,6 +140,17 @@ cd owncloud-server-docker ---- include::example$installation/docker/docker-compose.yml[] ---- ++ +[IMPORTANT] +==== +This example is a *starting point*, not a hardened deployment. Before you expose it to the internet, review the following: + +* *Change every credential.* All passwords in the `.env` file are placeholders. Replace every `CHANGE_ME_*` value (database, ownCloud admin, Collabora admin) with a strong, unique secret. Remove the `ADMIN_*` variables after the first logon as described below. +* *Keep the data tier private.* MariaDB and Redis are intentionally *not* published to the host and live on an internal `backend` network. Do not add `ports:` entries for them — an exposed, weakly-authenticated database or an unauthenticated Redis on a public host is trivially exploitable. +* *Do not expose the Collabora admin console.* The example restricts the CODE admin console (`/browser/dist/admincontrol`, `/cool/adminws`) to private source ranges via a Traefik `ipallowlist` middleware. Adjust the ranges to match your network; never open these paths to the internet. +* *Docker socket exposure.* The Traefik proxy mounts the Docker socket so it can read container labels. This grants control of the Docker daemon — effectively root on the host — and the `:ro` flag does *not* restrict the Docker API. For a hardened setup, place a scoped https://github.com/Tecnativa/docker-socket-proxy[docker-socket-proxy] in front of it. +* *Pin image versions.* `OWNCLOUD_IMAGE` and `COLLABORA_IMAGE` default to a floating tag. Pin them to specific versions for reproducible, auditable upgrades (see xref:#upgrading-owncloud[Upgrading ownCloud]). +==== . Create an `.env` configuration file in the same directory, which contains the required configuration settings. + @@ -190,7 +201,15 @@ a| xref:{config-sample}#override-cli-url[OWNCLOUD_OVERWRITE_CLI_URL] | `ADMIN_PASSWORD` | The webUI admin user’s password -| `admin` +| `CHANGE_ME_ADMIN` + +| `MYSQL_ROOT_PASSWORD` +| The MariaDB root password +| `CHANGE_ME_DB_ROOT` + +| `MYSQL_PASSWORD` +| The MariaDB `owncloud` user password (also used by ownCloud to connect) +| `CHANGE_ME_DB_APP` | `MARIADB_IMAGE` | The mariadb version @@ -219,7 +238,7 @@ Escape every dot with a backslash. | `COLLABORA_ADMIN_PASSWORD` | The Collabora admin console password -| `admin` +| `CHANGE_ME_COLLABORA` | `TRAEFIK_IMAGE` | The Traefik reverse proxy version @@ -294,7 +313,7 @@ Check that all the containers have successfully started, by running `docker comp | 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp |=== -Here you can see that the database, ownCloud, Redis and Collabora containers are running, and that the Traefik `proxy` container publishes ports `80` and `443`. ownCloud and Collabora are reachable over HTTPS at their respective domains through the proxy. +Here you can see that the database, ownCloud, Redis and Collabora containers are running, and that only the Traefik `proxy` container publishes ports (`80` and `443`) to the host. MariaDB and Redis expose their ports on the internal network only, never to the host. ownCloud and Collabora are reachable over HTTPS at their respective domains through the proxy. [TIP] ==== diff --git a/package.json b/package.json index 80dc21c6a..1fd477653 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "antora-bundle": "antora --stacktrace --cache-dir cache --redirect-facility static --clean --fetch --ui-bundle-url ../docs-ui/build/ui-bundle.zip --url https://doc.staging.owncloud.com site.yml", "antora-dev-local": "antora --stacktrace --cache-dir cache --redirect-facility static --clean --fetch --url http://localhost:8080 site-dev.yml", "antora-dev-bundle": "antora --stacktrace --cache-dir cache --redirect-facility static --clean --fetch --ui-bundle-url ../docs-ui/build/ui-bundle.zip --url http://localhost:8080 site-dev.yml", - "serve": "http-server public/ -d -i" + "serve": "http-server public/ -d -i", + "test:compose": "tests/docker-compose/smoke-test.sh" }, "dependencies": { "antora": "^3.1.15", diff --git a/tests/docker-compose/README.md b/tests/docker-compose/README.md new file mode 100644 index 000000000..05d3f00b5 --- /dev/null +++ b/tests/docker-compose/README.md @@ -0,0 +1,47 @@ +# docker-compose smoke test + +QA harness for the ownCloud + Collabora CODE docker-compose example that ships in +the admin manual +(`modules/admin_manual/examples/installation/docker/docker-compose.yml`). + +The example is written for a **production, internet-facing** deployment: it pulls +an image tag that Antora fills in at build time and it obtains real Let's Encrypt +certificates, which needs public DNS. Neither works on a throwaway machine, so +this harness layers two small files on top of the shipped example to make it +bootable **locally and in CI without changing the example itself**: + +| File | Purpose | +| --- | --- | +| `docker-compose.override.yml` | Drops the Let's Encrypt / ACME command flags so Traefik serves its built-in self-signed certificate. Everything else — split networks, no DB/Redis host ports, Collabora admin allowlist, HSTS — is inherited unchanged. | +| `test.env` | Concrete image pins, `*.localhost` hostnames, and throwaway credentials. | +| `smoke-test.sh` | Boots the merged stack, waits for health, and asserts the endpoints and the security invariants. | + +## Running + +``` +npm run test:compose +``` + +or directly: + +``` +tests/docker-compose/smoke-test.sh +``` + +Requires Docker with the Compose plugin, plus `curl` and `bash`. The script +always tears the stack down (`docker compose down -v`) on exit, including on +failure, and dumps container status + logs when something fails. + +## What it asserts + +1. **ownCloud is up** — `https://owncloud.localhost/status.php` returns JSON with + `"installed":true`. +2. **Collabora is up** — `https://collabora.localhost/hosting/discovery` returns a + WOPI `` document. +3. **Data tier is private (security regression guard)** — the merged + `docker compose config` publishes no `3306`/`6379` host binding, and neither + port answers on `127.0.0.1`. This guards the hardening that removed the + MariaDB/Redis host ports from the example. + +TLS is served with Traefik's self-signed certificate, so all requests use +`curl -k --resolve :443:127.0.0.1`. diff --git a/tests/docker-compose/docker-compose.override.yml b/tests/docker-compose/docker-compose.override.yml new file mode 100644 index 000000000..0d1b57020 --- /dev/null +++ b/tests/docker-compose/docker-compose.override.yml @@ -0,0 +1,44 @@ +# LOCAL TEST OVERRIDE — do NOT use in production. +# +# This file layers on top of the shipped example +# (modules/admin_manual/examples/installation/docker/docker-compose.yml) so the +# stack can be booted locally / in CI without public DNS or Let's Encrypt. +# +# It changes exactly one thing versus the shipped file: TLS. The production +# example obtains real certificates from Let's Encrypt via the ACME TLS-ALPN-01 +# challenge, which requires the public hostnames to resolve to this host — not +# possible on a throwaway CI runner. Here we: +# 1. drop the ACME command flags from Traefik, and +# 2. blank each router's `tls.certresolver` label (set by the base file) and +# pin `tls=true`, so the routers stay TLS-enabled but serve Traefik's +# built-in self-signed certificate. (A router that still references the now +# removed `letsencrypt` resolver is rejected by Traefik and never attaches +# to the websecure entrypoint, which breaks routing entirely.) +# The HTTPS path is still exercised end to end; smoke tests connect with +# `curl -k`. +# +# The security-hardened topology (split networks, no DB/Redis host ports, +# Collabora admin allowlist, HSTS) is inherited unchanged from the base file. +# Compose merges these list-form labels by key, so the entries below override +# only the matching keys from the base file and leave the rest intact. +services: + owncloud: + labels: + - traefik.http.routers.owncloud.tls=true + - traefik.http.routers.owncloud.tls.certresolver= + + collabora: + labels: + - traefik.http.routers.collabora.tls=true + - traefik.http.routers.collabora.tls.certresolver= + - traefik.http.routers.collabora-admin.tls=true + - traefik.http.routers.collabora-admin.tls.certresolver= + + proxy: + command: + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https diff --git a/tests/docker-compose/smoke-test.sh b/tests/docker-compose/smoke-test.sh new file mode 100755 index 000000000..bf310bbf8 --- /dev/null +++ b/tests/docker-compose/smoke-test.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# +# Smoke test for the shipped ownCloud + Collabora CODE docker-compose example. +# +# It boots the REAL example file +# (modules/admin_manual/examples/installation/docker/docker-compose.yml) with a +# local override + test env (self-signed TLS, pinned images, *.localhost hosts), +# waits for the containers to become healthy, and asserts that: +# 1. ownCloud answers on https://owncloud.localhost/status.php with installed=true +# 2. Collabora answers on https://collabora.localhost/hosting/discovery (WOPI XML) +# 3. MariaDB (3306) and Redis (6379) are NOT reachable on the host (security +# regression guard — the hardened compose must not publish them) +# +# The stack is always torn down (docker compose down -v) on exit. +# +# Usage: tests/docker-compose/smoke-test.sh +# Requires: docker (with the compose plugin) and curl. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +EXAMPLE_DIR="${REPO_ROOT}/modules/admin_manual/examples/installation/docker" + +BASE_COMPOSE="${EXAMPLE_DIR}/docker-compose.yml" +OVERRIDE_COMPOSE="${SCRIPT_DIR}/docker-compose.override.yml" +ENV_FILE="${SCRIPT_DIR}/test.env" + +PROJECT_NAME="oc-compose-smoke" + +# How long to wait for every container to report healthy. +HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-300}" +# How long to wait for the ownCloud first-run install to finish (status.php). +INSTALL_TIMEOUT="${INSTALL_TIMEOUT:-300}" + +OWNCLOUD_HOST="owncloud.localhost" +COLLABORA_HOST="collabora.localhost" + +compose() { + docker compose \ + --project-name "${PROJECT_NAME}" \ + --env-file "${ENV_FILE}" \ + -f "${BASE_COMPOSE}" \ + -f "${OVERRIDE_COMPOSE}" \ + "$@" +} + +log() { printf '\n\033[1;34m==>\033[0m %s\n' "$*"; } +pass() { printf '\033[1;32m ✓\033[0m %s\n' "$*"; } +fail() { printf '\033[1;31m ✗\033[0m %s\n' "$*" >&2; } + +cleanup() { + local rc=$? + if [ "${rc}" -ne 0 ]; then + log "FAILURE (exit ${rc}) — dumping container status and logs" + compose ps || true + compose logs --no-color --tail 100 || true + fi + log "Tearing down the stack" + compose down --volumes --remove-orphans || true + exit "${rc}" +} +trap cleanup EXIT + +# --------------------------------------------------------------------------- + +log "Validating the merged compose configuration" +compose config >/dev/null +pass "compose config is valid" + +log "Booting the stack" +compose up -d + +# 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. + unhealthy="" + 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 + unhealthy="${unhealthy} ${name#/}(${status})" + fi + done < <(compose ps -q) + + if [ -z "${unhealthy}" ]; then + pass "all containers with a healthcheck are healthy" + break + fi + if [ "$(date +%s)" -ge "${deadline}" ]; then + fail "timed out waiting for containers:${unhealthy}" + exit 1 + fi + sleep 5 +done + +# ownCloud status.php — poll until first-run install completes. +log "Checking ownCloud https://${OWNCLOUD_HOST}/status.php" +deadline=$(( $(date +%s) + INSTALL_TIMEOUT )) +while true; do + body=$(curl -sk --max-time 10 \ + --resolve "${OWNCLOUD_HOST}:443:127.0.0.1" \ + "https://${OWNCLOUD_HOST}/status.php" || true) + if printf '%s' "${body}" | grep -q '"installed":true'; then + pass "status.php reports installed=true" + printf ' %s\n' "${body}" + break + fi + if [ "$(date +%s)" -ge "${deadline}" ]; then + fail "status.php did not report installed=true in time; last body: ${body}" + exit 1 + fi + sleep 5 +done + +# Collabora WOPI discovery. +log "Checking Collabora https://${COLLABORA_HOST}/hosting/discovery" +disco=$(curl -sk --max-time 15 \ + --resolve "${COLLABORA_HOST}:443:127.0.0.1" \ + "https://${COLLABORA_HOST}/hosting/discovery" || true) +if printf '%s' "${disco}" | grep -q ''; then + pass "Collabora returned a WOPI discovery document" +else + fail "Collabora /hosting/discovery did not return a wopi-discovery document" + printf ' %s\n' "${disco}" + exit 1 +fi + +# Security regression guard: the data tier must NOT be published to the host. +log "Asserting MariaDB/Redis are not published to the host" +if compose config | grep -Eq 'published:\s*"?(3306|6379)"?'; then + fail "compose config publishes a data-tier port (3306/6379) to the host" + exit 1 +fi +pass "no data-tier host port bindings in the merged config" + +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 + fi +done +pass "ports 3306 and 6379 are closed on the host" + +log "All smoke-test assertions passed" diff --git a/tests/docker-compose/test.env b/tests/docker-compose/test.env new file mode 100644 index 000000000..d0155a812 --- /dev/null +++ b/tests/docker-compose/test.env @@ -0,0 +1,43 @@ +# LOCAL TEST ENV — do NOT use in production. Throwaway credentials only. +# +# Concrete values that make the shipped example bootable locally / in CI: +# - real image tags (the docs ship an Antora build-time placeholder for +# OWNCLOUD_IMAGE, which is not a valid tag on its own); +# - *.localhost hostnames (resolve to 127.0.0.1, no DNS needed); +# - test credentials (never reused anywhere real). + +# Pinned so a run is reproducible and CI is not surprised by a new release. +OWNCLOUD_IMAGE=10.15.0 +MARIADB_IMAGE=10.11 +REDIS_IMAGE=7 +COLLABORA_IMAGE=24.04.13.2.1 +# Pinned to v3.6 for the test harness (not v3.3 like the shipped example): the +# Docker provider in Traefik < 3.6 probes the daemon with API version 1.24 +# before negotiating, which Docker Engine >= 29 refuses. v3.6 negotiates 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. +TRAEFIK_IMAGE=v3.6 + +# *.localhost always resolves to 127.0.0.1; the smoke test uses curl --resolve. +OWNCLOUD_DOMAIN=owncloud.localhost +OWNCLOUD_TRUSTED_DOMAINS=owncloud.localhost +OWNCLOUD_OVERWRITE_CLI_URL=https://owncloud.localhost + +# Initial ownCloud admin account. +ADMIN_USERNAME=admin +ADMIN_PASSWORD=test-admin-pw + +# Database credentials (throwaway). +MYSQL_ROOT_PASSWORD=test-db-root-pw +MYSQL_PASSWORD=test-db-app-pw + +# Collabora CODE. +COLLABORA_DOMAIN=collabora.localhost +# Regex of the ownCloud host CODE accepts as WOPI host (dots escaped). +OWNCLOUD_DOMAIN_REGEX=owncloud\.localhost +COLLABORA_ADMIN_USERNAME=admin +COLLABORA_ADMIN_PASSWORD=test-collabora-pw + +# Unused locally (ACME is disabled in the override) but referenced by the base +# compose command list, so it must be defined to avoid an interpolation warning. +LETSENCRYPT_EMAIL=test@owncloud.localhost From 855f76af5633f6d24a958730fedf8d73993f47d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:51:21 +0200 Subject: [PATCH 03/10] fix(docker): pin Traefik to the frontend network for ownCloud routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- .../examples/installation/docker/docker-compose.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/admin_manual/examples/installation/docker/docker-compose.yml b/modules/admin_manual/examples/installation/docker/docker-compose.yml index 439ee2ef2..93c7d1332 100644 --- a/modules/admin_manual/examples/installation/docker/docker-compose.yml +++ b/modules/admin_manual/examples/installation/docker/docker-compose.yml @@ -45,6 +45,10 @@ services: - traefik.http.routers.owncloud.entrypoints=websecure - traefik.http.routers.owncloud.tls.certresolver=letsencrypt - traefik.http.services.owncloud.loadbalancer.server.port=8080 + # ownCloud is attached to two networks (frontend + backend). Tell Traefik + # which one to use to reach it, otherwise it picks one at random and may + # choose "backend", which the proxy cannot reach — breaking all routing. + - traefik.docker.network=owncloud_frontend # Enable HSTS on ownCloud only. Do NOT add frame-ancestors/X-Frame-Options # here or on Collabora: ownCloud embeds the Collabora editor in an iframe and # a restrictive framing policy would break document editing. @@ -204,5 +208,10 @@ networks: # "frontend" carries public traffic (proxy <-> ownCloud/Collabora). # "backend" is internal only (ownCloud <-> MariaDB/Redis) so the internet-facing # proxy and Collabora cannot reach the data tier directly. + # Explicit names (instead of the compose-generated "_frontend") so the + # 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: + name: owncloud_backend From 29b4dd6db9429e2fce6da9d39ed0dc08cc68290d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:08:13 +0200 Subject: [PATCH 04/10] fix(docker): make compose smoke test robust to SIGPIPE and CODE startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- tests/docker-compose/smoke-test.sh | 44 +++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/tests/docker-compose/smoke-test.sh b/tests/docker-compose/smoke-test.sh index bf310bbf8..6d8c5c6d9 100755 --- a/tests/docker-compose/smoke-test.sh +++ b/tests/docker-compose/smoke-test.sh @@ -104,7 +104,10 @@ while true; do body=$(curl -sk --max-time 10 \ --resolve "${OWNCLOUD_HOST}:443:127.0.0.1" \ "https://${OWNCLOUD_HOST}/status.php" || true) - if printf '%s' "${body}" | grep -q '"installed":true'; then + # Pure-bash substring match: do NOT pipe into `grep -q`. Under `set -o + # pipefail`, grep -q closes the pipe on the first match, printf then dies with + # SIGPIPE, and the pipeline reports failure even though the match succeeded. + if [[ "${body}" == *'"installed":true'* ]]; then pass "status.php reports installed=true" printf ' %s\n' "${body}" break @@ -116,22 +119,37 @@ while true; do sleep 5 done -# Collabora WOPI discovery. +# Collabora WOPI discovery. CODE has no healthcheck (so the health-wait above +# does not cover it) and, while it is still starting, the proxy returns 404/502 +# for this path. Poll until the discovery document is served or we time out. log "Checking Collabora https://${COLLABORA_HOST}/hosting/discovery" -disco=$(curl -sk --max-time 15 \ - --resolve "${COLLABORA_HOST}:443:127.0.0.1" \ - "https://${COLLABORA_HOST}/hosting/discovery" || true) -if printf '%s' "${disco}" | grep -q ''; then - pass "Collabora returned a WOPI discovery document" -else - fail "Collabora /hosting/discovery did not return a wopi-discovery document" - printf ' %s\n' "${disco}" - exit 1 -fi +deadline=$(( $(date +%s) + INSTALL_TIMEOUT )) +while true; do + disco=$(curl -sk --max-time 15 \ + --resolve "${COLLABORA_HOST}:443:127.0.0.1" \ + "https://${COLLABORA_HOST}/hosting/discovery" || true) + # Pure-bash substring match — see the status.php note above for why this must + # not be `printf ... | grep -q` under `set -o pipefail`. + if [[ "${disco}" == *''* ]]; then + pass "Collabora returned a WOPI discovery document" + break + fi + if [ "$(date +%s)" -ge "${deadline}" ]; then + fail "Collabora /hosting/discovery did not return a wopi-discovery document in time" + printf ' %s\n' "${disco}" + exit 1 + fi + sleep 5 +done # Security regression guard: the data tier must NOT be published to the host. +# Capture the config first, then match — piping `compose config` straight into +# `grep -Eq` is unsafe under `set -o pipefail`: a match makes grep close the +# pipe, compose dies with SIGPIPE, and the non-zero pipeline status inverts the +# result, silently hiding a real port exposure. log "Asserting MariaDB/Redis are not published to the host" -if compose config | grep -Eq 'published:\s*"?(3306|6379)"?'; then +merged_config=$(compose config) +if [[ "${merged_config}" =~ published:[[:space:]]*\"?(3306|6379)\"? ]]; then fail "compose config publishes a data-tier port (3306/6379) to the host" exit 1 fi From eb631cc945950d64e7c46c425c01fa3b07a77c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:37:10 +0200 Subject: [PATCH 05/10] docs(docker): pin Collabora image and drop no-op Dependabot docker entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- .github/dependabot.yml | 14 -------------- .../examples/installation/docker/dot.env | 4 +++- .../pages/installation/installing_with_docker.adoc | 2 +- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0f667fd88..b1646f161 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -27,17 +27,3 @@ updates: update-types: - minor - patch - - - package-ecosystem: docker - directory: "/modules/admin_manual/examples/installation/docker" - schedule: - interval: weekly - day: sunday - time: '22:00' - timezone: Etc/UTC - open-pull-requests-limit: 5 - groups: - minor-and-patch: - update-types: - - minor - - patch diff --git a/modules/admin_manual/examples/installation/docker/dot.env b/modules/admin_manual/examples/installation/docker/dot.env index fee46ef02..d68b3d528 100644 --- a/modules/admin_manual/examples/installation/docker/dot.env +++ b/modules/admin_manual/examples/installation/docker/dot.env @@ -22,7 +22,9 @@ COLLABORA_DOMAIN=collabora.example.com # Regex of the ownCloud host CODE accepts as WOPI host. Escape every dot with a # backslash so it is not treated as a regex wildcard. OWNCLOUD_DOMAIN_REGEX=owncloud\.example\.com -COLLABORA_IMAGE=latest +# Pin CODE to a specific version for reproducible, auditable upgrades rather than +# tracking the floating "latest" tag. Bump this deliberately when upgrading. +COLLABORA_IMAGE=24.04.13.2.1 COLLABORA_ADMIN_USERNAME=admin COLLABORA_ADMIN_PASSWORD=CHANGE_ME_COLLABORA diff --git a/modules/admin_manual/pages/installation/installing_with_docker.adoc b/modules/admin_manual/pages/installation/installing_with_docker.adoc index 85bd77fdd..9ea94673a 100644 --- a/modules/admin_manual/pages/installation/installing_with_docker.adoc +++ b/modules/admin_manual/pages/installation/installing_with_docker.adoc @@ -149,7 +149,7 @@ This example is a *starting point*, not a hardened deployment. Before you expose * *Keep the data tier private.* MariaDB and Redis are intentionally *not* published to the host and live on an internal `backend` network. Do not add `ports:` entries for them — an exposed, weakly-authenticated database or an unauthenticated Redis on a public host is trivially exploitable. * *Do not expose the Collabora admin console.* The example restricts the CODE admin console (`/browser/dist/admincontrol`, `/cool/adminws`) to private source ranges via a Traefik `ipallowlist` middleware. Adjust the ranges to match your network; never open these paths to the internet. * *Docker socket exposure.* The Traefik proxy mounts the Docker socket so it can read container labels. This grants control of the Docker daemon — effectively root on the host — and the `:ro` flag does *not* restrict the Docker API. For a hardened setup, place a scoped https://github.com/Tecnativa/docker-socket-proxy[docker-socket-proxy] in front of it. -* *Pin image versions.* `OWNCLOUD_IMAGE` and `COLLABORA_IMAGE` default to a floating tag. Pin them to specific versions for reproducible, auditable upgrades (see xref:#upgrading-owncloud[Upgrading ownCloud]). +* *Pin image versions.* `COLLABORA_IMAGE` ships pinned to a specific version; keep it (and `OWNCLOUD_IMAGE`) pinned rather than tracking a floating `latest` tag, so upgrades are deliberate, reproducible, and auditable (see xref:#upgrading-owncloud[Upgrading ownCloud]). ==== . Create an `.env` configuration file in the same directory, which contains the required configuration settings. From f49764ac88a5a9790541cf18facd9ed3b28035f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Mon, 27 Jul 2026 13:20:34 +0200 Subject: [PATCH 06/10] docs(docker): use literal image tags so Dependabot maintains the example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- .github/dependabot.yml | 17 +++++++++++++++++ .../installation/docker/docker-compose.yml | 14 ++++++++++---- .../examples/installation/docker/dot.env | 6 ------ .../installation/installing_with_docker.adoc | 18 +----------------- .../docker-compose/docker-compose.override.yml | 6 ++++++ tests/docker-compose/test.env | 14 +++++--------- 6 files changed, 39 insertions(+), 36 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b1646f161..ef061a1fc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -27,3 +27,20 @@ updates: update-types: - minor - patch + + # Docker images in the admin-manual compose example. Only literal image tags + # are updated here (owncloud/server uses ${OWNCLOUD_IMAGE} interpolation, which + # Dependabot cannot resolve, so it is intentionally left for manual bumps). + - package-ecosystem: docker + directory: "/modules/admin_manual/examples/installation/docker" + schedule: + interval: weekly + day: sunday + time: '22:00' + timezone: Etc/UTC + open-pull-requests-limit: 5 + groups: + minor-and-patch: + update-types: + - minor + - patch diff --git a/modules/admin_manual/examples/installation/docker/docker-compose.yml b/modules/admin_manual/examples/installation/docker/docker-compose.yml index 93c7d1332..f15fa2545 100644 --- a/modules/admin_manual/examples/installation/docker/docker-compose.yml +++ b/modules/admin_manual/examples/installation/docker/docker-compose.yml @@ -68,7 +68,10 @@ services: # Collabora Online Development Edition (CODE): the self-hosted office server # that ownCloud connects to as a WOPI client. See the "Enabling Collabora # Online (CODE)" section of the documentation for the required occ wiring. - image: collabora/code:${COLLABORA_IMAGE} + # Image tag pinned here (not in .env) so Dependabot keeps it current — see + # .github/dependabot.yml. Do not re-parameterize it: Dependabot cannot read + # ${VAR} interpolation. + image: collabora/code:24.04.13.2.1 container_name: owncloud_collabora restart: always environment: @@ -107,7 +110,8 @@ services: # ownCloud and Collabora containers. Routing/TLS is driven by the labels on # each service above; WebSocket upgrades (needed by CODE) are handled # transparently by Traefik. - image: traefik:${TRAEFIK_IMAGE} + # Pinned tag maintained by Dependabot (see .github/dependabot.yml). + image: traefik:v3.3 container_name: owncloud_proxy restart: always command: @@ -136,7 +140,8 @@ services: - frontend mariadb: - image: mariadb:${MARIADB_IMAGE} + # Pinned tag maintained by Dependabot (see .github/dependabot.yml). + image: mariadb:10.11 container_name: owncloud_mariadb restart: always # No host ports are published. MariaDB is reached only by ownCloud over the @@ -160,7 +165,8 @@ services: - backend redis: - image: redis:${REDIS_IMAGE} + # Pinned tag maintained by Dependabot (see .github/dependabot.yml). + image: redis:7 container_name: owncloud_redis restart: always # No host ports are published. Redis is reached only by ownCloud over the diff --git a/modules/admin_manual/examples/installation/docker/dot.env b/modules/admin_manual/examples/installation/docker/dot.env index d68b3d528..da41e9650 100644 --- a/modules/admin_manual/examples/installation/docker/dot.env +++ b/modules/admin_manual/examples/installation/docker/dot.env @@ -6,8 +6,6 @@ OWNCLOUD_IMAGE={latest-server-download-version} OWNCLOUD_DOMAIN=owncloud.example.com OWNCLOUD_TRUSTED_DOMAINS=owncloud.example.com OWNCLOUD_OVERWRITE_CLI_URL=https://owncloud.example.com -MARIADB_IMAGE=10.11 -REDIS_IMAGE=7 # Initial ownCloud web UI admin account (used for first setup only). ADMIN_USERNAME=admin @@ -22,12 +20,8 @@ COLLABORA_DOMAIN=collabora.example.com # Regex of the ownCloud host CODE accepts as WOPI host. Escape every dot with a # backslash so it is not treated as a regex wildcard. OWNCLOUD_DOMAIN_REGEX=owncloud\.example\.com -# Pin CODE to a specific version for reproducible, auditable upgrades rather than -# tracking the floating "latest" tag. Bump this deliberately when upgrading. -COLLABORA_IMAGE=24.04.13.2.1 COLLABORA_ADMIN_USERNAME=admin COLLABORA_ADMIN_PASSWORD=CHANGE_ME_COLLABORA # Reverse proxy (Traefik) and Let's Encrypt -TRAEFIK_IMAGE=v3.3 LETSENCRYPT_EMAIL=admin@example.com diff --git a/modules/admin_manual/pages/installation/installing_with_docker.adoc b/modules/admin_manual/pages/installation/installing_with_docker.adoc index 9ea94673a..74abe6f3f 100644 --- a/modules/admin_manual/pages/installation/installing_with_docker.adoc +++ b/modules/admin_manual/pages/installation/installing_with_docker.adoc @@ -149,7 +149,7 @@ This example is a *starting point*, not a hardened deployment. Before you expose * *Keep the data tier private.* MariaDB and Redis are intentionally *not* published to the host and live on an internal `backend` network. Do not add `ports:` entries for them — an exposed, weakly-authenticated database or an unauthenticated Redis on a public host is trivially exploitable. * *Do not expose the Collabora admin console.* The example restricts the CODE admin console (`/browser/dist/admincontrol`, `/cool/adminws`) to private source ranges via a Traefik `ipallowlist` middleware. Adjust the ranges to match your network; never open these paths to the internet. * *Docker socket exposure.* The Traefik proxy mounts the Docker socket so it can read container labels. This grants control of the Docker daemon — effectively root on the host — and the `:ro` flag does *not* restrict the Docker API. For a hardened setup, place a scoped https://github.com/Tecnativa/docker-socket-proxy[docker-socket-proxy] in front of it. -* *Pin image versions.* `COLLABORA_IMAGE` ships pinned to a specific version; keep it (and `OWNCLOUD_IMAGE`) pinned rather than tracking a floating `latest` tag, so upgrades are deliberate, reproducible, and auditable (see xref:#upgrading-owncloud[Upgrading ownCloud]). +* *Pin image versions.* The MariaDB, Redis, Collabora and Traefik images are pinned to specific tags directly in the `docker-compose.yml` file (kept current automatically via Dependabot in the docs repository); the ownCloud version is set with `OWNCLOUD_IMAGE` in the `.env` file. Keep them pinned rather than tracking a floating `latest` tag, so upgrades are deliberate, reproducible, and auditable (see xref:#upgrading-owncloud[Upgrading ownCloud]). ==== . Create an `.env` configuration file in the same directory, which contains the required configuration settings. @@ -211,14 +211,6 @@ a| xref:{config-sample}#override-cli-url[OWNCLOUD_OVERWRITE_CLI_URL] | The MariaDB `owncloud` user password (also used by ownCloud to connect) | `CHANGE_ME_DB_APP` -| `MARIADB_IMAGE` -| The mariadb version -| `10.11` - -| `REDIS_IMAGE` -| The redis version -| `7` - | `COLLABORA_DOMAIN` | The public hostname Collabora Online (CODE) is served at | `collabora.example.com` @@ -228,10 +220,6 @@ a| The ownCloud host that CODE accepts as WOPI host, as a regular expression. + Escape every dot with a backslash. | `owncloud\.example\.com` -| `COLLABORA_IMAGE` -| The Collabora Online (CODE) version -| `latest` - | `COLLABORA_ADMIN_USERNAME` | The Collabora admin console username | `admin` @@ -240,10 +228,6 @@ Escape every dot with a backslash. | The Collabora admin console password | `CHANGE_ME_COLLABORA` -| `TRAEFIK_IMAGE` -| The Traefik reverse proxy version -| `v3.3` - | `LETSENCRYPT_EMAIL` | The email address used for Let's Encrypt certificate registration | `admin@example.com` diff --git a/tests/docker-compose/docker-compose.override.yml b/tests/docker-compose/docker-compose.override.yml index 0d1b57020..22affb5c2 100644 --- a/tests/docker-compose/docker-compose.override.yml +++ b/tests/docker-compose/docker-compose.override.yml @@ -35,6 +35,12 @@ services: - traefik.http.routers.collabora-admin.tls.certresolver= proxy: + # Pinned to v3.6 for the test harness (the shipped example uses v3.3): the + # Docker provider in Traefik < 3.6 probes the daemon with API version 1.24 + # before negotiating, which Docker Engine >= 29 refuses. v3.6 negotiates + # 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 - --providers.docker.exposedbydefault=false diff --git a/tests/docker-compose/test.env b/tests/docker-compose/test.env index d0155a812..0a2274cd3 100644 --- a/tests/docker-compose/test.env +++ b/tests/docker-compose/test.env @@ -7,16 +7,12 @@ # - test credentials (never reused anywhere real). # Pinned so a run is reproducible and CI is not surprised by a new release. +# Only OWNCLOUD_IMAGE is set here: the base compose file uses +# owncloud/server:${OWNCLOUD_IMAGE} (its version tracks the docs release), while +# the MariaDB, Redis, Collabora and Traefik tags are literal in the base file. +# The harness pins Traefik to v3.6 via image: in docker-compose.override.yml +# (the shipped v3.3 fails against Docker Engine >= 29 — see the note there). OWNCLOUD_IMAGE=10.15.0 -MARIADB_IMAGE=10.11 -REDIS_IMAGE=7 -COLLABORA_IMAGE=24.04.13.2.1 -# Pinned to v3.6 for the test harness (not v3.3 like the shipped example): the -# Docker provider in Traefik < 3.6 probes the daemon with API version 1.24 -# before negotiating, which Docker Engine >= 29 refuses. v3.6 negotiates 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. -TRAEFIK_IMAGE=v3.6 # *.localhost always resolves to 127.0.0.1; the smoke test uses curl --resolve. OWNCLOUD_DOMAIN=owncloud.localhost From c90b1b34b374495adb57e8dabcfb6a3333e15c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:04:35 +0200 Subject: [PATCH 07/10] fix(docker): bump Traefik to v3.7 and make routing robust MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- .../installation/docker/docker-compose.yml | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/modules/admin_manual/examples/installation/docker/docker-compose.yml b/modules/admin_manual/examples/installation/docker/docker-compose.yml index f15fa2545..a93cfc7cf 100644 --- a/modules/admin_manual/examples/installation/docker/docker-compose.yml +++ b/modules/admin_manual/examples/installation/docker/docker-compose.yml @@ -1,3 +1,11 @@ +# Fixes the Compose project name, so the generated resource names (containers, +# networks, volumes) do not depend on the directory the stack is launched from. +# The "frontend" network at the bottom therefore always becomes +# "owncloud_frontend", which is the name the traefik.docker.network label on the +# ownCloud service refers to. If you run several copies of this stack on one +# host, change this and update that label to match "_frontend". +name: owncloud + # This oc-common template is a compose extension for ownCloud. # It is used for starting the main ownCloud instance and reused e.g. when defining listeners for WND. # This eases compose maintenance a lot. @@ -92,13 +100,21 @@ services: - traefik.http.routers.collabora.entrypoints=websecure - traefik.http.routers.collabora.tls.certresolver=letsencrypt - traefik.http.services.collabora.loadbalancer.server.port=9980 + # Pinned low, so the admin router below always wins regardless of how long + # COLLABORA_DOMAIN is (see the note there). + - traefik.http.routers.collabora.priority=100 # Admin console router (higher priority): the CODE admin console and its # WebSocket must NOT be reachable from the internet. Restrict them to # trusted source ranges. WOPI clients never need these paths. - traefik.http.routers.collabora-admin.rule=Host(`${COLLABORA_DOMAIN}`) && (PathPrefix(`/browser/dist/admincontrol`) || PathPrefix(`/cool/adminws`)) - traefik.http.routers.collabora-admin.entrypoints=websecure - traefik.http.routers.collabora-admin.tls.certresolver=letsencrypt - - traefik.http.routers.collabora-admin.priority=100 + # Priority must beat the "collabora" router above. Traefik auto-computes an + # unpinned router's priority from its rule-string length, so a long enough + # COLLABORA_DOMAIN would out-rank a small fixed number here and steal these + # paths — bypassing the allowlist. Pinning both routers removes the + # dependency on the domain length entirely. + - traefik.http.routers.collabora-admin.priority=200 - traefik.http.routers.collabora-admin.service=collabora - traefik.http.routers.collabora-admin.middlewares=collabora-admin-allowlist - traefik.http.middlewares.collabora-admin-allowlist.ipallowlist.sourcerange=127.0.0.1/32,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 @@ -111,7 +127,10 @@ services: # each service above; WebSocket upgrades (needed by CODE) are handled # transparently by Traefik. # Pinned tag maintained by Dependabot (see .github/dependabot.yml). - image: traefik:v3.3 + # Must be >= v3.6: the Docker provider in earlier releases probes the daemon + # with API version 1.24 before negotiating, which Docker Engine >= 29 rejects + # ("client version 1.24 is too old"), leaving Traefik with no routing config. + image: traefik:v3.7 container_name: owncloud_proxy restart: always command: @@ -214,10 +233,10 @@ networks: # "frontend" carries public traffic (proxy <-> ownCloud/Collabora). # "backend" is internal only (ownCloud <-> MariaDB/Redis) so the internet-facing # proxy and Collabora cannot reach the data tier directly. - # Explicit names (instead of the compose-generated "_frontend") so the - # traefik.docker.network label on the ownCloud service resolves regardless of - # the project/directory name the stack is launched from. + # These are project-scoped: the top-level "name: owncloud" above fixes the + # prefix, so they materialize as "owncloud_frontend" / "owncloud_backend". + # Traefik's traefik.docker.network label must name the *real* network; given a + # short name it warns and then silently falls back to the container's first + # network, which may be "backend" — unreachable from the proxy. frontend: - name: owncloud_frontend backend: - name: owncloud_backend From df33308cb92dff1cb332115c4328b46121b9b3ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:04:57 +0200 Subject: [PATCH 08/10] fix(tests): make the compose smoke test actually detect exposed DB ports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The data-tier guard probed MariaDB and Redis with "curl http://127.0.0.1:". 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 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- tests/docker-compose/README.md | 13 +- .../docker-compose.override.yml | 47 +++--- tests/docker-compose/smoke-test.sh | 159 +++++++++++------- tests/docker-compose/test.env | 5 +- 4 files changed, 126 insertions(+), 98 deletions(-) diff --git a/tests/docker-compose/README.md b/tests/docker-compose/README.md index 05d3f00b5..9d5affed0 100644 --- a/tests/docker-compose/README.md +++ b/tests/docker-compose/README.md @@ -12,7 +12,7 @@ bootable **locally and in CI without changing the example itself**: | File | Purpose | | --- | --- | -| `docker-compose.override.yml` | Drops the Let's Encrypt / ACME command flags so Traefik serves its built-in self-signed certificate. Everything else — split networks, no DB/Redis host ports, Collabora admin allowlist, HSTS — is inherited unchanged. | +| `docker-compose.override.yml` | Blanks each router's ACME `certresolver` label so Traefik serves its built-in self-signed certificate. Everything else — the Traefik version and its `command:` flags, split networks, no DB/Redis host ports, Collabora admin allowlist, HSTS — is inherited unchanged, so CI exercises the version and configuration the docs actually ship. | | `test.env` | Concrete image pins, `*.localhost` hostnames, and throwaway credentials. | | `smoke-test.sh` | Boots the merged stack, waits for health, and asserts the endpoints and the security invariants. | @@ -40,8 +40,15 @@ failure, and dumps container status + logs when something fails. WOPI `` document. 3. **Data tier is private (security regression guard)** — the merged `docker compose config` publishes no `3306`/`6379` host binding, and neither - port answers on `127.0.0.1`. This guards the hardening that removed the - MariaDB/Redis host ports from the example. + port accepts a TCP connection on `127.0.0.1`. This guards the hardening that + removed the MariaDB/Redis host ports from the example. The probe is a raw + `/dev/tcp` connect, not `curl`: MariaDB and Redis do not speak HTTP, so curl + never exits `0` against them even when the port is wide open, which would make + a curl-based check pass unconditionally. + +Because the harness reuses the example's fixed Compose project name (needed so +the `traefik.docker.network` label resolves to the same network the example +names), it cannot run alongside a real deployment of the example on one host. TLS is served with Traefik's self-signed certificate, so all requests use `curl -k --resolve :443:127.0.0.1`. diff --git a/tests/docker-compose/docker-compose.override.yml b/tests/docker-compose/docker-compose.override.yml index 22affb5c2..2156c3f7c 100644 --- a/tests/docker-compose/docker-compose.override.yml +++ b/tests/docker-compose/docker-compose.override.yml @@ -4,18 +4,26 @@ # (modules/admin_manual/examples/installation/docker/docker-compose.yml) so the # stack can be booted locally / in CI without public DNS or Let's Encrypt. # -# It changes exactly one thing versus the shipped file: TLS. The production -# example obtains real certificates from Let's Encrypt via the ACME TLS-ALPN-01 -# challenge, which requires the public hostnames to resolve to this host — not -# possible on a throwaway CI runner. Here we: -# 1. drop the ACME command flags from Traefik, and -# 2. blank each router's `tls.certresolver` label (set by the base file) and -# pin `tls=true`, so the routers stay TLS-enabled but serve Traefik's -# built-in self-signed certificate. (A router that still references the now -# removed `letsencrypt` resolver is rejected by Traefik and never attaches -# to the websecure entrypoint, which breaks routing entirely.) -# The HTTPS path is still exercised end to end; smoke tests connect with -# `curl -k`. +# It changes exactly one thing versus the shipped file: which certificate the +# TLS routers serve. The production example obtains real certificates from +# Let's Encrypt via the ACME TLS-ALPN-01 challenge, which requires the public +# hostnames to resolve to this host — not possible on a throwaway CI runner. +# Here each router's `tls.certresolver` label (set by the base file) is blanked +# and `tls=true` is pinned, so the routers stay TLS-enabled but serve Traefik's +# built-in self-signed certificate. (A router that still references a certresolver +# it cannot satisfy would fail to obtain a certificate.) +# +# Deliberately NOT overridden here: +# +# * Traefik's `command:` list. Compose *replaces* list values rather than +# merging them, so re-listing the flags would freeze a copy of the base +# file's Traefik configuration and silently stop future flag changes from +# reaching the CI-tested config. The base file's ACME flags are therefore +# inherited as-is: defining a certresolver is harmless when no router +# references it — Traefik never places an ACME order, and the routers above +# serve TRAEFIK DEFAULT CERT. +# +# * The Traefik `image:` tag. CI must exercise the same version the docs ship. # # The security-hardened topology (split networks, no DB/Redis host ports, # Collabora admin allowlist, HSTS) is inherited unchanged from the base file. @@ -33,18 +41,3 @@ services: - traefik.http.routers.collabora.tls.certresolver= - traefik.http.routers.collabora-admin.tls=true - traefik.http.routers.collabora-admin.tls.certresolver= - - proxy: - # Pinned to v3.6 for the test harness (the shipped example uses v3.3): the - # Docker provider in Traefik < 3.6 probes the daemon with API version 1.24 - # before negotiating, which Docker Engine >= 29 refuses. v3.6 negotiates - # 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 - - --providers.docker.exposedbydefault=false - - --entrypoints.web.address=:80 - - --entrypoints.websecure.address=:443 - - --entrypoints.web.http.redirections.entrypoint.to=websecure - - --entrypoints.web.http.redirections.entrypoint.scheme=https diff --git a/tests/docker-compose/smoke-test.sh b/tests/docker-compose/smoke-test.sh index 6d8c5c6d9..0af3e252e 100755 --- a/tests/docker-compose/smoke-test.sh +++ b/tests/docker-compose/smoke-test.sh @@ -26,7 +26,14 @@ BASE_COMPOSE="${EXAMPLE_DIR}/docker-compose.yml" OVERRIDE_COMPOSE="${SCRIPT_DIR}/docker-compose.override.yml" ENV_FILE="${SCRIPT_DIR}/test.env" -PROJECT_NAME="oc-compose-smoke" +# Must match the top-level "name:" in the shipped compose file. Compose derives +# the network names from the project name ("_frontend"), and the +# ownCloud service's traefik.docker.network label refers to "owncloud_frontend" +# by that exact name — a different project name would break proxy routing here +# while leaving the shipped example working, i.e. testing a different stack. +# Consequence: this harness reuses the example's container/network names, so it +# cannot run next to a real deployment of the same example on one host. +PROJECT_NAME="owncloud" # How long to wait for every container to report healthy. HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-300}" @@ -62,85 +69,102 @@ cleanup() { } trap cleanup EXIT +# Poll until "$1" (a shell snippet) succeeds, or "$2" seconds elapse. +# poll_until [on_timeout] +# Shared by the health / status.php / WOPI-discovery waits below so the +# deadline+sleep scaffolding exists once. +# +# and are `eval`ed, so callers pass them SINGLE-quoted +# on purpose: they must be re-evaluated on each iteration / at timeout. Double +# quoting would expand them once at call time and freeze the then-empty values +# into the diagnostics. Hence the `shellcheck disable=SC2016` markers at the +# call sites. +poll_until() { + local condition="$1" timeout="$2" description="$3" on_timeout="${4:-}" + local deadline=$(( $(date +%s) + timeout )) + while true; do + if eval "${condition}"; then + return 0 + fi + if [ "$(date +%s)" -ge "${deadline}" ]; then + fail "timed out after ${timeout}s waiting for ${description}" + [ -n "${on_timeout}" ] && eval "${on_timeout}" + return 1 + fi + sleep 5 + done +} + # --------------------------------------------------------------------------- log "Validating the merged compose configuration" compose config >/dev/null pass "compose config is valid" +# Clear any stale state from a previous run that died without its EXIT trap +# firing (CI OOM/timeout, SIGKILL). A leftover MariaDB volume still holds the +# old root password, so the healthcheck below would fail for reasons that have +# nothing to do with the compose example under test. +log "Removing any leftovers from a previous run" +compose down --volumes --remove-orphans + log "Booting the stack" compose up -d -# 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. - unhealthy="" - 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 - unhealthy="${unhealthy} ${name#/}(${status})" - fi - done < <(compose ps -q) +# Reports the containers that declare a healthcheck but are not yet healthy. +# One `docker inspect` call per container, emitting " " pairs. +unhealthy_containers() { + local cids + cids=$(compose ps -q) + [ -n "${cids}" ] || return 0 + # shellcheck disable=SC2086 + docker inspect \ + -f '{{if .State.Health}}{{printf "%s(%s)" .Name .State.Health.Status}}{{end}}' \ + ${cids} | sed 's|^/||' | grep -v '(healthy)$' || true +} - if [ -z "${unhealthy}" ]; then - pass "all containers with a healthcheck are healthy" - break - fi - if [ "$(date +%s)" -ge "${deadline}" ]; then - fail "timed out waiting for containers:${unhealthy}" - exit 1 - fi - sleep 5 -done +log "Waiting for containers to become healthy (timeout ${HEALTH_TIMEOUT}s)" +# shellcheck disable=SC2016 # deferred expansion is intended — see poll_until +poll_until '[ -z "$(unhealthy_containers)" ]' "${HEALTH_TIMEOUT}" \ + "containers to report healthy" \ + 'printf " still not healthy: %s\n" "$(unhealthy_containers | tr "\n" " ")"' \ + || exit 1 +pass "all containers with a healthcheck are healthy" + +# Fetches an HTTPS URL through the proxy and asserts the body contains a marker. +# `last_body` is kept for the timeout diagnostics. +# https_body_contains [max_time] +last_body="" +https_body_contains() { + local host="$1" path="$2" marker="$3" max_time="${4:-10}" + last_body=$(curl -sk --max-time "${max_time}" \ + --resolve "${host}:443:127.0.0.1" "https://${host}${path}" || true) + # Pure-bash substring match: do NOT pipe into `grep -q`. Under `set -o + # pipefail`, grep -q closes the pipe on the first match, the writer then dies + # with SIGPIPE, and the pipeline reports failure even though the match succeeded. + [[ "${last_body}" == *"${marker}"* ]] +} # ownCloud status.php — poll until first-run install completes. log "Checking ownCloud https://${OWNCLOUD_HOST}/status.php" -deadline=$(( $(date +%s) + INSTALL_TIMEOUT )) -while true; do - body=$(curl -sk --max-time 10 \ - --resolve "${OWNCLOUD_HOST}:443:127.0.0.1" \ - "https://${OWNCLOUD_HOST}/status.php" || true) - # Pure-bash substring match: do NOT pipe into `grep -q`. Under `set -o - # pipefail`, grep -q closes the pipe on the first match, printf then dies with - # SIGPIPE, and the pipeline reports failure even though the match succeeded. - if [[ "${body}" == *'"installed":true'* ]]; then - pass "status.php reports installed=true" - printf ' %s\n' "${body}" - break - fi - if [ "$(date +%s)" -ge "${deadline}" ]; then - fail "status.php did not report installed=true in time; last body: ${body}" - exit 1 - fi - sleep 5 -done +# shellcheck disable=SC2016 # deferred expansion is intended — see poll_until +poll_until "https_body_contains '${OWNCLOUD_HOST}' /status.php '\"installed\":true'" \ + "${INSTALL_TIMEOUT}" "status.php to report installed=true" \ + 'printf " last body: %s\n" "${last_body}"' \ + || exit 1 +pass "status.php reports installed=true" +printf ' %s\n' "${last_body}" # Collabora WOPI discovery. CODE has no healthcheck (so the health-wait above # does not cover it) and, while it is still starting, the proxy returns 404/502 # for this path. Poll until the discovery document is served or we time out. log "Checking Collabora https://${COLLABORA_HOST}/hosting/discovery" -deadline=$(( $(date +%s) + INSTALL_TIMEOUT )) -while true; do - disco=$(curl -sk --max-time 15 \ - --resolve "${COLLABORA_HOST}:443:127.0.0.1" \ - "https://${COLLABORA_HOST}/hosting/discovery" || true) - # Pure-bash substring match — see the status.php note above for why this must - # not be `printf ... | grep -q` under `set -o pipefail`. - if [[ "${disco}" == *''* ]]; then - pass "Collabora returned a WOPI discovery document" - break - fi - if [ "$(date +%s)" -ge "${deadline}" ]; then - fail "Collabora /hosting/discovery did not return a wopi-discovery document in time" - printf ' %s\n' "${disco}" - exit 1 - fi - sleep 5 -done +# shellcheck disable=SC2016 # deferred expansion is intended — see poll_until +poll_until "https_body_contains '${COLLABORA_HOST}' /hosting/discovery '' 15" \ + "${INSTALL_TIMEOUT}" "Collabora to serve a WOPI discovery document" \ + 'printf " last body: %s\n" "${last_body}"' \ + || exit 1 +pass "Collabora returned a WOPI discovery document" # Security regression guard: the data tier must NOT be published to the host. # Capture the config first, then match — piping `compose config` straight into @@ -155,12 +179,17 @@ if [[ "${merged_config}" =~ published:[[:space:]]*\"?(3306|6379)\"? ]]; then fi pass "no data-tier host port bindings in the merged config" +# Probe with a raw TCP connect, NOT curl: MariaDB and Redis do not speak HTTP, +# so `curl http://127.0.0.1:` never exits 0 even when the port is wide open +# (it returns 52 "empty reply" for an open port vs 7 "refused" for a closed one). +# A curl-based guard therefore always passes and could never catch the very +# regression it exists to catch. /dev/tcp succeeds on connect alone. 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" + if (exec 3<>"/dev/tcp/127.0.0.1/${port}") 2>/dev/null; then + fail "port ${port} accepted a TCP connection on 127.0.0.1 — it must not be exposed" exit 1 fi done -pass "ports 3306 and 6379 are closed on the host" +pass "ports 3306 and 6379 refuse connections on the host" log "All smoke-test assertions passed" diff --git a/tests/docker-compose/test.env b/tests/docker-compose/test.env index 0a2274cd3..6435cfb78 100644 --- a/tests/docker-compose/test.env +++ b/tests/docker-compose/test.env @@ -9,9 +9,8 @@ # Pinned so a run is reproducible and CI is not surprised by a new release. # Only OWNCLOUD_IMAGE is set here: the base compose file uses # owncloud/server:${OWNCLOUD_IMAGE} (its version tracks the docs release), while -# the MariaDB, Redis, Collabora and Traefik tags are literal in the base file. -# The harness pins Traefik to v3.6 via image: in docker-compose.override.yml -# (the shipped v3.3 fails against Docker Engine >= 29 — see the note there). +# the MariaDB, Redis, Collabora and Traefik tags are literal in the base file and +# are used here exactly as shipped. OWNCLOUD_IMAGE=10.15.0 # *.localhost always resolves to 127.0.0.1; the smoke test uses curl --resolve. From e6fce5bd478f6079042aa2443d32aab9f20cc806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:05:04 +0200 Subject: [PATCH 09/10] ci(docker): track the harness with Dependabot and drop a duplicate step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- .github/dependabot.yml | 6 +++++- .github/workflows/compose-test.yml | 10 ++-------- .gitignore | 4 ---- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ef061a1fc..2a290f67f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -32,7 +32,11 @@ updates: # are updated here (owncloud/server uses ${OWNCLOUD_IMAGE} interpolation, which # Dependabot cannot resolve, so it is intentionally left for manual bumps). - package-ecosystem: docker - directory: "/modules/admin_manual/examples/installation/docker" + directories: + - "/modules/admin_manual/examples/installation/docker" + # The compose smoke-test harness. It inherits the example's image tags, but + # is listed so any image pinned here in future is tracked too. + - "/tests/docker-compose" schedule: interval: weekly day: sunday diff --git a/.github/workflows/compose-test.yml b/.github/workflows/compose-test.yml index 89e9edc3a..31a57980d 100644 --- a/.github/workflows/compose-test.yml +++ b/.github/workflows/compose-test.yml @@ -26,13 +26,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Validate the merged compose configuration - run: | - docker compose \ - --env-file tests/docker-compose/test.env \ - -f modules/admin_manual/examples/installation/docker/docker-compose.yml \ - -f tests/docker-compose/docker-compose.override.yml \ - config - + # smoke-test.sh validates the merged compose config as its first action, + # then boots the stack and asserts on it. - name: Boot the stack and run smoke tests run: tests/docker-compose/smoke-test.sh diff --git a/.gitignore b/.gitignore index 6bef151f4..38cb90749 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,3 @@ tmp/ .DS_Store .vscode/ .idea/ - -# docker-compose smoke-test artifacts -tests/docker-compose/acme/ -tests/docker-compose/.env From d25ba364706e60e55a908c2d9eb0a6244ab1aac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:05:15 +0200 Subject: [PATCH 10/10] docs(docker): document the fallbacks the Traefik example removed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- .../installation/installing_with_docker.adoc | 27 +++++++++++++++++++ .../backup_and_restore/backup.adoc | 15 +++++++++++ 2 files changed, 42 insertions(+) diff --git a/modules/admin_manual/pages/installation/installing_with_docker.adoc b/modules/admin_manual/pages/installation/installing_with_docker.adoc index 74abe6f3f..ccb44642c 100644 --- a/modules/admin_manual/pages/installation/installing_with_docker.adoc +++ b/modules/admin_manual/pages/installation/installing_with_docker.adoc @@ -124,6 +124,33 @@ Because the Collabora editor is embedded into the ownCloud web UI in an iframe a 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. +[NOTE] +.Internal-only, offline, or externally proxied deployments +==== +Let's Encrypt validates a domain over the public internet, so the example's default configuration needs publicly resolvable names. Adapt it as follows if that does not describe your setup: + +* *No public DNS / air-gapped:* replace the ACME configuration on the `proxy` service with your own certificate. Drop the three `--certificatesresolvers.letsencrypt.*` flags, mount your certificate and key into the container, point a https://doc.traefik.io/traefik/https/tls/[TLS store or dynamic configuration file] at them, and replace `traefik.http.routers..tls.certresolver=letsencrypt` with `traefik.http.routers..tls=true` on both the `owncloud` and `collabora` services. A self-signed or internal-CA certificate works, but every browser and desktop/mobile client must trust it — Collabora is loaded in an iframe, and clients reject untrusted certificates. +* *Plain IP address or a single host name:* not supported together with Collabora. The editor requires two distinct HTTPS host names, and a certificate cannot be issued for a bare IP address. Use hostnames from a domain you control, internal DNS plus an internal CA, or omit the `collabora` service. +* *Your own reverse proxy in front:* see the tip below on publishing a port instead of using the bundled proxy. +==== + +[TIP] +.Fronting the stack with an existing reverse proxy +==== +If you already terminate TLS elsewhere (for example an existing nginx, HAProxy, or a hardware load balancer), you do not need the bundled `proxy` service. Remove it, then publish ownCloud's port on the host and forward to it from your own proxy: + +[source,yml] +---- + owncloud: + <<: *oc-common + container_name: owncloud_server + ports: + - "8080:8080" +---- + +Bind to a specific interface (for example `127.0.0.1:8080:8080`) if the proxy runs on the same host, so the unencrypted port is not reachable from the network. Your proxy must forward the `X-Forwarded-*` headers and be listed in the xref:configuration/server/reverse_proxy_configuration.adoc[reverse proxy configuration], and Collabora still needs its own HTTPS host name routed to port `9980` of the `collabora` container. +==== + . Create a new project directory at a location of choice: + -- diff --git a/modules/admin_manual/pages/maintenance/backup_and_restore/backup.adoc b/modules/admin_manual/pages/maintenance/backup_and_restore/backup.adoc index 44c1f7d2c..25a39b6b1 100644 --- a/modules/admin_manual/pages/maintenance/backup_and_restore/backup.adoc +++ b/modules/admin_manual/pages/maintenance/backup_and_restore/backup.adoc @@ -98,6 +98,21 @@ sudo mysqldump \ owncloud > owncloud-dbbackup_`date +"%Y%m%d"`.bak ---- +[NOTE] +.Docker: the database is not reachable from the host +==== +In the xref:installation/installing_with_docker.adoc[Docker Compose setup], MariaDB deliberately publishes no host port — it is only reachable on the stack's internal network, so `-h localhost` finds nothing on the host. Run the dump inside the database container instead, from the directory holding your `docker-compose.yml`: + +[source,bash] +---- +docker compose exec -T mariadb \ + mysqldump --single-transaction -u root -p"$MYSQL_ROOT_PASSWORD" \ + owncloud > owncloud-dbbackup_`date +"%Y%m%d"`.bak +---- + +`-T` disables pseudo-TTY allocation, without which the redirected dump would be corrupted by carriage returns. Do not add a `ports:` entry for MariaDB just to enable a host-side backup; that exposes the database to the network. +==== + === SQLite [source,bash]