Skip to content

Add Seafile app with optional SeaDoc#4993

Draft
mreid-tt wants to merge 32 commits into
truenas:masterfrom
mreid-tt:seafile-app
Draft

Add Seafile app with optional SeaDoc#4993
mreid-tt wants to merge 32 commits into
truenas:masterfrom
mreid-tt:seafile-app

Conversation

@mreid-tt

@mreid-tt mreid-tt commented May 16, 2026

Copy link
Copy Markdown
Contributor

App Addition

  • I have opened an issue to discuss this app addition before submitting this pull request.

AI

  • Part or All of this PR was generated by an LLM.

Description

Adds Seafile to the community train.

Seafile is an open source cloud storage system with privacy preservation and teamwork features. This package includes:

  • Core Seafile server (seafileltd/seafile-mc:13.0.24) with MariaDB (mariadb:12.3.2) and Valkey (valkey/valkey:9.1.0)
  • Nginx reverse proxy (nginxinc/nginx-unprivileged:1.31.1) with optional TrueNAS certificate support for HTTPS
  • Optional SeaDoc server (seafileltd/sdoc-server:2.0.9) for collaborative document editing

App Information

Testing

Tested locally with:

  • basic-values.yaml
  • seadoc-enabled.yaml
  • all-features-enabled.yaml
  • custom-storage.yaml
  • https-values.yaml

All tests passed successfully (render and deployment).

Icons and Screenshots

Please upload the following to the CDN:

Special Notes

  • The seafile-mc image uses baseimage-docker's my_init as PID 1, which requires root. The container runs as root; the init system drops privileges for seafile processes internally.
  • Requires SETUID/SETGID capabilities. Applied via cap_add since the library defaults to cap_drop: [ALL].
  • SeaDoc server is conditional — when enable_seadoc is true, the sdoc-server container is deployed alongside seafile.
  • Nginx (nginxinc/nginx-unprivileged) serves as the reverse proxy with dynamic port mapping (ports >= 1024), eliminating the need for NET_BIND_SERVICE capability. Nginx config is injected via Docker configs.
  • HTTPS via TrueNAS certificate manager — selecting a certificate enables TLS on a separate HTTPS port (30441) with HTTP to HTTPS redirect on 30440.
  • Valkey is used as the cache server with --stop-writes-on-bgsave-error no to ensure compatibility with Seafile's redis client.
  • All image tags are pinned to concrete versions.
  • Tested on Ubuntu 22.04 VM with Docker Compose. All five test configurations deployed successfully with web UI accessible.

Checklist

  • App runs successfully locally
  • Only modified files under /ix-dev/ or /library/
  • README.md included
  • Multiple test scenarios tested (5 test files)
  • questions.yaml has clear descriptions and follows structure of existing apps
  • All automated CI checks pass

@mreid-tt mreid-tt changed the title feat: add Seafile 13 app with Caddy reverse proxy and optional SeaDoc Add Seafile app with optional SeaDoc May 16, 2026
mreid-tt added 11 commits May 16, 2026 12:53
- Rename MariaDB vars: db -> mariadb_container, db_image -> mariadb_image
- Run Caddy as non-root with NET_BIND_SERVICE, enable healthcheck
- Use db_user/db_name consts instead of hardcoded values
- Fix redis dependency: service_started -> service_healthy
- Add additional_storage question with full host_path/ix_volume/cifs/nfs support
- Add Labels Configuration group and question
- Add acl_entries + acl blocks to all storage questions
- Add notes_body const with post-install credentials note
- Remove inconsistent auto_permissions from db_data/redis_data
- Fix Memory label to (in MB), trim README to 3 lines
- Fix redis run_as_context description
- Normalize host_ips in test values, ensure trailing newlines
- User enters bare hostname (no port), port appended automatically
- Only appends :port when non-standard (not 80/443)
- Also feeds into SeaDoc URL and extra_hosts
- Add TCP healthcheck to seadoc container
- seadoc always runs on the same server behind Caddy
- No need for user-configurable seadoc_server_url override
- Match upstream behavior of always auto-constructing the URL
@stavros-k stavros-k marked this pull request as draft May 18, 2026 16:20
@mreid-tt

Copy link
Copy Markdown
Contributor Author

Hey @stavros-k, hope you’re doing well. If there are any recommended changes or suggested next steps, please let me know. I’m very eager to help get this into the catalog.

Comment on lines +122 to +125
{% set caddy = tpl.add_container(values.consts.caddy_container_name, "caddy_image") %}
{% do caddy.add_network(seafile_net) %}
{% do caddy.set_user(values.run_as.user, values.run_as.group) %}
{% do caddy.add_caps(["NET_BIND_SERVICE"]) %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The caddy file that seafile docs suggest is just an example. Users can and probably will want to customize it by a lot.
Also if they have already a reverse proxy in their network, they probably dont want another one.

If seafile is added, it wont be with caddy.

Still this is a very quick review, I have not checked anything else, but this is a blocker.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick review, appreciate you taking the time.

You raise a good point about Caddy. My understanding is that the upstream uses it for path-based routing to SeaDoc (/sdoc-server/), and without it I'm not sure how SeaDoc would be reachable within the isolated network of a TrueNAS app. But I may be missing something.

FWIW, I noticed Windmill in the community train also bundles Caddy, so there's some precedent, though I don't know if that's considered a different scenario.

If you think there's a cleaner approach that avoids bundling Caddy, I'm happy to follow your guidance. Otherwise, if Caddy is a hard no, we can drop SeaDoc from the app and keep it simpler. Let me know what you'd prefer.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mreid-tt as I see SeaDoc does not have to be /sdoc-server/
you can configure it as SEADOC_SERVER_URL without /sdoc-server/

@Krysztal Krysztal May 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mreid-tt if you want I can share my test custom app yaml for truenas with seafile without Caddy

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @stavros-k, just following up to see if you've had a chance to complete your review of this PR. I was also wondering if you've had an opportunity to consider my response and the question I raised. Please let me know if there's any additional information I can provide.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mreid-tt as I see SeaDoc does not have to be /sdoc-server/ you can configure it as SEADOC_SERVER_URL without /sdoc-server/

Thanks for the suggestion @Krysztal. I explored removing the /sdoc-server/ path prefix, but decided to keep it for a few reasons:

  1. The upstream documentation and default deployment use /sdoc-server/ as the standard path
  2. With the nginx reverse proxy, path-based routing to the seadoc container is clean and well-tested
  3. The nginx config follows the reference config from the upstream "Use other reverse proxy" docs, which also uses /sdoc-server/
  4. Keeping the upstream default makes it easier for users to follow upstream troubleshooting guides

That said, I confirmed during testing that HTTP+SeaDoc works correctly — document editing and revision workflows both pass with the /sdoc-server/ path.

mreid-tt and others added 2 commits June 17, 2026 14:51
- Remove Caddy reverse proxy container
- Add nginx (nginxinc/nginx-unprivileged:1.31.1) as replacement
- Pin all image versions to concrete tags (seafile:13.0.24,
  sdoc-server:2.0.9, mariadb:10.11.18, redis:7.4.9-alpine)
- Change default ports to 30042/30043 to avoid CI conflicts
- Fix explicit ENABLE_SEADOC env var (was only set when true)
- Add extra_hosts for seafile container backend resolution
- Fix Host header to use $http_host (includes port) for CSRF
- Add /health endpoint for reliable healthchecks
- Remove nginx_data storage volume (certs via Docker configs)
- Drop NET_BIND_SERVICE capability (dynamic ports >= 1024)
@mreid-tt mreid-tt marked this pull request as ready for review June 17, 2026 20:09
{% set seafile_proto = "https" if values.network.certificate_id else "http" %}
{% set seafile_port = values.network.https_port.port_number if values.network.certificate_id else values.network.web_port.port_number %}
{% set default_port = 443 if values.network.certificate_id else 80 %}
{% set seafile_hostname = values.seafile.server_hostname ~ (":" ~ seafile_port if seafile_port != default_port else "") %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not always append the :443 or :80?
We can also just strip these 2 suffixes without having to do all these checks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified to always append the port from network config. The user provides a bare hostname and the template adds the port (e.g., hostname:30440). No conditional logic.

{% do seafile.environment.add_env("REDIS_HOST", values.consts.redis_container_name) %}
{% do seafile.environment.add_env("REDIS_PORT", values.consts.redis_port) %}
{% do seafile.environment.add_env("REDIS_PASSWORD", values.database.redis_password) %}
{% do seafile.environment.add_env("ENABLE_GO_FILESERVER", "true") %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bool values are also working no need to make them strings

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to native bools. Also made ENABLE_SEADOC explicitly set on every startup — previously it was only set when true, and the seafile-mc image defaults to enabled, which caused the UI to show SeaDoc features for users who didn't enable it.

{% do seadoc.environment.add_env("DB_PORT", values.consts.mariadb_port) %}
{% do seadoc.environment.add_env("DB_USER", values.consts.db_user) %}
{% do seadoc.environment.add_env("DB_PASSWORD", values.database.db_password) %}
{% do seadoc.environment.add_env("DB_NAME", "seahub_db") %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does this connect to a different database that the one that we create?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SeaDoc connects to seahub_db — the upstream seadoc.yml for version 13.0 uses SEAFILE_MYSQL_DB_SEAHUB_DB_NAME (defaults to seahub_db) by design. SeaDoc is an extension of Seahub (the web interface), not the core file server, so it uses the seahub database.

{% do seafile.environment.add_user_envs(values.seafile.additional_envs) %}
{% do seafile.add_extra_host(values.seafile.server_hostname, "host-gateway") %}

{% do seafile.add_storage("/shared", values.storage.data) %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does it share with seadoc?

@mreid-tt mreid-tt Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The seafile and seadoc containers both mount storage at /shared, but they map to different host paths (values.storage.data and values.storage.seadoc_data respectively). They don't share data — the paths just happen to have the same internal container name.

Comment thread ix-dev/community/seafile/ix_values.yaml Outdated
repository: mariadb
tag: 10.11.18
redis_image:
repository: redis

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we use redis while ALL other apps use valkey?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to valkey/valkey:9.1.0. Had to add --stop-writes-on-bgsave-error no because Seafile's redis client triggers BGSAVE on first session write, and valkey's default blocks writes when RDB persistence encounters issues — this caused a 500 error on the login page on first access. Verified working with this change.

tag: 1.0.2

consts:
seafile_container_name: seafile

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do all containers get a seafile suffix? this will generate a weirdly repeated container name

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Renamed to bare names: seafile-dbdb (matching the upstream seafile-server.yml service name), seafile-nginxnginx, seafile-seadocseadoc. This matches the convention used by wger, n8n, and others.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think we have any apps that names the container db

Comment thread ix-dev/community/seafile/ix_values.yaml Outdated
nginx_container_name: seafile-nginx
seadoc_container_name: seafile-seadoc
perms_container_name: permissions
mariadb_port: 3306

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are these consts here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Container name consts are defined in every community app and are used across the template (env vars, depends_on, networks). Removed redis_port and mariadb_port consts — those are just standard ports and are now written directly where they're used, matching how other apps handle them.

{% do seadoc.environment.add_env("NON_ROOT", "false") %}
{% do seadoc.healthcheck.set_test("tcp", {"port": 80}) %}
{% do seadoc.depends.add_dependency(values.consts.mariadb_container_name, "service_healthy") %}
{% do perm_container.add_or_skip_action("seadoc_data", values.storage.seadoc_data, {"uid": 0, "gid": 0, "mode": "check"}) %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need permissions action here if it runs as root?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the permissions action for seadoc_data. Since the seadoc container runs as root, it can create files with any ownership it needs.

{% from "macros/nginx.conf" import nginx_config %}
{% set tpl = ix_lib.base.render.Render(values) %}

{% if not values.seafile.server_hostname %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isnt this enforced by the questions schema?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. Removed the Jinja validation — the required: true in questions.yaml already prevents empty values.

@stavros-k

Copy link
Copy Markdown
Contributor

Why do we ship seadoc with seafile? Isn't that a compatible WOPI server?
ie like collabora? Shouldnt we have it separately?

tag: 13.0.24
mariadb_image:
repository: mariadb
tag: 10.11.18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is mariadb 2 major versions behind?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The upstream seafile-server.yml uses ${SEAFILE_DB_IMAGE:-mariadb:10.11} as its default, so we matched that initially. Updated to mariadb:12.3.2 to match what other apps use. Verified working in local tests.

Comment thread ix-dev/community/seafile/questions.yaml Outdated
label: Server Hostname
description: |
The hostname or IP address where Seafile will be accessible.</br>
Do not include the port — it will be added automatically based on the network configuration.</br>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's going to cause issues if you construct it from network config.
One can have it listen on port 1234 on LAN, but 443 on external.
Your method will pin the hostname to xxx:1234 and will be invalid when accessed from outside.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to: the user provides a bare hostname, and the template appends the port from the network config. The user controls the hostname, so they can enter their external domain regardless of the internal port.

Comment thread ix-dev/community/seafile/questions.yaml Outdated
description: Memory limit for Seafile.
schema:
type: int
default: 2048

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this default different from ALL other apps?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The upstream recommends 2GB minimum for Seafile CE, but the community catalog standard is 4096 as the default across virtually all apps. Changed to 4096 for consistency.

@mreid-tt

mreid-tt commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Why do we ship seadoc with seafile? Isn't that a compatible WOPI server?
ie like collabora? Shouldnt we have it separately?

SeaDoc is included as part of Seafile because it's Seafile-specific — it shares Seafile's MariaDB database, requires Seafile's JWT secret, and fetches document content through Seafile's HTTP API. It cannot function without a Seafile server. The upstream docs treat it the same way, listing seadoc.yml alongside seafile-server.yml in the default COMPOSE_FILE.

That said, if you feel the added complexity isn't worth it, happy to drop SeaDoc and keep the app simpler. We'll defer to your judgment.

truenas added 2 commits June 22, 2026 15:32
- Replace Caddy with nginx (nginxinc/nginx-unprivileged:1.31.1)
- Remove NET_BIND_SERVICE capability (dynamic ports >= 1024)
- Switch to valkey/valkey:9.1.0 with --stop-writes-on-bgsave-error no
- Update MariaDB to 12.3.2
- Rename containers to bare names: seafile-db→db, seafile-nginx→nginx, seafile-seadoc→seadoc
- Move passwords under seafile: dict, remove database section
- Simplify hostname: user provides bare hostname, template appends port
- Set start_period(120) for fresh DB init
- Change memory default to 4096, cpu to 2
- Remove mariadb_port/redis_port consts, hardcode ports
- Remove seadoc permissions action (runs as root)
- Remove server_hostname template validation (schema enforces)
- Remove healthcheck timing overrides, use native bools
- Update test values for new structure
@mreid-tt

mreid-tt commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Thank you @stavros-k for the thorough review. I've worked through each of your comments — all CI checks are now passing. Let me know if there's anything else you'd like me to address.

EDIT: Regarding running as non-root — I attempted setting NON_ROOT=true on both the seafile and seadoc containers. The image creates a seafile user (uid 8000) internally when this is set. However, the seafile-mc image's init process creates directories and files as root during its bootstrap, before the permission check runs. This causes the permission validation to fail on every startup, as the newly created files are still root-owned.

The upstream's own documentation for non-root mode requires a manual chmod -R a+rwx on the host after the first deploy, then a container restart — which isn't practical in an automated deployment. The upstream defaults to root for both containers, so I've left it that way for now.

@mreid-tt

Copy link
Copy Markdown
Contributor Author

I've pushed a commit to run seafile and seadoc as non-root. The image supports NON_ROOT=true which creates a seafile user (uid 8000) internally and drops privileges. I also added an nginx dependency on seadoc (when enabled) to fix a startup ordering issue.

Two things needed custom workarounds:

  1. A startup script injected via Docker config — The image's init process creates directories as root during boot, then checks that they're owned by uid 8000. The permissions container can't fix files that don't exist yet, so I inject a script at /etc/my_init.d/99_fix_permissions.sh that runs chown after the directories are created but before the ownership check. This uses the image's built-in extension mechanism rather than modifying its entrypoint.

  2. Nginx dependency on seadoc — Nginx resolves upstream hostnames at startup. Without this dependency, seadoc may not be resolvable yet when nginx parses its config, causing nginx to fail.

Happy to revert to root mode if either of these workarounds is undesirable — the upstream defaults to root for both containers.

@mreid-tt mreid-tt marked this pull request as ready for review June 23, 2026 09:49
"volume": values.storage.redis_data,
} %}
{% set cache = tpl.deps.redis(values.consts.redis_container_name, "redis_image", cache_config, perm_container) %}
{% do cache.container.set_command(["--port", "6379", "--requirepass", values.seafile.redis_password, "--stop-writes-on-bgsave-error", "no"]) %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this?

{% do seafile.environment.add_env("ENABLE_GO_FILESERVER", true) %}
{% do seafile.environment.add_env("JWT_PRIVATE_KEY", values.seafile.jwt_private_key) %}
{% do seafile.environment.add_env("NON_ROOT", true) %}
{% do seafile.configs.add("fix-perms-seafile", "#!/bin/sh\nchown -R 8000:8000 /shared\n", "/etc/my_init.d/99_fix_permissions.sh", "0500") %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely no.
The permissions container purposefully does not alter data permissions.
Bails out on non empty dirs. And a bunch of other checks.

This changes every time permissions of a directory that could contain data.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And to address the elephant in the room.
Its is not "NON ROOT". PID 1 is still running as root.

Comment on lines +3 to +18
server {
listen {{ values.network.web_port.port_number }};
location /health {
return 200;
}
return 301 https://$host{{ ":" ~ values.network.https_port.port_number if values.network.https_port.port_number != 443 else "" }}$request_uri;
}

server {
listen {{ values.network.https_port.port_number }} ssl;
ssl_certificate {{ values.consts.ssl_cert_path }};
ssl_certificate_key {{ values.consts.ssl_key_path }};
{% else %}
server {
listen {{ values.network.web_port.port_number }};
{% endif %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still dont get why we need both?

location /health {
return 200;
}
return 301 https://$host{{ ":" ~ values.network.https_port.port_number if values.network.https_port.port_number != 443 else "" }}$request_uri;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...?

@stavros-k stavros-k marked this pull request as draft June 23, 2026 14:36
truenas added 2 commits June 23, 2026 11:02
Reverts non-root changes (NON_ROOT, fix-perms scripts, perms actions)
due to reviewer concerns. Keeps nginx dependency on seadoc (needed
for startup ordering). Renames mariadb container from db to mariadb
to match convention used by all other community apps.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants