- Nix 96.2%
- Shell 3.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| modules | ||
| pkgs/euro-office-documentserver | ||
| tests | ||
| tools | ||
| .envrc | ||
| .gitignore | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
| renovate.json | ||
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 mainopencloudprocess. Do not also runopencloud collaboration serverseparately — you'd getbind: address already in useon port 9300. If you already useOC_ADD_RUN_SERVICESfor other services, override it with all services in one comma-separated value (the module only sets amkDefault). configureNginxcreates a plain proxy vhost for thewopiUrlhostname to127.0.0.1:9300; TLS is added via the normalservices.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/discoveryreturns 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 containingonlyofficeinjournalctl -u euro-office-docservice, package and config are out of sync (this repo's VM test guards against that).opencloud.servicecrash-loops withinvalid iss value, URL must start with https://—services.opencloud.urlmust behttps://(the embedded IDP only allowshttp://for localhost).- collaboration
/healthzreturns 500 withnetlinkrib: address family not supported— the systemd hardening of the opencloud unit blocksAF_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. Checkjournalctl -u euro-office-converterfor blocked downloads; on same-host or private-network setups thewopiUrlmust be listed inallowedDownloadUrls(orallowLocalConnections = trueset). bind: address already in useon 9300 — the collaboration service is running twice; remove any separateopencloud collaboration serverunit.
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 viapassthru.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
versionpins inpkgs/euro-office-documentserver/{package,x2t}.nixagainst 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
- Read the new tag's submodule pins:
curl -s https://api.github.com/repos/Euro-Office/DocumentServer/git/trees/<tag>(themode: 160000entries are the submodule revs). - Update the revs in
pkgs/euro-office-documentserver/{package,x2t}.nixand theversion. - Refresh source hashes:
nix flake prefetch github:Euro-Office/<repo>/<rev>. - Rebuild; fix any
npmDepsHashmismatches by copying thegot:value. - If web-apps' lockfile changed upstream, regenerate
web-apps-package-lock.json:npm install --package-lock-only --ignore-scriptsin a checkout ofbuild/with the grunt-inline devDependencies stripped (see the postPatch inx2t.nix). - If the release changes the database schema, apply its migration scripts
manually. The module only ever runs the idempotent
createdb.sql— it deliberately never runsremovetbl.sql, which would dropdoc_changes(unsaved in-flight edits) on every service start. 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).