From 6a17bf5ec31ae44547463332c7026f3a0d76c135 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:56:40 +0000 Subject: [PATCH 1/4] feat: support an explicit public Nextcloud URL Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/nextcloud-entrypoint.sh | 13 +++++++++++-- .docker/scripts/proxy/services.sh | 2 +- dev-worker | 4 +++- docker-compose.yml | 11 +++++++---- docs/advanced-setup.md | 18 ++++++++++++++++++ tests/worker/contract.bats | 10 ++++++++++ 6 files changed, 50 insertions(+), 8 deletions(-) diff --git a/.docker/scripts/nextcloud-entrypoint.sh b/.docker/scripts/nextcloud-entrypoint.sh index 29ea2d1..693f015 100644 --- a/.docker/scripts/nextcloud-entrypoint.sh +++ b/.docker/scripts/nextcloud-entrypoint.sh @@ -145,10 +145,19 @@ if ! occ status | grep -q 'installed: true'; then fi if [[ -n "${NEXTCLOUD_HOST:-}" ]]; then - echo "🔧 Setting trusted domain and overwritehost to ${NEXTCLOUD_HOST} ..." + nextcloud_protocol="${NEXTCLOUD_PROTOCOL:-https}" + case "${nextcloud_protocol}" in + http|https) ;; + *) + echo "❌ Unsupported NEXTCLOUD_PROTOCOL: ${nextcloud_protocol}" >&2 + exit 1 + ;; + esac + + echo "🔧 Setting canonical Nextcloud URL to ${nextcloud_protocol}://${NEXTCLOUD_HOST} ..." occ config:system:set trusted_domains 1 --value "${NEXTCLOUD_HOST}" occ config:system:set overwritehost --value "${NEXTCLOUD_HOST}" - occ config:system:set overwriteprotocol --value https + occ config:system:set overwriteprotocol --value "${nextcloud_protocol}" fi # Run cron diff --git a/.docker/scripts/proxy/services.sh b/.docker/scripts/proxy/services.sh index a2494b8..4a7724e 100644 --- a/.docker/scripts/proxy/services.sh +++ b/.docker/scripts/proxy/services.sh @@ -39,7 +39,7 @@ disconnect_proxy_from_project_network() { report_environment_ready() { set -- \ - -e ENV_NEXTCLOUD_URL="https://${PROJECT_NAME:-}.localhost" \ + -e ENV_NEXTCLOUD_URL="${NEXTCLOUD_PROTOCOL:-https}://${NEXTCLOUD_HOST:-${PROJECT_NAME:-}.localhost}" \ -e ENV_ADMIN_USER="${NEXTCLOUD_ADMIN_USER:-admin}" \ -e ENV_ADMIN_PASSWORD="${NEXTCLOUD_ADMIN_PASSWORD:-admin}" \ -e ENV_NEXTCLOUD_BRANCH="${VERSION_NEXTCLOUD:-master}" diff --git a/dev-worker b/dev-worker index 2c0f2fc..cbbaae3 100644 --- a/dev-worker +++ b/dev-worker @@ -25,6 +25,8 @@ Environment: DB_SQL_MODE= Optional global SQL mode for MySQL-compatible backends PHP_VERSION= Existing PHP image selector VERSION_NEXTCLOUD= Existing Nextcloud ref selector + NEXTCLOUD_HOST= Public hostname (default: .localhost) + NEXTCLOUD_PROTOCOL=http|https Public protocol (default: https) NCDD_WORKSPACE= Optional directory mounted as /var/www/html/apps-extra WORKER_READY_TIMEOUT=180 Seconds to wait for a clean installation EOF @@ -193,7 +195,7 @@ case "$command" in compose exec -T nextcloud occ status ;; urls) - printf 'Nextcloud: https://%s.localhost\n' "$COMPOSE_PROJECT_NAME" + printf 'Nextcloud: %s://%s\n' "${NEXTCLOUD_PROTOCOL:-https}" "${NEXTCLOUD_HOST:-$COMPOSE_PROJECT_NAME.localhost}" printf 'Mailpit: https://%s-mailpit.localhost\n' "$COMPOSE_PROJECT_NAME" ;; db-exec) diff --git a/docker-compose.yml b/docker-compose.yml index 99d1054..003ddd7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,7 +37,8 @@ services: - MAIL_SMTPPORT=${MAIL_SMTPPORT:-1025} - MAIL_SMTPHOST=${MAIL_SMTPHOST:-mailpit} - VERSION_NEXTCLOUD=${VERSION_NEXTCLOUD:-master} - - NEXTCLOUD_HOST=${COMPOSE_PROJECT_NAME}.localhost + - NEXTCLOUD_HOST=${NEXTCLOUD_HOST:-${COMPOSE_PROJECT_NAME}.localhost} + - NEXTCLOUD_PROTOCOL=${NEXTCLOUD_PROTOCOL:-https} - AUTOINSTALL=${AUTOINSTALL:-1} - XDEBUG_CONFIG - XDEBUG_MODE @@ -57,9 +58,9 @@ services: - ${WORKER_VOLUMES_DIR:-./volumes}/nginx/includes:/etc/nginx/conf.d/includes/ - ${WORKER_VOLUMES_DIR:-./volumes}/nginx/certs:/certs environment: - - VIRTUAL_HOST=${COMPOSE_PROJECT_NAME}.localhost + - VIRTUAL_HOST=${NEXTCLOUD_HOST:-${COMPOSE_PROJECT_NAME}.localhost} - VIRTUAL_PORT=80 - - SELF_SIGNED_HOST=${COMPOSE_PROJECT_NAME}.localhost + - SELF_SIGNED_HOST=${NEXTCLOUD_HOST:-${COMPOSE_PROJECT_NAME}.localhost} proxy-coordinator: image: docker:29.8.1-cli@sha256:018edbc908e08fcc9dbf029c812c34251e9b4719e6f71ca0e5eae2a987d014ca entrypoint: ["/bin/sh", "/usr/local/lib/librecode/proxy-coordinator.sh"] @@ -77,6 +78,8 @@ services: - NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER:-admin} - NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD:-admin} - VERSION_NEXTCLOUD=${VERSION_NEXTCLOUD:-master} + - NEXTCLOUD_HOST=${NEXTCLOUD_HOST:-${COMPOSE_PROJECT_NAME}.localhost} + - NEXTCLOUD_PROTOCOL=${NEXTCLOUD_PROTOCOL:-https} healthcheck: test: ["CMD-SHELL", "test -f /tmp/librecode-proxy-ready"] interval: 1s @@ -120,7 +123,7 @@ services: - ${WORKER_VOLUMES_DIR:-./volumes}/nextcloud:/var/www/html working_dir: /var/www/html environment: - - PLAYWRIGHT_BASE_URL=${PLAYWRIGHT_BASE_URL:-https://${COMPOSE_PROJECT_NAME}.localhost} + - PLAYWRIGHT_BASE_URL=${PLAYWRIGHT_BASE_URL:-${NEXTCLOUD_PROTOCOL:-https}://${NEXTCLOUD_HOST:-${COMPOSE_PROJECT_NAME}.localhost}} - NODE_ENV=test - CI=${CI:-false} - VIRTUAL_HOST=${COMPOSE_PROJECT_NAME}-playwright.localhost diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index dbddf12..5af724e 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -114,6 +114,24 @@ docker compose \ up ``` +## Public Nextcloud URL + +By default, each environment is available through the shared proxy at +`https://.localhost`. + +Remote development platforms may expose the proxy through a different public +hostname. Set `NEXTCLOUD_HOST` and, when needed, `NEXTCLOUD_PROTOCOL` so the +proxy routing and Nextcloud canonical URL stay consistent: + +```bash +NEXTCLOUD_HOST=example.dev NEXTCLOUD_PROTOCOL=https docker compose up +``` + +These values configure the routed host, Nextcloud `trusted_domains`, +`overwritehost`, `overwriteprotocol`, diagnostics and the default Playwright +base URL. Do not bypass the proxy by forwarding the nginx service directly +under a different hostname. + ## Multiple environments Multiple checkouts can run at the same time. Each checkout keeps its own Compose network, while the shared development proxy connects to the active project networks. diff --git a/tests/worker/contract.bats b/tests/worker/contract.bats index 02181f0..4d91525 100644 --- a/tests/worker/contract.bats +++ b/tests/worker/contract.bats @@ -102,3 +102,13 @@ setup() { workflow="$REPO_ROOT/.github/workflows/worker-tests.yml" ! grep -Eq "matrix\.mariadb ==|MARIADB_VERSION\" = \"[0-9]|maria-[0-9]+-[0-9]+" "$workflow" } + + +@test "worker accepts an explicit public Nextcloud URL" { + run env DB_TYPE=sqlite NEXTCLOUD_HOST=example.test NEXTCLOUD_PROTOCOL=https sh "$WORKER" public-url config + [ "$status" -eq 0 ] + [[ "$output" == *"NEXTCLOUD_HOST: example.test"* ]] + [[ "$output" == *"NEXTCLOUD_PROTOCOL: https"* ]] + [[ "$output" == *"VIRTUAL_HOST: example.test"* ]] + [[ "$output" == *"PLAYWRIGHT_BASE_URL: https://example.test"* ]] +} From ef366f2f7b5e3ce3bd34b90d6d5fe25f90dff35b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:57:34 +0000 Subject: [PATCH 2/4] feat: expose an optional worker web port Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/public-port.yml | 11 +++++++++++ dev-worker | 31 ++++++++++++++++++++++++++----- docs/advanced-setup.md | 19 +++++++++++++++++-- tests/worker/contract.bats | 19 +++++++++++++++++++ 4 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 .docker/public-port.yml diff --git a/.docker/public-port.yml b/.docker/public-port.yml new file mode 100644 index 0000000..02a1b6b --- /dev/null +++ b/.docker/public-port.yml @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +services: + nginx: + ports: + - name: nextcloud-public + target: 80 + published: ${NEXTCLOUD_PORT:?NEXTCLOUD_PORT is required} + host_ip: 127.0.0.1 + protocol: tcp diff --git a/dev-worker b/dev-worker index cbbaae3..362a400 100644 --- a/dev-worker +++ b/dev-worker @@ -27,6 +27,7 @@ Environment: VERSION_NEXTCLOUD= Existing Nextcloud ref selector NEXTCLOUD_HOST= Public hostname (default: .localhost) NEXTCLOUD_PROTOCOL=http|https Public protocol (default: https) + NEXTCLOUD_PORT= Optional host port exposing this worker's nginx NCDD_WORKSPACE= Optional directory mounted as /var/www/html/apps-extra WORKER_READY_TIMEOUT=180 Seconds to wait for a clean installation EOF @@ -59,6 +60,21 @@ esac repo_root="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" db_type="${DB_TYPE:-mysql}" workspace="${NCDD_WORKSPACE:-}" +nextcloud_port="${NEXTCLOUD_PORT:-}" + +if [ -n "$nextcloud_port" ]; then + case "$nextcloud_port" in + *[!0-9]*|'') + echo "Invalid NEXTCLOUD_PORT: $nextcloud_port" >&2 + exit 2 + ;; + esac + if [ "$nextcloud_port" -lt 1 ] || [ "$nextcloud_port" -gt 65535 ]; then + echo "Invalid NEXTCLOUD_PORT: $nextcloud_port" >&2 + exit 2 + fi + export NEXTCLOUD_PORT="$nextcloud_port" +fi if [ -n "$workspace" ]; then [ -d "$workspace" ] || { @@ -108,13 +124,18 @@ else fi compose() { + set -- docker compose --project-directory "$repo_root" \ + --file "$repo_root/docker-compose.yml" "$@" + if [ -n "$workspace" ]; then - docker compose --project-directory "$repo_root" \ - --file "$repo_root/docker-compose.yml" \ - --file "$repo_root/.docker/workspace.yml" "$@" - else - docker compose --project-directory "$repo_root" --file "$repo_root/docker-compose.yml" "$@" + set -- "$@" --file "$repo_root/.docker/workspace.yml" fi + + if [ -n "$nextcloud_port" ]; then + set -- "$@" --file "$repo_root/.docker/public-port.yml" + fi + + "$@" } wait_for_mariadb() { diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index 5af724e..a2bfa80 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -129,8 +129,23 @@ NEXTCLOUD_HOST=example.dev NEXTCLOUD_PROTOCOL=https docker compose up These values configure the routed host, Nextcloud `trusted_domains`, `overwritehost`, `overwriteprotocol`, diagnostics and the default Playwright -base URL. Do not bypass the proxy by forwarding the nginx service directly -under a different hostname. +base URL. + +For remote platforms that expose applications by forwarding a host port, a +worker can additionally publish its nginx service on a caller-selected loopback +port: + +```bash +NEXTCLOUD_HOST=remote.example.dev \ +NEXTCLOUD_PROTOCOL=https \ +NEXTCLOUD_PORT=18080 \ +DB_TYPE=sqlite \ +sh ./dev-worker remote up +``` + +The local development default remains the shared HTTPS proxy. Use +`NEXTCLOUD_PORT` only when the remote platform needs a concrete per-worker +port. The caller is responsible for assigning a non-conflicting port. ## Multiple environments diff --git a/tests/worker/contract.bats b/tests/worker/contract.bats index 4d91525..d8da1b1 100644 --- a/tests/worker/contract.bats +++ b/tests/worker/contract.bats @@ -112,3 +112,22 @@ setup() { [[ "$output" == *"VIRTUAL_HOST: example.test"* ]] [[ "$output" == *"PLAYWRIGHT_BASE_URL: https://example.test"* ]] } + + +@test "worker can publish its nginx on a caller-selected host port" { + run env DB_TYPE=sqlite NEXTCLOUD_PORT=18080 sh "$WORKER" public-port config + [ "$status" -eq 0 ] + [[ "$output" == *"published: \"18080\""* ]] + [[ "$output" == *"target: 80"* ]] + [[ "$output" == *"host_ip: 127.0.0.1"* ]] +} + +@test "worker rejects invalid public ports" { + run env NEXTCLOUD_PORT=not-a-port sh "$WORKER" invalid-port config + [ "$status" -eq 2 ] + [[ "$output" == *"Invalid NEXTCLOUD_PORT"* ]] + + run env NEXTCLOUD_PORT=70000 sh "$WORKER" invalid-port config + [ "$status" -eq 2 ] + [[ "$output" == *"Invalid NEXTCLOUD_PORT"* ]] +} From 9389f0bbf5aa9dc0597d3c81b8f53d1abb946fbf Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:59:02 +0000 Subject: [PATCH 3/4] fix: compose worker extension files before commands Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- dev-worker | 27 ++++++++++++++++----------- docker-compose.yml | 2 +- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/dev-worker b/dev-worker index 362a400..27624b7 100644 --- a/dev-worker +++ b/dev-worker @@ -124,18 +124,23 @@ else fi compose() { - set -- docker compose --project-directory "$repo_root" \ - --file "$repo_root/docker-compose.yml" "$@" - - if [ -n "$workspace" ]; then - set -- "$@" --file "$repo_root/.docker/workspace.yml" - fi - - if [ -n "$nextcloud_port" ]; then - set -- "$@" --file "$repo_root/.docker/public-port.yml" + if [ -n "$workspace" ] && [ -n "$nextcloud_port" ]; then + docker compose --project-directory "$repo_root" \ + --file "$repo_root/docker-compose.yml" \ + --file "$repo_root/.docker/workspace.yml" \ + --file "$repo_root/.docker/public-port.yml" "$@" + elif [ -n "$workspace" ]; then + docker compose --project-directory "$repo_root" \ + --file "$repo_root/docker-compose.yml" \ + --file "$repo_root/.docker/workspace.yml" "$@" + elif [ -n "$nextcloud_port" ]; then + docker compose --project-directory "$repo_root" \ + --file "$repo_root/docker-compose.yml" \ + --file "$repo_root/.docker/public-port.yml" "$@" + else + docker compose --project-directory "$repo_root" \ + --file "$repo_root/docker-compose.yml" "$@" fi - - "$@" } wait_for_mariadb() { diff --git a/docker-compose.yml b/docker-compose.yml index 003ddd7..131043b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -123,7 +123,7 @@ services: - ${WORKER_VOLUMES_DIR:-./volumes}/nextcloud:/var/www/html working_dir: /var/www/html environment: - - PLAYWRIGHT_BASE_URL=${PLAYWRIGHT_BASE_URL:-${NEXTCLOUD_PROTOCOL:-https}://${NEXTCLOUD_HOST:-${COMPOSE_PROJECT_NAME}.localhost}} + - PLAYWRIGHT_BASE_URL=${PLAYWRIGHT_BASE_URL:-https://${NEXTCLOUD_HOST:-${COMPOSE_PROJECT_NAME}.localhost}} - NODE_ENV=test - CI=${CI:-false} - VIRTUAL_HOST=${COMPOSE_PROJECT_NAME}-playwright.localhost From 9c3acb588226e22556fb0b9dce0b48c036bdcb8a Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 5 Oct 2026 00:11:32 +0000 Subject: [PATCH 4/4] refactor: keep remote access behind the shared proxy Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/public-port.yml | 11 ----------- dev-worker | 28 +--------------------------- docs/advanced-setup.md | 19 ++++--------------- tests/worker/contract.bats | 19 ------------------- 4 files changed, 5 insertions(+), 72 deletions(-) delete mode 100644 .docker/public-port.yml diff --git a/.docker/public-port.yml b/.docker/public-port.yml deleted file mode 100644 index 02a1b6b..0000000 --- a/.docker/public-port.yml +++ /dev/null @@ -1,11 +0,0 @@ -# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -services: - nginx: - ports: - - name: nextcloud-public - target: 80 - published: ${NEXTCLOUD_PORT:?NEXTCLOUD_PORT is required} - host_ip: 127.0.0.1 - protocol: tcp diff --git a/dev-worker b/dev-worker index 27624b7..2c6273b 100644 --- a/dev-worker +++ b/dev-worker @@ -27,7 +27,6 @@ Environment: VERSION_NEXTCLOUD= Existing Nextcloud ref selector NEXTCLOUD_HOST= Public hostname (default: .localhost) NEXTCLOUD_PROTOCOL=http|https Public protocol (default: https) - NEXTCLOUD_PORT= Optional host port exposing this worker's nginx NCDD_WORKSPACE= Optional directory mounted as /var/www/html/apps-extra WORKER_READY_TIMEOUT=180 Seconds to wait for a clean installation EOF @@ -60,22 +59,6 @@ esac repo_root="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" db_type="${DB_TYPE:-mysql}" workspace="${NCDD_WORKSPACE:-}" -nextcloud_port="${NEXTCLOUD_PORT:-}" - -if [ -n "$nextcloud_port" ]; then - case "$nextcloud_port" in - *[!0-9]*|'') - echo "Invalid NEXTCLOUD_PORT: $nextcloud_port" >&2 - exit 2 - ;; - esac - if [ "$nextcloud_port" -lt 1 ] || [ "$nextcloud_port" -gt 65535 ]; then - echo "Invalid NEXTCLOUD_PORT: $nextcloud_port" >&2 - exit 2 - fi - export NEXTCLOUD_PORT="$nextcloud_port" -fi - if [ -n "$workspace" ]; then [ -d "$workspace" ] || { echo "NCDD_WORKSPACE is not a directory: $workspace" >&2 @@ -124,19 +107,10 @@ else fi compose() { - if [ -n "$workspace" ] && [ -n "$nextcloud_port" ]; then - docker compose --project-directory "$repo_root" \ - --file "$repo_root/docker-compose.yml" \ - --file "$repo_root/.docker/workspace.yml" \ - --file "$repo_root/.docker/public-port.yml" "$@" - elif [ -n "$workspace" ]; then + if [ -n "$workspace" ]; then docker compose --project-directory "$repo_root" \ --file "$repo_root/docker-compose.yml" \ --file "$repo_root/.docker/workspace.yml" "$@" - elif [ -n "$nextcloud_port" ]; then - docker compose --project-directory "$repo_root" \ - --file "$repo_root/docker-compose.yml" \ - --file "$repo_root/.docker/public-port.yml" "$@" else docker compose --project-directory "$repo_root" \ --file "$repo_root/docker-compose.yml" "$@" diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index a2bfa80..410ff6f 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -131,21 +131,10 @@ These values configure the routed host, Nextcloud `trusted_domains`, `overwritehost`, `overwriteprotocol`, diagnostics and the default Playwright base URL. -For remote platforms that expose applications by forwarding a host port, a -worker can additionally publish its nginx service on a caller-selected loopback -port: - -```bash -NEXTCLOUD_HOST=remote.example.dev \ -NEXTCLOUD_PROTOCOL=https \ -NEXTCLOUD_PORT=18080 \ -DB_TYPE=sqlite \ -sh ./dev-worker remote up -``` - -The local development default remains the shared HTTPS proxy. Use -`NEXTCLOUD_PORT` only when the remote platform needs a concrete per-worker -port. The caller is responsible for assigning a non-conflicting port. +For remote platforms, forward the shared proxy's HTTPS port and set the public +hostname to the hostname assigned by that platform. The proxy remains the only +HTTP entry point; individual workers do not publish their nginx service on +separate host ports. ## Multiple environments diff --git a/tests/worker/contract.bats b/tests/worker/contract.bats index d8da1b1..b1ab07c 100644 --- a/tests/worker/contract.bats +++ b/tests/worker/contract.bats @@ -110,24 +110,5 @@ setup() { [[ "$output" == *"NEXTCLOUD_HOST: example.test"* ]] [[ "$output" == *"NEXTCLOUD_PROTOCOL: https"* ]] [[ "$output" == *"VIRTUAL_HOST: example.test"* ]] - [[ "$output" == *"PLAYWRIGHT_BASE_URL: https://example.test"* ]] } - -@test "worker can publish its nginx on a caller-selected host port" { - run env DB_TYPE=sqlite NEXTCLOUD_PORT=18080 sh "$WORKER" public-port config - [ "$status" -eq 0 ] - [[ "$output" == *"published: \"18080\""* ]] - [[ "$output" == *"target: 80"* ]] - [[ "$output" == *"host_ip: 127.0.0.1"* ]] -} - -@test "worker rejects invalid public ports" { - run env NEXTCLOUD_PORT=not-a-port sh "$WORKER" invalid-port config - [ "$status" -eq 2 ] - [[ "$output" == *"Invalid NEXTCLOUD_PORT"* ]] - - run env NEXTCLOUD_PORT=70000 sh "$WORKER" invalid-port config - [ "$status" -eq 2 ] - [[ "$output" == *"Invalid NEXTCLOUD_PORT"* ]] -}