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 b317d3e..68d671f 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,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..c27fd40 --- /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:latest + 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.yml b/compose.yml index b5c7d53..6284d67 100644 --- a/compose.yml +++ b/compose.yml @@ -95,7 +95,7 @@ services: failure_action: rollback order: start-first labels: - - "coop-cloud.${STACK_NAME}.version=15.0.0+34.0.1-fpm" + - "coop-cloud.${STACK_NAME}.version=15.1.0+34.0.1-fpm" - "coop-cloud.${STACK_NAME}.timeout=${TIMEOUT}" - "backupbot.backup=${ENABLE_BACKUPS:-true}" - "backupbot.backup.volumes.redis=false" 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/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.