The summary printed at the end of every run says passive FTPS behind NAT is
fixed by re-running with FTP_NAT=... That could never work: the ftp-nat
substitution sat inside `if [[ ! -f "$CONF" ]]`, so on any re-run the conf
existed, the substitution was skipped, and ftp-nat stayed commented out.
cfg/copyparty.conf is the ONLY route FTP_NAT has to the service -- nothing
in docker-compose.yml interpolates it and the copyparty service is given no
environment, so .env's copy is a record, not the live setting. The remedial
run logged "exists; leaving it alone" as a green [+], reported healthy, and
reprinted the same advice. Passive FTPS failed exactly as before, with
nothing to distinguish "you did it wrong" from "it didn't take" -- and
because the template's commented example carries a literal IP, grepping the
conf shows an ftp-nat line with an address in it.
Lift the application out of the creation guard so it runs against an
existing conf too, and match `#?` so an already-set value (ISP change, typo)
is corrected rather than only the commented template line. Warn instead of
guessing if no ftp-nat line exists at all -- appending at EOF would land in
[accounts]. Kept above the chown, since `sed -i` rewrites as root.
`|| true` on the current-value read is load-bearing: on a still-commented
conf the grep matches nothing, and under `set -o pipefail` that would abort
the script inside the assignment -- the exact trap fixed in bf52426.
Also restart copyparty when the conf changed: compose will not recreate a
service whose image and config are unchanged, so `up -d` alone would leave
the edit on disk and the old value in the running process. And record the
value in .env so the two files do not disagree about what is deployed.
Verified: commented template applies; identical value is a no-op; a
different value is corrected; a conf with no ftp-nat line warns without
corruption; empty FTP_NAT skips the block entirely.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
623 lines
32 KiB
Bash
623 lines
32 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# deploy.sh -- deploy copyparty (caddy + copyparty file server) on Alpine,
|
|
# Debian or Alma. Single-node; runs as root.
|
|
#
|
|
# What this does:
|
|
# 1. Installs docker + compose if missing.
|
|
# 2. Lays down the stack files in $STACK_DIR.
|
|
# 3. Generates cfg/copyparty.conf on first run (random admin password) and a
|
|
# self-signed cfg/ftps.pem for FTPS; neither is overwritten on re-run.
|
|
# 4. Generates .env on first run, pinning COPYPARTY_TAG to the newest release.
|
|
# 5. Prompts for required values not preset (COPYPARTY_DOMAIN, ACME_EMAIL).
|
|
# 6. Opens 80/443 + SFTP/FTPS/passive ports on the host firewall if present.
|
|
# 7. Pulls images, brings the stack up, waits for health.
|
|
# 8. Installs + schedules update.sh (security-notices-aware) unless disabled.
|
|
#
|
|
# Caddy fronts the web UI / WebDAV over TLS; SFTP and FTPS are published directly
|
|
# by copyparty (not proxied). copyparty trusts Caddy's X-Forwarded-For so logs
|
|
# and bans use the real client IP (see cfg/copyparty.conf `xff-src`).
|
|
#
|
|
# Idempotent: re-run to apply config changes / pull new images.
|
|
#
|
|
# Self-contained: docker-compose.yml, Caddyfile, copyparty.conf.example,
|
|
# .env.example and update.sh are embedded as a base64 tar.gz at the bottom of
|
|
# this file. Rebuild with build.sh after editing the loose source files.
|
|
#
|
|
# Usage:
|
|
# bash deploy.sh # interactive prompts
|
|
# COPYPARTY_DOMAIN=files.example.com ACME_EMAIL=me@x.com \
|
|
# SKIP_PROMPTS=1 bash deploy.sh # non-interactive
|
|
# DATA_DIR=/mnt/disk/files FTP_NAT=203.0.113.10 bash deploy.sh
|
|
# SKIP_DOCKER_INSTALL=1 bash deploy.sh
|
|
|
|
set -euo pipefail
|
|
|
|
: "${STACK_DIR:=/srv/copyparty}"
|
|
: "${SKIP_DOCKER_INSTALL:=0}"
|
|
: "${FORCE:=0}"
|
|
: "${SKIP_PROMPTS:=0}"
|
|
[[ "$SKIP_PROMPTS" == "1" ]] && FORCE=1
|
|
: "${COPYPARTY_DOMAIN:=}"
|
|
: "${ACME_EMAIL:=}"
|
|
: "${COPYPARTY_IMAGE:=copyparty/ac}"
|
|
: "${COPYPARTY_TAG:=latest}"
|
|
: "${PUID:=1000}"
|
|
: "${PGID:=1000}"
|
|
: "${DATA_DIR:=/srv/copyparty/data}"
|
|
: "${BIND_ADDR:=}"
|
|
: "${SFTP_PORT:=3922}"
|
|
: "${FTPS_PORT:=3990}"
|
|
: "${FTP_PASV_RANGE:=12000-12099}"
|
|
: "${FTP_NAT:=}"
|
|
: "${UPDATE_POLICY:=latest}"
|
|
: "${VC_FEED:=advisories}"
|
|
: "${COPYPARTY_AUTOUPDATE:=1}" # 0 = install update.sh but don't schedule it
|
|
: "${CADDY_TAG:=2-alpine}"
|
|
|
|
log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; }
|
|
warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; }
|
|
die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; }
|
|
|
|
[[ $EUID -eq 0 ]] || die "Run as root."
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# OS detection + Docker install (Alpine / Debian / Alma). Inlined (this deploy.sh
|
|
# is self-contained / scp'd standalone) rather than sourced from oslib.sh.
|
|
# ---------------------------------------------------------------------------
|
|
osfam() {
|
|
local id="" like=""
|
|
if [[ -r /etc/os-release ]]; then
|
|
id="$(. /etc/os-release 2>/dev/null && echo "${ID:-}")"
|
|
like="$(. /etc/os-release 2>/dev/null && echo "${ID_LIKE:-}")"
|
|
fi
|
|
case " $id $like " in
|
|
*" alpine "*) echo alpine ;;
|
|
*" debian "*|*" ubuntu "*) echo debian ;;
|
|
*" rhel "*|*" fedora "*|*" centos "*) echo rhel ;;
|
|
*) echo "${id:-unknown}" ;;
|
|
esac
|
|
}
|
|
|
|
pkg_install() { # best-effort install of a package across the three families
|
|
case "$(osfam)" in
|
|
alpine) apk add -q "$@" || true ;;
|
|
debian) DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "$@" || true ;;
|
|
rhel) dnf install -y -q "$@" || true ;;
|
|
esac
|
|
}
|
|
|
|
# fetch a URL to stdout with whatever's available (curl or wget).
|
|
fetch() {
|
|
if command -v curl >/dev/null 2>&1; then curl -fsSL "$1" 2>/dev/null
|
|
elif command -v wget >/dev/null 2>&1; then wget -qO- "$1" 2>/dev/null
|
|
fi
|
|
}
|
|
|
|
install_docker() {
|
|
if command -v docker >/dev/null 2>&1; then
|
|
log "Docker already installed: $(docker --version)"
|
|
else
|
|
log "Installing Docker (OS: $(osfam))..."
|
|
case "$(osfam)" in
|
|
alpine) apk add -q docker docker-cli-compose openrc ;;
|
|
debian|rhel) command -v curl >/dev/null 2>&1 || pkg_install curl
|
|
curl -fsSL https://get.docker.com | sh ;;
|
|
*) die "Unsupported OS for auto Docker install. Set SKIP_DOCKER_INSTALL=1 and install Docker yourself." ;;
|
|
esac
|
|
fi
|
|
if command -v rc-update >/dev/null 2>&1; then
|
|
rc-update add docker default >/dev/null 2>&1 || true
|
|
rc-service docker status >/dev/null 2>&1 || rc-service docker start
|
|
elif command -v systemctl >/dev/null 2>&1; then
|
|
systemctl enable --now docker >/dev/null 2>&1 || systemctl start docker || true
|
|
fi
|
|
}
|
|
|
|
open_web_ports() {
|
|
# Register web (80/443) + SFTP/FTPS + the passive-FTP range. Prefer the host
|
|
# firewall (harden-firewall.sh); else ufw/firewalld if active.
|
|
#
|
|
# NOTE: published Docker ports reach the host via nat/FORWARD and BYPASS the
|
|
# INPUT firewall, so this is belt-and-braces + self-documentation. Pin the
|
|
# real exposure with BIND_ADDR and copyparty's own access control.
|
|
local pasv_colon="${FTP_PASV_RANGE/-/:}" # 12000-12099 -> 12000:12099 (iptables/ufw/ports.d)
|
|
local pasv_hyphen="${FTP_PASV_RANGE}" # 12000-12099 (firewalld)
|
|
if [[ -d /etc/firewall/ports.d && -x /usr/local/sbin/firewall-apply ]]; then
|
|
log "Registering 80,443,${SFTP_PORT},${FTPS_PORT},${pasv_colon}/tcp with host firewall..."
|
|
printf '80/tcp\n443/tcp\n%s/tcp\n%s/tcp\n%s/tcp\n' \
|
|
"$SFTP_PORT" "$FTPS_PORT" "$pasv_colon" > /etc/firewall/ports.d/copyparty.rule
|
|
/usr/local/sbin/firewall-apply
|
|
elif command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q '^Status: active'; then
|
|
log "ufw active -- allowing web + SFTP/FTPS..."
|
|
local p
|
|
for p in 80/tcp 443/tcp "${SFTP_PORT}/tcp" "${FTPS_PORT}/tcp" "${pasv_colon}/tcp"; do
|
|
ufw allow "$p" >/dev/null
|
|
done
|
|
elif command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
|
|
log "firewalld active -- allowing web + SFTP/FTPS..."
|
|
firewall-cmd -q --add-service=http --permanent
|
|
firewall-cmd -q --add-service=https --permanent
|
|
firewall-cmd -q --add-port="${SFTP_PORT}/tcp" --permanent
|
|
firewall-cmd -q --add-port="${FTPS_PORT}/tcp" --permanent
|
|
firewall-cmd -q --add-port="${pasv_hyphen}/tcp" --permanent
|
|
firewall-cmd -q --reload
|
|
fi
|
|
}
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Extract embedded archive
|
|
# ----------------------------------------------------------------------------
|
|
SCRIPT_DIR=$(mktemp -d -t copyparty-deploy.XXXXXX)
|
|
trap 'rm -rf "$SCRIPT_DIR"' EXIT
|
|
|
|
extract_archive() {
|
|
grep -a -A 9999999 '^__ARCHIVE_BELOW__$' "$0" \
|
|
| tail -n +2 \
|
|
| base64 -d \
|
|
| tar -xz -C "$SCRIPT_DIR"
|
|
}
|
|
|
|
if grep -q -a '^__ARCHIVE_BELOW__$' "$0"; then
|
|
log "Extracting embedded deployment files..."
|
|
extract_archive
|
|
else
|
|
die "No embedded archive found. Run build.sh to embed deployment files."
|
|
fi
|
|
|
|
EMBEDDED=(docker-compose.yml Caddyfile copyparty.conf.example .env.example update.sh)
|
|
for f in "${EMBEDDED[@]}"; do
|
|
[[ -f "$SCRIPT_DIR/$f" ]] || die "Embedded archive missing $f"
|
|
done
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Prompt for required vars
|
|
# ----------------------------------------------------------------------------
|
|
prompt() {
|
|
local varname="$1" message="$2"
|
|
local -n ref="$varname"
|
|
if [[ -z "${ref:-}" ]]; then
|
|
[[ "$SKIP_PROMPTS" == "1" ]] && die "$varname required (set it in the environment; running with SKIP_PROMPTS=1)."
|
|
read -r -p "$message: " ref
|
|
[[ -n "$ref" ]] || die "$varname required."
|
|
fi
|
|
}
|
|
|
|
prompt COPYPARTY_DOMAIN "Public hostname for the web UI (e.g. files.example.com)"
|
|
prompt ACME_EMAIL "Let's Encrypt email"
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Docker + firewall
|
|
# ----------------------------------------------------------------------------
|
|
if [[ "$SKIP_DOCKER_INSTALL" != "1" ]]; then
|
|
install_docker
|
|
fi
|
|
open_web_ports
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Stack directory + files
|
|
# ----------------------------------------------------------------------------
|
|
log "Setting up $STACK_DIR..."
|
|
install -d -m 0750 "$STACK_DIR"
|
|
install -m 0644 "$SCRIPT_DIR/docker-compose.yml" "$STACK_DIR/docker-compose.yml"
|
|
install -m 0644 "$SCRIPT_DIR/Caddyfile" "$STACK_DIR/Caddyfile"
|
|
install -m 0755 "$SCRIPT_DIR/update.sh" "$STACK_DIR/update.sh"
|
|
install -d -m 0750 "$STACK_DIR/cfg"
|
|
|
|
# Data root (bind-mounted at /w), owned by the runtime UID/GID.
|
|
install -d -m 0750 "$DATA_DIR"
|
|
chown "$PUID:$PGID" "$DATA_DIR"
|
|
|
|
ENV_FILE="$STACK_DIR/.env"
|
|
set_env() { # <KEY> <value>: update KEY in .env, or append if absent
|
|
local key="$1" val="$2" esc
|
|
esc=${val//\\/\\\\}; esc=${esc//|/\\|}; esc=${esc//&/\\&}
|
|
if grep -qE "^${key}=" "$ENV_FILE"; then
|
|
sed -i -e "s|^${key}=.*|${key}=${esc}|" "$ENV_FILE"
|
|
else
|
|
printf '%s=%s\n' "$key" "$val" >> "$ENV_FILE"
|
|
fi
|
|
}
|
|
|
|
# Pin COPYPARTY_TAG to the newest release so the running version is explicit.
|
|
if [[ "$COPYPARTY_TAG" == "latest" || -z "$COPYPARTY_TAG" ]]; then
|
|
_v="$(fetch "https://api.github.com/repos/9001/copyparty/releases/latest" \
|
|
| grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"[^"]+"' | head -n1 \
|
|
| sed -E 's/.*"v?([0-9][^"]*)".*/\1/')"
|
|
if [[ -n "${_v:-}" ]]; then COPYPARTY_TAG="$_v"; log "Pinned COPYPARTY_TAG=${_v} (newest release)."
|
|
else warn "Could not resolve the newest release; using tag 'latest'."; COPYPARTY_TAG="latest"; fi
|
|
fi
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Generate copyparty.conf (admin password) + self-signed FTPS cert -- once.
|
|
# ----------------------------------------------------------------------------
|
|
CONF="$STACK_DIR/cfg/copyparty.conf"
|
|
if [[ ! -f "$CONF" ]]; then
|
|
ADMIN_PW="$(head -c 32 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 24)"
|
|
log "Generating $CONF (admin account)..."
|
|
install -m 0640 "$SCRIPT_DIR/copyparty.conf.example" "$CONF"
|
|
sed -i "s|__ADMIN_PW__|${ADMIN_PW}|" "$CONF"
|
|
else
|
|
log "$CONF exists; keeping it."
|
|
fi
|
|
|
|
# FTP_NAT has exactly one route to the running service: this conf file. Nothing
|
|
# in docker-compose.yml interpolates it and the copyparty service is given no
|
|
# environment, so .env's copy is a record, not the live setting. The summary at
|
|
# the end tells operators to fix passive FTPS by re-running with FTP_NAT=... --
|
|
# so that has to apply to an EXISTING conf too, not only a freshly created one.
|
|
# Must stay above the chown further down, since `sed -i` rewrites as root.
|
|
CONF_CHANGED=0
|
|
if [[ -n "$FTP_NAT" ]]; then
|
|
# `|| true` is load-bearing: on a still-commented conf the grep matches
|
|
# nothing, and under `set -o pipefail` that would abort this assignment.
|
|
cur_nat="$(grep -E '^[[:space:]]*ftp-nat:' "$CONF" | head -n1 | sed -E 's/^[[:space:]]*ftp-nat:[[:space:]]*//; s/[[:space:]]*#.*$//; s/[[:space:]]*$//' || true)"
|
|
if [[ "$cur_nat" == "$FTP_NAT" ]]; then
|
|
: # already applied
|
|
elif grep -qE '^[[:space:]]*#?[[:space:]]*ftp-nat:' "$CONF"; then
|
|
log "Setting 'ftp-nat: ${FTP_NAT}' in $CONF (was: ${cur_nat:-unset})."
|
|
# `#?` so an already-set value is corrected, not just the commented
|
|
# template line.
|
|
sed -i -E "s|^[[:space:]]*#?[[:space:]]*ftp-nat:.*| ftp-nat: ${FTP_NAT}|" "$CONF"
|
|
CONF_CHANGED=1
|
|
else
|
|
warn "No ftp-nat line in $CONF to set. Add ' ftp-nat: ${FTP_NAT}' under [global] by hand."
|
|
fi
|
|
fi
|
|
# Read back the admin password for the summary (works on first run + re-runs).
|
|
# Strip the leading 'admin:', any trailing inline #comment, and surrounding space.
|
|
ADMIN_PW="$(grep -E '^[[:space:]]*admin:' "$CONF" | head -n1 \
|
|
| sed -E 's/^[[:space:]]*admin:[[:space:]]*//; s/[[:space:]]*#.*$//; s/[[:space:]]*$//')"
|
|
|
|
FTPS_PEM="$STACK_DIR/cfg/ftps.pem"
|
|
if [[ ! -f "$FTPS_PEM" ]]; then
|
|
command -v openssl >/dev/null 2>&1 || { log "Installing openssl for the FTPS cert..."; pkg_install openssl; }
|
|
command -v openssl >/dev/null 2>&1 || die "openssl is required to mint the FTPS cert; install it and re-run."
|
|
log "Generating self-signed FTPS certificate (CN=${COPYPARTY_DOMAIN})..."
|
|
_k="$(mktemp)"; _c="$(mktemp)"
|
|
if ! openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
|
|
-keyout "$_k" -out "$_c" -subj "/CN=${COPYPARTY_DOMAIN}" \
|
|
-addext "subjectAltName=DNS:${COPYPARTY_DOMAIN}" 2>/dev/null; then
|
|
# older openssl without -addext
|
|
openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
|
|
-keyout "$_k" -out "$_c" -subj "/CN=${COPYPARTY_DOMAIN}"
|
|
fi
|
|
cat "$_k" "$_c" > "$FTPS_PEM" # copyparty --cert wants key + chain concatenated
|
|
rm -f "$_k" "$_c"
|
|
chmod 0640 "$FTPS_PEM"
|
|
else
|
|
log "$FTPS_PEM exists; leaving it."
|
|
fi
|
|
|
|
# Everything copyparty (running as PUID) must read/write under /cfg.
|
|
chown -R "$PUID:$PGID" "$STACK_DIR/cfg"
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Seed .env (first run only)
|
|
# ----------------------------------------------------------------------------
|
|
if [[ ! -f "$ENV_FILE" ]]; then
|
|
log "Seeding $ENV_FILE..."
|
|
install -m 0600 "$SCRIPT_DIR/.env.example" "$ENV_FILE"
|
|
set_env COPYPARTY_DOMAIN "$COPYPARTY_DOMAIN"
|
|
set_env ACME_EMAIL "$ACME_EMAIL"
|
|
set_env COPYPARTY_IMAGE "$COPYPARTY_IMAGE"
|
|
set_env COPYPARTY_TAG "$COPYPARTY_TAG"
|
|
set_env PUID "$PUID"
|
|
set_env PGID "$PGID"
|
|
set_env DATA_DIR "$DATA_DIR"
|
|
set_env BIND_ADDR "$BIND_ADDR"
|
|
set_env SFTP_PORT "$SFTP_PORT"
|
|
set_env FTPS_PORT "$FTPS_PORT"
|
|
set_env FTP_PASV_RANGE "$FTP_PASV_RANGE"
|
|
set_env FTP_NAT "$FTP_NAT"
|
|
set_env UPDATE_POLICY "$UPDATE_POLICY"
|
|
set_env VC_FEED "$VC_FEED"
|
|
set_env CADDY_TAG "$CADDY_TAG"
|
|
else
|
|
log ".env exists; leaving it alone."
|
|
fi
|
|
|
|
# .env's FTP_NAT is a record rather than the live setting, but let it disagree
|
|
# with the conf and the next reader cannot tell which one is deployed.
|
|
if [[ -n "$FTP_NAT" ]]; then
|
|
cur_env_nat=$(sed -n 's/^FTP_NAT=//p' "$ENV_FILE" | tail -n1)
|
|
if [[ "$cur_env_nat" != "$FTP_NAT" ]]; then
|
|
set_env FTP_NAT "$FTP_NAT"
|
|
log "Recorded FTP_NAT=${FTP_NAT} in $ENV_FILE."
|
|
fi
|
|
fi
|
|
|
|
# Validate required values landed.
|
|
missing=()
|
|
for var in COPYPARTY_DOMAIN ACME_EMAIL; do
|
|
grep -E "^${var}=.+$" "$ENV_FILE" >/dev/null || missing+=("$var")
|
|
done
|
|
(( ${#missing[@]} == 0 )) || die "Missing values in $ENV_FILE: ${missing[*]}"
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Confirm
|
|
# ----------------------------------------------------------------------------
|
|
if [[ "$FORCE" != "1" ]]; then
|
|
cat <<EOF
|
|
|
|
About to pull images and start the copyparty stack from $STACK_DIR.
|
|
|
|
Caddy will request a Let's Encrypt cert for ${COPYPARTY_DOMAIN}. DNS for that
|
|
name must already point at this host, and ports 80/443 must be reachable from
|
|
the internet. SFTP (${SFTP_PORT}) and FTPS (${FTPS_PORT}) will also be opened.
|
|
|
|
Continue? [y/N]
|
|
EOF
|
|
read -r ans
|
|
[[ "${ans,,}" == "y" || "${ans,,}" == "yes" ]] || { warn "Aborted."; exit 0; }
|
|
fi
|
|
|
|
cd "$STACK_DIR"
|
|
log "Pulling images..."
|
|
docker compose pull
|
|
|
|
log "Starting stack..."
|
|
docker compose up -d --remove-orphans
|
|
|
|
# compose does not recreate a service whose image and compose config are
|
|
# unchanged, so a conf edit on a re-run would sit on disk while the running
|
|
# process keeps the old ftp-nat. Restart it explicitly.
|
|
if [[ "$CONF_CHANGED" == "1" ]]; then
|
|
log "copyparty.conf changed; restarting copyparty to apply it..."
|
|
docker compose restart copyparty
|
|
fi
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Wait for health
|
|
# ----------------------------------------------------------------------------
|
|
log "Waiting for services to become healthy (up to 120s)..."
|
|
deadline=$(( $(date +%s) + 120 ))
|
|
while (( $(date +%s) < deadline )); do
|
|
status=$(docker compose ps --format '{{.Service}} {{.Health}}' 2>/dev/null || true)
|
|
unhealthy=$(echo "$status" | awk '$2 != "healthy" && $2 != "" {print $1}')
|
|
if [[ -z "$unhealthy" && -n "$status" ]]; then
|
|
log "All services healthy."
|
|
break
|
|
fi
|
|
sleep 5
|
|
done
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Install + schedule the updater
|
|
# ----------------------------------------------------------------------------
|
|
if [[ "$COPYPARTY_AUTOUPDATE" != "0" ]]; then
|
|
log "Scheduling the copyparty updater (policy=${UPDATE_POLICY})..."
|
|
STACK_DIR="$STACK_DIR" UPDATE_POLICY="$UPDATE_POLICY" VC_FEED="$VC_FEED" \
|
|
bash "$STACK_DIR/update.sh" install || warn "Updater scheduling failed (non-fatal)."
|
|
else
|
|
log "COPYPARTY_AUTOUPDATE=0 -- update.sh installed but not scheduled."
|
|
fi
|
|
|
|
echo
|
|
log "Stack status:"
|
|
docker compose ps
|
|
echo
|
|
cat <<EOF
|
|
================================================================
|
|
DEPLOYED -- copyparty file server
|
|
|
|
Web UI: https://${COPYPARTY_DOMAIN}
|
|
SFTP: sftp -P ${SFTP_PORT} admin@${COPYPARTY_DOMAIN}
|
|
FTPS: explicit TLS on port ${FTPS_PORT}
|
|
lftp -u admin -e 'set ftp:ssl-force true; set ssl:verify-certificate no' \\
|
|
ftp://${COPYPARTY_DOMAIN}:${FTPS_PORT}
|
|
Stack dir: ${STACK_DIR}
|
|
|
|
Login: user 'admin' / password '${ADMIN_PW}'
|
|
(stored in ${STACK_DIR}/cfg/copyparty.conf)
|
|
|
|
Files: ${DATA_DIR} (mounted at /w; shared at the site root)
|
|
|
|
Notes:
|
|
* FTPS uses a self-signed cert (${STACK_DIR}/cfg/ftps.pem). Clients must accept
|
|
it, or swap in Caddy's Let's Encrypt cert -- see deployments/copyparty/README.md.
|
|
* Passive FTPS through NAT needs FTP_NAT set to this host's public IP (re-run
|
|
with FTP_NAT=..., or set ftp-nat in cfg/copyparty.conf).
|
|
* Real client IP comes from Caddy via X-Forwarded-For (trusted with xff-src).
|
|
|
|
Add users / open shares:
|
|
edit ${STACK_DIR}/cfg/copyparty.conf, then:
|
|
docker compose exec copyparty kill -s USR1 1 # hot-reload, no restart
|
|
|
|
Updates (security-notices aware):
|
|
${STACK_DIR}/update.sh check # current vs latest + any advisory match
|
|
${STACK_DIR}/update.sh update # update now (TARGET_VERSION=x.y.z to pin)
|
|
Scheduled daily; change UPDATE_POLICY (latest|security|off) in
|
|
${STACK_DIR}/.env or /etc/copyparty-update.conf, or: update.sh uninstall
|
|
|
|
Manage:
|
|
docker compose logs -f
|
|
docker compose pull && docker compose up -d # manual update
|
|
docker compose down # stop, keep data
|
|
docker compose down -v # stop, keep host data dir
|
|
|
|
Or just re-run this script -- it's idempotent.
|
|
================================================================
|
|
EOF
|
|
|
|
# IMPORTANT: do not put any code below this exit. Everything after the
|
|
# __ARCHIVE_BELOW__ marker is the embedded tar.gz payload (base64), appended by
|
|
# build.sh.
|
|
exit 0
|
|
__ARCHIVE_BELOW__
|
|
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
|
|
VKYylalMZSrT/036L64TOhwAoAAA
|