#!/bin/sh
# =============================================================================
# install-seafile.sh - install Seafile Server (Community Edition) on (almost)
# any Linux distribution and register it as a service that starts on boot.
# =============================================================================
# Seafile 13.x Community Edition is deployed with Docker, which is also the one
# method that behaves identically on every distribution. This script:
#
# 1. installs Docker Engine + the Compose v2 plugin if they are missing,
# using whatever fits your distro (apt, dnf, yum, zypper, pacman, apk,
# xbps; static-binary fallback for the Compose plugin),
# 2. downloads Seafile's official compose files for the chosen release and
# generates
/.env with random secrets,
# 3. installs a service called "seafile" (systemd unit, or OpenRC script)
# so the stack starts at boot and is managed with systemctl/rc-service,
# 4. starts everything and waits until the web UI is up.
#
# Usage: sudo sh install-seafile.sh [install|upgrade|uninstall] [options]
# sudo sh install-seafile.sh --help
#
# POSIX sh only (works with dash, ash/busybox, bash) - no bashisms.
# =============================================================================
set -eu
umask 022
PROG=${0##*/}
# ------------------------------- defaults ------------------------------------
CMD=install
RELEASE=${SEAFILE_RELEASE:-13.0}
INSTALL_DIR=${SEAFILE_DIR:-/opt/seafile}
DATA_DIR=${SEAFILE_DATA_DIR:-/opt}
SF_HOST=${SEAFILE_HOSTNAME:-}
PROTOCOL=${SEAFILE_PROTOCOL:-}
ADMIN_EMAIL=${SEAFILE_ADMIN_EMAIL:-}
ADMIN_PASSWORD=${SEAFILE_ADMIN_PASSWORD:-}
TZ_NAME=${SEAFILE_TIMEZONE:-}
WAIT_TIMEOUT=${SEAFILE_WAIT_TIMEOUT:-900}
WITH_SEADOC=1
START_NOW=1
AUTO_DOCKER=1
ASSUME_YES=0
SKIP_CHECKS=0
SERVICE=seafile
UNIT_DIR=${SEAFILE_UNIT_DIR:-/etc/systemd/system}
INITD_DIR=${SEAFILE_INITD_DIR:-/etc/init.d}
NL='
'
PM=
INIT=none
ARCH=
OS_ID=unknown
OS_NAME=Linux
INTERACTIVE=0
FRESH=1
PASSWORD_GENERATED=0
ENV_FINAL=
ENV_FILE=
DOCKER_BIN=
# ------------------------------- helpers -------------------------------------
if [ -t 1 ]; then
C_RED=$(printf '\033[31m'); C_YEL=$(printf '\033[33m')
C_GRN=$(printf '\033[32m'); C_OFF=$(printf '\033[0m')
else
C_RED=; C_YEL=; C_GRN=; C_OFF=
fi
log() { printf '%s==>%s %s\n' "$C_GRN" "$C_OFF" "$*"; }
warn() { printf '%sWARNING:%s %s\n' "$C_YEL" "$C_OFF" "$*" >&2; }
die() { printf '%sERROR:%s %s\n' "$C_RED" "$C_OFF" "$*" >&2; exit 1; }
have() { command -v "$1" >/dev/null 2>&1; }
try() { "$@" >/dev/null 2>&1; }
usage() {
cat < or admin@seafile.local)
--admin-password PASS First admin password (default: random). Prefer the
SEAFILE_ADMIN_PASSWORD variable - arguments show up in 'ps'.
--release X.Y Seafile release line to deploy (default: $RELEASE)
--dir PATH Where the compose files and .env live (default: $INSTALL_DIR)
--data-dir PATH Base directory for persistent data (default: $DATA_DIR);
creates seafile-data, seafile-mysql, seafile-caddy, seadoc-data
--timezone TZ e.g. Europe/Berlin (default: this machine's time zone)
--no-seadoc Do not deploy the SeaDoc collaborative editor
--no-start Prepare everything but do not start the stack
--no-docker-install Fail instead of installing Docker automatically
--skip-checks Skip the port / RAM / container-name preflight checks
-y, --yes Never prompt; use defaults for whatever is not given
-h, --help Show this help
Environment variables: SEAFILE_HOSTNAME, SEAFILE_PROTOCOL, SEAFILE_ADMIN_EMAIL,
SEAFILE_ADMIN_PASSWORD, SEAFILE_RELEASE, SEAFILE_DIR, SEAFILE_DATA_DIR,
SEAFILE_TIMEZONE (same meaning as the options above).
EOF
}
set_opt() { # option value
case $1 in
--hostname) SF_HOST=$2 ;;
--protocol) PROTOCOL=$2 ;;
--admin-email) ADMIN_EMAIL=$2 ;;
--admin-password) ADMIN_PASSWORD=$2 ;;
--release) RELEASE=$2 ;;
--dir) INSTALL_DIR=$2 ;;
--data-dir) DATA_DIR=$2 ;;
--timezone) TZ_NAME=$2 ;;
esac
}
parse_args() {
while [ $# -gt 0 ]; do
case $1 in
install|upgrade|uninstall) CMD=$1 ;;
--hostname|--protocol|--admin-email|--admin-password|--release|--dir|--data-dir|--timezone)
[ $# -ge 2 ] || die "Option $1 needs a value."
set_opt "$1" "$2"
shift ;;
--hostname=*|--protocol=*|--admin-email=*|--admin-password=*|--release=*|--dir=*|--data-dir=*|--timezone=*)
set_opt "${1%%=*}" "${1#*=}" ;;
--no-seadoc) WITH_SEADOC=0 ;;
--no-start) START_NOW=0 ;;
--no-docker-install) AUTO_DOCKER=0 ;;
--skip-checks) SKIP_CHECKS=1 ;;
-y|--yes) ASSUME_YES=1 ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown argument: $1 (see --help)" ;;
esac
shift
done
}
validate_inputs() {
case $RELEASE in
latest) ;;
*) printf '%s' "$RELEASE" | grep -Eq '^[0-9]+\.[0-9]+$' \
|| die "--release must look like 13.0 (or be 'latest')." ;;
esac
case $PROTOCOL in
''|http|https) ;;
*) die "--protocol must be http or https." ;;
esac
INSTALL_DIR=${INSTALL_DIR%/}
DATA_DIR=${DATA_DIR%/}
for _p in "$INSTALL_DIR" "$DATA_DIR"; do
case $_p in
/*) ;;
*) die "Paths must be absolute: '$_p'" ;;
esac
case $_p in
*[!A-Za-z0-9_./-]*) die "Unsupported characters in path '$_p' (allowed: letters, digits, _ . / -)." ;;
esac
done
if [ -n "$SF_HOST" ]; then
printf '%s' "$SF_HOST" | grep -Eq '^[A-Za-z0-9.:-]+$' || die "Invalid --hostname '$SF_HOST'."
fi
if [ -n "$ADMIN_EMAIL" ]; then
printf '%s' "$ADMIN_EMAIL" | grep -Eq '^[^@[:space:]]+@[^@[:space:]]+\.[^@[:space:]]+$' \
|| die "Invalid --admin-email '$ADMIN_EMAIL'."
fi
case $ADMIN_PASSWORD in
*"'"*|*"$NL"*) die "The admin password must not contain a single quote or a newline." ;;
esac
ENV_FINAL=$INSTALL_DIR/.env
ENV_FILE=$ENV_FINAL
}
require_root() {
if [ "$(id -u)" -eq 0 ]; then return 0; fi
if have sudo && [ -f "$0" ]; then
log "Not running as root - re-executing with sudo."
exec sudo -E sh "$0" "$@"
fi
die "Run this script as root (for example: sudo sh $PROG)."
}
# ------------------------- small utility functions ---------------------------
fetch() { # url destination
if have curl; then
curl -fsSL --retry 3 --connect-timeout 20 -o "$2" "$1"
elif have wget; then
wget -q -O "$2" "$1"
else
return 1
fi
}
rand() { # N random alphanumeric characters
LC_ALL=C tr -dc 'A-Za-z0-9' /dev/null \
| awk '{ for (i = 1; i < NF; i++) if ($i == "src") { print $(i + 1); exit } }')
fi
if [ -z "$_ip" ] && have hostname; then
_ip=$(hostname -I 2>/dev/null | awk '{ print $1 }')
fi
printf '%s' "${_ip:-localhost}"
}
detect_tz() {
_tz=
if [ -r /etc/timezone ]; then
_tz=$(head -n 1 /etc/timezone | tr -d '[:space:]')
fi
if [ -z "$_tz" ] && [ -L /etc/localtime ]; then
_tz=$(readlink /etc/localtime | sed 's|.*/zoneinfo/||')
fi
if [ -z "$_tz" ] && have timedatectl; then
_tz=$(timedatectl show -p Timezone --value 2>/dev/null || true)
fi
case $_tz in
''|*[!A-Za-z0-9_/+-]*) _tz=Etc/UTC ;;
esac
if [ -d /usr/share/zoneinfo ] && [ ! -e "/usr/share/zoneinfo/$_tz" ]; then
_tz=Etc/UTC
fi
printf '%s' "$_tz"
}
ask() { # prompt default -> answer on stdout (prompt goes to stderr)
printf '%s [%s]: ' "$1" "$2" >&2
_a=
read -r _a || _a=
printf '%s' "${_a:-$2}"
}
confirm() {
printf '%s [Y/n]: ' "$1" >&2
_a=
read -r _a || _a=
case $_a in n|N|no|NO|No) return 1 ;; esac
return 0
}
port_in_use() { # true if something listens on TCP port $1 (reads /proc: no ss/netstat needed)
_hex=$(printf '%04X' "$1")
for _f in /proc/net/tcp /proc/net/tcp6; do
[ -r "$_f" ] || continue
if awk -v h="$_hex" 'NR > 1 && $4 == "0A" { n = split($2, a, ":"); if (toupper(a[n]) == h) f = 1 } END { exit !f }' "$_f"; then
return 0
fi
done
return 1
}
# --- .env editing (values are single-quoted only when they need to be) ---
env_quote() {
case $1 in
*[!A-Za-z0-9_.,:@/+=-]*) printf "'%s'" "$1" ;;
*) printf '%s' "$1" ;;
esac
}
set_env() { # key value : replace KEY=... in $ENV_FILE, or append it
_v=$(env_quote "$2")
_tmp=$(mktemp "$ENV_FILE.XXXXXX")
SE_KEY=$1 SE_VAL=$_v awk '
BEGIN { k = ENVIRON["SE_KEY"]; v = ENVIRON["SE_VAL"]; done = 0 }
{
i = index($0, "=")
if (!done && i > 1 && substr($0, 1, i - 1) == k) { print k "=" v; done = 1 }
else print
}
END { if (!done) print k "=" v }
' "$ENV_FILE" > "$_tmp"
cat "$_tmp" > "$ENV_FILE"
rm -f "$_tmp"
}
get_env() { # key -> value (quotes stripped) from $ENV_FILE
sed -n "s/^$1=//p" "$ENV_FILE" | head -n 1 | tr -d "'\""
}
# ------------------------- platform detection --------------------------------
osr() { sed -n "s/^$1=//p" /etc/os-release 2>/dev/null | head -n 1 | tr -d "\"'"; }
detect_init() {
case ${SEAFILE_INIT_SYSTEM:-} in
systemd|openrc|none) INIT=$SEAFILE_INIT_SYSTEM; return 0 ;;
esac
if [ -d /run/systemd/system ] && have systemctl; then
INIT=systemd
elif have rc-service && have rc-update; then
INIT=openrc
else
INIT=none
fi
}
detect_platform() {
[ "$(uname -s)" = Linux ] || die "This installer supports Linux only."
case $(uname -m) in
x86_64|amd64) ARCH=x86_64 ;;
aarch64|arm64) ARCH=aarch64 ;;
*) die "Unsupported CPU architecture '$(uname -m)': Seafile's images exist for x86_64 and arm64 only." ;;
esac
if [ -r /etc/os-release ]; then
OS_ID=$(osr ID); OS_NAME=$(osr PRETTY_NAME)
fi
if [ -z "$OS_ID" ]; then OS_ID=unknown; fi
if [ -z "$OS_NAME" ]; then OS_NAME=Linux; fi
for _p in apt-get dnf yum zypper pacman apk xbps-install; do
if have "$_p"; then PM=$_p; break; fi
done
detect_init
}
# --------------------------- package manager glue ----------------------------
pm_refresh() {
case $PM in
apt-get) apt-get update ;;
zypper) zypper --non-interactive refresh ;;
*) return 0 ;;
esac
}
pm_install() { # packages...
case $PM in
apt-get) DEBIAN_FRONTEND=noninteractive apt-get install -y "$@" ;;
dnf) dnf install -y "$@" ;;
yum) yum install -y "$@" ;;
zypper) zypper --non-interactive install --no-recommends "$@" ;;
pacman) pacman -S --needed --noconfirm "$@" ;;
apk) apk add --no-cache "$@" ;;
xbps-install) xbps-install -Sy "$@" ;;
*) return 1 ;;
esac
}
ensure_downloader() {
if have curl || have wget; then return 0; fi
[ -n "$PM" ] || die "Neither curl nor wget is installed, and no supported package manager was found."
log "Installing curl..."
pm_refresh || true
pm_install curl ca-certificates || die "Could not install curl; please install curl or wget and re-run."
}
# ------------------------------- Docker --------------------------------------
docker_up() { have docker && docker info >/dev/null 2>&1; }
compose_ok() { have docker && docker compose version >/dev/null 2>&1; }
docker_convenience_script() {
log "Running Docker's official convenience script (https://get.docker.com)..."
_s=$(mktemp)
if fetch https://get.docker.com "$_s" && sh "$_s"; then
rm -f "$_s"
return 0
fi
rm -f "$_s"
return 1
}
docker_distro_packages() {
warn "Docker's own installer did not work on $OS_NAME; trying the distribution's Docker package instead."
case $PM in
apt-get) pm_refresh && pm_install docker.io ;;
dnf|yum) pm_install docker || pm_install moby-engine ;;
*) return 1 ;;
esac
}
install_docker_engine() {
case $PM in
pacman) pm_install docker docker-compose || pm_install docker ;;
apk) pm_install docker docker-cli-compose || pm_install docker ;;
xbps-install) pm_install docker docker-compose || pm_install docker ;;
zypper) pm_refresh || true
pm_install docker docker-compose || pm_install docker || docker_convenience_script ;;
apt-get|dnf|yum)
if [ "$OS_ID" = amzn ]; then
pm_install docker
else
docker_convenience_script || docker_distro_packages
fi ;;
*) return 1 ;;
esac
}
start_docker_service() {
case $INIT in
systemd)
try systemctl enable --now docker || try systemctl start docker || true ;;
openrc)
try rc-update add docker default || true
try rc-service docker start || true ;;
*)
if [ -d /etc/sv/docker ] && [ -d /var/service ]; then
ln -sf /etc/sv/docker /var/service/docker
elif have service; then
try service docker start || true
elif [ -x /etc/init.d/docker ]; then
try /etc/init.d/docker start || true
fi ;;
esac
_i=0
while [ "$_i" -lt 60 ]; do
if docker info >/dev/null 2>&1; then return 0; fi
sleep 1
_i=$((_i + 1))
done
return 1
}
install_compose_plugin() {
log "Docker Compose v2 plugin not found - installing it..."
case $PM in
apt-get) try pm_install docker-compose-v2 || try pm_install docker-compose-plugin || true ;;
dnf|yum) try pm_install docker-compose-plugin || true ;;
zypper) try pm_install docker-compose || true ;;
esac
if compose_ok; then return 0; fi
# Fallback that works on every distro: the official static binary from GitHub.
_dest=/usr/local/lib/docker/cli-plugins
_url=https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$ARCH
_tmp=$(mktemp)
mkdir -p "$_dest"
if ! fetch "$_url" "$_tmp"; then
rm -f "$_tmp"
die "Could not download the Compose plugin from $_url"
fi
if have sha256sum && fetch "$_url.sha256" "$_tmp.sha256" 2>/dev/null; then
_want=$(awk '{ print $1; exit }' "$_tmp.sha256")
_got=$(sha256sum "$_tmp" | awk '{ print $1 }')
if printf '%s' "$_want" | grep -Eq '^[0-9a-f]{64}$' && [ "$_want" != "$_got" ]; then
rm -f "$_tmp" "$_tmp.sha256"
die "Checksum mismatch for the downloaded Compose plugin - refusing to install it."
fi
fi
rm -f "$_tmp.sha256"
cp "$_tmp" "$_dest/docker-compose"
chmod 755 "$_dest/docker-compose"
rm -f "$_tmp"
}
ensure_docker() {
if docker_up && compose_ok; then
log "Docker and Compose are already available: $(docker --version)"
return 0
fi
if ! have docker; then
[ "$AUTO_DOCKER" -eq 1 ] || die "Docker is not installed (re-run without --no-docker-install to install it automatically)."
[ -n "$PM" ] || die "No supported package manager found. Install Docker Engine + Compose v2 manually (https://docs.docker.com/engine/install/) and re-run."
ensure_downloader
log "Installing Docker Engine on $OS_NAME (package manager: $PM)..."
install_docker_engine || die "Automatic Docker installation failed on $OS_NAME (on Arch, run 'pacman -Syu' and reboot first if packages are not found). Install Docker Engine + the Compose v2 plugin manually and re-run."
fi
start_docker_service || die "The Docker daemon is not running and could not be started."
if ! compose_ok; then
[ "$AUTO_DOCKER" -eq 1 ] || die "The Docker Compose v2 plugin is missing (re-run without --no-docker-install)."
ensure_downloader
install_compose_plugin
fi
compose_ok || die "'docker compose' still does not work. Please install the Docker Compose v2 plugin manually."
log "Docker is ready: $(docker --version)"
}
# ------------------------- preflight checks ----------------------------------
preflight() {
if [ "$SKIP_CHECKS" -eq 1 ]; then return 0; fi
_mem=$(awk '/^MemTotal:/ { print int($2 / 1024) }' /proc/meminfo 2>/dev/null || true)
if [ -n "$_mem" ] && [ "$_mem" -lt 1800 ]; then
warn "Only ${_mem} MB of RAM detected. MariaDB, Redis, Seafile, Caddy and SeaDoc together may be slow or get OOM-killed. Consider more RAM/swap, or --no-seadoc."
fi
if [ "$FRESH" -eq 1 ]; then
for _port in 80 443; do
if port_in_use "$_port"; then
die "TCP port $_port is already in use (Seafile's Caddy proxy needs ports 80 and 443). Stop the other service first, or use --skip-checks."
fi
done
fi
if [ "$ARCH" != x86_64 ]; then
warn "Running on $ARCH: Seafile publishes arm64 images, but 'docker compose pull' will tell you if one is missing."
fi
}
check_name_conflicts() {
if [ "$SKIP_CHECKS" -eq 1 ] || [ "$FRESH" -eq 0 ]; then return 0; fi
_existing=$(docker ps -a --format '{{.Names}}' 2>/dev/null || true)
for _n in seafile seafile-mysql seafile-redis seafile-caddy seadoc; do
if printf '%s\n' "$_existing" | grep -qx "$_n"; then
die "A Docker container named '$_n' already exists. Remove/rename it first (docker rm -f $_n) or use --skip-checks."
fi
done
}
# ------------------------- settings & configuration --------------------------
resolve_settings() {
if [ -z "$SF_HOST" ]; then
_def=$(detect_ip)
if [ "$INTERACTIVE" -eq 1 ]; then
SF_HOST=$(ask "Domain name or IP address for Seafile" "$_def")
else
SF_HOST=$_def
fi
fi
printf '%s' "$SF_HOST" | grep -Eq '^[A-Za-z0-9.:-]+$' || die "Invalid hostname '$SF_HOST'."
if [ -z "$PROTOCOL" ]; then
if wants_https "$SF_HOST"; then PROTOCOL=https; else PROTOCOL=http; fi
fi
if [ -z "$ADMIN_EMAIL" ]; then
if [ "$PROTOCOL" = https ] && ! is_ip "$SF_HOST"; then
_def=admin@$SF_HOST
else
_def=admin@seafile.local
fi
if [ "$INTERACTIVE" -eq 1 ]; then
ADMIN_EMAIL=$(ask "Admin e-mail address (used as login name)" "$_def")
else
ADMIN_EMAIL=$_def
fi
printf '%s' "$ADMIN_EMAIL" | grep -Eq '^[^@[:space:]]+@[^@[:space:]]+\.[^@[:space:]]+$' \
|| die "Invalid admin e-mail '$ADMIN_EMAIL'."
fi
if [ -z "$ADMIN_PASSWORD" ]; then
ADMIN_PASSWORD=$(rand 20)
PASSWORD_GENERATED=1
elif [ "${#ADMIN_PASSWORD}" -lt 8 ]; then
warn "The admin password is shorter than 8 characters - please change it after the first login."
fi
if [ -z "$TZ_NAME" ]; then TZ_NAME=$(detect_tz); fi
}
DOC_BASE=
GH_BASE=
get_remote() { # path-relative-to-docker-dir destination
fetch "$DOC_BASE/$1" "$2" 2>/dev/null && return 0
fetch "$GH_BASE/$1" "$2" 2>/dev/null && return 0
return 1
}
download_files() {
_ref=$RELEASE
if [ "$RELEASE" = latest ]; then _ref=master; fi
DOC_BASE=https://manual.seafile.com/$RELEASE/repo/docker
GH_BASE=https://raw.githubusercontent.com/haiwen/seafile-admin-docs/$_ref/manual/repo/docker
log "Downloading Seafile $RELEASE compose files..."
get_remote ce/env "$ENV_FILE" \
|| die "Could not download the .env template for Seafile $RELEASE (is --release valid? is the network up?)."
get_remote ce/seafile-server.yml "$INSTALL_DIR/seafile-server.yml" \
|| die "Could not download seafile-server.yml for Seafile $RELEASE."
get_remote caddy.yml "$INSTALL_DIR/caddy.yml" \
|| die "Could not download caddy.yml for Seafile $RELEASE."
if [ "$WITH_SEADOC" -eq 1 ]; then
get_remote seadoc.yml "$INSTALL_DIR/seadoc.yml" \
|| die "Could not download seadoc.yml for Seafile $RELEASE."
fi
grep -q '^JWT_PRIVATE_KEY=' "$ENV_FILE" || die "The downloaded .env template looks wrong (no JWT_PRIVATE_KEY)."
grep -q '^services:' "$INSTALL_DIR/seafile-server.yml" || die "The downloaded seafile-server.yml looks wrong."
}
write_env() {
chmod 600 "$ENV_FILE"
set_env SEAFILE_SERVER_HOSTNAME "$SF_HOST"
set_env SEAFILE_SERVER_PROTOCOL "$PROTOCOL"
set_env TIME_ZONE "$TZ_NAME"
set_env BASIC_STORAGE_PATH "$DATA_DIR"
set_env JWT_PRIVATE_KEY "$(rand 48)"
set_env SEAFILE_MYSQL_DB_PASSWORD "$(rand 24)"
set_env INIT_SEAFILE_MYSQL_ROOT_PASSWORD "$(rand 24)"
set_env INIT_SEAFILE_ADMIN_EMAIL "$ADMIN_EMAIL"
set_env INIT_SEAFILE_ADMIN_PASSWORD "$ADMIN_PASSWORD"
if [ "$WITH_SEADOC" -eq 0 ]; then
set_env COMPOSE_FILE "seafile-server.yml,caddy.yml"
set_env ENABLE_SEADOC false
fi
}
make_dirs() {
mkdir -p "$INSTALL_DIR" "$DATA_DIR/seafile-data" "$DATA_DIR/seafile-mysql/db" "$DATA_DIR/seafile-caddy"
if [ "$WITH_SEADOC" -eq 1 ]; then mkdir -p "$DATA_DIR/seadoc-data"; fi
}
# ------------------------- the service (the "daemon") ------------------------
install_service() {
DOCKER_BIN=$(command -v docker) || die "Cannot locate the docker binary."
case $INIT in
systemd)
mkdir -p "$UNIT_DIR"
_dep=
if try systemctl cat docker.service; then _dep="Requires=docker.service"; fi
cat > "$UNIT_DIR/$SERVICE.service" < "$INITD_DIR/$SERVICE" </dev/null || true)
if [ "$_h" = healthy ]; then return 0; fi
if docker logs --tail 300 seafile 2>&1 | grep -q 'Seahub is started'; then return 0; fi
sleep 5
_t=$((_t + 5))
if [ $((_t % 30)) -eq 0 ]; then log " ...still starting (${_t}s)"; fi
done
return 1
}
row() { printf ' %-46s %s\n' "$1" "$2"; }
print_manage_help() {
printf '\nUseful commands:\n'
case $INIT in
systemd)
row "systemctl status|start|stop|restart $SERVICE" "(control the stack)"
row "journalctl -u $SERVICE" "(service log)" ;;
openrc)
row "rc-service $SERVICE status|start|stop|restart" "(control the stack)" ;;
*)
row "cd $INSTALL_DIR && docker compose ps|stop|up -d" "(control the stack)" ;;
esac
row "docker logs -f seafile" "(Seafile application log)"
row "sudo sh $PROG upgrade" "(pull newer images of this release line)"
}
print_summary() {
_url=$(get_env SEAFILE_SERVER_PROTOCOL)://$(get_env SEAFILE_SERVER_HOSTNAME)
_data=$(get_env BASIC_STORAGE_PATH)
printf '\n URL %s\n' "$_url"
printf ' Admin %s\n' "$(get_env INIT_SEAFILE_ADMIN_EMAIL)"
if [ "$PASSWORD_GENERATED" -eq 1 ]; then
printf ' Password %s (generated - change it after logging in)\n' "$ADMIN_PASSWORD"
elif [ "$FRESH" -eq 1 ]; then
printf ' Password (the one you supplied)\n'
else
printf ' Password see INIT_SEAFILE_ADMIN_PASSWORD in %s (valid only until you change it in the web UI)\n' "$ENV_FINAL"
fi
printf ' Settings %s (contains secrets, mode 600)\n' "$ENV_FINAL"
printf ' Data %s/seafile-data, %s/seafile-mysql\n' "${_data:-$DATA_DIR}" "${_data:-$DATA_DIR}"
if [ "$(get_env SEAFILE_SERVER_PROTOCOL)" = https ]; then
printf '\nHTTPS: Caddy requests a Let'"'"'s Encrypt certificate on first start, so the DNS record\nfor %s must point to this machine and ports 80/443 must be reachable.\n' "$(get_env SEAFILE_SERVER_HOSTNAME)"
fi
print_manage_help
}
# ------------------------------ commands -------------------------------------
do_install() {
if [ -f "$ENV_FINAL" ]; then FRESH=0; fi
if [ "$FRESH" -eq 1 ]; then resolve_settings; fi
log "System: $OS_NAME ($ARCH) | package manager: ${PM:-none} | init system: $INIT"
if [ "$FRESH" -eq 1 ] && [ "$INTERACTIVE" -eq 1 ]; then
printf '\nSeafile %s will be installed with:\n address %s://%s\n admin %s\n files %s (compose + .env) and %s (data)\n SeaDoc %s\n\n' \
"$RELEASE" "$PROTOCOL" "$SF_HOST" "$ADMIN_EMAIL" "$INSTALL_DIR" "$DATA_DIR" \
"$(if [ "$WITH_SEADOC" -eq 1 ]; then echo yes; else echo no; fi)"
confirm "Continue?" || die "Aborted by user."
fi
preflight
ensure_docker
check_name_conflicts
if [ "$FRESH" -eq 1 ]; then
make_dirs
ENV_FILE=$INSTALL_DIR/.env.new
download_files
write_env
mv -f "$ENV_FILE" "$ENV_FINAL"
ENV_FILE=$ENV_FINAL
else
log "Existing configuration found in $INSTALL_DIR - keeping it (secrets are not regenerated)."
if [ -n "$SF_HOST$PROTOCOL$ADMIN_EMAIL$ADMIN_PASSWORD$TZ_NAME" ]; then
warn "Settings given on the command line are ignored for an existing installation - edit $ENV_FINAL and restart the service instead."
fi
fi
compose_in_dir config -q \
|| die "The compose configuration is invalid. Fix $ENV_FINAL (or delete it and re-run to regenerate it)."
_have_service=1
install_service || _have_service=0
if [ "$START_NOW" -eq 0 ]; then
if [ "$_have_service" -eq 0 ]; then
_how="cd $INSTALL_DIR && docker compose up -d"
elif [ "$INIT" = openrc ]; then
_how="rc-service $SERVICE start"
else
_how="systemctl start $SERVICE"
fi
log "Prepared but not started (--no-start). Start it with: $_how"
print_summary
return 0
fi
start_stack
if wait_ready; then
log "Seafile is up and running."
print_summary
else
warn "Seafile did not report 'ready' within ${WAIT_TIMEOUT}s. The first start can be slow on small machines."
warn "Check progress with: docker logs -f seafile"
print_summary
exit 2
fi
}
do_upgrade() {
[ -f "$ENV_FINAL" ] || die "No Seafile installation found in $INSTALL_DIR (nothing to upgrade)."
if ! docker_up || ! compose_ok; then
die "Docker (with the Compose plugin) is not available or not running."
fi
log "Pulling the newest images for the installed release line..."
compose_in_dir pull || die "Pulling the images failed."
compose_in_dir up -d --remove-orphans || die "Recreating the containers failed."
if wait_ready; then
log "Upgrade finished and Seafile is healthy."
else
warn "Seafile did not report 'ready' in time - check: docker logs -f seafile"
exit 2
fi
log "Note: this follows the pinned tag (e.g. 13.0-latest). For a new major/minor line such as 14.0, read Seafile's upgrade notes first and back up your data."
}
do_uninstall() {
log "Removing the $SERVICE service and containers (your data is NOT deleted)..."
case $INIT in
systemd)
try systemctl disable --now "$SERVICE" || true
rm -f "$UNIT_DIR/$SERVICE.service"
try systemctl daemon-reload || true ;;
openrc)
try rc-service "$SERVICE" stop || true
try rc-update del "$SERVICE" default || true
rm -f "$INITD_DIR/$SERVICE" ;;
esac
if [ -f "$ENV_FINAL" ] && docker_up && compose_ok; then
compose_in_dir down || warn "'docker compose down' reported a problem."
fi
_data=$DATA_DIR
if [ -f "$ENV_FINAL" ]; then _data=$(get_env BASIC_STORAGE_PATH); fi
log "Done. Kept: $INSTALL_DIR (compose files + .env) and the data under ${_data:-$DATA_DIR}/ (seafile-data, seafile-mysql, seafile-caddy, seadoc-data)."
log "Delete those directories yourself only if you really want to erase everything."
}
# -------------------------------- main ---------------------------------------
main() {
for _a in "$@"; do
case $_a in -h|--help) usage; exit 0 ;; esac
done
require_root "$@"
parse_args "$@"
validate_inputs
if [ "$ASSUME_YES" -eq 0 ] && [ -t 0 ]; then INTERACTIVE=1; fi
detect_platform
case $CMD in
install) do_install ;;
upgrade) do_upgrade ;;
uninstall) do_uninstall ;;
esac
}
main "$@"