diff --git a/.env.sample b/.env.sample index 3e84dc5..d8854ed 100644 --- a/.env.sample +++ b/.env.sample @@ -69,6 +69,13 @@ DEFAULT_QUOTA="10 GB" # APPS="$APPS onlyoffice" # SECRET_ONLYOFFICE_JWT_VERSION=v1 +# Euro-Office runs its own document server in this stack; EUROOFFICE_DOMAIN +# needs its own DNS record pointing at this host. +# COMPOSE_FILE="$COMPOSE_FILE:compose.eurooffice.yml" +# EUROOFFICE_DOMAIN=eurooffice.example.com +# APPS="$APPS eurooffice" +# SECRET_EUROOFFICE_JWT_VERSION=v1 + # COMPOSE_FILE="$COMPOSE_FILE:compose.bbb.yml" # BBB_URL=https://talk.example.org/bigbluebutton/ # trailing slash! # SECRET_BBB_SECRET_VERSION=v1 diff --git a/README.md b/README.md index dd86ffa..bbeb214 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,40 @@ Then set the onlyoffice JWT secret from the onlyoffice installation: * `abra app secret insert onlyoffice_jwt v1 ` * `abra app cmd app install_onlyoffice` +### Euro-Office Integration + +Euro-Office is the AGPL fork of OnlyOffice that powers "Nextcloud Office" from +Nextcloud 34 onwards. Like OnlyOffice it uses a client-side document-server +architecture, so a separate document server is required — but this overlay runs +that document server **inside the same stack**, so there is no external host to +manage. The browser talks to it directly, so it needs its own public HTTPS +domain (`EUROOFFICE_DOMAIN`) with a DNS record pointing at this host. + +`abra app config ` + +Enable the overlay and set the document server's domain: +``` +COMPOSE_FILE="$COMPOSE_FILE:compose.eurooffice.yml" +EUROOFFICE_DOMAIN=eurooffice.example.com +APPS="$APPS eurooffice" +SECRET_EUROOFFICE_JWT_VERSION=v1 +``` + +The overlay runs the document server with its own Postgres and RabbitMQ services +(the image's bundled Postgres is unreliable). The document server schema is +seeded into that Postgres automatically on first init (see +`eurooffice-createdb.sql`). The Postgres is internal-only and uses trust auth, so +the only secret to manage is the JWT; generate it, deploy, then wire up the +Nextcloud app: + +* `abra app secret generate -a ` +* `abra app deploy ` +* `abra app cmd app install_eurooffice` + +> Note: the document server needs ~4 GB RAM (8 GB for multi-user) and pulls the +> `ghcr.io/euro-office/documentserver` image, which currently only publishes a +> `latest` tag (no semver / Renovate pinning yet). + ### BBB Integration `abra app config ` diff --git a/abra.sh b/abra.sh index af39f71..c21047f 100644 --- a/abra.sh +++ b/abra.sh @@ -6,11 +6,13 @@ export MY_CNF_VERSION=v6 export ENTRYPOINT_VERSION=v3 export ENTRYPOINT_WHITEBOARD_VERSION=v1 export ENTRYPOINT_TALK_VERSION=v1 +export ENTRYPOINT_EUROOFFICE_VERSION=v3 +export EUROOFFICE_CREATEDB_VERSION=v1 export CRONTAB_VERSION=v1 export PG_BACKUP_VERSION=v2 run_occ() { - su -p www-data -s /bin/sh -c "/var/www/html/occ $@" + su -p www-data -s /bin/sh -c "/var/www/html/occ $*" } install_apps() { @@ -83,6 +85,13 @@ install_onlyoffice() { set_app_config onlyoffice customizationForcesave true } +install_eurooffice() { + install_apps eurooffice + set_app_config eurooffice DocumentServerUrl "https://${EUROOFFICE_DOMAIN}" + set_app_config eurooffice jwt_secret "$(cat /run/secrets/eurooffice_jwt)" + set_app_config eurooffice customizationForcesave true +} + install_collabora() { install_apps richdocuments set_app_config richdocuments wopi_url "$COLLABORA_URL" diff --git a/compose.eurooffice.yml b/compose.eurooffice.yml new file mode 100644 index 0000000..e2a6062 --- /dev/null +++ b/compose.eurooffice.yml @@ -0,0 +1,118 @@ +version: "3.8" +services: + app: + secrets: + - eurooffice_jwt + environment: + - EUROOFFICE_DOMAIN + + eurooffice: + image: ghcr.io/euro-office/documentserver:v9.3.2 + stdin_open: true + depends_on: + - eurooffice-db + - eurooffice-rabbitmq + networks: + - proxy + - internal + environment: + - JWT_ENABLED=true + - JWT_SECRET_FILE=/run/secrets/eurooffice_jwt + # Use external Postgres + RabbitMQ instead of the flaky bundled ones. + # (The all-in-one image ships an uncleanly-shut-down Postgres data dir + # whose crash recovery exceeds pg_ctl's start timeout -> restart loop.) + - DB_TYPE=postgres + - DB_HOST=eurooffice-db + - DB_PORT=5432 + - DB_NAME=eurooffice + - DB_USER=eurooffice + - AMQP_URI=amqp://guest:guest@eurooffice-rabbitmq + volumes: + - eurooffice_data:/var/lib/euro-office + - eurooffice_config:/etc/euro-office + - eurooffice_logs:/var/log/euro-office + - eurooffice_fonts:/usr/share/fonts/custom + secrets: + - eurooffice_jwt + configs: + - source: entrypoint_eurooffice + target: /custom-entrypoint.sh + mode: 555 + entrypoint: /custom-entrypoint.sh + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/healthcheck"] + interval: 30s + timeout: 10s + retries: 10 + start_period: 3m + deploy: + update_config: + failure_action: rollback + order: start-first + labels: + - "traefik.enable=true" + - "traefik.swarm.network=proxy" + - "traefik.http.services.${STACK_NAME}_eurooffice.loadbalancer.server.port=80" + - "traefik.http.routers.${STACK_NAME}_eurooffice.rule=Host(`${EUROOFFICE_DOMAIN}`)" + - "traefik.http.routers.${STACK_NAME}_eurooffice.entrypoints=web-secure" + - "traefik.http.routers.${STACK_NAME}_eurooffice.tls.certresolver=${LETS_ENCRYPT_ENV}" + - "traefik.http.routers.${STACK_NAME}_eurooffice.middlewares=${STACK_NAME}_eurooffice-fwdproto" + - "traefik.http.middlewares.${STACK_NAME}_eurooffice-fwdproto.headers.customRequestHeaders.X-Forwarded-Proto=https" + + eurooffice-db: + image: postgres:16-alpine + networks: + - internal + environment: + - POSTGRES_DB=eurooffice + - POSTGRES_USER=eurooffice + # Internal-only DB holding transient editing state; trust auth on the + # private overlay network avoids managing a Swarm secret for it. + - POSTGRES_HOST_AUTH_METHOD=trust + volumes: + - eurooffice_db:/var/lib/postgresql/data + configs: + # Seed the document server schema on first init. The all-in-one image only + # creates its schema in the *bundled* Postgres; with an external DB the + # docservice starts against an empty DB, errors on missing task_result / + # doc_changes, never binds its port, and gets healthcheck-killed in a loop. + - source: eurooffice_createdb + target: /docker-entrypoint-initdb.d/createdb.sql + healthcheck: + test: ["CMD", "pg_isready", "-U", "eurooffice"] + interval: 30s + timeout: 10s + retries: 10 + start_period: 1m + + eurooffice-rabbitmq: + image: rabbitmq:4.3.2 + networks: + - internal + healthcheck: + test: rabbitmq-diagnostics -q ping + interval: 30s + timeout: 10s + retries: 10 + start_period: 1m + +secrets: + eurooffice_jwt: + external: true + name: ${STACK_NAME}_eurooffice_jwt_${SECRET_EUROOFFICE_JWT_VERSION} + +volumes: + eurooffice_data: + eurooffice_config: + eurooffice_logs: + eurooffice_fonts: + eurooffice_db: + +configs: + entrypoint_eurooffice: + name: ${STACK_NAME}_entrypoint_eurooffice_${ENTRYPOINT_EUROOFFICE_VERSION} + file: entrypoint.eurooffice.sh.tmpl + template_driver: golang + eurooffice_createdb: + name: ${STACK_NAME}_eurooffice_createdb_${EUROOFFICE_CREATEDB_VERSION} + file: eurooffice-createdb.sql diff --git a/compose.fulltextsearch.yml b/compose.fulltextsearch.yml index f6cd53d..3cf29da 100644 --- a/compose.fulltextsearch.yml +++ b/compose.fulltextsearch.yml @@ -29,7 +29,7 @@ services: mode: 0600 searchindexer: - image: nextcloud:32.0.13-fpm + image: nextcloud:34.0.2-fpm volumes: - nextcloud:/var/www/html/ - nextapps:/var/www/html/custom_apps:cached diff --git a/compose.yml b/compose.yml index 55cb91c..a0643b4 100644 --- a/compose.yml +++ b/compose.yml @@ -48,7 +48,7 @@ services: start_period: 5m app: - image: nextcloud:32.0.13-fpm + image: nextcloud:34.0.2-fpm depends_on: - db configs: @@ -95,7 +95,7 @@ services: failure_action: rollback order: start-first labels: - - "coop-cloud.${STACK_NAME}.version=13.1.1+32.0.12-fpm" + - "coop-cloud.${STACK_NAME}.version=15.1.0+34.0.2-fpm" - "coop-cloud.${STACK_NAME}.timeout=${TIMEOUT}" - "backupbot.backup=${ENABLE_BACKUPS:-true}" - "backupbot.backup.volumes.redis=false" @@ -109,7 +109,7 @@ services: start_period: 15m cron: - image: nextcloud:32.0.13-fpm + image: nextcloud:34.0.2-fpm volumes: - nextcloud:/var/www/html/ - nextapps:/var/www/html/custom_apps:cached diff --git a/entrypoint.eurooffice.sh.tmpl b/entrypoint.eurooffice.sh.tmpl new file mode 100644 index 0000000..70bc27e --- /dev/null +++ b/entrypoint.eurooffice.sh.tmpl @@ -0,0 +1,30 @@ +#!/bin/bash + +set -eu + +# Read a Swarm secret file (_FILE) into the plain env var the +# Euro-Office document server expects, then hand off to its own entrypoint. +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + + if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then + echo >&2 "error: both $var and $fileVar are set (but are exclusive)" + exit 1 + fi + + local val="$def" + if [ "${!var:-}" ]; then + val="${!var}" + elif [ "${!fileVar:-}" ]; then + val="$(< "${!fileVar}")" + fi + + export "$var"="$val" + unset "$fileVar" +} + +file_env "JWT_SECRET" + +exec /entrypoint.sh diff --git a/eurooffice-createdb.sql b/eurooffice-createdb.sql new file mode 100644 index 0000000..2c1cf1c --- /dev/null +++ b/eurooffice-createdb.sql @@ -0,0 +1,73 @@ +-- +-- Create schema onlyoffice +-- + +-- CREATE DATABASE onlyoffice ENCODING = 'UTF8' CONNECTION LIMIT = -1; + +-- ---------------------------- +-- Table structure for doc_changes +-- ---------------------------- +CREATE TABLE IF NOT EXISTS "doc_changes" ( +"tenant" varchar(255) COLLATE "default" NOT NULL, +"id" varchar(255) COLLATE "default" NOT NULL, +"change_id" int4 NOT NULL, +"user_id" varchar(255) COLLATE "default" NOT NULL, +"user_id_original" varchar(255) COLLATE "default" NOT NULL, +"user_name" varchar(255) COLLATE "default" NOT NULL, +"change_data" text COLLATE "default" NOT NULL, +"change_date" timestamp without time zone NOT NULL, +PRIMARY KEY ("tenant", "id", "change_id") +) +WITH (OIDS=FALSE); + +-- ---------------------------- +-- Table structure for task_result +-- ---------------------------- +CREATE TABLE IF NOT EXISTS "task_result" ( +"tenant" varchar(255) COLLATE "default" NOT NULL, +"id" varchar(255) COLLATE "default" NOT NULL, +"status" int2 NOT NULL, +"status_info" int4 NOT NULL, +"created_at" timestamp without time zone DEFAULT NOW(), +"last_open_date" timestamp without time zone NOT NULL, +"user_index" int4 NOT NULL DEFAULT 1, +"change_id" int4 NOT NULL DEFAULT 0, +"callback" text COLLATE "default" NOT NULL, +"baseurl" text COLLATE "default" NOT NULL, +"password" text COLLATE "default" NULL, +"additional" text COLLATE "default" NULL, +PRIMARY KEY ("tenant", "id") +) +WITH (OIDS=FALSE); + +CREATE OR REPLACE FUNCTION merge_db(_tenant varchar(255), _id varchar(255), _status int2, _status_info int4, _last_open_date timestamp without time zone, _user_index int4, _change_id int4, _callback text, _baseurl text, OUT isupdate char(5), OUT userindex int4) AS +$$ +DECLARE + t_var "task_result"."user_index"%TYPE; +BEGIN + LOOP + -- first try to update the key + -- note that "a" must be unique + IF ((_callback <> '') IS TRUE) AND ((_baseurl <> '') IS TRUE) THEN + UPDATE "task_result" SET last_open_date=_last_open_date, user_index=user_index+1,callback=_callback,baseurl=_baseurl WHERE tenant = _tenant AND id = _id RETURNING user_index into userindex; + ELSE + UPDATE "task_result" SET last_open_date=_last_open_date, user_index=user_index+1 WHERE tenant = _tenant AND id = _id RETURNING user_index into userindex; + END IF; + IF found THEN + isupdate := 'true'; + RETURN; + END IF; + -- not there, so try to insert the key + -- if someone else inserts the same key concurrently, + -- we could get a unique-key failure + BEGIN + INSERT INTO "task_result"(tenant, id, status, status_info, last_open_date, user_index, change_id, callback, baseurl) VALUES(_tenant, _id, _status, _status_info, _last_open_date, _user_index, _change_id, _callback, _baseurl) RETURNING user_index into userindex; + isupdate := 'false'; + RETURN; + EXCEPTION WHEN unique_violation THEN + -- do nothing, and loop to try the UPDATE again + END; + END LOOP; +END; +$$ +LANGUAGE plpgsql; diff --git a/release/14.0.0+33.0.6-fpm b/release/14.0.0+33.0.6-fpm new file mode 100644 index 0000000..5b87f76 --- /dev/null +++ b/release/14.0.0+33.0.6-fpm @@ -0,0 +1,10 @@ +Upgrades Nextcloud from 32.0.11 to 33.0.6 (major version upgrade). + +IMPORTANT: +- Nextcloud does NOT support downgrades. Take a backup before deploying. +- Do not skip major versions: your instance must be on the latest 32.x before + upgrading to 33. If you are on an older 32.x, deploy 32.0.11 first. +- After deploying, check the logs and run any pending repair/upgrade steps: + `abra app cmd app run_occ '"app:update --all"'` +- Review app (plug-in) compatibility with Nextcloud 33 before upgrading; some + apps may need to be updated or temporarily disabled. diff --git a/release/15.0.0+34.0.1-fpm b/release/15.0.0+34.0.1-fpm new file mode 100644 index 0000000..934eb56 --- /dev/null +++ b/release/15.0.0+34.0.1-fpm @@ -0,0 +1,13 @@ +Upgrades Nextcloud from 33.0.6 to 34.0.1 (major version upgrade). + +IMPORTANT: +- Nextcloud does NOT support downgrades. Take a backup before deploying. +- Do not skip major versions: your instance must be on the latest 33.x before + upgrading to 34. If you are on 32.x, deploy 14.0.0+33.0.6-fpm first. +- After deploying, check the logs and run any pending repair/upgrade steps: + `abra app cmd app run_occ '"app:update --all"'` +- Review app (plug-in) compatibility with Nextcloud 34 before upgrading; some + apps may need to be updated or temporarily disabled. +- PostgreSQL: Nextcloud 34 requires PostgreSQL >= 14 (the recipe already ships + pgautoupgrade 14). PostgreSQL 14 is now the minimum, so plan a bump to a newer + PostgreSQL before the next Nextcloud major. diff --git a/release/15.1.0+34.0.1-fpm b/release/15.1.0+34.0.1-fpm new file mode 100644 index 0000000..017d2b1 --- /dev/null +++ b/release/15.1.0+34.0.1-fpm @@ -0,0 +1,25 @@ +Adds an optional Euro-Office integration (compose.eurooffice.yml). + +Euro-Office is the AGPL fork of OnlyOffice that powers "Nextcloud Office" from +Nextcloud 34 onwards. This overlay runs the Euro-Office document server inside +the stack, so there is no external document server to manage. + +This change is additive: existing installs are unaffected unless you opt in. + +To enable it (`abra app config `): +- COMPOSE_FILE="$COMPOSE_FILE:compose.eurooffice.yml" +- EUROOFFICE_DOMAIN=eurooffice.example.com +- APPS="$APPS eurooffice" +- SECRET_EUROOFFICE_JWT_VERSION=v1 + +Then: +- Create a DNS record for EUROOFFICE_DOMAIN pointing at this host (the browser + talks to the document server directly over HTTPS). +- `abra app secret generate -a ` +- `abra app deploy ` +- `abra app cmd app install_eurooffice` + +Notes: +- The document server needs ~4 GB RAM (8 GB recommended for multi-user). +- The `ghcr.io/euro-office/documentserver` image currently only publishes a + `latest` tag (no semver pinning yet), so it is not tracked by Renovate. diff --git a/release/15.1.2+34.0.2-fpm b/release/15.1.2+34.0.2-fpm new file mode 100644 index 0000000..13a7731 --- /dev/null +++ b/release/15.1.2+34.0.2-fpm @@ -0,0 +1,2 @@ +Upgrade to latest nextcloud v34.0.2 +Merge latest origin from coop-cloud/nextcloud:13.1.4