Initial commit: Seafile single-container image for TrueNAS SCALE

This commit is contained in:
nicholai 2025-09-16 12:38:41 -06:00
commit ae8fe0026d
13 changed files with 1274 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
seafile-server/
seahub/
.seafile-data/
.DS_Store

88
Dockerfile Normal file
View File

@ -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"]

162
README.md Normal file
View File

@ -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).

View File

@ -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

381
docker/entrypoint.sh Normal file
View File

@ -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 - <<PY >"${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 <<PY || true
from django.contrib.auth import get_user_model
User = get_user_model()
email="${ADMIN_EMAIL}"
password="${ADMIN_PASSWORD}"
if not User.objects.filter(email=email).exists():
User.objects.create_superuser(email=email, password=password)
else:
u = User.objects.get(email=email)
if not u.is_superuser:
u.is_superuser = True
u.save()
PY
fi
popd >/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

36
docker/gunicorn.conf.py Normal file
View File

@ -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')}",
]

33
docker/healthcheck.sh Normal file
View File

@ -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 "$@"

100
docker/init_seahub.sh Normal file
View File

@ -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 <<PY || true
from django.contrib.auth import get_user_model
User = get_user_model()
email="${ADMIN_EMAIL}"
password="${ADMIN_PASSWORD}"
if not User.objects.filter(email=email).exists():
User.objects.create_superuser(email=email, password=password)
else:
u = User.objects.get(email=email)
if not u.is_superuser:
u.is_superuser = True
u.save()
PY
fi
popd >/dev/null
touch "${sentinel}"
log "Seahub initialization complete."
}
main "$@"

100
docker/nginx.conf.template Normal file
View File

@ -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";
# }
}
}

View File

@ -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

View File

@ -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"))

View File

@ -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

187
implementation_plan.md Normal file
View File

@ -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.