Files
BetterDesk/docker/entrypoint.sh
T
UNITRONIX 2e4bca238a chore: release version 3.5.4 [version-bump]
Stable hotfix: Address Book ACL (#342) and legacy SQLite role CHECK migration.
2026-08-01 16:54:51 +02:00

229 lines
9.4 KiB
Bash

#!/bin/sh
# BetterDesk — All-in-One Container Entrypoint
# Runs Go server + Node.js console via supervisord
set -e
echo "========================================"
echo " BetterDesk All-in-One Container"
echo " Version: ${BETTERDESK_IMAGE_VERSION:-3.5.4}"
echo "========================================"
echo ""
echo "Components:"
echo " Go Server: signal + relay + API (ports 21115-21121)"
echo " Node.js Console: web panel (port ${PORT:-5000})"
echo " Client API: port ${API_PORT:-21121}"
echo ""
echo "Configuration:"
echo " NODE_ENV: ${NODE_ENV:-production}"
echo " DB_TYPE: ${DB_TYPE:-sqlite}"
echo " ENCRYPTED_ONLY: ${ENCRYPTED_ONLY:-1}"
echo " DATA_DIR: ${DATA_DIR:-/app/data}"
echo ""
# Public Docker examples use ADMIN_*; seed both the Go API and Node.js console.
if [ -n "${ADMIN_USERNAME:-}" ]; then
if [ -z "${INIT_ADMIN_USER:-}" ]; then
export INIT_ADMIN_USER="$ADMIN_USERNAME"
fi
if [ -z "${DEFAULT_ADMIN_USERNAME:-}" ]; then
export DEFAULT_ADMIN_USERNAME="$ADMIN_USERNAME"
fi
fi
if [ -n "${ADMIN_PASSWORD:-}" ]; then
if [ -z "${INIT_ADMIN_PASS:-}" ]; then
export INIT_ADMIN_PASS="$ADMIN_PASSWORD"
fi
if [ -z "${DEFAULT_ADMIN_PASSWORD:-}" ]; then
export DEFAULT_ADMIN_PASSWORD="$ADMIN_PASSWORD"
fi
fi
# Ensure data directories exist and have correct permissions
mkdir -p /opt/rustdesk /app/data /var/log/betterdesk 2>/dev/null || true
chown -R betterdesk:betterdesk /opt/rustdesk /app/data /var/log/betterdesk 2>/dev/null || true
# Write a file as betterdesk. Fresh named volumes inherit image ownership
# (UID 10001); with compose cap_drop:ALL root has no CAP_DAC_OVERRIDE and
# cannot create files there (Permission denied on .api_key, issue #299).
write_as_betterdesk() {
# usage: write_as_betterdesk <path> <content>
_wad_path="$1"
_wad_content="$2"
if command -v su-exec >/dev/null 2>&1; then
printf '%s\n' "$_wad_content" | su-exec betterdesk sh -c "umask 077; cat > \"$_wad_path\""
else
printf '%s\n' "$_wad_content" | su -s /bin/sh betterdesk -c "umask 077; cat > \"$_wad_path\""
fi
}
touch_as_betterdesk() {
if command -v su-exec >/dev/null 2>&1; then
su-exec betterdesk touch "$1"
else
su -s /bin/sh betterdesk -c "touch \"$1\""
fi
}
# Bootstrap API key (shared between Go server and Node.js console)
API_KEY_FILE="/opt/rustdesk/.api_key"
if [ -z "${API_KEY:-}" ] && [ ! -f "$API_KEY_FILE" ]; then
if command -v openssl >/dev/null 2>&1; then
API_KEY=$(openssl rand -hex 32)
else
API_KEY=$(cat /dev/urandom | head -c 32 | od -An -tx1 | tr -d ' \n')
fi
write_as_betterdesk "$API_KEY_FILE" "$API_KEY"
echo "Auto-generated API key → $API_KEY_FILE"
elif [ -n "${API_KEY:-}" ] && [ ! -f "$API_KEY_FILE" ]; then
write_as_betterdesk "$API_KEY_FILE" "$API_KEY"
echo "API key from env → $API_KEY_FILE"
fi
# Default enrollment policy.
# Fresh volumes default to "managed": stock RustDesk clients are queued for
# operator approval instead of connecting silently. Pre-existing installs keep
# their current behavior (Go default "open", or whatever was set via the panel
# and persisted in the database). A volume that already contains a server key
# or SQLite database is treated as pre-existing.
if [ -z "${ENROLLMENT_MODE:-}" ]; then
ENROLLMENT_SENTINEL="/opt/rustdesk/.enrollment_initialized"
if [ ! -f "$ENROLLMENT_SENTINEL" ]; then
if [ -f /opt/rustdesk/db_v2.sqlite3 ] || [ -f /opt/rustdesk/id_ed25519 ]; then
echo "Enrollment: preserving existing policy (pre-existing volume)"
else
export ENROLLMENT_MODE="managed"
echo "Enrollment: managed (fresh install — new devices need approval)"
fi
touch_as_betterdesk "$ENROLLMENT_SENTINEL" 2>/dev/null || true
fi
fi
# Always export so supervisord's %(ENV_ENROLLMENT_MODE)s interpolation resolves.
# An empty value is ignored by the Go server (keeps default/DB-restored mode).
export ENROLLMENT_MODE="${ENROLLMENT_MODE:-}"
# Verify write access — SQLite WAL mode requires writable directory (Issue #78)
if ! touch_as_betterdesk /opt/rustdesk/.write_test 2>/dev/null; then
echo ""
echo "ERROR: /opt/rustdesk is NOT writable by the betterdesk user (UID 10001)."
echo " SQLite WAL mode requires write access to the database directory."
echo " If using bind mounts, run: chown -R 10001:10001 /path/to/your/data"
echo " Or use Docker named volumes instead of bind mounts."
echo ""
fi
rm -f /opt/rustdesk/.write_test 2>/dev/null || true
# Fix private key permissions (volume mounts may preserve wrong UID/mode)
if [ -f /opt/rustdesk/id_ed25519 ]; then
chmod 600 /opt/rustdesk/id_ed25519 2>/dev/null || true
chown betterdesk:betterdesk /opt/rustdesk/id_ed25519 2>/dev/null || true
fi
# BD-2026-007: Warn about weak default secrets
if [ -n "${SESSION_SECRET}" ] && [ ${#SESSION_SECRET} -lt 32 ]; then
echo "WARNING [SECURITY]: SESSION_SECRET is shorter than 32 characters — generate a stronger secret"
fi
if [ -n "${ADMIN_PASSWORD}" ] && [ ${#ADMIN_PASSWORD} -lt 12 ]; then
echo "WARNING [SECURITY]: ADMIN_PASSWORD is shorter than 12 characters — use a stronger password"
fi
# Determine database DSN for Go server
# DB_URL env var is read by Go server's config.LoadEnv()
if [ "${DB_TYPE}" = "postgres" ] || [ "${DB_TYPE}" = "postgresql" ]; then
if [ -n "${DATABASE_URL}" ]; then
export DB_URL="${DATABASE_URL}"
echo "Database: PostgreSQL (${DATABASE_URL%%@*}@***)"
else
echo "WARNING: DB_TYPE=postgres but DATABASE_URL not set — falling back to SQLite"
export DB_URL="/opt/rustdesk/db_v2.sqlite3"
fi
else
export DB_URL="${DB_PATH:-/opt/rustdesk/db_v2.sqlite3}"
export AUTH_DB_PATH="${AUTH_DB_PATH:-/app/data/auth.db}"
echo "Database: SQLite (${DB_URL})"
echo "Panel sync: ${AUTH_DB_PATH}"
fi
# Wait for PostgreSQL if configured
if [ "${DB_TYPE}" = "postgres" ] || [ "${DB_TYPE}" = "postgresql" ]; then
if [ -n "${DATABASE_URL}" ]; then
echo "Waiting for PostgreSQL..."
PG_HOST=$(echo "$DATABASE_URL" | sed -n 's|.*@\([^:/]*\).*|\1|p')
PG_PORT=$(echo "$DATABASE_URL" | sed -n 's|.*:\([0-9]*\)/.*|\1|p')
PG_PORT=${PG_PORT:-5432}
RETRIES=0
MAX_RETRIES=30
while [ "$RETRIES" -lt "$MAX_RETRIES" ]; do
if nc -z "$PG_HOST" "$PG_PORT" 2>/dev/null; then
echo " PostgreSQL is ready ($PG_HOST:$PG_PORT)"
break
fi
RETRIES=$((RETRIES + 1))
echo " Waiting... ($RETRIES/$MAX_RETRIES)"
sleep 2
done
if [ "$RETRIES" -ge "$MAX_RETRIES" ]; then
echo " WARNING: PostgreSQL not reachable — starting anyway"
fi
fi
fi
# Ensure Go server uses correct signal port (not NODE.js PORT)
export SIGNAL_PORT="${SIGNAL_PORT:-21116}"
export SIGNAL_RATE_LIMIT_PER_IP="${SIGNAL_RATE_LIMIT_PER_IP:-20}"
# Ensure Node.js Client API binds to all interfaces (not just localhost)
export API_HOST="${API_HOST:-0.0.0.0}"
export HOST="${HOST:-0.0.0.0}"
# Relay server address: if not explicitly set, try to auto-detect public IP.
# Inside Docker, the Go server's own detection may return the container's
# internal IP (172.x.x.x) which is unreachable by remote clients.
if [ -z "${RELAY_SERVERS:-}" ]; then
DETECTED_IP=""
if command -v curl >/dev/null 2>&1; then
DETECTED_IP=$(curl -4 -sf --max-time 5 https://checkip.amazonaws.com 2>/dev/null \
|| curl -4 -sf --max-time 5 https://api.ipify.org 2>/dev/null \
|| curl -4 -sf --max-time 5 https://ifconfig.me/ip 2>/dev/null || true)
DETECTED_IP=$(echo "$DETECTED_IP" | tr -d '[:space:]')
fi
if [ -n "$DETECTED_IP" ]; then
# Verify it's not a private/Docker IP
case "$DETECTED_IP" in
10.*|172.1[6-9].*|172.2[0-9].*|172.3[0-1].*|192.168.*|127.*)
echo "WARNING: Auto-detected IP ($DETECTED_IP) is private — relay may fail for remote clients."
echo " Set RELAY_SERVERS=YOUR.PUBLIC.IP in docker-compose.single.yml"
;;
*)
export RELAY_SERVERS="$DETECTED_IP"
echo "Relay IP: $DETECTED_IP (auto-detected)"
;;
esac
else
echo "WARNING: Could not auto-detect public IP for relay."
echo " Set RELAY_SERVERS=YOUR.PUBLIC.IP in docker-compose.single.yml"
fi
else
echo "Relay IP: $RELAY_SERVERS (from env)"
fi
export RELAY_SERVERS="${RELAY_SERVERS:-}"
# Always export so supervisord's %(ENV_*)s interpolation resolves (#299).
# Compose usually sets these; Portainer / bare docker run may omit them.
export NTP_SERVERS="${NTP_SERVERS:-pool.ntp.org,time.google.com,time.cloudflare.com}"
export BILLING_MAX_CLOCK_SKEW_MS="${BILLING_MAX_CLOCK_SKEW_MS:-2000}"
export BILLING_REQUIRE_SYNCED_CLOCK="${BILLING_REQUIRE_SYNCED_CLOCK:-1}"
export BILLING_TRUST_OS_NTP="${BILLING_TRUST_OS_NTP:-Y}"
echo ""
echo "Starting services via supervisord..."
if [ "${HTTPS_ENABLED:-false}" = "true" ]; then
echo " Web Console: https://localhost:${HTTPS_PORT:-5443}"
echo " HTTP redirect: http://localhost:${PORT:-5000} → https://…:${HTTPS_PORT:-5443}"
else
echo " Web Console: http://localhost:${PORT:-5000}"
fi
echo " Go API: http://localhost:21121/api"
echo "========================================"
echo ""
# Start supervisord (manages both processes)
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/betterdesk.conf