From ae8fe0026db4b019b92ade18f717173464b0cf41 Mon Sep 17 00:00:00 2001 From: nicholai Date: Tue, 16 Sep 2025 12:38:41 -0600 Subject: [PATCH] Initial commit: Seafile single-container image for TrueNAS SCALE --- .gitignore | 4 + Dockerfile | 88 +++++++ README.md | 162 ++++++++++++ docker/ccnet.conf.template | 9 + docker/entrypoint.sh | 381 +++++++++++++++++++++++++++++ docker/gunicorn.conf.py | 36 +++ docker/healthcheck.sh | 33 +++ docker/init_seahub.sh | 100 ++++++++ docker/nginx.conf.template | 100 ++++++++ docker/seafile.conf.template | 18 ++ docker/seahub_settings.py.template | 64 +++++ docker/supervisord.conf.template | 92 +++++++ implementation_plan.md | 187 ++++++++++++++ 13 files changed, 1274 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker/ccnet.conf.template create mode 100644 docker/entrypoint.sh create mode 100644 docker/gunicorn.conf.py create mode 100644 docker/healthcheck.sh create mode 100644 docker/init_seahub.sh create mode 100644 docker/nginx.conf.template create mode 100644 docker/seafile.conf.template create mode 100644 docker/seahub_settings.py.template create mode 100644 docker/supervisord.conf.template create mode 100644 implementation_plan.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c656cd0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +seafile-server/ +seahub/ +.seafile-data/ +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e1ac486 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,88 @@ +# Seafile single-container image for TrueNAS SCALE Dragonfish +# Base: Debian Bookworm Slim for stable apt packages (nginx, mariadb, redis) +FROM debian:bookworm-slim + +ENV DEBIAN_FRONTEND=noninteractive \ + LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 \ + TZ=UTC \ + SEAFILE_HOME=/opt/seafile \ + SEAFILE_DATA_DIR=/data/seafile-data \ + SEAFILE_CONF_DIR=/data/conf \ + SEAHUB_MEDIA_DIR=/data/seahub-media \ + LOG_DIR=/data/logs + +# Optional build-time args (not used to download by default; runtime entrypoint handles artifacts) +ARG SEAFILE_VERSION="" +ARG SEAFILE_TGZ_URL="" + +# OS packages +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + nginx \ + supervisor \ + mariadb-server \ + redis-server \ + python3 \ + python3-venv \ + python3-pip \ + curl \ + ca-certificates \ + tzdata \ + procps \ + gosu; \ + rm -rf /var/lib/apt/lists/* + +# System users/groups (many packages create their own, we ensure 'seafile' app user) +RUN set -eux; \ + groupadd -r seafile; \ + useradd -r -g seafile -d ${SEAFILE_HOME} -s /usr/sbin/nologin seafile || true; \ + mkdir -p ${SEAFILE_HOME} ${SEAFILE_HOME}/docker \ + /data/conf /data/seafile-data /data/db /data/redis /data/seahub-media /data/logs /data/ssl \ + /var/log/nginx /var/run/nginx; \ + chown -R seafile:seafile ${SEAFILE_HOME}; \ + chown -R www-data:www-data /var/log/nginx /var/run/nginx; \ + # MariaDB and Redis dirs will be owned by respective users at runtime init + true + +# Copy runtime scripts and templates (will be rendered at container start) +# Expect these files to be created in repo under docker/ +COPY docker/ ${SEAFILE_HOME}/docker/ + +# Make scripts executable +RUN set -eux; \ + find ${SEAFILE_HOME}/docker -type f -name "*.sh" -exec chmod +x {} \;; \ + chmod 0644 ${SEAFILE_HOME}/docker/supervisord.conf.template || true; \ + chmod 0644 ${SEAFILE_HOME}/docker/nginx.conf.template || true; \ + chmod 0644 ${SEAFILE_HOME}/docker/gunicorn.conf.py || true; \ + chmod 0644 ${SEAFILE_HOME}/docker/seahub_settings.py.template || true + +# Environment defaults (can be overridden by TrueNAS app env) +ENV SEAFILE_SERVER_HOSTNAME=localhost \ + SEAFILE_SERVER_URL=http://localhost \ + ADMIN_EMAIL= \ + ADMIN_PASSWORD= \ + DB_ROOT_PASSWORD= \ + DB_NAME=seafile \ + DB_USER=seafile \ + DB_PASSWORD= \ + DB_NAME_SEAHUB=seahub_db \ + DB_NAME_SEAFILE=seafile_db \ + DB_NAME_CCNET=ccnet_db \ + REDIS_URL=redis://127.0.0.1:6379/0 \ + TIMEZONE=UTC \ + NGINX_MAX_BODY=200m \ + SSL_ENABLE=false + +# Ports +EXPOSE 80 443 + +# Healthcheck +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=10 CMD [ -x "${SEAFILE_HOME}/docker/healthcheck.sh" ] && ${SEAFILE_HOME}/docker/healthcheck.sh || exit 1 + +VOLUME ["/data"] + +# Entrypoint manages idempotent bootstrap then hands off to supervisord +ENTRYPOINT ["/opt/seafile/docker/entrypoint.sh"] +CMD ["start"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..89d8a99 --- /dev/null +++ b/README.md @@ -0,0 +1,162 @@ +Seafile Single-Container Image for TrueNAS SCALE Dragonfish +============================================================ + +This repository packages Seafile Server Core (seafile-server) and Seahub (Django web UI) into a single container suitable for TrueNAS SCALE Dragonfish Custom App deployment. + +Components inside the container: +- Nginx reverse proxy (ports 80/443) → proxies to internal Gunicorn (Seahub) and the Go fileserver +- Gunicorn serving Seahub (Django) on 127.0.0.1:8000 +- Seafile core (seaf-server) and fileserver (Go) via seafile.sh +- MariaDB (in-container) +- Redis (in-container) +- Single persistent volume at /data storing configuration, databases, media, and logs + +Persistent layout under /data: +- /data/conf → central Seafile/Seahub config (seafile.conf, ccnet.conf, seahub_settings.py, seahub_secret_key.txt) +- /data/seafile-data → Seafile data store +- /data/db → MariaDB datadir +- /data/redis → Redis data +- /data/seahub-media → Seahub uploads/media +- /data/logs → logs (nginx, seahub, seafile, supervisord, mariadb, redis) +- /data/ssl → TLS certs (optional if terminating TLS in-container) + +Files +----- +- Dockerfile → Debian Bookworm base image with nginx, supervisor, mariadb, redis, python venv +- docker/entrypoint.sh → Idempotent bootstrap and configuration renderer, then execs supervisord +- docker/supervisord.conf.template → supervisord programs for mariadb, redis, seafile core, seahub, nginx +- docker/nginx.conf.template → reverse proxy and upload limits +- docker/gunicorn.conf.py → Gunicorn settings for Seahub +- docker/seahub_settings.py.template → rendered to /data/conf/seahub_settings.py +- docker/seafile.conf.template → rendered to /data/conf/seafile.conf +- docker/ccnet.conf.template → rendered to /data/conf/ccnet.conf +- docker/init_seahub.sh → one-shot collectstatic, migrations, optional admin creation +- docker/healthcheck.sh → container healthcheck + +Environment Variables +--------------------- +Required at first run (recommended): +- SEAFILE_SERVER_HOSTNAME: your public hostname (e.g., files.example.com) +- SEAFILE_SERVER_URL: full base URL (e.g., https://files.example.com) +- DB_ROOT_PASSWORD: MariaDB root password for bootstrap (required on first run) +- DB_PASSWORD: password for application DB user (seafile) + +Optional (defaults in Dockerfile): +- DB_USER=seafile +- DB_NAME (generic bootstrap DB used by the plan; typically not required by Seafile) +- DB_NAME_SEAHUB=seahub_db +- DB_NAME_SEAFILE=seafile_db +- DB_NAME_CCNET=ccnet_db +- REDIS_URL=redis://127.0.0.1:6379/0 +- TIMEZONE=UTC +- NGINX_MAX_BODY=200m (upload size limit) +- SSL_ENABLE=false (if you want nginx to terminate TLS with /data/ssl/{fullchain.pem,privkey.pem}, adjust nginx template or provide a values override) +- ADMIN_EMAIL and ADMIN_PASSWORD (optional) → auto-create admin user on first run +- SEAFILE_TGZ_URL (optional) → URL to an official Seafile server release tarball. If provided, the entrypoint will download and extract it into /opt/seafile/seafile-server-latest + +Notes on Releases +----------------- +By default, this image expects an official Seafile server release to be present at: +- /opt/seafile/seafile-server-latest + +You can satisfy this in one of two ways: +1) Provide SEAFILE_TGZ_URL (preferred): The entrypoint will download and extract on first start. +2) Bake or mount a release: Place the extracted release at /opt/seafile/seafile-server-latest (e.g., by modifying the Dockerfile to ADD/COPY it, or by mounting in TrueNAS using an additional hostPath volume). + +TrueNAS SCALE Custom App Configuration +-------------------------------------- +- Image: build and push the built Docker image, then reference it in your custom app +- Ports: + - TCP 80 → host or ingress (required) + - TCP 443 → host or ingress (optional if terminating TLS externally) +- Storage: + - PVC mounted at /data (ReadWriteOnce) +- Environment: + - SEAFILE_SERVER_HOSTNAME=files.example.com + - SEAFILE_SERVER_URL=https://files.example.com + - DB_ROOT_PASSWORD=your-root-password + - DB_PASSWORD=your-app-password + - DB_NAME_SEAHUB=seahub_db + - DB_NAME_SEAFILE=seafile_db + - DB_NAME_CCNET=ccnet_db + - ADMIN_EMAIL=admin@example.com (optional) + - ADMIN_PASSWORD=change-me (optional) + - NGINX_MAX_BODY=200m (adjust as desired) + - TIMEZONE=UTC + - SEAFILE_TGZ_URL=https://example.com/path/to/seafile-server_X.Y.Z_x86-64.tar.gz (optional, recommended) +- Healthcheck: + - Container includes a HEALTHCHECK that probes Seahub and the fileserver. + +First Run Flow +-------------- +On container start, the entrypoint will: +1) Create /data subdirectories; write /etc/redis/redis.conf +2) Initialize MariaDB at /data/db if empty: + - Set root password (DB_ROOT_PASSWORD) + - Create DBs: DB_NAME_SEAHUB, DB_NAME_SEAFILE, DB_NAME_CCNET + - Create user DB_USER with DB_PASSWORD; grant privileges +3) Optionally download and extract Seafile server release if SEAFILE_TGZ_URL is provided +4) Render configs: + - /etc/nginx/nginx.conf from template + - /etc/supervisor/supervisord.conf from template + - /data/conf/seahub_settings.py from template (uses env and SECRET_KEY) + - /data/conf/seafile.conf and /data/conf/ccnet.conf from templates +5) Create Python venv and install Seahub requirements from the release +6) Supervisord starts: + - mariadbd + - redis-server + - seafile (seafile.sh start) + - seahub (runs docker/init_seahub.sh once, then gunicorn) + - nginx + +Nginx Routing +------------- +- / → Gunicorn (Seahub) at 127.0.0.1:8000 +- /seafhttp → fileserver at 127.0.0.1:8082 +- /media → /data/seahub-media + +Build & Run (Local) +------------------- +Build: +- docker build -t seafile-single:local . + +Run (example): +- mkdir -p /host/seafile-data +- docker run -it --rm \ + -e SEAFILE_SERVER_HOSTNAME=localhost \ + -e SEAFILE_SERVER_URL=http://localhost \ + -e DB_ROOT_PASSWORD=changeme \ + -e DB_PASSWORD=changeme \ + -e ADMIN_EMAIL=admin@example.com \ + -e ADMIN_PASSWORD=changeme \ + -p 80:80 \ + -v /host/seafile-data:/data \ + seafile-single:local + +Then open http://localhost and log in with the admin credentials. + +From-Source Variant (Optional) +------------------------------ +If you need to build from the provided sources instead of using official releases: +- seafile-server (C, autotools) found under ./seafile-server +- fileserver (Go) under ./seafile-server/fileserver +- Seahub (Django) under ./seahub + +You will need to: +- Install build dependencies (see seafile-server/configure.ac) +- Build and install seafile core and fileserver into /opt/seafile/seafile-server-latest +- Use seahub/requirements.txt for Python dependencies +- Ensure templates in docker/ still render configs into /data/conf + +Testing Checklist +----------------- +- Healthcheck: container becomes healthy (Seahub login page, fileserver protocol-version endpoint) +- Admin creation: login with ADMIN_EMAIL/ADMIN_PASSWORD +- Upload flow: create library, upload small file; verify in /data/seafile-data +- Persistence: stop/start container; confirm data intact +- Logs: /data/logs/* should not contain critical errors + +Known Notes +----------- +- The container expects a compatible Seafile server release and Seahub version. +- If terminating TLS at Nginx inside the container, copy certs to /data/ssl and adapt nginx.conf.template accordingly (or add a values override in TrueNAS). diff --git a/docker/ccnet.conf.template b/docker/ccnet.conf.template new file mode 100644 index 0000000..333d7d8 --- /dev/null +++ b/docker/ccnet.conf.template @@ -0,0 +1,9 @@ +# Rendered by entrypoint.sh into /data/conf/ccnet.conf + +[General] +SERVICE_URL = {{SERVER_URL}} + +# Optional settings: +# USER_NAME = seafile +# NAME = Seafile +# PORT = 10001 diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..34648f4 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,381 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Globals / defaults +SEAFILE_HOME=${SEAFILE_HOME:-/opt/seafile} +SEAF_RELEASE_DIR_DEFAULT="${SEAFILE_HOME}/seafile-server-latest" +SEAF_RELEASE_DIR="${SEAF_RELEASE_DIR:-$SEAF_RELEASE_DIR_DEFAULT}" + +SEAFILE_CONF_DIR=${SEAFILE_CONF_DIR:-/data/conf} +SEAFILE_DATA_DIR=${SEAFILE_DATA_DIR:-/data/seafile-data} +SEAHUB_MEDIA_DIR=${SEAHUB_MEDIA_DIR:-/data/seahub-media} +LOG_DIR=${LOG_DIR:-/data/logs} +TIMEZONE=${TIMEZONE:-UTC} +SEAFILE_SERVER_HOSTNAME=${SEAFILE_SERVER_HOSTNAME:-localhost} +SEAFILE_SERVER_URL=${SEAFILE_SERVER_URL:-http://localhost} +REDIS_URL=${REDIS_URL:-redis://127.0.0.1:6379/0} +DB_NAME=${DB_NAME:-seafile} +DB_USER=${DB_USER:-seafile} +DB_PASSWORD=${DB_PASSWORD:-} +DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD:-} +NGINX_MAX_BODY=${NGINX_MAX_BODY:-200m} +SSL_ENABLE=${SSL_ENABLE:-false} + +# Commands +MYSQLD_BIN="/usr/sbin/mariadbd" +MYSQL_BIN="/usr/bin/mysql" +MYSQLADMIN_BIN="/usr/bin/mysqladmin" +MARIADB_INSTALL_DB="/usr/bin/mariadb-install-db" +REDIS_SERVER_BIN="/usr/bin/redis-server" +NGINX_BIN="/usr/sbin/nginx" +SUPERVISORD_BIN="/usr/bin/supervisord" + +SUPERVISORD_CONF="/etc/supervisor/supervisord.conf" +NGINX_CONF="/etc/nginx/nginx.conf" +REDIS_CONF="/etc/redis/redis.conf" + +HEALTH_URL_SEAHUB="http://127.0.0.1:8000/accounts/login/" +HEALTH_URL_FILESERVER="http://127.0.0.1:8082/protocol-version" + +log() { + echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] $*" +} + +die() { + echo "ERROR: $*" >&2 + exit 1 +} + +ensure_dirs() { + log "Ensuring directory layout under /data and runtime dirs" + mkdir -p \ + "${SEAFILE_HOME}" \ + "${SEAFILE_HOME}/releases" \ + "${SEAFILE_CONF_DIR}" \ + "${SEAFILE_DATA_DIR}" \ + /data/db \ + /data/redis \ + "${SEAHUB_MEDIA_DIR}" \ + "${LOG_DIR}/nginx" \ + "${LOG_DIR}/seahub" \ + "${LOG_DIR}/seafile" \ + "${LOG_DIR}/supervisor" \ + "${LOG_DIR}/mariadb" \ + "${LOG_DIR}/redis" \ + /data/ssl \ + /run/mysqld \ + /run/redis + + chown -R seafile:seafile "${SEAFILE_HOME}" "${SEAFILE_CONF_DIR}" "${SEAFILE_DATA_DIR}" "${SEAHUB_MEDIA_DIR}" "${LOG_DIR}" + chown -R mysql:mysql /data/db /run/mysqld || true + chown -R redis:redis /data/redis /run/redis || true + chown -R www-data:www-data /var/log/nginx || true +} + +configure_timezone() { + log "Configuring timezone: ${TIMEZONE}" + echo "${TIMEZONE}" >/etc/timezone + ln -sf "/usr/share/zoneinfo/${TIMEZONE}" /etc/localtime || true +} + +# Basic poor-man's template renderer: replace {{VAR}} tokens using sed +# Usage: render_file TEMPLATE DEST +render_file() { + local template="$1" + local dest="$2" + [[ -f "$template" ]] || die "Template not found: $template" + + sed \ + -e "s|{{SERVER_NAME}}|${SEAFILE_SERVER_HOSTNAME}|g" \ + -e "s|{{SERVER_URL}}|${SEAFILE_SERVER_URL}|g" \ + -e "s|{{NGINX_MAX_BODY}}|${NGINX_MAX_BODY}|g" \ + -e "s|{{SEAF_RELEASE_DIR}}|${SEAF_RELEASE_DIR}|g" \ + -e "s|{{SEAFILE_CONF_DIR}}|${SEAFILE_CONF_DIR}|g" \ + -e "s|{{SEAFILE_DATA_DIR}}|${SEAFILE_DATA_DIR}|g" \ + -e "s|{{SEAHUB_MEDIA_DIR}}|${SEAHUB_MEDIA_DIR}|g" \ + -e "s|{{LOG_DIR}}|${LOG_DIR}|g" \ + -e "s|{{DB_NAME}}|${DB_NAME}|g" \ + -e "s|{{DB_USER}}|${DB_USER}|g" \ + -e "s|{{DB_PASSWORD}}|${DB_PASSWORD}|g" \ + -e "s|{{DB_NAME_SEAFILE}}|${DB_NAME_SEAFILE:-seafile_db}|g" \ + -e "s|{{DB_NAME_CCNET}}|${DB_NAME_CCNET:-ccnet_db}|g" \ + -e "s|{{REDIS_URL}}|${REDIS_URL}|g" \ + <"$template" >"$dest" +} + +init_redis() { + log "Writing Redis config" + cat >"${REDIS_CONF}" <<'EOF' +bind 127.0.0.1 ::1 +protected-mode yes +port 6379 +daemonize no +supervised no +pidfile /run/redis/redis-server.pid +dir /data/redis +save 900 1 +save 300 10 +save 60 10000 +appendonly yes +EOF + chown -R redis:redis /data/redis +} + +wait_for_mysql_socket() { + local timeout="${1:-60}" + local i=0 + while ! "${MYSQLADMIN_BIN}" ping --silent --protocol=socket --socket=/run/mysqld/mysqld.sock >/dev/null 2>&1; do + sleep 1 + i=$((i+1)) + if [[ $i -ge $timeout ]]; then + die "MariaDB did not become ready in ${timeout}s" + fi + done +} + +init_mariadb() { + if [[ -d /data/db/mysql ]]; then + log "MariaDB data directory already initialized" + return + fi + [[ -n "${DB_ROOT_PASSWORD}" ]] || die "DB_ROOT_PASSWORD must be set for MariaDB bootstrap" + log "Initializing MariaDB datadir at /data/db" + install -d -o mysql -g mysql /data/db /run/mysqld + "${MARIADB_INSTALL_DB}" --user=mysql --datadir=/data/db --skip-test-db + + log "Starting temporary MariaDB to set root password and create DB/user" + "${MYSQLD_BIN}" \ + --datadir=/data/db \ + --socket=/run/mysqld/mysqld.sock \ + --bind-address=127.0.0.1 \ + --skip-networking=0 \ + --skip-name-resolve \ + --skip-symbolic-links \ + --log-bin=OFF \ + --pid-file=/run/mysqld/mysqld.pid \ + --user=mysql >/dev/null 2>&1 & + local mysqld_pid=$! + + wait_for_mysql_socket 60 + + log "Configuring root password and seafile database" + "${MYSQL_BIN}" --protocol=socket --socket=/run/mysqld/mysqld.sock -uroot <<-SQL + ALTER USER 'root'@'localhost' IDENTIFIED BY '${DB_ROOT_PASSWORD}'; + FLUSH PRIVILEGES(); +SQL + + "${MYSQL_BIN}" --protocol=socket --socket=/run/mysqld/mysqld.sock -uroot -p"${DB_ROOT_PASSWORD}" <<-SQL + CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + CREATE DATABASE IF NOT EXISTS \`${DB_NAME_SEAFILE:-seafile_db}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + CREATE DATABASE IF NOT EXISTS \`${DB_NAME_CCNET:-ccnet_db}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + CREATE DATABASE IF NOT EXISTS \`${DB_NAME_SEAHUB:-seahub_db}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + + CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASSWORD}'; + + GRANT ALL PRIVILEGES ON \`${DB_NAME}\`.* TO '${DB_USER}'@'localhost'; + GRANT ALL PRIVILEGES ON \`${DB_NAME_SEAFILE:-seafile_db}\`.* TO '${DB_USER}'@'localhost'; + GRANT ALL PRIVILEGES ON \`${DB_NAME_CCNET:-ccnet_db}\`.* TO '${DB_USER}'@'localhost'; + GRANT ALL PRIVILEGES ON \`${DB_NAME_SEAHUB:-seahub_db}\`.* TO '${DB_USER}'@'localhost'; + + FLUSH PRIVILEGES(); +SQL + + log "Shutting down temporary MariaDB" + "${MYSQLADMIN_BIN}" --protocol=socket --socket=/run/mysqld/mysqld.sock -uroot -p"${DB_ROOT_PASSWORD}" shutdown || true + wait $mysqld_pid || true +} + +# Optional runtime download of official Seafile server release if not present. +# If SEAFILE_TGZ_URL or SEAFILE_VERSION provided, try to fetch. +fetch_seafile_release_if_needed() { + if [[ -d "${SEAF_RELEASE_DIR}" ]]; then + log "Seafile release directory present: ${SEAF_RELEASE_DIR}" + return + fi + local url="${SEAFILE_TGZ_URL:-}" + if [[ -z "${url}" && -n "${SEAFILE_VERSION:-}" ]]; then + # Fallback to official static host if only version is provided + url="https://download.seadrive.org/seafile-server_${SEAFILE_VERSION}_x86-64.tar.gz" + fi + + if [[ -z "${url}" ]]; then + log "No Seafile release present and no SEAFILE_TGZ_URL provided. Skipping download (expect release to be baked into image or mounted)." + return + fi + + log "Downloading Seafile release from ${url}" + mkdir -p "${SEAFILE_HOME}/releases" + local tgz="${SEAFILE_HOME}/releases/seafile-server.tgz" + curl -fsSL "${url}" -o "${tgz}" + tar -xzf "${tgz}" -C "${SEAFILE_HOME}/releases" + # Try to locate extracted dir + local extracted + extracted="$(tar -tzf "${tgz}" | head -1 | cut -d/ -f1)" + if [[ -n "${extracted}" && -d "${SEAFILE_HOME}/releases/${extracted}" ]]; then + ln -s "${SEAFILE_HOME}/releases/${extracted}" "${SEAF_RELEASE_DIR_DEFAULT}" + log "Linked ${SEAF_RELEASE_DIR_DEFAULT} -> ${SEAFILE_HOME}/releases/${extracted}" + else + log "Could not detect extracted directory; ensure ${SEAF_RELEASE_DIR} exists" + fi +} + +init_seahub_secret_key() { + local keyfile="${SEAFILE_CONF_DIR}/seahub_secret_key.txt" + if [[ ! -s "${keyfile}" ]]; then + log "Generating Django SECRET_KEY" + python3 - <<'PY' +import secrets, string, os, sys +alphabet = string.ascii_letters + string.digits + string.punctuation +key = ''.join(secrets.choice(alphabet) for _ in range(64)) +print(key) +PY + python3 - <"${keyfile}" +import secrets, string +alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)" +print(''.join(secrets.choice(alphabet) for _ in range(64))) +PY + chown seafile:seafile "${keyfile}" + chmod 600 "${keyfile}" + fi +} + +render_configs() { + log "Rendering Nginx and Supervisord configs from templates" + render_file "${SEAFILE_HOME}/docker/nginx.conf.template" "${NGINX_CONF}" + render_file "${SEAFILE_HOME}/docker/supervisord.conf.template" "${SUPERVISORD_CONF}" +} + +init_python_and_requirements() { + log "Setting up Python virtual environment" + local venv="${SEAFILE_HOME}/venv" + if [[ ! -x "${venv}/bin/python" ]]; then + python3 -m venv "${venv}" + "${venv}/bin/pip" install --no-cache-dir --upgrade pip wheel setuptools + fi + + # Install Seahub requirements from release if available + local req="${SEAF_RELEASE_DIR}/seahub/requirements.txt" + if [[ -f "${req}" ]]; then + log "Installing Seahub Python requirements from ${req}" + "${venv}/bin/pip" install --no-cache-dir -r "${req}" + else + log "Seahub requirements.txt not found at ${req}. Ensure release is present or install separately." + fi +} + +init_seahub_settings() { + init_seahub_secret_key + local settings_target="${SEAFILE_CONF_DIR}/seahub_settings.py" + if [[ ! -f "${settings_target}" ]]; then + log "Rendering seahub_settings.py" + render_file "${SEAFILE_HOME}/docker/seahub_settings.py.template" "${settings_target}" + chown seafile:seafile "${settings_target}" + fi +} + +init_seafile_confs() { + # Render seafile core config files if missing + local seafile_conf="${SEAFILE_CONF_DIR}/seafile.conf" + local ccnet_conf="${SEAFILE_CONF_DIR}/ccnet.conf" + + if [[ ! -f "${seafile_conf}" ]]; then + log "Rendering seafile.conf" + render_file "${SEAFILE_HOME}/docker/seafile.conf.template" "${seafile_conf}" + chown seafile:seafile "${seafile_conf}" + fi + + if [[ ! -f "${ccnet_conf}" ]]; then + log "Rendering ccnet.conf" + render_file "${SEAFILE_HOME}/docker/ccnet.conf.template" "${ccnet_conf}" + chown seafile:seafile "${ccnet_conf}" + fi +} + +django_manage() { + # Run Django manage.py commands within release's seahub directory using venv python + local venv="${SEAFILE_HOME}/venv" + local seahub_dir="${SEAF_RELEASE_DIR}/seahub" + + [[ -x "${venv}/bin/python" ]] || die "Python venv missing" + [[ -d "${seahub_dir}" ]] || { log "Seahub dir not found at ${seahub_dir}, skipping manage.py"; return; } + + pushd "${seahub_dir}" >/dev/null + # Ensure env + export PYTHONPATH="${SEAF_RELEASE_DIR}:${PYTHONPATH:-}" + export DJANGO_SETTINGS_MODULE="seahub.settings" + export CCNET_CONF_DIR="${SEAFILE_CONF_DIR}" + export SEAFILE_CENTRAL_CONF_DIR="${SEAFILE_CONF_DIR}" + export SEAFILE_CONF_DIR="${SEAFILE_CONF_DIR}" + export SEAFILE_DATA_DIR="${SEAFILE_DATA_DIR}" + + log "Running Django collectstatic (noinput)" + "${venv}/bin/python" manage.py collectstatic --noinput || true + + log "Running Django migrate" + "${venv}/bin/python" manage.py migrate --noinput || true + + if [[ -n "${ADMIN_EMAIL}" && -n "${ADMIN_PASSWORD}" ]]; then + log "Ensuring admin user ${ADMIN_EMAIL}" + "${venv}/bin/python" manage.py shell </dev/null +} + +wait_for_services() { + log "Waiting for MariaDB socket..." + wait_for_mysql_socket 60 + + log "Waiting for Redis..." + local i=0 + while ! (echo >/dev/tcp/127.0.0.1/6379) 2>/dev/null; do + sleep 1 + i=$((i+1)) + if [[ $i -ge 60 ]]; then + die "Redis did not open port 6379 in 60s" + fi + done +} + +start_supervisord() { + log "Starting supervisord" + exec "${SUPERVISORD_BIN}" -n -c "${SUPERVISORD_CONF}" +} + +bootstrap() { + ensure_dirs + configure_timezone + init_redis + init_mariadb + fetch_seafile_release_if_needed + render_configs + init_python_and_requirements + init_seahub_settings + init_seafile_confs + # Note: seafile core DB schema is created/managed by seafile services; Seahub migrations handled above +} + +case "${1:-start}" in + start) + bootstrap + start_supervisord + ;; + bash|sh) + exec "$@" + ;; + *) + # Pass-through to allow running arbitrary commands + exec "$@" + ;; +esac diff --git a/docker/gunicorn.conf.py b/docker/gunicorn.conf.py new file mode 100644 index 0000000..c725801 --- /dev/null +++ b/docker/gunicorn.conf.py @@ -0,0 +1,36 @@ +# Gunicorn configuration for Seahub (Django) +# Used by supervisord program `seahub` +# Note: Logs go to /data/logs/seahub; supervisord also captures stdout/stderr + +import multiprocessing +import os + +# Bind to localhost; nginx proxies to this +bind = "127.0.0.1:8000" + +# Workers/threads +workers = max(multiprocessing.cpu_count(), 2) +worker_class = "gthread" +threads = 4 +preload_app = True + +# Timeouts: uploads can be long-running +timeout = 1200 +graceful_timeout = 60 +keepalive = 5 + +# Logging +accesslog = "/data/logs/seahub/gunicorn.access.log" +errorlog = "/data/logs/seahub/gunicorn.error.log" +loglevel = "info" + +# Security / proxy +forwarded_allow_ips = "*" +proxy_allow_ips = "*" + +# Env vars (supervisord sets DJANGO_SETTINGS_MODULE, PYTHONPATH, etc.) +raw_env = [ + f"SEAFILE_CONF_DIR={os.environ.get('SEAFILE_CONF_DIR', '/data/conf')}", + f"SEAFILE_CENTRAL_CONF_DIR={os.environ.get('SEAFILE_CENTRAL_CONF_DIR', '/data/conf')}", + f"SEAFILE_DATA_DIR={os.environ.get('SEAFILE_DATA_DIR', '/data/seafile-data')}", +] diff --git a/docker/healthcheck.sh b/docker/healthcheck.sh new file mode 100644 index 0000000..80ce456 --- /dev/null +++ b/docker/healthcheck.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Container healthcheck for Seafile single-container stack +# Returns 0 if both Seahub (via nginx) and fileserver respond, non-zero otherwise. +set -Eeuo pipefail + +curl_opts=(--silent --show-error --fail --location --connect-timeout 2 --max-time 5) + +check_seahub() { + local code + code="$(curl "${curl_opts[@]}" -o /dev/null -w "%{http_code}" "http://127.0.0.1/accounts/login/")" || return 1 + # Accept 2xx and 3xx + if [[ "${code}" =~ ^2[0-9]{2}$ || "${code}" =~ ^3[0-9]{2}$ ]]; then + return 0 + fi + return 1 +} + +check_fileserver() { + # Try direct fileserver port first + if curl "${curl_opts[@]}" "http://127.0.0.1:8082/protocol-version" >/dev/null 2>&1; then + return 0 + fi + # Fallback through nginx route + curl "${curl_opts[@]}" "http://127.0.0.1/seafhttp/protocol-version" >/dev/null 2>&1 +} + +main() { + check_seahub || exit 1 + check_fileserver || exit 1 + exit 0 +} + +main "$@" diff --git a/docker/init_seahub.sh b/docker/init_seahub.sh new file mode 100644 index 0000000..1795387 --- /dev/null +++ b/docker/init_seahub.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# One-shot initializer for Seahub (collectstatic, migrate, optional admin creation) +# Run under supervisord before starting gunicorn +set -Eeuo pipefail + +SEAFILE_HOME=${SEAFILE_HOME:-/opt/seafile} +SEAF_RELEASE_DIR_DEFAULT="${SEAFILE_HOME}/seafile-server-latest" +SEAF_RELEASE_DIR="${SEAF_RELEASE_DIR:-$SEAF_RELEASE_DIR_DEFAULT}" +SEAFILE_CONF_DIR=${SEAFILE_CONF_DIR:-/data/conf} +SEAFILE_DATA_DIR=${SEAFILE_DATA_DIR:-/data/seafile-data} + +ADMIN_EMAIL=${ADMIN_EMAIL:-} +ADMIN_PASSWORD=${ADMIN_PASSWORD:-} + +MYSQLADMIN_BIN=${MYSQLADMIN_BIN:-/usr/bin/mysqladmin} +MYSQL_SOCKET=${MYSQL_SOCKET:-/run/mysqld/mysqld.sock} + +log() { + echo "[seahub-init $(date +'%Y-%m-%dT%H:%M:%S%z')] $*" +} + +wait_for_mariadb() { + local timeout="${1:-120}" + local i=0 + log "Waiting for MariaDB to become ready..." + while ! "${MYSQLADMIN_BIN}" ping --silent --protocol=socket --socket="${MYSQL_SOCKET}" >/dev/null 2>&1; do + sleep 1 + i=$((i+1)) + if [[ $i -ge $timeout ]]; then + log "MariaDB did not become ready in ${timeout}s" + return 1 + fi + done + log "MariaDB is ready." +} + +main() { + # Avoid re-running if we've already initialized (basic sentinel) + local sentinel="${SEAFILE_CONF_DIR}/.seahub_initialized" + if [[ -f "${sentinel}" ]]; then + log "Seahub already initialized, nothing to do." + return 0 + fi + + wait_for_mariadb || true + + local venv="${SEAFILE_HOME}/venv" + local seahub_dir="${SEAF_RELEASE_DIR}/seahub" + + if [[ ! -x "${venv}/bin/python" ]]; then + log "Python venv missing at ${venv}; skipping init." + return 0 + fi + if [[ ! -d "${seahub_dir}" ]]; then + log "Seahub directory missing at ${seahub_dir}; skipping init." + return 0 + fi + + pushd "${seahub_dir}" >/dev/null + export PYTHONPATH="${SEAF_RELEASE_DIR}:${PYTHONPATH:-}" + export DJANGO_SETTINGS_MODULE="seahub.settings" + export CCNET_CONF_DIR="${SEAFILE_CONF_DIR}" + export SEAFILE_CENTRAL_CONF_DIR="${SEAFILE_CONF_DIR}" + export SEAFILE_CONF_DIR="${SEAFILE_CONF_DIR}" + export SEAFILE_DATA_DIR="${SEAFILE_DATA_DIR}" + + log "Running collectstatic --noinput" + if ! "${venv}/bin/python" manage.py collectstatic --noinput; then + log "collectstatic failed (continuing)" + fi + + log "Running migrate --noinput" + if ! "${venv}/bin/python" manage.py migrate --noinput; then + log "migrate failed (continuing)" + fi + + if [[ -n "${ADMIN_EMAIL}" && -n "${ADMIN_PASSWORD}" ]]; then + log "Ensuring admin user ${ADMIN_EMAIL}" + "${venv}/bin/python" manage.py shell </dev/null + + touch "${sentinel}" + log "Seahub initialization complete." +} + +main "$@" diff --git a/docker/nginx.conf.template b/docker/nginx.conf.template new file mode 100644 index 0000000..0e3ccb2 --- /dev/null +++ b/docker/nginx.conf.template @@ -0,0 +1,100 @@ +# Nginx config for Seafile single-container +# Rendered by entrypoint.sh -> /etc/nginx/nginx.conf + +user www-data; +worker_processes auto; +pid /run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + server_tokens off; + + client_max_body_size {{NGINX_MAX_BODY}}; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log {{LOG_DIR}}/nginx/access.log main; + error_log {{LOG_DIR}}/nginx/error.log warn; + + upstream seahub { + server 127.0.0.1:8000; + keepalive 32; + } + + upstream fileserver { + server 127.0.0.1:8082; + keepalive 32; + } + + server { + listen 80 default_server; + listen [::]:80 default_server; + server_name {{SERVER_NAME}}; + + # If you terminate TLS here, add: + # listen 443 ssl http2; + # ssl_certificate /data/ssl/fullchain.pem; + # ssl_certificate_key /data/ssl/privkey.pem; + + # Seahub (Django) app + location / { + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 1200; + proxy_connect_timeout 90; + proxy_pass http://seahub; + } + + # Seafile Go fileserver + location /seafhttp { + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + + # Large uploads + client_max_body_size {{NGINX_MAX_BODY}}; + proxy_request_buffering off; + proxy_buffering off; + + proxy_read_timeout 1200; + proxy_connect_timeout 90; + + proxy_pass http://fileserver; + } + + # Seahub media (user uploads, avatars) stored on /data + location /media { + alias {{SEAHUB_MEDIA_DIR}}; + access_log off; + expires 7d; + add_header Cache-Control "public"; + } + + # Optional: serve collected static if needed (usually handled by Django collectstatic) + # location /assets { + # alias {{SEAF_RELEASE_DIR}}/seahub/static; + # access_log off; + # expires 7d; + # add_header Cache-Control "public"; + # } + } +} diff --git a/docker/seafile.conf.template b/docker/seafile.conf.template new file mode 100644 index 0000000..51477fd --- /dev/null +++ b/docker/seafile.conf.template @@ -0,0 +1,18 @@ +# Rendered by entrypoint.sh into /data/conf/seafile.conf + +[general] +seafile_data_dir = {{SEAFILE_DATA_DIR}} + +[database] +type = mysql +host = 127.0.0.1 +port = 3306 +user = {{DB_USER}} +password = {{DB_PASSWORD}} +db_name = {{DB_NAME_SEAFILE}} +ccnet_db_name = {{DB_NAME_CCNET}} +connection_charset = utf8mb4 + +[fileserver] +host = 127.0.0.1 +port = 8082 diff --git a/docker/seahub_settings.py.template b/docker/seahub_settings.py.template new file mode 100644 index 0000000..4547769 --- /dev/null +++ b/docker/seahub_settings.py.template @@ -0,0 +1,64 @@ +# Generated by container entrypoint at runtime +# This file is rendered into /data/conf/seahub_settings.py + +import os + +DEBUG = False + +# Secret key is stored in a file under /data/conf +SEAFILE_CONF_DIR = os.environ.get("SEAFILE_CONF_DIR", "/data/conf") +SEAHUB_MEDIA_DIR = os.environ.get("SEAHUB_MEDIA_DIR", "/data/seahub-media") +SECRET_KEY_FILE = os.path.join(SEAFILE_CONF_DIR, "seahub_secret_key.txt") +with open(SECRET_KEY_FILE, "r") as f: + SECRET_KEY = f.read().strip() + +# External URL and hostnames +SEAFILE_SERVER_URL = os.environ.get("SEAFILE_SERVER_URL", "http://localhost") +SEAFILE_SERVER_HOSTNAME = os.environ.get("SEAFILE_SERVER_HOSTNAME", "localhost") + +# Fileserver (Go) endpoint behind nginx at /seafhttp +FILE_SERVER_ROOT = f"{SEAFILE_SERVER_URL.rstrip('/')}/seafhttp" + +SITE_BASE_URL = SEAFILE_SERVER_URL +ALLOWED_HOSTS = [SEAFILE_SERVER_HOSTNAME, "127.0.0.1", "localhost"] + +# Database (Seahub Django DB) +DATABASES = { + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": os.environ.get("DB_NAME_SEAHUB", "seahub_db"), + "USER": os.environ.get("DB_USER", "seafile"), + "PASSWORD": os.environ.get("DB_PASSWORD", ""), + "HOST": "127.0.0.1", + "PORT": "3306", + "OPTIONS": {"charset": "utf8mb4"}, + } +} + +# Cache (Redis) +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.redis.RedisCache", + "LOCATION": os.environ.get("REDIS_URL", "redis://127.0.0.1:6379/0"), + "TIMEOUT": 300, + "OPTIONS": {}, + } +} + +# Timezone +TIME_ZONE = os.environ.get("TIMEZONE", "UTC") + +# Media (avatars, uploads) +MEDIA_ROOT = SEAHUB_MEDIA_DIR +MEDIA_URL = "/media/" + +# Security/Proxy headers (nginx sits in front) +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + +# Optional email configuration can be provided via environment variables if desired +# Example: +# EMAIL_USE_TLS = True +# EMAIL_HOST = os.environ.get("EMAIL_HOST", "") +# EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "") +# EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "") +# EMAIL_PORT = int(os.environ.get("EMAIL_PORT", "587")) diff --git a/docker/supervisord.conf.template b/docker/supervisord.conf.template new file mode 100644 index 0000000..d359b49 --- /dev/null +++ b/docker/supervisord.conf.template @@ -0,0 +1,92 @@ +; Supervisord configuration for single-container Seafile stack +; Rendered by entrypoint.sh -> /etc/supervisor/supervisord.conf + +[supervisord] +nodaemon=true +user=root +logfile={{LOG_DIR}}/supervisor/supervisord.log +pidfile=/run/supervisord.pid +childlogdir={{LOG_DIR}}/supervisor +logfile_maxbytes=50MB +logfile_backups=5 + +[supervisorctl] +serverurl=unix:///run/supervisor.sock + +[unix_http_server] +file=/run/supervisor.sock +chmod=0700 + +; ------------ Programs ------------ + +; 1) MariaDB +[program:mariadb] +command=/usr/sbin/mariadbd --datadir=/data/db --socket=/run/mysqld/mysqld.sock --bind-address=127.0.0.1 --skip-name-resolve --skip-symbolic-links --log-bin=OFF --user=mysql +autostart=true +autorestart=true +startretries=10 +startsecs=5 +priority=10 +stdout_logfile={{LOG_DIR}}/mariadb/stdout.log +stderr_logfile={{LOG_DIR}}/mariadb/stderr.log +stdout_logfile_maxbytes=20MB +stderr_logfile_maxbytes=20MB +environment=MYSQL_UNIX_PORT="/run/mysqld/mysqld.sock" + +; 2) Redis +[program:redis] +command=/usr/bin/redis-server /etc/redis/redis.conf +autostart=true +autorestart=true +startretries=10 +startsecs=2 +priority=20 +stdout_logfile={{LOG_DIR}}/redis/stdout.log +stderr_logfile={{LOG_DIR}}/redis/stderr.log +stdout_logfile_maxbytes=20MB +stderr_logfile_maxbytes=20MB + +; 3) Seafile Core + Fileserver (via seafile.sh -> seafile-controller) +[program:seafile] +command=/bin/bash -lc "for i in {1..60}; do /usr/bin/mysqladmin --protocol=socket --socket=/run/mysqld/mysqld.sock ping --silent && break || sleep 1; done; {{SEAF_RELEASE_DIR}}/seafile.sh start" +directory={{SEAF_RELEASE_DIR}} +user=seafile +autostart=true +autorestart=true +startretries=10 +startsecs=8 +priority=30 +stdout_logfile={{LOG_DIR}}/seafile/seafile.stdout.log +stderr_logfile={{LOG_DIR}}/seafile/seafile.stderr.log +stdout_logfile_maxbytes=50MB +stderr_logfile_maxbytes=50MB +environment=CCNET_CONF_DIR="{{SEAFILE_CONF_DIR}}",SEAFILE_CONF_DIR="{{SEAFILE_CONF_DIR}}",SEAFILE_CENTRAL_CONF_DIR="{{SEAFILE_CONF_DIR}}",SEAFILE_DATA_DIR="{{SEAFILE_DATA_DIR}}" + +; 4) Seahub via Gunicorn +[program:seahub] +command=/bin/bash -lc "/opt/seafile/docker/init_seahub.sh && exec /opt/seafile/venv/bin/gunicorn -c /opt/seafile/docker/gunicorn.conf.py seahub.wsgi:application" +directory={{SEAF_RELEASE_DIR}}/seahub +user=seafile +autostart=true +autorestart=true +startretries=10 +startsecs=8 +priority=40 +stdout_logfile={{LOG_DIR}}/seahub/gunicorn.stdout.log +stderr_logfile={{LOG_DIR}}/seahub/gunicorn.stderr.log +stdout_logfile_maxbytes=50MB +stderr_logfile_maxbytes=50MB +environment=DJANGO_SETTINGS_MODULE="seahub.settings",PYTHONPATH="{{SEAF_RELEASE_DIR}}",CCNET_CONF_DIR="{{SEAFILE_CONF_DIR}}",SEAFILE_CONF_DIR="{{SEAFILE_CONF_DIR}}",SEAFILE_CENTRAL_CONF_DIR="{{SEAFILE_CONF_DIR}}",SEAFILE_DATA_DIR="{{SEAFILE_DATA_DIR}}" + +; 5) Nginx +[program:nginx] +command=/usr/sbin/nginx -g "daemon off;" +autostart=true +autorestart=true +startretries=10 +startsecs=2 +priority=50 +stdout_logfile={{LOG_DIR}}/nginx/stdout.log +stderr_logfile={{LOG_DIR}}/nginx/stderr.log +stdout_logfile_maxbytes=20MB +stderr_logfile_maxbytes=20MB diff --git a/implementation_plan.md b/implementation_plan.md new file mode 100644 index 0000000..cc97a82 --- /dev/null +++ b/implementation_plan.md @@ -0,0 +1,187 @@ +# Implementation Plan + +[Overview] +Package Seafile Server Core (seafile-server) and Seahub (Django web UI) into a single-container image suitable for TrueNAS SCALE Dragonfish Custom App deployment. + +This implementation builds a single container that runs all required services for a functional Seafile deployment: MariaDB (in-container as per decision), Redis, Seafile file server (Go), Seafile core daemons, and Seahub behind an in-container Nginx reverse proxy. The container exposes HTTP/HTTPS and uses a single persistent volume at /data to store configuration, databases, media, and logs. The plan prioritizes reliability and repeatability on TrueNAS SCALE, using official Seafile release artifacts by default while documenting an alternative-from-source build path for the provided repositories. + +Key runtime components: +- Nginx reverse proxy (ports 80/443) → proxies to internal gunicorn (Seahub) and fileserver +- gunicorn serving Seahub (Django) on 127.0.0.1:8000 +- Seafile "fileserver" (Go) on 127.0.0.1:8082 +- Seafile core services (seaf-server and related daemons) +- MariaDB (in-container) for metadata +- Redis (in-container) for caching/pubsub +- One persistent mount at /data containing conf, databases, media, and logs + +Assumptions: +- Database: In-container MariaDB (user confirmed). +- Web: In-container Nginx reverse proxy (for TLS termination and path routing). +- Redis: In-container Redis (to keep single-container constraint). +- Persistent storage: Single mount /data (with subdirectories). +- Build source: Default to official Seafile release artifacts for server daemons; include an optional from-source path for the provided seafile-server and seahub repositories. + +[Types] +Configuration and environment are expressed via environment variables and YAML/INI-like files written at container start. + +Type definitions and configuration contracts: +- EnvConfig (environment variables): + - SEAFILE_SERVER_HOSTNAME: string (required), hostname (e.g., files.example.com) + - SEAFILE_SERVER_URL: string (required), public base URL (e.g., https://files.example.com) + - ADMIN_EMAIL: string (optional, used at first-run to create admin) + - ADMIN_PASSWORD: string (optional, used at first-run) + - DB_ROOT_PASSWORD: string (required for MariaDB bootstrap) + - DB_NAME: string, default "seafile" + - DB_USER: string, default "seafile" + - DB_PASSWORD: string, required + - REDIS_URL: string, default "redis://127.0.0.1:6379/0" + - TIMEZONE: string, default "UTC" + - NGINX_MAX_BODY: string, default "50m" + - SEAFILE_DATA_DIR: string, default "/data/seafile-data" + - SEAFILE_CONF_DIR: string, default "/data/conf" + - SEAHUB_MEDIA_DIR: string, default "/data/seahub-media" + - LOG_DIR: string, default "/data/logs" + - SSL_ENABLE: bool, default "false" (TrueNAS may handle TLS; if true, place certs at /data/ssl) +- FileLayout: + - /data/conf/: central Seafile/Seahub config (ccnet/seafile.conf, seahub_settings.py) + - /data/seafile-data/: seafile data store + - /data/db/: MariaDB datadir + - /data/redis/: Redis data (if persistence desired) + - /data/seahub-media/: uploads and media + - /data/logs/: logs (nginx, seahub, seafile, supervisord) + - /data/ssl/: TLS certs (optional) +- NginxConfig: + - server_name: from SEAFILE_SERVER_HOSTNAME + - upstreams: + - seahub: 127.0.0.1:8000 + - fileserver: 127.0.0.1:8082 + - routes: + - /: proxy to seahub, static caching rules + - /seafhttp: proxy_pass to fileserver, client_max_body_size NGINX_MAX_BODY + - /media: alias to /opt/seafile/seahub/media (static), or to collected static path +- DBConfig: + - Databases: seafile [and related], initialized via official seafile init scripts or seafile-admin tooling from release package + - User and grants as per Seafile docs + +[Files] +Single Dockerfile-based image and runtime scripts; all persistent state under /data. + +New files to be created: +- Dockerfile + - Multi-stage (optional) to fetch official Seafile release tarball and build Go fileserver if not bundled + - Installs: nginx, supervisor, mariadb-server, redis-server, python3, python3-venv, pip, required OS libs, curl, tzdata + - Sets up non-root user (e.g., seafile) and folders under /opt/seafile +- docker/entrypoint.sh + - Idempotent bootstrap: initialize /data layout, MariaDB, Redis, Seafile configs, Django SECRET_KEY, admin user + - Runs database migrations and static collection for Seahub + - Starts supervisord +- docker/supervisord.conf + - Programs: + - mariadbd + - redis-server + - seafile core (seaf-server and any required service supervisors) + - fileserver (Go binary) + - gunicorn (Seahub) + - nginx +- docker/nginx.conf + - Reverse proxy configuration and size limits +- docker/gunicorn.conf.py + - Workers, timeouts, bind address 127.0.0.1:8000 +- docker/init_db.sh + - Initializes MariaDB root password, creates seafile DB/user +- docker/healthcheck.sh + - Checks HTTP 200 on /ping or /accounts/login/ and fileserver /ping if available +- docker/seahub_settings.py.template + - Generates seahub_settings.py under /data/conf +- docker/requirements-override.txt (optional) + - Pin/override Python deps if necessary for release compatibility + +Existing files to be modified: +- None inside provided source trees; implementation uses release artifacts and runtime-generated config under /data. + +Files to be deleted or moved: +- None. + +Configuration file updates: +- /data/conf/seahub_settings.py generated with database, cache (Redis), ALLOWED_HOSTS, SITE_BASE_URL, media/static paths, SECRET_KEY +- /data/conf/seafile.conf generated with service URLs and seafile-data path +- Nginx server_name and upstreams from env vars + +[Functions] +Shell-level entrypoint functions and service supervisors are introduced. + +New functions (shell) with purpose: +- entrypoint.sh:init_layout() + - Create /data subdirectories; set permissions for seafile/nginx/mysql/redis users. +- entrypoint.sh:init_mariadb() + - Initialize mariadb datadir if empty; secure install; create seafile DB/user with grants. +- entrypoint.sh:init_redis() + - Write minimal redis.conf (dir /data/redis, protected-mode yes). +- entrypoint.sh:init_seafile() + - If /data/conf empty, unpack official seafile-server release, run init scripts to generate seafile.conf, seahub_settings.py template; place into /data/conf; create seafile-data dir. +- entrypoint.sh:init_seahub() + - Create Python venv, pip install -r requirements; generate SECRET_KEY if missing; run Django collectstatic; perform DB migrations; optionally create admin user if ADMIN_EMAIL and ADMIN_PASSWORD present. +- entrypoint.sh:configure_nginx() + - Render nginx.conf using env vars; enable gzip, client_max_body_size, proxy headers, X-Accel config if needed. +- entrypoint.sh:wait_for_services() + - Wait for MariaDB and Redis sockets ready. +- entrypoint.sh:run_supervisord() + - Start all services via supervisord. + +Modified functions (N/A in existing codebase; these are new runtime scripts). + +Removed functions: +- N/A. + +[Classes] +No new application-level classes; infra relies on supervisord processes and Nginx. Django app (Seahub) remains unmodified. + +New classes: +- N/A. + +Modified classes: +- N/A. + +Removed classes: +- N/A. + +[Dependencies] +Container-level OS and language dependencies and Seafile release artifacts. + +New packages (apt): +- nginx, supervisor, mariadb-server, redis-server +- python3, python3-venv, python3-pip +- curl, ca-certificates, tzdata +- build deps only if compiling from source variant: build-essential, autoconf, automake, libtool, pkg-config, libglib2.0-dev, libevent-dev, libjansson-dev, uuid-dev, libsqlite3-dev, libssl-dev, zlib1g-dev, libmysqlclient-dev, libarchive-dev, libcurl4-openssl-dev, libhiredis-dev, libjwt-dev, libargon2-dev, golang + +Python packages (from seahub/requirements.txt, vetted for release compatibility): +- Django 5.2.*, DRF 3.16.*, gunicorn 23.*, mysqlclient 2.2.*, redis 6.2.*, and listed dependencies. + +Integration requirements: +- Official Seafile server release tarball (server core + scripts) matching Seahub version +- For from-source variant, align seafile-server and seahub versions and run autogen/configure/make install flow plus Go fileserver build. + +[Testing] +Container functional tests via healthchecks and basic web/API flow. + +Test requirements and validation strategies: +- Healthcheck: curl http://127.0.0.1/accounts/login/ returns 200 after startup +- Fileserver: curl http://127.0.0.1/seafhttp/ returns expected status or ping endpoint +- Admin creation: login with ADMIN_EMAIL/ADMIN_PASSWORD succeeds +- Upload flow: create test library, upload a small file via UI; verify in seafile-data +- Persistence: stop/start container; verify data preserved at /data +- Logs: verify absence of critical errors in /data/logs/* + +[Implementation Order] +Implement in layers to minimize complexity: image base, config generation, service orchestration, then polish and tests. + +1) Dockerfile: base image, OS deps, users, directories, copy scripts/templates. +2) Download and lay down official Seafile release artifacts in /opt/seafile (or document from-source alternative). +3) Add Python venv creation and pip install for Seahub requirements. +4) Add Nginx, gunicorn, Redis, MariaDB, and supervisord configuration files. +5) Implement entrypoint.sh to initialize /data, DBs, configs, static, migrations, admin user. +6) Wire supervisord programs for services; ensure start order and restart policies. +7) Add healthcheck.sh and Docker HEALTHCHECK. +8) Expose ports 80/443; document TrueNAS mount /data and required env vars. +9) Smoke test locally with docker run and validate UI, uploads, persistence. +10) Document alternative from-source build path (autotools + Go) for provided repositories if explicit source-build is later required.