Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .docker/database-services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ services:
# runs with the application user, which cannot read mysql.component there.
image: mysql:8.4@sha256:b3b90af2a6552ae30c266fdb7d5dd55f3afb72404bb78d37fe8a23eb857fd3fb
volumes:
- ../volumes/mysql/init:/docker-entrypoint-initdb.d
- ../volumes/mysql/data:/var/lib/mysql
- ${WORKER_VOLUMES_DIR:-../volumes}/mysql/init:/docker-entrypoint-initdb.d
- ${WORKER_VOLUMES_DIR:-../volumes}/mysql/data:/var/lib/mysql
ports:
- target: 3306
published: "3306-3325"
Expand All @@ -20,7 +20,7 @@ services:
pgsql:
image: postgres:13-alpine@sha256:fb9065b6e3e213bdc07edd372a5b2a26245840b7fb65d1fd8b6700106d51805c
volumes:
- ../volumes/postgres/data:/var/lib/postgresql/data
- ${WORKER_VOLUMES_DIR:-../volumes}/postgres/data:/var/lib/postgresql/data
ports:
- target: 5432
published: "5432-5451"
Expand Down
22 changes: 15 additions & 7 deletions .docker/scripts/nextcloud-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ install -d -o www-data -g www-data \
config \
apps-extra

db_type="${DB_TYPE:-${DB_HOST:-mysql}}"
db_host="${DB_HOST:-$db_type}"
db_driver="${DB_DRIVER:-$db_type}"

export DB_TYPE="$db_type"
export DB_HOST="$db_host"
export DB_DRIVER="$db_driver"

# Wait for database
php /var/www/scripts/wait-for-db.php

Expand All @@ -35,22 +43,22 @@ fi
# Set configurations, if needed
if [[ ! -f "config/config.php" && ${AUTOINSTALL} -eq 1 ]]; then
echo "⌛️ Starting installation ..."
if [[ ${DB_HOST} == 'mysql' ]]; then
occ maintenance:install --verbose --database="${DB_HOST}" --database-name="${MYSQL_DATABASE}" --database-host="${DB_HOST}" --database-port= --database-user="${MYSQL_USER}" --database-pass="${MYSQL_PASSWORD}" --admin-user="${NEXTCLOUD_ADMIN_USER}" --admin-pass="${NEXTCLOUD_ADMIN_PASSWORD}" --admin-email="${NEXTCLOUD_ADMIN_EMAIL}"
if [[ "${db_type}" == 'mysql' ]]; then
occ maintenance:install --verbose --database="${db_driver}" --database-name="${MYSQL_DATABASE}" --database-host="${db_host}" --database-port= --database-user="${MYSQL_USER}" --database-pass="${MYSQL_PASSWORD}" --admin-user="${NEXTCLOUD_ADMIN_USER}" --admin-pass="${NEXTCLOUD_ADMIN_PASSWORD}" --admin-email="${NEXTCLOUD_ADMIN_EMAIL}"
install_cmd_status=$?
elif [[ "${DB_HOST}" == 'pgsql' ]]; then
occ maintenance:install --verbose --database="${DB_HOST}" --database-name="${POSTGRES_DB}" --database-host="${DB_HOST}" --database-port= --database-user="${POSTGRES_USER}" --database-pass="${POSTGRES_PASSWORD}" --admin-user="${NEXTCLOUD_ADMIN_USER}" --admin-pass="${NEXTCLOUD_ADMIN_PASSWORD}" --admin-email="${NEXTCLOUD_ADMIN_EMAIL}"
elif [[ "${db_type}" == 'pgsql' ]]; then
occ maintenance:install --verbose --database="${db_driver}" --database-name="${POSTGRES_DB}" --database-host="${db_host}" --database-port= --database-user="${POSTGRES_USER}" --database-pass="${POSTGRES_PASSWORD}" --admin-user="${NEXTCLOUD_ADMIN_USER}" --admin-pass="${NEXTCLOUD_ADMIN_PASSWORD}" --admin-email="${NEXTCLOUD_ADMIN_EMAIL}"
install_cmd_status=$?
else
occ maintenance:install --verbose --admin-user="${NEXTCLOUD_ADMIN_USER}" --admin-pass="${NEXTCLOUD_ADMIN_PASSWORD}" --admin-email="${NEXTCLOUD_ADMIN_EMAIL}"
occ maintenance:install --verbose --database="${db_driver}" --admin-user="${NEXTCLOUD_ADMIN_USER}" --admin-pass="${NEXTCLOUD_ADMIN_PASSWORD}" --admin-email="${NEXTCLOUD_ADMIN_EMAIL}"
install_cmd_status=$?
fi

if [[ ${install_cmd_status} -ne 0 ]]; then
db_reset_hint="volumes/nextcloud/config and volumes/nextcloud/data"
if [[ ${DB_HOST} == 'mysql' ]]; then
if [[ "${db_type}" == 'mysql' ]]; then
db_reset_hint="volumes/mysql/data, ${db_reset_hint}"
elif [[ ${DB_HOST} == 'pgsql' ]]; then
elif [[ "${db_type}" == 'pgsql' ]]; then
db_reset_hint="volumes/postgres/data, ${db_reset_hint}"
fi

Expand Down
12 changes: 7 additions & 5 deletions .docker/scripts/wait-for-db.php
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
#!/usr/bin/env php
<?php
$dbName = (getenv('DB_HOST') === 'mysql' ? '🐬' : '🐘' ) . getenv('DB_HOST');
$dbType = getenv('DB_TYPE') ?: (getenv('DB_HOST') ?: 'mysql');
$dbHost = getenv('DB_HOST') ?: $dbType;
$dbName = ($dbType === 'mysql' ? '🐬' : ($dbType === 'pgsql' ? '🐘' : '💾')) . $dbType;

echo "⌛ Waiting for database $dbName\n";

function dbIsUp(string $dbName): bool {
try {
if (getenv('DB_HOST') === 'mysql') {
$dsn = getenv('DB_HOST') . ':dbname='.getenv('MYSQL_DATABASE').';host='.getenv('DB_HOST');
if ($GLOBALS['dbType'] === 'mysql') {
$dsn = 'mysql:dbname='.getenv('MYSQL_DATABASE').';host='.$GLOBALS['dbHost'];
new PDO($dsn, getenv('MYSQL_USER'), getenv('MYSQL_PASSWORD'));
} elseif (getenv('DB_HOST') === 'pgsql') {
$dsn = getenv('DB_HOST') . ':dbname='.getenv('POSTGRES_DB').';host='.getenv('DB_HOST');
} elseif ($GLOBALS['dbType'] === 'pgsql') {
$dsn = 'pgsql:dbname='.getenv('POSTGRES_DB').';host='.$GLOBALS['dbHost'];
new PDO($dsn, getenv('POSTGRES_USER'), getenv('POSTGRES_PASSWORD'));
} else {
// Will use SQLite
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/proxy-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
detik-install: false
file-install: false
- name: Run unit tests
run: bats tests/proxy/lease.bats tests/proxy/infrastructure.bats tests/proxy/services.bats tests/proxy/diagnostics.bats tests/proxy/compose-policy.bats
run: bats tests/proxy/lease.bats tests/proxy/infrastructure.bats tests/proxy/services.bats tests/proxy/diagnostics.bats tests/proxy/compose-policy.bats tests/worker/contract.bats

integration:
name: Proxy Docker integration tests
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

# Docker volumes
/volumes
/.workers

.env
docker-compose.override.yml
137 changes: 137 additions & 0 deletions dev-worker
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/bin/sh
# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later

set -eu

usage() {
cat <<'EOF'
Usage: sh ./dev-worker <worker-id> <command> [arguments...]

Commands:
up Start the isolated worker and wait for Nextcloud installation
exec <command...> Run a command in the worker's Nextcloud container
logs Print worker logs
config Render the resolved Compose configuration
status Run occ status in the worker
destroy Stop the worker and remove only its mutable state

Environment:
DB_TYPE=mysql|pgsql Database backend for the foundation lifecycle
DB_HOST=<hostname> Connection hostname (defaults to DB_TYPE)
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
EOF
}

[ "$#" -ge 2 ] || {
usage >&2
exit 2
}

worker_id="$1"
command="$2"
shift 2

case "$worker_id" in
*[!a-z0-9_-]*|'')
echo "Invalid worker id: $worker_id" >&2
exit 2
;;
esac

case "$worker_id" in
[a-z0-9]*) ;;
*)
echo "Worker id must start with a lowercase letter or digit: $worker_id" >&2
exit 2
;;
esac

repo_root="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
db_type="${DB_TYPE:-mysql}"

case "$db_type" in
mysql|pgsql) ;;
sqlite|mariadb)
echo "DB_TYPE=$db_type is reserved by the worker contract but is not implemented by this foundation yet." >&2
exit 3
;;
*)
echo "Unsupported DB_TYPE: $db_type" >&2
exit 2
;;
esac

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

export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-ncdev-$worker_id}"
export WORKER_VOLUMES_DIR="$worker_volumes_dir"
export DB_TYPE="$db_type"
export DB_HOST="${DB_HOST:-$db_type}"

compose() {
docker compose --project-directory "$repo_root" --file "$repo_root/docker-compose.yml" "$@"
}

wait_until_ready() {
timeout="${WORKER_READY_TIMEOUT:-180}"
started="$(date +%s)"

while :; do
if compose exec -T nextcloud occ status 2>/dev/null | grep -q 'installed: true'; then
return 0
fi

now="$(date +%s)"
if [ $((now - started)) -ge "$timeout" ]; then
echo "Worker $worker_id did not become ready within ${timeout}s." >&2
compose logs --no-color nextcloud >&2 || true
return 1
fi

sleep 2
done
}

case "$command" in
up)
mkdir -p "$worker_volumes_dir"
compose up -d
wait_until_ready
;;
exec)
[ "$#" -gt 0 ] || {
echo "exec requires a command" >&2
exit 2
}
compose exec -T nextcloud "$@"
;;
logs)
compose logs --no-color "$@"
;;
config)
mkdir -p "$worker_volumes_dir"
compose config "$@"
;;
status)
compose exec -T nextcloud occ status
;;
destroy)
compose down --remove-orphans
case "$worker_root" in
"$repo_root"/.workers/"$worker_id"|"$repo_root"/.workers/"$worker_id"/) ;;
*)
echo "Refusing to remove unexpected worker path: $worker_root" >&2
exit 4
;;
esac
rm -rf -- "$worker_root"
;;
*)
usage >&2
exit 2
;;
esac
30 changes: 16 additions & 14 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@ services:
database:
extends:
file: .docker/database-services.yml
service: ${DB_HOST:-mysql}
service: ${DB_TYPE:-${DB_HOST:-mysql}}
networks:
default:
aliases:
- ${DB_HOST:-mysql}
- ${DB_HOST:-${DB_TYPE:-mysql}}
nextcloud:
image: ghcr.io/librecodecoop/nextcloud-dev-php${PHP_VERSION:-83}:latest
# build:
# context: .docker/
# dockerfile: Dockerfile.php${PHP_VERSION:-83}
volumes:
- ./.docker/scripts/wait-for-db.php:/var/www/scripts/wait-for-db.php
- ./volumes/php:/var/www/php-config
- ${WORKER_VOLUMES_DIR:-./volumes}/php:/var/www/php-config
- ~/.composer:/var/www/.composer/
- ~/.npm:/var/www/.npm/
- ./volumes/nextcloud:/var/www/html
- ${WORKER_VOLUMES_DIR:-./volumes}/nextcloud:/var/www/html
restart: unless-stopped
depends_on:
- database
Expand All @@ -28,7 +28,9 @@ services:
- NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER:-admin}
- NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD:-admin}
- NEXTCLOUD_ADMIN_EMAIL=${NEXTCLOUD_ADMIN_EMAIL:-a@test.tld}
- DB_HOST=${DB_HOST:-mysql}
- DB_TYPE=${DB_TYPE:-${DB_HOST:-mysql}}
- DB_HOST=${DB_HOST:-${DB_TYPE:-mysql}}
- DB_DRIVER=${DB_DRIVER:-${DB_TYPE:-${DB_HOST:-mysql}}}
- MYSQL_DATABASE=${MYSQL_DATABASE:-nextcloud}
- MYSQL_USER=${MYSQL_USER:-nextcloud}
- MYSQL_PASSWORD=${MYSQL_PASSWORD:-nextcloud}
Expand Down Expand Up @@ -56,9 +58,9 @@ services:
depends_on:
- nextcloud
volumes:
- ./volumes/nextcloud:/var/www/html:ro
- ./volumes/nginx/includes:/etc/nginx/conf.d/includes/
- ./volumes/nginx/certs:/certs
- ${WORKER_VOLUMES_DIR:-./volumes}/nextcloud:/var/www/html:ro
- ${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_PORT=80
Expand Down Expand Up @@ -97,9 +99,9 @@ services:
- eurooffice
restart: always
volumes:
- ./volumes/eurooffice:/home/eurooffice/.eurooffice
- ./volumes/eurooffice/local.json:/etc/onlyoffice/documentserver/local.json
- ./volumes/nextcloud/apps-extra/eurooffice-plugin:/var/www/onlyoffice/documentserver/sdkjs-plugins/libresign:ro
- ${WORKER_VOLUMES_DIR:-./volumes}/eurooffice:/home/eurooffice/.eurooffice
- ${WORKER_VOLUMES_DIR:-./volumes}/eurooffice/local.json:/etc/onlyoffice/documentserver/local.json
- ${WORKER_VOLUMES_DIR:-./volumes}/nextcloud/apps-extra/eurooffice-plugin:/var/www/onlyoffice/documentserver/sdkjs-plugins/libresign:ro
environment:
- EXAMPLE_ENABLED=true
- JWT_SECRET=${JWT_SECRET:-my_jwt_secret}
Expand All @@ -116,7 +118,7 @@ services:
- playwright
user: pwuser
volumes:
- ./volumes/nextcloud:/var/www/html
- ${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}
Expand All @@ -137,7 +139,7 @@ services:
- signal
restart: unless-stopped
volumes:
- ./volumes/signal:/home/.local/share/signal-cli
- ${WORKER_VOLUMES_DIR:-./volumes}/signal:/home/.local/share/signal-cli
environment:
- LOG_LEVEL=info
- VIRTUAL_HOST=${COMPOSE_PROJECT_NAME}-signal.localhost
Expand All @@ -159,4 +161,4 @@ services:
- WHATSAPP_WEBHOOK_EVENTS=${WHATSAPP_WEBHOOK_EVENTS:-message,message.ack,group.participants,chat_presence}
- WHATSAPP_AUTO_REPLY="Conta de sistema. Para falar com a LibreCode, mande email para contato@libresign.coop"
volumes:
- ./volumes/go-whatsapp-web:/usr/src/app/files
- ${WORKER_VOLUMES_DIR:-./volumes}/go-whatsapp-web:/usr/src/app/files
39 changes: 36 additions & 3 deletions docs/advanced-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,50 @@ MySQL is used by default:
docker compose up
```

The supported `DB_HOST` values are:
Use `DB_TYPE` to select the database backend and keep `DB_HOST` for the
connection hostname.

- `mysql`
The currently implemented backends are:

- `mysql` (default)
- `pgsql`

To use PostgreSQL:

```bash
DB_HOST=pgsql docker compose up
DB_TYPE=pgsql docker compose up
```

Existing callers that use `DB_HOST=pgsql docker compose up` remain supported
for compatibility. New automation should use `DB_TYPE`.

### Isolated workers

The `dev-worker` helper gives automation a deterministic Compose project and
mutable data directory per worker:

```bash
sh ./dev-worker test-a up
sh ./dev-worker test-a exec occ status
sh ./dev-worker test-a logs
sh ./dev-worker test-a destroy
```

Select the existing PHP and Nextcloud dimensions in the same invocation:

```bash
PHP_VERSION=83 VERSION_NEXTCLOUD=stable35 DB_TYPE=pgsql \
sh ./dev-worker test-pg up
```

Each worker stores mutable state under `.workers/<worker-id>/volumes` and uses
its own Compose project name. Destroying one worker removes only that worker's
Compose resources and mutable directory.

The lifecycle currently enables MySQL and PostgreSQL. SQLite and MariaDB are
reserved by the `DB_TYPE` contract and are added by their database-specific
workstreams.

## Optional services

Optional services are enabled with Docker Compose profiles:
Expand Down
Loading
Loading