No description
  • Nix 96.2%
  • Shell 3.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
renovate 9a4fd3ffda
All checks were successful
buildbot/nix-eval Build done.
buildbot/nix-build Build done.
buildbot/nix-effects Build done.
Merge pull request 'Lock file maintenance' (#4) from renovate/lock-file-maintenance into master
2026-07-27 00:13:32 -04:00
modules seccomp: x2t needs @pkey; add conversion round-trip test 2026-07-17 22:04:53 -04:00
pkgs/euro-office-documentserver fix CI unit tests: TestPICT wstring_convert removal, moved embed/internal test 2026-07-17 22:26:42 -04:00
tests seccomp: x2t needs @pkey; add conversion round-trip test 2026-07-17 22:04:53 -04:00
tools rename eurooffice -> euro-office everywhere; add upstream parity check 2026-07-17 17:26:13 -04:00
.envrc initial commit 2026-07-17 16:04:27 -04:00
.gitignore initial commit 2026-07-17 16:04:27 -04:00
flake.lock Lock file maintenance 2026-07-27 00:00:32 -04:00
flake.nix fix: ci 2026-07-17 22:11:14 -04:00
README.md fix: ci 2026-07-17 22:11:14 -04:00
renovate.json fix: ci 2026-07-17 22:11:14 -04:00

nix-euro-office

Euro-Office Document Server — the European-sovereignty fork of ONLYOFFICE Document Server — packaged for Nix, with NixOS modules to use it as the office suite of an OpenCloud deployment via WOPI.

The package is built from source (x2t converter, sdkjs, web-apps and the node services), based on nixpkgs' onlyoffice-documentserver packaging with all sources swapped for the Euro-Office forks pinned by the Euro-Office/DocumentServer release tag (currently v9.3.2).

Flake outputs

Output What it is
packages.x86_64-linux.euro-office-documentserver the document server (also default)
packages.x86_64-linux.x2t just the file converter
overlays.default adds euro-office-documentserver to nixpkgs
nixosModules.documentserver services.euro-office-documentserver
nixosModules.opencloud-integration services.opencloud.euro-office
nixosModules.default both modules
checks.x86_64-linux.documentserver VM test: health + WOPI discovery
checks.x86_64-linux.opencloud-integration VM test: two-node OpenCloud WOPI wiring
checks.x86_64-linux.caddy-proxy VM test: full suite behind Caddy

Architecture

OpenCloud is the WOPI host: its built-in collaboration service exposes the WOPI endpoints through which the document server reads and writes files, and registers the editor with OpenCloud's app registry. The document server is the WOPI client serving the actual editors to the browser.

Three public HTTPS hostnames are involved. All three must be reachable from the user's browser; additionally the office and WOPI hosts must be able to reach each other server-side:

Role Example Backed by
OpenCloud web UI cloud.example.com services.opencloud (port 9200)
WOPI endpoint wopi.example.com OpenCloud collaboration service (port 9300)
Document server office.example.com services.euro-office-documentserver (nginx)
browser ──► cloud.example.com   (OpenCloud UI, opens the editor page)
browser ──► office.example.com  (editor JS/assets, websocket)
office  ──► wopi.example.com    (document server fetches/stores the file)
cloud   ──► office.example.com  (collaboration service reads /hosting/discovery)

Setup

1. Add the flake input

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    nix-euro-office = {
      url = "github:<you>/nix-euro-office";   # or a local path
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };
}

inputs.nixpkgs.follows is recommended: the package is large, and following your system nixpkgs avoids a second evaluation of it (the packaging tracks nixos-unstable; substantial nixpkgs version skew may need new npm hashes).

2. Create the secrets

Three secrets are needed. Generate them once and deploy them with your usual secret mechanism (agenix, sops-nix, /run/keys, …):

# a) nginx secure-link nonce — an nginx config *fragment*, must be readable
#    by BOTH nginx and the euro-office group; the secret must not contain '$'
$ echo "set \$secure_link_secret \"$(openssl rand -hex 32)\";" > euro-office-nonce.conf

# b) document server JWT secret — plain string in a file
$ openssl rand -hex 32 > euro-office-jwt

# c) WOPI token secret — systemd EnvironmentFile format;
#    opencloud init does NOT generate this one, because the collaboration
#    service is not part of OpenCloud's default service set
$ echo "COLLABORATION_WOPI_SECRET=$(openssl rand -hex 32)" > opencloud-wopi.env

3. NixOS configuration

{ inputs, ... }:
{
  imports = [ inputs.nix-euro-office.nixosModules.default ];

  # ── document server ────────────────────────────────────────────────────
  services.euro-office-documentserver = {
    enable = true;
    hostname = "office.example.com";
    securityNonceFile = "/run/keys/euro-office-nonce.conf";
    jwtSecretFile = "/run/keys/euro-office-jwt";
    # required when OpenCloud lives on the same host / a private address:
    # the converter downloads documents from the WOPI URL, and private IPs
    # are blocked by default; this allowlists exactly the WOPI URL
    # (allowLocalConnections = true opens all private addresses instead)
    allowedDownloadUrls = [ "https://wopi.example.com/" ];
  };

  # the module creates the vhost; add TLS the usual way
  services.nginx.virtualHosts."office.example.com" = {
    enableACME = true;
    forceSSL = true;
  };

  # ── OpenCloud + WOPI wiring ────────────────────────────────────────────
  services.opencloud = {
    enable = true;
    url = "https://cloud.example.com";     # must be https:// — the embedded
                                           # IDP rejects http issuers
    # ... your existing OpenCloud configuration ...

    euro-office = {
      enable = true;
      documentServerUrl = "https://office.example.com";
      wopiUrl = "https://wopi.example.com";
      wopiSecretFile = "/run/keys/opencloud-wopi.env";
      configureNginx = true;               # vhost wopi.example.com -> :9300
    };
  };

  services.nginx.virtualHosts."wopi.example.com" = {
    enableACME = true;
    forceSSL = true;
  };

  # if you don't already front OpenCloud itself with nginx:
  services.nginx.virtualHosts."cloud.example.com" = {
    enableACME = true;
    forceSSL = true;
    locations."/" = {
      proxyPass = "https://127.0.0.1:9200"; # self-signed upstream
      proxyWebsockets = true;
      extraConfig = "proxy_ssl_verify off;";
    };
  };
}

Notes on the moving parts:

  • The integration module sets OC_ADD_RUN_SERVICES = "collaboration" so the collaboration service runs inside the main opencloud process. Do not also run opencloud collaboration server separately — you'd get bind: address already in use on port 9300. If you already use OC_ADD_RUN_SERVICES for other services, override it with all services in one comma-separated value (the module only sets a mkDefault).
  • configureNginx creates a plain proxy vhost for the wopiUrl hostname to 127.0.0.1:9300; TLS is added via the normal services.nginx.virtualHosts."wopi.example.com" options as shown.
  • If the document server uses a certificate the collaboration service can't verify (self-signed / internal CA), set services.opencloud.euro-office.insecure = true.

Using Caddy (or another proxy) at the edge

nginx cannot be removed entirely: for the document server it is part of the application, not just a proxy — it serves the static editor assets from the Nix store and validates the signed download links (secure_link), which Caddy has no equivalent for. The supported pattern is to demote nginx to an internal component on a loopback port and let Caddy own 80/443:

{
  services.euro-office-documentserver = {
    enable = true;
    hostname = "office.example.com";
    securityNonceFile = "/run/keys/euro-office-nonce.conf";
    jwtSecretFile = "/run/keys/euro-office-jwt";
    allowedDownloadUrls = [ "https://wopi.example.com/" ];
    # pass Caddy's X-Forwarded-Proto through to the document server,
    # so generated editor URLs keep the https scheme
    behindReverseProxy = true;
  };

  # bind the module's nginx vhost away from Caddy's ports
  services.nginx.virtualHosts."office.example.com".listen = [
    { addr = "127.0.0.1"; port = 8080; }
  ];

  services.opencloud.euro-office = {
    enable = true;
    documentServerUrl = "https://office.example.com";
    wopiUrl = "https://wopi.example.com";
    wopiSecretFile = "/run/keys/opencloud-wopi.env";
    # configureNginx stays false (default): Caddy proxies WOPI directly
  };

  services.caddy = {
    enable = true;
    virtualHosts."office.example.com".extraConfig = ''
      reverse_proxy 127.0.0.1:8080
    '';
    virtualHosts."wopi.example.com".extraConfig = ''
      reverse_proxy 127.0.0.1:9300
    '';
    virtualHosts."cloud.example.com".extraConfig = ''
      reverse_proxy https://127.0.0.1:9200 {
        transport http {
          tls_insecure_skip_verify
        }
      }
    '';
  };
}

Caddy handles TLS (ACME) and websockets for all three hostnames; only office.example.com has nginx behind it. The caddy-proxy VM test runs exactly this topology and asserts that the WOPI discovery document generates https:// editor URLs through the proxy chain.

4. First switch and verification

After nixos-rebuild switch:

# units up?
$ systemctl status euro-office-docservice euro-office-converter opencloud

# document server healthy, WOPI discovery populated?
$ curl -s https://office.example.com/healthcheck          # -> true
$ curl -s https://office.example.com/hosting/discovery | head -c 300
# expect <wopi-discovery> XML with urlsrc= editor endpoints

# collaboration service healthy? (debug endpoint, on the OpenCloud host)
$ curl -s http://127.0.0.1:9304/healthz                   # -> 200

# WOPI endpoint reachable from the office host?
office$ curl -sI https://wopi.example.com/ | head -1

Then log into the OpenCloud web UI: office documents get an "Open in Euro-Office" action, and New document / spreadsheet / presentation creates files that open in the editor. Co-editing works by opening the same document from two accounts.

Troubleshooting

  • /hosting/discovery returns an (almost) empty XML document — the docservice cannot find its document templates. The fork hardcodes the /var/www/euro-office/... layout; if you see paths containing onlyoffice in journalctl -u euro-office-docservice, package and config are out of sync (this repo's VM test guards against that).
  • opencloud.service crash-loops with invalid iss value, URL must start with https://services.opencloud.url must be https:// (the embedded IDP only allows http:// for localhost).
  • collaboration /healthz returns 500 with netlinkrib: address family not supported — the systemd hardening of the opencloud unit blocks AF_NETLINK; the integration module adds it, so this indicates the module isn't imported/enabled.
  • Editor opens but the document never loads / save fails — usually the document server cannot reach wopiUrl. Check journalctl -u euro-office-converter for blocked downloads; on same-host or private-network setups the wopiUrl must be listed in allowedDownloadUrls (or allowLocalConnections = true set).
  • bind: address already in use on 9300 — the collaboration service is running twice; remove any separate opencloud collaboration server unit.

State, users, ports

state /var/lib/euro-office/documentserver
WOPI proof keys /var/lib/euro-office/wopi (generated on first start, persistent)
runtime config /run/euro-office/config (rendered by preStart from module options)
service user/group euro-office (nginx is added to the group)
database postgresql, db/user euro-office, socket auth
queue rabbitmq (amqp://guest:guest@localhost:5672 by default)
docservice port 8000 (behind nginx on the hostname vhost)
collaboration ports 9300 (WOPI http), 9301 (grpc), 9304 (debug/health)

The euro-office path naming comes from the fork's own server configuration and cannot be changed independently of it.

VM tests

$ nix build .#checks.x86_64-linux.documentserver        # health + WOPI discovery
$ nix build .#checks.x86_64-linux.opencloud-integration # two-node WOPI wiring
$ nix flake check                                       # everything (needs KVM)

The two-node test boots one VM with the document server and one with OpenCloud + collaboration, and asserts discovery, /healthz and cross-node reachability in both directions.

Parity with upstream artifacts

Upstream's distributed artifact is the release .deb on GitHub (the ghcr.io/euro-office/documentserver images are CI build stages, not a runnable server). tools/parity-check.sh [tag] [deb] builds the local package, downloads the deb and compares the pieces that must match:

  • document-templates, dictionaries, docs-formats JSON — byte-identical
  • web-apps / sdkjs editor sets and production-linux.json — identical
  • default.json — identical except the sql defaults the deb postinstall rewrites (the NixOS module sets these anyway)
  • x2t converter version banner — identical

Intentional differences: the Nix package pre-generates fonts at build time (the deb does it in postinstall), runs the node services from node_modules instead of pkg'd binaries, and omits the deb's npm/, core-fonts and license convenience copies. Minified JS is compared by file set, not bytes — JS minification is not bit-reproducible across toolchains.

CI and automated updates

CI is buildbot-nix: buildbot evaluates the flake and builds everything under checks, so that attrset is the CI definition. It contains:

  • the packages themselves (euro-office-documentserver, x2t),
  • x2t-unit-tests — an aggregate that forces all 14 x2t component test derivations (doctrenderer, graphics, UnicodeConverter, …), which were previously only reachable via passthru.tests,
  • the three VM tests (documentserver, opencloud-integration, caddy-proxy).

Locally the same thing is nix flake check (needs KVM for the VM tests).

Dependency updates come from the Renovate service already running on the infra; renovate.json is the repo-side config:

  • flake.lock — per-input bumps plus weekly lock file maintenance, automerged (Forgejo auto-merge-when-checks-succeed) once buildbot reports green.
  • Euro-Office releases — a regex manager watches the version pins in pkgs/euro-office-documentserver/{package,x2t}.nix against Euro-Office/DocumentServer GitHub releases. A new release opens a PR that bumps only the version strings; its CI fails by design, and the PR body points at the manual steps below to finish the bump.

The only repo-external step is making sure buildbot-nix and Renovate pick this repo up (topic / repo list, depending on how discovery is configured server-side).

Updating to a new Euro-Office release

  1. Read the new tag's submodule pins: curl -s https://api.github.com/repos/Euro-Office/DocumentServer/git/trees/<tag> (the mode: 160000 entries are the submodule revs).
  2. Update the revs in pkgs/euro-office-documentserver/{package,x2t}.nix and the version.
  3. Refresh source hashes: nix flake prefetch github:Euro-Office/<repo>/<rev>.
  4. Rebuild; fix any npmDepsHash mismatches by copying the got: value.
  5. If web-apps' lockfile changed upstream, regenerate web-apps-package-lock.json: npm install --package-lock-only --ignore-scripts in a checkout of build/ with the grunt-inline devDependencies stripped (see the postPatch in x2t.nix).
  6. If the release changes the database schema, apply its migration scripts manually. The module only ever runs the idempotent createdb.sql — it deliberately never runs removetbl.sql, which would drop doc_changes (unsaved in-flight edits) on every service start.
  7. nix flake check — the VM tests catch broken WOPI wiring.

License

The packaging in this repo follows nixpkgs conventions. Euro-Office itself is AGPL-3.0 (see the upstream repositories).