#!/bin/bash set -euo pipefail log() { local level="$1" shift local message="$*" local timestamp timestamp="$(date '+%Y-%m-%d %H:%M:%S')" echo "[$timestamp] [$level] $message" } APP_DIR="/app/code" DATA_DIR="/app/data" LOG_DIR="$DATA_DIR/logs" CONFIG_DIR="$DATA_DIR/config" TMP_DIR="$DATA_DIR/tmp" SECRETS_DIR="$DATA_DIR/secrets" MUSEUM_RUNTIME_DIR="$DATA_DIR/museum" MUSEUM_CONFIG_DIR="$MUSEUM_RUNTIME_DIR/configurations" MUSEUM_CONFIG="$MUSEUM_CONFIG_DIR/local.yaml" MUSEUM_BIN="/app/museum-bin/museum" WEB_SOURCE_DIR="/app/web" WEB_RUNTIME_DIR="$DATA_DIR/web" CADDY_CONFIG="$DATA_DIR/Caddyfile" STARTUP_FLAG="$DATA_DIR/startup.lock" mkdir -p "$LOG_DIR" "$CONFIG_DIR" "$TMP_DIR" "$SECRETS_DIR" "$MUSEUM_RUNTIME_DIR" "$WEB_RUNTIME_DIR" "$MUSEUM_CONFIG_DIR" chown -R cloudron:cloudron "$DATA_DIR" log INFO "Starting Ente for Cloudron" if ! command -v setpriv >/dev/null 2>&1; then log ERROR "setpriv command not found" exit 1 fi if [ -f "$STARTUP_FLAG" ]; then log WARN "Previous startup did not finish cleanly; removing flag" rm -f "$STARTUP_FLAG" fi touch "$STARTUP_FLAG" trap 'rm -f "$STARTUP_FLAG"' EXIT BASE_URL="${CLOUDRON_APP_ORIGIN:-https://$CLOUDRON_APP_FQDN}" BASE_URL="${BASE_URL%/}" RP_ID="${CLOUDRON_APP_FQDN:-${CLOUDRON_APP_DOMAIN:-localhost}}" API_ORIGIN="${BASE_URL}/api" log INFO "Application base URL: $BASE_URL" log INFO "Relying party ID: $RP_ID" log INFO "API origin: $API_ORIGIN" S3_CONFIG_FILE="$CONFIG_DIR/s3.env" if [ ! -f "$S3_CONFIG_FILE" ]; then cat > "$S3_CONFIG_FILE" <<'EOF_S3' # S3 configuration for Ente (required) # Provide credentials for an S3-compatible object storage and restart the app. # # Supported environment variables (either set here or via Cloudron env vars): # S3_ENDPOINT=https://example.s3-provider.com # S3_REGION=us-east-1 # S3_BUCKET=ente-data # S3_ACCESS_KEY=your-access-key # S3_SECRET_KEY=your-secret-key # S3_PREFIX=optional/path/prefix # # Example for Cloudflare R2 (replace placeholders): #S3_ENDPOINT=https://.r2.cloudflarestorage.com #S3_REGION=auto #S3_BUCKET=ente #S3_ACCESS_KEY=R2_ACCESS_KEY #S3_SECRET_KEY=R2_SECRET_KEY EOF_S3 chown cloudron:cloudron "$S3_CONFIG_FILE" chmod 600 "$S3_CONFIG_FILE" log INFO "Created S3 configuration template at $S3_CONFIG_FILE" fi set +u if [ -f "$S3_CONFIG_FILE" ]; then # shellcheck disable=SC1090 . "$S3_CONFIG_FILE" fi set -u S3_ENDPOINT="${S3_ENDPOINT:-${ENTE_S3_ENDPOINT:-}}" S3_REGION="${S3_REGION:-${ENTE_S3_REGION:-}}" S3_BUCKET="${S3_BUCKET:-${ENTE_S3_BUCKET:-}}" S3_ACCESS_KEY="${S3_ACCESS_KEY:-${ENTE_S3_ACCESS_KEY:-}}" S3_SECRET_KEY="${S3_SECRET_KEY:-${ENTE_S3_SECRET_KEY:-}}" S3_PREFIX="${S3_PREFIX:-${ENTE_S3_PREFIX:-}}" if [ -z "$S3_ENDPOINT" ] || [ -z "$S3_REGION" ] || [ -z "$S3_BUCKET" ] || [ -z "$S3_ACCESS_KEY" ] || [ -z "$S3_SECRET_KEY" ]; then log ERROR "Missing S3 configuration. Update $S3_CONFIG_FILE or set environment variables." log ERROR "The application will start in configuration mode. Please configure S3 and restart." S3_NOT_CONFIGURED=true else S3_NOT_CONFIGURED=false fi if [ "$S3_NOT_CONFIGURED" = "false" ]; then S3_ENDPOINT_HOST="${S3_ENDPOINT#https://}" S3_ENDPOINT_HOST="${S3_ENDPOINT_HOST#http://}" S3_ENDPOINT_HOST="${S3_ENDPOINT_HOST%%/}" S3_ENDPOINT_PATH="${S3_ENDPOINT_HOST#*/}" if [ "$S3_ENDPOINT_PATH" != "$S3_ENDPOINT_HOST" ]; then if [ -z "$S3_PREFIX" ]; then S3_PREFIX="$S3_ENDPOINT_PATH" fi S3_ENDPOINT_HOST="${S3_ENDPOINT_HOST%%/*}" fi log INFO "Using S3 endpoint $S3_ENDPOINT_HOST (region $S3_REGION, bucket $S3_BUCKET)" else S3_ENDPOINT_HOST="s3.example.com" log WARN "S3 not configured - using placeholder values" fi MASTER_KEY_FILE="$SECRETS_DIR/master_key" HASH_KEY_FILE="$SECRETS_DIR/hash_key" JWT_SECRET_FILE="$SECRETS_DIR/jwt_secret" SESSION_SECRET_FILE="$SECRETS_DIR/session_secret" SMTP_HOST="${CLOUDRON_MAIL_SMTP_SERVER:-}" SMTP_PORT="${CLOUDRON_MAIL_SMTP_PORT:-25}" SMTP_ENCRYPTION="${CLOUDRON_MAIL_SMTP_ENCRYPTION:-}" if [ -n "${CLOUDRON_MAIL_SMTPS_PORT:-}" ]; then SMTP_PORT="${CLOUDRON_MAIL_SMTPS_PORT}" SMTP_ENCRYPTION="tls" if [ -n "${CLOUDRON_MAIL_DOMAIN:-}" ]; then SMTP_HOST="mail.${CLOUDRON_MAIL_DOMAIN}" fi fi SMTP_USERNAME="${CLOUDRON_MAIL_SMTP_USERNAME:-}" SMTP_PASSWORD="${CLOUDRON_MAIL_SMTP_PASSWORD:-}" SMTP_EMAIL="${CLOUDRON_MAIL_FROM:-no-reply@$RP_ID}" SMTP_SENDER_NAME="${CLOUDRON_MAIL_FROM_DISPLAY_NAME:-Ente}" if [ -n "$SMTP_HOST" ]; then log INFO "SMTP configured for $SMTP_HOST:$SMTP_PORT (encryption: ${SMTP_ENCRYPTION:-none})" else log INFO "SMTP not configured; Museum will skip outbound email" fi normalize_b64() { local value="$1" value="$(printf '%s' "$value" | tr -d '\r\n')" value="$(printf '%s' "$value" | tr '-_' '+/')" local mod=$(( ${#value} % 4 )) if [ $mod -eq 2 ]; then value="${value}==" elif [ $mod -eq 3 ]; then value="${value}=" elif [ $mod -eq 1 ]; then value="" fi printf '%s' "$value" } normalize_b64url() { local value="$1" value="$(printf '%s' "$value" | tr -d '\r\n')" value="$(printf '%s' "$value" | tr '+/' '-_')" local mod=$(( ${#value} % 4 )) if [ $mod -eq 2 ]; then value="${value}==" elif [ $mod -eq 3 ]; then value="${value}=" elif [ $mod -eq 1 ]; then value="" fi printf '%s' "$value" } generate_b64() { local bytes="$1" openssl rand -base64 "$bytes" | tr -d '\n' } generate_b64url() { local bytes="$1" openssl rand -base64 "$bytes" | tr '+/' '-_' | tr -d '\n' } ensure_secret() { local file="$1" local bytes="$2" local mode="$3" local current="" if [ -f "$file" ]; then current="$(tr -d '\n' < "$file")" fi if [ "$mode" = "b64" ]; then current="$(normalize_b64 "$current")" if [ -z "$current" ]; then current="$(generate_b64 "$bytes")" fi else current="$(normalize_b64url "$current")" if [ -z "$current" ]; then current="$(generate_b64url "$bytes")" fi fi printf '%s ' "$current" > "$file" } ensure_secret "$MASTER_KEY_FILE" 32 b64 ensure_secret "$HASH_KEY_FILE" 64 b64 ensure_secret "$JWT_SECRET_FILE" 32 b64url ensure_secret "$SESSION_SECRET_FILE" 32 b64url MASTER_KEY="$(tr -d '\n' < "$MASTER_KEY_FILE")" HASH_KEY="$(tr -d '\n' < "$HASH_KEY_FILE")" JWT_SECRET="$(tr -d '\n' < "$JWT_SECRET_FILE")" SESSION_SECRET="$(tr -d '\n' < "$SESSION_SECRET_FILE")" chown cloudron:cloudron "$MASTER_KEY_FILE" "$HASH_KEY_FILE" "$JWT_SECRET_FILE" "$SESSION_SECRET_FILE" chmod 600 "$MASTER_KEY_FILE" "$HASH_KEY_FILE" "$JWT_SECRET_FILE" "$SESSION_SECRET_FILE" log INFO "Ensuring Museum runtime assets" sync_dir() { local source="$1" local target="$2" if [ -d "$source" ]; then log INFO "Syncing $(basename "$source") into data directory" rm -rf "$target" cp -a "$source" "$target" chown -R cloudron:cloudron "$target" else log WARN "Missing expected directory: $source" fi } sync_dir "$APP_DIR/server/migrations" "$MUSEUM_RUNTIME_DIR/migrations" sync_dir "$APP_DIR/server/web-templates" "$MUSEUM_RUNTIME_DIR/web-templates" sync_dir "$APP_DIR/server/mail-templates" "$MUSEUM_RUNTIME_DIR/mail-templates" sync_dir "$APP_DIR/server/assets" "$MUSEUM_RUNTIME_DIR/assets" if [ ! -x "$MUSEUM_BIN" ]; then log ERROR "Museum binary not found at $MUSEUM_BIN" exit 1 fi if [ ! -f "$MUSEUM_CONFIG" ]; then log INFO "Rendering Museum configuration" cat > "$MUSEUM_CONFIG" <> "$MUSEUM_CONFIG" </dev/null; then # Replace plain URL if sed -i "s|$search|$replace|g" "$file" 2>/dev/null; then file_changed=true fi # Replace backslash-escaped URL (common in JavaScript strings) if sed -i "s|$search_escaped_slash|$replace_escaped_slash|g" "$file" 2>/dev/null; then file_changed=true fi # Replace double-backslash-escaped URL (common in JSON) if sed -i "s|${search//\//\\\\/}|${replace//\//\\\\/}|g" "$file" 2>/dev/null; then file_changed=true fi if [ "$file_changed" = true ]; then chown cloudron:cloudron "$file" count=$((count + 1)) fi fi done < <(find "$WEB_RUNTIME_DIR" -type f \( -name "*.js" -o -name "*.json" -o -name "*.html" -o -name "*.css" -o -name "*.txt" \) -print0) if [ "$count" -gt 0 ]; then log INFO "Replaced '$search' with '$replace' in $count frontend files" fi } if [ -d "$WEB_RUNTIME_DIR" ]; then log INFO "Rewriting frontend endpoints for local deployment" FRONTEND_REPLACEMENTS=( "ENTE_API_ORIGIN_PLACEHOLDER|$API_ORIGIN" "ENTE_ALBUMS_ORIGIN_PLACEHOLDER|$BASE_URL/albums" "https://api.ente.io|$API_ORIGIN" "https://accounts.ente.io|$BASE_URL/accounts" "https://auth.ente.io|$BASE_URL/auth" "https://cast.ente.io|$BASE_URL/cast" "https://photos.ente.io|$BASE_URL/photos" "https://web.ente.io|$BASE_URL/photos" "https://albums.ente.io|$BASE_URL/albums" "https://family.ente.io|$BASE_URL/family" "https://ente.io|$BASE_URL" ) OLD_IFS="$IFS" for entry in "${FRONTEND_REPLACEMENTS[@]}"; do IFS="|" read -r search replace <<<"$entry" rewrite_frontend_reference "$search" "$replace" done IFS="$OLD_IFS" fi log INFO "Ensuring CLI configuration" CLI_HOME="$DATA_DIR/home/.ente" mkdir -p "$CLI_HOME" cat > "$CLI_HOME/config.yaml" < "$CADDY_CONFIG" < "$TMP_DIR/caddy-validate.log" 2>&1; then cat "$TMP_DIR/caddy-validate.log" log ERROR "Caddy configuration validation failed" exit 1 fi log INFO "Testing PostgreSQL connectivity" if ! PGPASSWORD="$CLOUDRON_POSTGRESQL_PASSWORD" psql -h "$CLOUDRON_POSTGRESQL_HOST" -p "$CLOUDRON_POSTGRESQL_PORT" \ -U "$CLOUDRON_POSTGRESQL_USERNAME" -d "$CLOUDRON_POSTGRESQL_DATABASE" -c "SELECT 1" >/dev/null 2>&1; then log ERROR "Unable to connect to PostgreSQL" exit 1 fi if [ "$S3_NOT_CONFIGURED" = "true" ]; then log WARN "S3 not configured - creating configuration page" mkdir -p "$WEB_RUNTIME_DIR/config" cat > "$WEB_RUNTIME_DIR/config/index.html" <<'EOF_CONFIG' Ente Configuration Required

Ente Configuration Required

S3 Storage Not Configured

Ente requires S3-compatible object storage to function. Please configure your S3 credentials.

Configuration Steps

  1. Open the Cloudron dashboard
  2. Go to your Ente app and open the Terminal
  3. Edit /app/data/config/s3.env:
    nano /app/data/config/s3.env
  4. Add your S3 credentials:
    S3_ENDPOINT=https://your-s3-endpoint.com
    S3_REGION=your-region
    S3_BUCKET=your-bucket-name
    S3_ACCESS_KEY=your-access-key
    S3_SECRET_KEY=your-secret-key
  5. Save the file and restart the app from the Cloudron dashboard

For more information, see the Ente S3 Configuration Guide.

EOF_CONFIG chown -R cloudron:cloudron "$WEB_RUNTIME_DIR/config" log INFO "Starting Caddy in configuration mode" setpriv --reuid=cloudron --regid=cloudron --init-groups caddy file-server --listen :3080 --root "$WEB_RUNTIME_DIR/config" & CADDY_PID=$! MUSEUM_PID="" else log INFO "Starting Museum server and Caddy" setpriv --reuid=cloudron --regid=cloudron --init-groups /bin/bash -lc "cd '$MUSEUM_RUNTIME_DIR' && exec stdbuf -oL '$MUSEUM_BIN'" & MUSEUM_PID=$! setpriv --reuid=cloudron --regid=cloudron --init-groups caddy run --config "$CADDY_CONFIG" --watch & CADDY_PID=$! fi terminate() { log INFO "Shutting down services" if [ -n "$MUSEUM_PID" ]; then kill "$MUSEUM_PID" 2>/dev/null || true fi if [ -n "$CADDY_PID" ]; then kill "$CADDY_PID" 2>/dev/null || true fi if [ -n "$MUSEUM_PID" ]; then wait "$MUSEUM_PID" 2>/dev/null || true fi if [ -n "$CADDY_PID" ]; then wait "$CADDY_PID" 2>/dev/null || true fi } trap terminate TERM INT if [ -n "$MUSEUM_PID" ]; then wait -n "$MUSEUM_PID" "$CADDY_PID" else wait "$CADDY_PID" fi EXIT_CODE=$? terminate log ERROR "Service exited unexpectedly" exit "$EXIT_CODE"