Add Seafile app with optional SeaDoc#4993
Conversation
…n, fix custom-storage paths
- 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
|
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. |
| {% 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"]) %} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@mreid-tt as I see SeaDoc does not have to be /sdoc-server/
you can configure it as SEADOC_SERVER_URL without /sdoc-server/
There was a problem hiding this comment.
@mreid-tt if you want I can share my test custom app yaml for truenas with seafile without Caddy
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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:
- The upstream documentation and default deployment use
/sdoc-server/as the standard path - With the nginx reverse proxy, path-based routing to the seadoc container is clean and well-tested
- The nginx config follows the reference config from the upstream "Use other reverse proxy" docs, which also uses
/sdoc-server/ - 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.
- 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)
| {% 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 "") %} |
There was a problem hiding this comment.
why not always append the :443 or :80?
We can also just strip these 2 suffixes without having to do all these checks.
There was a problem hiding this comment.
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") %} |
There was a problem hiding this comment.
bool values are also working no need to make them strings
There was a problem hiding this comment.
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") %} |
There was a problem hiding this comment.
why does this connect to a different database that the one that we create?
There was a problem hiding this comment.
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) %} |
There was a problem hiding this comment.
what does it share with seadoc?
There was a problem hiding this comment.
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.
| repository: mariadb | ||
| tag: 10.11.18 | ||
| redis_image: | ||
| repository: redis |
There was a problem hiding this comment.
why do we use redis while ALL other apps use valkey?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
why do all containers get a seafile suffix? this will generate a weirdly repeated container name
There was a problem hiding this comment.
Good point. Renamed to bare names: seafile-db → db (matching the upstream seafile-server.yml service name), seafile-nginx → nginx, seafile-seadoc → seadoc. This matches the convention used by wger, n8n, and others.
There was a problem hiding this comment.
I dont think we have any apps that names the container db
| nginx_container_name: seafile-nginx | ||
| seadoc_container_name: seafile-seadoc | ||
| perms_container_name: permissions | ||
| mariadb_port: 3306 |
There was a problem hiding this comment.
why are these consts here?
There was a problem hiding this comment.
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"}) %} |
There was a problem hiding this comment.
why do we need permissions action here if it runs as root?
There was a problem hiding this comment.
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 %} |
There was a problem hiding this comment.
isnt this enforced by the questions schema?
There was a problem hiding this comment.
You're right. Removed the Jinja validation — the required: true in questions.yaml already prevents empty values.
|
Why do we ship seadoc with seafile? Isn't that a compatible WOPI server? |
| tag: 13.0.24 | ||
| mariadb_image: | ||
| repository: mariadb | ||
| tag: 10.11.18 |
There was a problem hiding this comment.
why is mariadb 2 major versions behind?
There was a problem hiding this comment.
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.
| 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> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| description: Memory limit for Seafile. | ||
| schema: | ||
| type: int | ||
| default: 2048 |
There was a problem hiding this comment.
Why is this default different from ALL other apps?
There was a problem hiding this comment.
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.
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 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. |
- 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
|
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 The upstream's own documentation for non-root mode requires a manual |
|
I've pushed a commit to run seafile and seadoc as non-root. The image supports Two things needed custom workarounds:
Happy to revert to root mode if either of these workarounds is undesirable — the upstream defaults to root for both containers. |
| "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"]) %} |
| {% 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") %} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Still does not make sense, as it seems like it DOES chown
https://github.com/haiwen/seafile-docker/blob/f4906bc142b6ad422745bde407fdacb216045696/scripts/scripts_9.0/enterpoint.sh#L26-L37
There was a problem hiding this comment.
And to address the elephant in the room.
Its is not "NON ROOT". PID 1 is still running as root.
| 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 %} |
There was a problem hiding this comment.
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; |
… on seadoc" This reverts commit a92abc3.
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.
App Addition
AI
Description
Adds Seafile to the community train.
Seafile is an open source cloud storage system with privacy preservation and teamwork features. This package includes:
seafileltd/seafile-mc:13.0.24) with MariaDB (mariadb:12.3.2) and Valkey (valkey/valkey:9.1.0)nginxinc/nginx-unprivileged:1.31.1) with optional TrueNAS certificate support for HTTPSseafileltd/sdoc-server:2.0.9) for collaborative document editingApp Information
Testing
Tested locally with:
All tests passed successfully (render and deployment).
Icons and Screenshots
Please upload the following to the CDN:
Special Notes
my_initas PID 1, which requires root. The container runs as root; the init system drops privileges for seafile processes internally.SETUID/SETGIDcapabilities. Applied viacap_addsince the library defaults tocap_drop: [ALL].enable_seadocis true, the sdoc-server container is deployed alongside seafile.nginxinc/nginx-unprivileged) serves as the reverse proxy with dynamic port mapping (ports >= 1024), eliminating the need forNET_BIND_SERVICEcapability. Nginx config is injected via Docker configs.30441) with HTTP to HTTPS redirect on30440.--stop-writes-on-bgsave-error noto ensure compatibility with Seafile's redis client.Checklist