Files
oikos/containers/125-seafile.md
Claudio on hubris 1ad56435e5 seafile: enable Metadata management + Views (Pro)
ENABLE_METADATA_MANAGEMENT=True + METADATA_SERVER_URL=http://seafile-md-server:8084 in seahub_settings.py. Library owners can now toggle metadata per library and create table/gallery/kanban Views over their files.
2026-05-12 19:25:14 +02:00

14 KiB

125 — seafile

Seafile Pro Edition 13.0 — exploratory deployment to evaluate as a possible nextcloud (114) replacement. Stood up on 2026-05-12, upgraded from CE → Pro the same day (free tier, ≤3 users). No data migrated from NC; NC stays untouched.

At a glance

  • Hostname: seafile
  • IP: 192.168.8.185
  • Privilege: privileged (Debian 12, nesting + keyctl)
  • Resources: 4 cores / 8 GiB RAM / 32 GiB rootfs / 1 GiB swap
  • Mounts: /mnt/library/mnt/library (Seafile storage under /mnt/library/seafile/data, ES indices under /mnt/library/seafile/elasticsearch)
  • Public hostname: files.hubris.networkcaddy (121)192.168.8.185:80
  • Reachable from: LAN + Netbird mesh (no public ingress)
  • Edition: Pro free tier (no seafile-license.txt, ≤3 users limit) — api2/server-info/ returns features: [seafile-basic, seafile-pro, client-sso-via-local-browser]

Stack

Docker-compose at /opt/seafile/, six services on a private seafile-net bridge:

Service Image Role
seafile seafileltd/seafile-pro-mc:13.0-latest Seahub + seaf-server + Apache (Pro flavour; uses Redis for cache). Binds 192.168.8.185:80.
db mariadb:10.11 ccnet / seafile / seahub databases
redis redis:7-alpine session + cache backend
elasticsearch elasticsearch:8.15.0 full-text search backend (Pro-only)
seafile-md-server seafileltd/seafile-md-server:13.0-latest extended-metadata server (Pro-only). Internal-only on seafile-md-server:8084 over the docker bridge; no host port. Enabled on a per-library basis via the Seahub UI.
thumbnail-server seafileltd/thumbnail-server:13.0-latest offload thumbnail generation from Seahub (Pro-only). Binds 192.168.8.185:8081; Caddy routes /thumbnail/* to it. Calls back into Seahub at http://seafile to verify per-request permissions.

Compose layout:

File Purpose
/opt/seafile/seafile-server.yml upstream-verbatim from manual.seafile.com/13.0/repo/docker/ce/ (image swapped to pro-mc via .env)
/opt/seafile/elasticsearch.yml upstream-derived; local tweaks for LXC — bootstrap.memory_lock=false, mem_limit: 2g, ES_JAVA_OPTS=-Xms1g -Xmx1g, no ulimits.memlock (privileged LXC can't reliably hold memlock=unlimited)
/opt/seafile/md-server.yml derived from manual.seafile.com/13.0/repo/docker/metadata-server/md-server.yml — the upstream exposes port 8084 on the host; we drop the host port mapping (internal-only) and add depends_on: db (healthy), redis. Mounts ${SEAFILE_VOLUME}:/shared so it can read storage objects directly.
/opt/seafile/thumbnail-server.yml derived from manual.seafile.com/13.0/repo/docker/thumbnail-server/thumbnail-server.yml. Upstream binds port 80 on the host; we rebind to 192.168.8.185:8081 to avoid colliding with the main seafile container which already owns :80. INNER_SEAHUB_SERVICE_URL=http://seafile (the Seahub container's DNS name on the docker bridge) so it can verify perms.
/opt/seafile/docker-compose.override.yml local: publishes 192.168.8.185:80, extra_hosts: auth.hubris.network:192.168.8.175, drops bundled-caddy labels (labels: !reset [])
/opt/seafile/.env mode 600, not git-trackedSEAFILE_IMAGE=seafileltd/seafile-pro-mc:13.0-latest, MD_IMAGE=seafileltd/seafile-md-server:13.0-latest, THUMBNAIL_SERVER_IMAGE=seafileltd/thumbnail-server:13.0-latest, SEAFILE_ELASTICSEARCH_IMAGE=elasticsearch:8.15.0, SEAFILE_ELASTICSEARCH_VOLUME=/mnt/library/seafile/elasticsearch, INNER_SEAHUB_SERVICE_URL=http://seafile, COMPOSE_FILE='seafile-server.yml,elasticsearch.yml,md-server.yml,thumbnail-server.yml,docker-compose.override.yml', DB passwords, JWT_PRIVATE_KEY, INIT_SEAFILE_ADMIN_*, SEAFILE_SERVER_HOSTNAME=files.hubris.network, SEAFILE_SERVER_PROTOCOL=https, ENABLE_SEADOC=false

Do not rotate JWT_PRIVATE_KEY — it invalidates every active session and library access token.

Storage layout

  • /mnt/library/seafile/data/ — bind-mounted to /shared in the seafile container. Holds config (seafile/conf/), logs (seafile/logs/), and the heavy block/fs/commit trees (seafile/seafile-data/storage/). Group-owned :media 2775.
  • /opt/seafile/mysql/ — mariadb data (small, stays on rootfs).

Metadata / Views

Pro's per-library extended metadata (file tags, custom columns, table/gallery/kanban Views) is wired up:

  • seahub_settings.py between # BEGIN-HUBRIS-METADATA / # END-HUBRIS-METADATA:
    ENABLE_METADATA_MANAGEMENT = True
    METADATA_SERVER_URL = 'http://seafile-md-server:8084'
    
  • The seafile-md-server container (see Stack table above) does the indexing; Seahub calls it via the docker-bridge DNS name seafile-md-server.

Per-library activation: owner of a library opens it in the web UI → top-right menu → Metadata management → toggle on. The first PUT to /api/v2.1/repos/<id>/metadata/ causes the md-server to index the library (see seaf-md-server.log: "start initializing the metadata of the repo …"). Once enabled, the library sidebar shows "Views" where the user can create table / gallery / kanban views.

Pure-API enable for testing:

curl -X PUT -H "Authorization: Token <token>" https://files.hubris.network/api/v2.1/repos/<repo_id>/metadata/
# GET the same path returns: {"enabled":true, "tags_enabled":true, "show_view":true, ...}

Auth

Native OAuth2/OIDC against authentik (124). Configuration in /mnt/library/seafile/data/seafile/conf/seahub_settings.py between the # BEGIN-HUBRIS-OAUTH / # END-HUBRIS-OAUTH markers.

Authentik side:

  • Provider: Provider for Seafile (OAuth2/OIDC, confidential, signing key = self-signed cert)
  • Application slug: seafile
  • Redirect URI: https://files.hubris.network/oauth/callback/ (trailing slash mandatory)
  • Launch URL: https://files.hubris.network/
  • Policy binding: authentik Admins group
  • Provisioned via ak shell script (idempotent — OAuth2Provider.objects.get_or_create(name="Provider for Seafile"))

Seahub side:

  • ENABLE_OAUTH = True
  • OAUTH_AUTHORIZATION_URL / OAUTH_TOKEN_URL / OAUTH_USER_INFO_URLhttps://auth.hubris.network/application/o/{authorize,token,userinfo}/
  • OAUTH_SCOPE = ['openid', 'profile', 'email']
  • OAUTH_ATTRIBUTE_MAP = {'sub': (True, 'uid'), 'email': (True, 'email'), 'name': (False, 'name')}sub is the stable identifier; without it the early if not uid: check renders the generic "Error, please contact administrator" page.
  • OAUTH_CREATE_UNKNOWN_USER = True, OAUTH_ACTIVATE_USER_AFTER_CREATION = True — JIT provisioning enabled.
  • CLIENT_SSO_VIA_LOCAL_BROWSER = True — required so the iOS Seafile Pro app bounces OAuth through Safari (system browser) instead of the in-app webview; the webview path cannot complete the round-trip back to the app.
  • Authentik provider scope mappings — the OAuth2 provider MUST have openid, profile, email scope mappings attached (property_mappings), otherwise userinfo returns only sub and the OAUTH_ATTRIBUTE_MAP required-claim check fails.

The Seahub login page renders a "Single Sign-On" button that 302s through /sso//oauth/login/ → Authentik flow.

Reverse-proxy settings (also in seahub_settings.py)

SERVICE_URL = "https://files.hubris.network"
FILE_SERVER_ROOT = "https://files.hubris.network/seafhttp"
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
CSRF_TRUSTED_ORIGINS = ['https://files.hubris.network']

DNS / mesh

  • LAN clients resolve files.hubris.network192.168.8.175 via dnsmasq on LXC 124 (entry: address=/files.hubris.network/192.168.8.175).
  • Netbird mesh peers reach 192.168.8.185 through the existing 192.168.8.0/24 route advertised by the hubris peer.
  • The Seafile container itself needs auth.hubris.network → 192.168.8.175 mapped at the Docker level (Docker's resolver doesn't follow LXC /etc/hosts or the dnsmasq split-horizon), so docker-compose.override.yml carries extra_hosts: auth.hubris.network:192.168.8.175. See mesh migration for the pattern.

Caddy block (LXC 121)

files.hubris.network {
    tls {
        dns ionos {env.IONOS_AUTH_API_TOKEN}
    }
    encode zstd gzip
    request_body {
        max_size 10GB
    }
    # /thumbnail/* → Pro thumbnail-server on LXC 125:8081
    @thumb path /thumbnail/*
    handle @thumb {
        reverse_proxy 192.168.8.185:8081 {
            header_up X-Forwarded-Proto https
            header_up X-Forwarded-Host {host}
            header_up Host {host}
            transport http {
                dial_timeout 30s
                read_timeout 1h
                write_timeout 1h
            }
        }
    }
    # Everything else → main seafile container on LXC 125:80
    handle {
        reverse_proxy 192.168.8.185:80 {
            header_up X-Forwarded-Proto https
            header_up X-Forwarded-Host {host}
            header_up Host {host}
            # Strip IETF resumable-upload negotiation headers (iOS Pro app workaround).
            header_up -Upload-Draft-Interop-Version
            header_up -Upload-Complete
            header_up -Upload-Offset
            header_up -Upload-Length
            transport http {
                dial_timeout 30s
                read_timeout 1h
                write_timeout 1h
            }
            flush_interval -1
        }
    }
}

No import authentik — Seafile handles OAuth natively, forward-auth would block the /oauth/callback/ round-trip.

Why exploration-only (and not a Nextcloud cutover)

The mulita (120) photo stack is tightly coupled to Nextcloud — it reads storage roots from /mnt/library/homecloud/{user}/files/, proxies thumbnails through NC's /core/preview, and consumes NC webhook_listeners for file events. Migrating off Nextcloud would require re-architecting all three integration points first.

For now: stand up Seafile, kick the tires, decide later whether to:

  1. Keep NC for photos and move docs/files to Seafile.
  2. Refactor mulita off NC, then full cutover.
  3. Roll back (Seafile is rollback-safe — NC is untouched throughout).

Changelog

2026-05-12 — enable Metadata management + Views

Added ENABLE_METADATA_MANAGEMENT = True and METADATA_SERVER_URL = 'http://seafile-md-server:8084' to seahub_settings.py (between BEGIN-HUBRIS-METADATA / END-HUBRIS-METADATA markers), restarted the seafile container. Library owners can now enable metadata per-library and create table/gallery/kanban Views. Verified end-to-end by enabling metadata on a throwaway admin-owned library — GET /api/v2.1/repos/<id>/metadata/ returns {"enabled":true, "show_view":true, ...} and seaf-md-server.log shows the indexer ran.

2026-05-12 — add metadata + thumbnail servers

Two more Pro services online: seafile-md-server (extended metadata, internal-only on :8084 over the docker bridge, no host port — enabled per-library from the Seahub UI) and thumbnail-server (offload thumbnail generation from Seahub, host-bound to 192.168.8.185:8081). Caddy now splits /thumbnail/* → :8081 and everything else → :80; both blocks keep the resumable-upload header strip + 1 h transport timeouts. INNER_SEAHUB_SERVICE_URL=http://seafile in .env so the thumbnail server can call the Seahub container by DNS name for per-request permission checks (verified end-to-end: admin token → 403 for dtoro's repo via POST /api/v2.1/internal/repos/.../check-thumbnail/user-token/, i.e. routing works and perms are enforced).

2026-05-12 — CE → Pro upgrade

Image swapped to seafileltd/seafile-pro-mc:13.0-latest; added elasticsearch:8.15.0 as a new service for Pro's full-text search. Free Pro tier (≤3 users, no license). MariaDB dump saved at /mnt/library/seafile/backups/seafile-all-20260512-183001.sql.gz (1.1 MB compressed) before the swap; recovery path is gunzip … | mariadb -uroot against a fresh CE container if needed. ES tuned for the LXC: bootstrap.memory_lock=false, no ulimits.memlock, 1 GiB JVM heap, 2 GiB container mem_limit. Host vm.max_map_count already at 1,048,576 (well above the 262,144 ES minimum). Existing data + JIT users (admin, dtoro) survived the migration; libraries intact (Iphone with 20 files / 53.1 MB carried through).

2026-05-12 — Caddy: strip IETF resumable-upload headers

Patched files.hubris.network block to drop Upload-Draft-Interop-Version, Upload-Complete, Upload-Offset, Upload-Length from the upstream-bound request, and bumped reverse_proxy timeouts (read_timeout 1h, write_timeout 1h). Reason: iOS Seafile Pro 4.0.2 negotiates resumable uploads via these headers; bundled fileserver doesn't speak the draft → uploads stalled ~60s and got cancelled. Stripping forces the client to fall back to plain multipart, which works.

2026-05-12 — initial deployment

LXC 125 created (privileged Debian 12, 4 c / 8 GiB / 32 GiB, IP 192.168.8.185). Seafile CE 13.0 docker-compose stack at /opt/seafile/, storage on /mnt/library/seafile/data. Authentik OAuth provider + Application provisioned via ak shell (slug seafile, redirect URI https://files.hubris.network/oauth/callback/). Caddy site block added on LXC 121, dnsmasq entry on LXC 124. End-to-end OAuth handshake verified to the Authentik flow page. No data migration — exploratory deployment alongside Nextcloud.