Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .docker/consumer-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later

services:
nextcloud:
volumes:
- type: bind
source: ${APP_SOURCE:?APP_SOURCE must point to the downstream app checkout}
target: ${APP_CONTAINER_PATH:?APP_CONTAINER_PATH must be set by the consumer contract}
nginx:
volumes:
- type: bind
source: ${APP_SOURCE:?APP_SOURCE must point to the downstream app checkout}
target: ${APP_CONTAINER_PATH:?APP_CONTAINER_PATH must be set by the consumer contract}
read_only: true
33 changes: 33 additions & 0 deletions .github/workflows/proxy-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,36 @@ jobs:
if [ "$MARIADB_VERSION" = 10.11 ]; then
DB_TYPE=mariadb sh ./dev-worker maria-peer destroy || true
fi

consumer-worker:
name: Downstream app consumer integration
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Build PHP 8.3 development image
run: docker build -f .docker/Dockerfile.php83 -t ghcr.io/librecodecoop/nextcloud-dev-php83:latest .docker
- name: Start two downstream consumers
run: |
APP_SOURCE="$GITHUB_WORKSPACE/tests/worker/fixtures/consumer-a" APP_ID=fixture APP_SETUP_HOOK=setup.sh DB_TYPE=sqlite sh ./dev-worker consumer-a up
APP_SOURCE="$GITHUB_WORKSPACE/tests/worker/fixtures/consumer-b" APP_ID=fixture APP_SETUP_HOOK=setup.sh DB_TYPE=sqlite sh ./dev-worker consumer-b up
- name: Verify source and hook isolation
run: |
test "$(APP_SOURCE="$GITHUB_WORKSPACE/tests/worker/fixtures/consumer-a" APP_ID=fixture DB_TYPE=sqlite sh ./dev-worker consumer-a exec cat /var/www/html/apps-extra/fixture/identity.txt)" = alpha
test "$(APP_SOURCE="$GITHUB_WORKSPACE/tests/worker/fixtures/consumer-b" APP_ID=fixture DB_TYPE=sqlite sh ./dev-worker consumer-b exec cat /var/www/html/apps-extra/fixture/identity.txt)" = beta
test "$(APP_SOURCE="$GITHUB_WORKSPACE/tests/worker/fixtures/consumer-a" APP_ID=fixture DB_TYPE=sqlite sh ./dev-worker consumer-a exec cat /var/www/html/data/consumer-hook)" = alpha
test "$(APP_SOURCE="$GITHUB_WORKSPACE/tests/worker/fixtures/consumer-b" APP_ID=fixture DB_TYPE=sqlite sh ./dev-worker consumer-b exec cat /var/www/html/data/consumer-hook)" = beta
- name: Verify teardown is worker-scoped
run: |
APP_SOURCE="$GITHUB_WORKSPACE/tests/worker/fixtures/consumer-a" APP_ID=fixture DB_TYPE=sqlite sh ./dev-worker consumer-a destroy
APP_SOURCE="$GITHUB_WORKSPACE/tests/worker/fixtures/consumer-b" APP_ID=fixture DB_TYPE=sqlite sh ./dev-worker consumer-b status | grep -q 'installed: true'
- name: Collect consumer logs
if: failure()
run: |
APP_SOURCE="$GITHUB_WORKSPACE/tests/worker/fixtures/consumer-a" APP_ID=fixture DB_TYPE=sqlite sh ./dev-worker consumer-a logs || true
APP_SOURCE="$GITHUB_WORKSPACE/tests/worker/fixtures/consumer-b" APP_ID=fixture DB_TYPE=sqlite sh ./dev-worker consumer-b logs || true
- name: Destroy remaining consumer
if: always()
run: |
APP_SOURCE="$GITHUB_WORKSPACE/tests/worker/fixtures/consumer-a" APP_ID=fixture DB_TYPE=sqlite sh ./dev-worker consumer-a destroy || true
APP_SOURCE="$GITHUB_WORKSPACE/tests/worker/fixtures/consumer-b" APP_ID=fixture DB_TYPE=sqlite sh ./dev-worker consumer-b destroy || true
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ and other advanced configuration, see the

- [Advanced setup](docs/advanced-setup.md)
- [App development](docs/apps-development.md)
- [Downstream app consumers](docs/downstream-consumers.md)
- [FAQ](docs/faq.md)
42 changes: 41 additions & 1 deletion dev-worker
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ Environment:
PHP_VERSION=<version> Existing PHP image selector
VERSION_NEXTCLOUD=<ref> Existing Nextcloud ref selector
WORKER_READY_TIMEOUT=180 Seconds to wait for a clean installation
APP_SOURCE=<path> Optional downstream app checkout to mount
APP_ID=<id> App directory name under apps-extra
APP_SETUP_HOOK=<path> Optional POSIX shell hook relative to APP_SOURCE
EOF
}

Expand Down Expand Up @@ -65,6 +68,34 @@ esac

worker_root="$repo_root/.workers/$worker_id"
worker_volumes_dir="$worker_root/volumes"
consumer_enabled=0

if [ -n "${APP_SOURCE:-}" ] || [ -n "${APP_ID:-}" ] || [ -n "${APP_SETUP_HOOK:-}" ]; then
[ -n "${APP_SOURCE:-}" ] || { echo "APP_SOURCE is required when configuring a downstream app" >&2; exit 2; }
[ -n "${APP_ID:-}" ] || { echo "APP_ID is required when configuring a downstream app" >&2; exit 2; }
[ -d "$APP_SOURCE" ] || { echo "APP_SOURCE is not a directory: $APP_SOURCE" >&2; exit 2; }

case "$APP_ID" in
*[!a-z0-9_-]*|'') echo "Invalid APP_ID: $APP_ID" >&2; exit 2 ;;
esac
case "$APP_ID" in
[a-z0-9]*) ;;
*) echo "APP_ID must start with a lowercase letter or digit: $APP_ID" >&2; exit 2 ;;
esac

if [ -n "${APP_SETUP_HOOK:-}" ]; then
case "$APP_SETUP_HOOK" in
/*|../*|*/../*|*/..) echo "APP_SETUP_HOOK must be a relative path without '..': $APP_SETUP_HOOK" >&2; exit 2 ;;
esac
[ -f "$APP_SOURCE/$APP_SETUP_HOOK" ] || { echo "APP_SETUP_HOOK does not exist: $APP_SOURCE/$APP_SETUP_HOOK" >&2; exit 2; }
fi

APP_SOURCE="$(CDPATH= cd -- "$APP_SOURCE" && pwd)"
export APP_SOURCE
export APP_ID
export APP_CONTAINER_PATH="/var/www/html/apps-extra/$APP_ID"
consumer_enabled=1
fi

export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-ncdev-$worker_id}"
export WORKER_VOLUMES_DIR="$worker_volumes_dir"
Expand All @@ -89,7 +120,13 @@ else
fi

compose() {
docker compose --project-directory "$repo_root" --file "$repo_root/docker-compose.yml" "$@"
if [ "$consumer_enabled" -eq 1 ]; then
docker compose --project-directory "$repo_root" \
--file "$repo_root/docker-compose.yml" \
--file "$repo_root/.docker/consumer-app.yml" "$@"
else
docker compose --project-directory "$repo_root" --file "$repo_root/docker-compose.yml" "$@"
fi
}

wait_for_mariadb() {
Expand Down Expand Up @@ -151,6 +188,9 @@ case "$command" in
compose up -d
fi
wait_until_ready
if [ "$consumer_enabled" -eq 1 ] && [ -n "${APP_SETUP_HOOK:-}" ]; then
compose exec -T -u www-data nextcloud sh "$APP_CONTAINER_PATH/$APP_SETUP_HOOK"
fi
;;
exec)
[ "$#" -gt 0 ] || {
Expand Down
52 changes: 25 additions & 27 deletions docs/apps-development.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
# Start development of apps

You will need create (or clone) the folder of the app that you will work inside the folder `volumes/nextcloud/apps-extra`.

It's not required install all dependencis like php or nodejs to develop apps, with this project is only use the bash in container to compile app.

## Sample

Using the [LibreSign](https://github.com/LibreSign/libresign):

To install LibreSign in the structure of develop is required [up servicer](#up-services). After nextcloud config and install.
- open folder `volumes/nextcloud/app-extra`
- clone project with `git clone https://github.com/LibreSign/libresign.git`
- open bash in nextcloud container with `docker compose exec -u www-data nextcloud bash`
- go to folder `apps-extra/libresign`
```bash
cd apps-extra/libresign
```
- Now you can run all the necessaries commands to build the project, i.e:
```bash
# download composer dependencies
composer install
# download JS dependencies
npm ci
# build and watch JS changes
npm run watch
```
# Develop a Nextcloud app

For a one-off local checkout, an app can still be placed under
`volumes/nextcloud/apps-extra` and used from the `nextcloud` container.

For app repositories that need a reusable development/devcontainer contract,
prefer the [downstream app consumer](downstream-consumers.md) workflow instead
of cloning or copying the full Docker Compose topology.

The consumer contract mounts the app checkout into `apps-extra` while this
repository remains responsible for Nextcloud, database, proxy, mail and runtime
services.

Example:

```bash
APP_SOURCE=/work/my-app APP_ID=my_app DB_TYPE=sqlite \
sh ./dev-worker my-app up

APP_SOURCE=/work/my-app APP_ID=my_app DB_TYPE=sqlite \
sh ./dev-worker my-app exec sh -lc 'cd /var/www/html/apps-extra/my_app && composer install'
```

See [Downstream app consumers](downstream-consumers.md) for setup hooks and the
minimal Compose adapter used by devcontainers.

⬅️ [Back to index](../README.md)
66 changes: 66 additions & 0 deletions docs/downstream-consumers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Downstream app consumers

`nextcloud-docker-development` can provide the canonical Nextcloud runtime for
another app repository without copying the infrastructure topology into that
repository.

## Worker contract

Point the worker at an app checkout with:

- `APP_SOURCE`: host path to the app checkout;
- `APP_ID`: directory name under `/var/www/html/apps-extra`;
- `APP_SETUP_HOOK`: optional POSIX shell script relative to the app checkout,
executed as the mapped `www-data` user after Nextcloud is ready.

Example:

```bash
APP_SOURCE=/work/my-app APP_ID=my_app APP_SETUP_HOOK=.devcontainer/setup.sh \
DB_TYPE=sqlite sh ./dev-worker my-app-worker up

APP_SOURCE=/work/my-app APP_ID=my_app DB_TYPE=sqlite \
sh ./dev-worker my-app-worker exec occ app:enable my_app

APP_SOURCE=/work/my-app APP_ID=my_app DB_TYPE=sqlite \
sh ./dev-worker my-app-worker destroy
```

The app is mounted into the Nextcloud and nginx services. The normal worker
contract still owns Compose naming, mutable state, database selection, runtime
logs and teardown.

## Devcontainer adapter

Docker Compose `include` can keep the infrastructure model owned by this
repository while a downstream repository keeps only a small adapter.

A consumer can keep a local Compose file similar to:

```yaml
include:
- path:
- ${NCDD_ROOT:?set NCDD_ROOT}/docker-compose.yml
- ${NCDD_ROOT:?set NCDD_ROOT}/.docker/consumer-app.yml
project_directory: ${NCDD_ROOT:?set NCDD_ROOT}
```

Set `APP_SOURCE` to an absolute path, `APP_ID` to the app directory name
and `APP_CONTAINER_PATH=/var/www/html/apps-extra/$APP_ID` before running Compose.
The downstream devcontainer can then use the included `nextcloud` service as its
runtime service.

This adapter does not redefine Nextcloud, database, proxy, mail or Redis
services. Changes to that infrastructure remain owned by
`nextcloud-docker-development`.

Docker Compose 2.20+ is required for the top-level `include` feature.

## Isolation

Each worker still receives its own Compose project and mutable directory. Two
consumers may therefore use the same `APP_ID` and container path while mounting
different source checkouts without sharing Nextcloud state.

The worker ID is an isolation namespace for development convenience, not a
security boundary between untrusted workloads sharing the same Docker daemon.
15 changes: 15 additions & 0 deletions tests/worker/contract.bats
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,18 @@ setup() {
grep -q 'pdo_sqlite' "$dockerfile"
done
}

@test "downstream app contract mounts an app checkout without duplicating the base topology" {
run env APP_SOURCE="$REPO_ROOT/tests/worker/fixtures/consumer-a" APP_ID=fixture DB_TYPE=sqlite sh "$WORKER" consumer-a config
[ "$status" -eq 0 ]
[[ "$output" == *"$REPO_ROOT/tests/worker/fixtures/consumer-a"* ]]
[[ "$output" == *"/var/www/html/apps-extra/fixture"* ]]
}

@test "downstream app contract rejects unsafe identifiers and setup hook traversal" {
run env APP_SOURCE="$REPO_ROOT/tests/worker/fixtures/consumer-a" APP_ID='../fixture' DB_TYPE=sqlite sh "$WORKER" consumer-a config
[ "$status" -eq 2 ]

run env APP_SOURCE="$REPO_ROOT/tests/worker/fixtures/consumer-a" APP_ID=fixture APP_SETUP_HOOK='../setup.sh' DB_TYPE=sqlite sh "$WORKER" consumer-a config
[ "$status" -eq 2 ]
}
1 change: 1 addition & 0 deletions tests/worker/fixtures/consumer-a/identity.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
alpha
6 changes: 6 additions & 0 deletions tests/worker/fixtures/consumer-a/setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh
# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later

set -eu
cat "$(dirname "$0")/identity.txt" > /var/www/html/data/consumer-hook
1 change: 1 addition & 0 deletions tests/worker/fixtures/consumer-b/identity.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
beta
6 changes: 6 additions & 0 deletions tests/worker/fixtures/consumer-b/setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh
# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later

set -eu
cat "$(dirname "$0")/identity.txt" > /var/www/html/data/consumer-hook
Loading