From d24352c80560cdc4650058fbfded5437a38b38f2 Mon Sep 17 00:00:00 2001 From: Andreas Dueren Date: Mon, 17 Nov 2025 13:18:12 -0600 Subject: [PATCH] Support optional S3 replication --- BUILD-INSTRUCTIONS.md | 2 +- POSTINSTALL.md | 1 + README.md | 2 +- start.sh | 254 +++++++++++++++++++++++++++++++++++++----- 4 files changed, 229 insertions(+), 30 deletions(-) diff --git a/BUILD-INSTRUCTIONS.md b/BUILD-INSTRUCTIONS.md index 406de04..64dbac9 100644 --- a/BUILD-INSTRUCTIONS.md +++ b/BUILD-INSTRUCTIONS.md @@ -20,7 +20,7 @@ cloudron install \ ``` ## After Install -1. **S3** – In Cloudron File Manager open `/app/data/config/s3.env`, fill in your endpoint/region/bucket/access/secret, then restart the app from the dashboard. +1. **S3** – In Cloudron File Manager open `/app/data/config/s3.env`, fill in your endpoint/region/bucket/access/secret, then restart the app from the dashboard. Optional replication: add both `S3_SECONDARY_*` (second hot bucket) **and** `S3_DERIVED_*` (derived/cold bucket) variables to mirror uploads across three independent buckets. Replication is only enabled when all three buckets are present. See Ente’s [object storage guide](https://ente.io/help/self-hosting/administration/object-storage) for example configs. 2. **Secondary hostnames** – During installation Cloudron now prompts for hostnames for the Accounts/Auth/Cast/Albums/Family web apps (powered by `httpPorts`). Ensure matching DNS records exist that point to the primary app domain. If you use Cloudron-managed DNS, those records are created automatically; otherwise create CNAME/A records such as `accounts.`. Once DNS propagates, use the dedicated hosts (defaults shown below — substitute the names you selected during install): diff --git a/POSTINSTALL.md b/POSTINSTALL.md index 1abde2d..47f895e 100644 --- a/POSTINSTALL.md +++ b/POSTINSTALL.md @@ -53,6 +53,7 @@ Supported variables: - `S3_ACCESS_KEY` - `S3_SECRET_KEY` - `S3_PREFIX` (optional path prefix) +- Optional replication: define `S3_SECONDARY_*` **and** `S3_DERIVED_*` (endpoints, keys, secrets, DC names) to mirror uploads to a second hot bucket and a third derived/cold bucket. Replication is only enabled when all three buckets are configured; otherwise the app stays in single-bucket mode. See [Ente’s object storage guide](https://ente.io/help/self-hosting/administration/object-storage) for sample setups and discussion of reliability. ## Required: Secondary Hostnames diff --git a/README.md b/README.md index 467cd3a..b8b12f0 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ The app is configured automatically using Cloudron's environment variables for: After installing on Cloudron remember to: -1. Open the File Manager for the app, edit `/app/data/config/s3.env` with your object storage endpoint/keys, and restart the app. If you are using Cloudflare R2 or another S3-compatible service, configure the bucket’s CORS policy to allow the Ente frontends (e.g. `https://ente.due.ren`, `https://accounts.due.ren`, `https://cast.due.ren`, etc.) so that cast/slideshow playback can fetch signed URLs directly from storage. +1. Open the File Manager for the app, edit `/app/data/config/s3.env` with your object storage endpoint/keys, and restart the app. If you are using Cloudflare R2 or another S3-compatible service, configure the bucket’s CORS policy to allow the Ente frontends (e.g. `https://ente.due.ren`, `https://accounts.due.ren`, `https://cast.due.ren`, etc.) so that cast/slideshow playback can fetch signed URLs directly from storage. Replication requires **three** buckets: the primary (`S3_*`), a secondary hot bucket (`S3_SECONDARY_*`) and a derived/cold bucket (`S3_DERIVED_*`). Once all three are configured the package will automatically enable replication. See the [object storage guide](https://ente.io/help/self-hosting/administration/object-storage) for sample layouts and reliability notes. 2. When prompted during installation, pick hostnames for the Accounts/Auth/Cast/Albums/Family web apps (they are exposed via Cloudron `httpPorts`). Ensure matching DNS records exist; Cloudron-managed DNS creates them automatically, otherwise point CNAME/A records such as `accounts.` at the primary hostname. 3. To persist tweaks to Museum (for example, seeding super-admin or whitelist entries), create `/app/data/config/museum.override.yaml`. Its contents are appended to the generated `museum/configurations/local.yaml` on every start, so you only need to declare the keys you want to override. ```yaml diff --git a/start.sh b/start.sh index b7a0a0d..12a2588 100755 --- a/start.sh +++ b/start.sh @@ -177,6 +177,23 @@ if [ ! -f "$S3_CONFIG_FILE" ]; then # S3_ACCESS_KEY=your-access-key # S3_SECRET_KEY=your-secret-key # S3_PREFIX=optional/path/prefix +# Optional replication settings (secondary object storage): +# S3_SECONDARY_ENDPOINT=https://secondary.s3-provider.com +# S3_SECONDARY_REGION=us-west-1 +# S3_SECONDARY_BUCKET=ente-data-backup +# S3_SECONDARY_ACCESS_KEY=secondary-access-key +# S3_SECONDARY_SECRET_KEY=secondary-secret-key +# S3_SECONDARY_PREFIX=optional/path/prefix +# S3_SECONDARY_DC=b2-us-west +# S3_DERIVED_ENDPOINT=https://cold.s3-provider.com +# S3_DERIVED_REGION=eu-central-1 +# S3_DERIVED_BUCKET=ente-derived +# S3_DERIVED_ACCESS_KEY=derived-access-key +# S3_DERIVED_SECRET_KEY=derived-secret-key +# S3_DERIVED_PREFIX=optional/path/prefix +# S3_DERIVED_DC=scw-eu-fr-v3 +# Replication requires configuring both the secondary hot storage and the derived +# storage buckets. Leave these unset to run with a single bucket. # # Example for Cloudflare R2 (replace placeholders): #S3_ENDPOINT=https://.r2.cloudflarestorage.com @@ -186,8 +203,14 @@ if [ ! -f "$S3_CONFIG_FILE" ]; then #S3_SECRET_KEY=R2_SECRET_KEY #S3_FORCE_PATH_STYLE=true #S3_PRIMARY_DC=b2-eu-cen -#S3_SECONDARY_DC=b2-eu-cen #S3_DERIVED_DC=b2-eu-cen +# Optional replication to Backblaze B2: +#S3_SECONDARY_ENDPOINT=https://s3.us-west-002.backblazeb2.com +#S3_SECONDARY_REGION=us-west-002 +#S3_SECONDARY_BUCKET=ente +#S3_SECONDARY_ACCESS_KEY=B2_ACCESS_KEY +#S3_SECONDARY_SECRET_KEY=B2_SECRET_KEY +#S3_SECONDARY_DC=b2-us-west # # Example for Backblaze B2 (replace placeholders): #S3_ENDPOINT=https://s3.us-west-002.backblazeb2.com @@ -197,8 +220,14 @@ if [ ! -f "$S3_CONFIG_FILE" ]; then #S3_SECRET_KEY=B2_SECRET_KEY #S3_FORCE_PATH_STYLE=true #S3_PRIMARY_DC=b2-eu-cen -#S3_SECONDARY_DC=b2-eu-cen #S3_DERIVED_DC=b2-eu-cen +# Optional replication to Cloudflare R2: +#S3_SECONDARY_ENDPOINT=https://.r2.cloudflarestorage.com +#S3_SECONDARY_REGION=auto +#S3_SECONDARY_BUCKET=ente-replica +#S3_SECONDARY_ACCESS_KEY=R2_ACCESS_KEY +#S3_SECONDARY_SECRET_KEY=R2_SECRET_KEY +#S3_SECONDARY_DC=r2-eu EOF_S3 chown cloudron:cloudron "$S3_CONFIG_FILE" chmod 600 "$S3_CONFIG_FILE" @@ -212,6 +241,25 @@ if [ -f "$S3_CONFIG_FILE" ]; then fi set -u +parse_s3_endpoint() { + local raw="$1" + local prefix="$2" + local host_var="$3" + local prefix_var="$4" + local host="${raw#https://}" + host="${host#http://}" + host="${host%%/}" + local path="${host#*/}" + if [ "$path" != "$host" ]; then + if [ -z "$prefix" ]; then + prefix="$path" + fi + host="${host%%/*}" + fi + printf -v "$host_var" "%s" "$host" + printf -v "$prefix_var" "%s" "$prefix" +} + S3_ENDPOINT="${S3_ENDPOINT:-${ENTE_S3_ENDPOINT:-}}" S3_REGION="${S3_REGION:-${ENTE_S3_REGION:-}}" S3_BUCKET="${S3_BUCKET:-${ENTE_S3_BUCKET:-}}" @@ -219,6 +267,25 @@ 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:-}}" +S3_SECONDARY_ENDPOINT="${S3_SECONDARY_ENDPOINT:-${ENTE_S3_SECONDARY_ENDPOINT:-}}" +S3_SECONDARY_REGION="${S3_SECONDARY_REGION:-${ENTE_S3_SECONDARY_REGION:-}}" +S3_SECONDARY_BUCKET="${S3_SECONDARY_BUCKET:-${ENTE_S3_SECONDARY_BUCKET:-}}" +S3_SECONDARY_ACCESS_KEY="${S3_SECONDARY_ACCESS_KEY:-${ENTE_S3_SECONDARY_ACCESS_KEY:-}}" +S3_SECONDARY_SECRET_KEY="${S3_SECONDARY_SECRET_KEY:-${ENTE_S3_SECONDARY_SECRET_KEY:-}}" +S3_SECONDARY_PREFIX="${S3_SECONDARY_PREFIX:-${ENTE_S3_SECONDARY_PREFIX:-}}" +S3_SECONDARY_DC_RAW="${ENTE_S3_SECONDARY_DC:-}" +S3_SECONDARY_ENABLED=false +S3_SECONDARY_ENDPOINT_HOST="" + +S3_DERIVED_ENDPOINT="${S3_DERIVED_ENDPOINT:-${ENTE_S3_DERIVED_ENDPOINT:-}}" +S3_DERIVED_REGION="${S3_DERIVED_REGION:-${ENTE_S3_DERIVED_REGION:-}}" +S3_DERIVED_BUCKET="${S3_DERIVED_BUCKET:-${ENTE_S3_DERIVED_BUCKET:-}}" +S3_DERIVED_ACCESS_KEY="${S3_DERIVED_ACCESS_KEY:-${ENTE_S3_DERIVED_ACCESS_KEY:-}}" +S3_DERIVED_SECRET_KEY="${S3_DERIVED_SECRET_KEY:-${ENTE_S3_DERIVED_SECRET_KEY:-}}" +S3_DERIVED_PREFIX="${S3_DERIVED_PREFIX:-${ENTE_S3_DERIVED_PREFIX:-}}" +S3_DERIVED_CUSTOM=false +S3_DERIVED_ENDPOINT_HOST="" + 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." @@ -228,24 +295,15 @@ else 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)" + parse_s3_endpoint "$S3_ENDPOINT" "$S3_PREFIX" S3_ENDPOINT_HOST S3_PREFIX + parse_s3_endpoint "$S3_DERIVED_ENDPOINT" "$S3_DERIVED_PREFIX" S3_DERIVED_ENDPOINT_HOST S3_DERIVED_PREFIX S3_REGION_LOWER="$(printf '%s' "$S3_REGION" | tr '[:upper:]' '[:lower:]')" if printf '%s' "$S3_ENDPOINT_HOST" | grep -q '\.r2\.cloudflarestorage\.com$' && [ "$S3_REGION_LOWER" != "auto" ]; then log WARN "Cloudflare R2 endpoints require S3_REGION=auto; current value '$S3_REGION' may cause upload failures" fi else S3_ENDPOINT_HOST="s3.example.com" + S3_DERIVED_ENDPOINT_HOST="$S3_ENDPOINT_HOST" log WARN "S3 not configured - using placeholder values" fi @@ -261,9 +319,98 @@ S3_FORCE_PATH_STYLE="$(printf '%s' "$S3_FORCE_PATH_STYLE_RAW" | tr '[:upper:]' ' S3_ARE_LOCAL_BUCKETS="$(printf '%s' "${S3_ARE_LOCAL_BUCKETS:-${ENTE_S3_ARE_LOCAL_BUCKETS:-false}}" | tr '[:upper:]' '[:lower:]')" S3_PRIMARY_DC="${ENTE_S3_PRIMARY_DC:-b2-eu-cen}" -S3_SECONDARY_DC="${ENTE_S3_SECONDARY_DC:-$S3_PRIMARY_DC}" S3_DERIVED_DC="${ENTE_S3_DERIVED_DC:-$S3_PRIMARY_DC}" +S3_SECONDARY_ENV_PRESENT=false +for value in "$S3_SECONDARY_ENDPOINT" "$S3_SECONDARY_REGION" "$S3_SECONDARY_BUCKET" "$S3_SECONDARY_ACCESS_KEY" "$S3_SECONDARY_SECRET_KEY" "$S3_SECONDARY_PREFIX" "$S3_SECONDARY_DC_RAW"; do + if [ -n "$value" ]; then + S3_SECONDARY_ENV_PRESENT=true + break + fi +done + +if [ "$S3_NOT_CONFIGURED" = "false" ] && [ "$S3_SECONDARY_ENV_PRESENT" = true ]; then + S3_SECONDARY_REGION="${S3_SECONDARY_REGION:-$S3_REGION}" + S3_SECONDARY_BUCKET="${S3_SECONDARY_BUCKET:-$S3_BUCKET}" + S3_SECONDARY_PREFIX="${S3_SECONDARY_PREFIX:-$S3_PREFIX}" + MISSING_SECONDARY_VARS=() + [ -z "$S3_SECONDARY_ENDPOINT" ] && MISSING_SECONDARY_VARS+=("S3_SECONDARY_ENDPOINT") + [ -z "$S3_SECONDARY_ACCESS_KEY" ] && MISSING_SECONDARY_VARS+=("S3_SECONDARY_ACCESS_KEY") + [ -z "$S3_SECONDARY_SECRET_KEY" ] && MISSING_SECONDARY_VARS+=("S3_SECONDARY_SECRET_KEY") + if [ "${#MISSING_SECONDARY_VARS[@]}" -gt 0 ]; then + log ERROR "Secondary S3 configuration incomplete (missing: ${MISSING_SECONDARY_VARS[*]}). Replication disabled." + S3_SECONDARY_ENABLED=false + S3_SECONDARY_DC="" + else + S3_SECONDARY_ENABLED=true + if [ -n "$S3_SECONDARY_DC_RAW" ]; then + S3_SECONDARY_DC="$S3_SECONDARY_DC_RAW" + else + S3_SECONDARY_DC="${S3_PRIMARY_DC}-secondary" + fi + fi +else + S3_SECONDARY_ENABLED=false + S3_SECONDARY_DC="" +fi + +S3_DERIVED_ENV_PRESENT=false +for value in "$S3_DERIVED_ENDPOINT" "$S3_DERIVED_REGION" "$S3_DERIVED_BUCKET" "$S3_DERIVED_ACCESS_KEY" "$S3_DERIVED_SECRET_KEY" "$S3_DERIVED_PREFIX"; do + if [ -n "$value" ]; then + S3_DERIVED_ENV_PRESENT=true + break + fi +done + +if [ "$S3_NOT_CONFIGURED" = "false" ]; then + if [ "$S3_DERIVED_ENV_PRESENT" = true ]; then + S3_DERIVED_REGION="${S3_DERIVED_REGION:-$S3_REGION}" + S3_DERIVED_BUCKET="${S3_DERIVED_BUCKET:-$S3_BUCKET}" + S3_DERIVED_PREFIX="${S3_DERIVED_PREFIX:-$S3_PREFIX}" + MISSING_DERIVED_VARS=() + [ -z "$S3_DERIVED_ENDPOINT" ] && MISSING_DERIVED_VARS+=("S3_DERIVED_ENDPOINT") + [ -z "$S3_DERIVED_ACCESS_KEY" ] && MISSING_DERIVED_VARS+=("S3_DERIVED_ACCESS_KEY") + [ -z "$S3_DERIVED_SECRET_KEY" ] && MISSING_DERIVED_VARS+=("S3_DERIVED_SECRET_KEY") + if [ "${#MISSING_DERIVED_VARS[@]}" -gt 0 ]; then + log ERROR "Derived S3 configuration incomplete (missing: ${MISSING_DERIVED_VARS[*]}). Falling back to primary bucket for derived assets." + S3_DERIVED_CUSTOM=false + S3_DERIVED_ENDPOINT="$S3_ENDPOINT" + S3_DERIVED_REGION="$S3_REGION" + S3_DERIVED_BUCKET="$S3_BUCKET" + S3_DERIVED_ACCESS_KEY="$S3_ACCESS_KEY" + S3_DERIVED_SECRET_KEY="$S3_SECRET_KEY" + S3_DERIVED_PREFIX="$S3_PREFIX" + else + S3_DERIVED_CUSTOM=true + fi + else + S3_DERIVED_CUSTOM=false + S3_DERIVED_ENDPOINT="$S3_ENDPOINT" + S3_DERIVED_REGION="$S3_REGION" + S3_DERIVED_BUCKET="$S3_BUCKET" + S3_DERIVED_ACCESS_KEY="$S3_ACCESS_KEY" + S3_DERIVED_SECRET_KEY="$S3_SECRET_KEY" + S3_DERIVED_PREFIX="$S3_PREFIX" + fi +else + S3_DERIVED_CUSTOM=false +fi + +if [ "$S3_NOT_CONFIGURED" = "false" ] && [ "$S3_SECONDARY_ENABLED" = true ]; then + parse_s3_endpoint "$S3_SECONDARY_ENDPOINT" "$S3_SECONDARY_PREFIX" S3_SECONDARY_ENDPOINT_HOST S3_SECONDARY_PREFIX +else + S3_SECONDARY_ENDPOINT_HOST="" +fi + +S3_REPLICATION_ENABLED=false +if [ "$S3_SECONDARY_ENABLED" = true ] && [ "$S3_DERIVED_CUSTOM" = true ]; then + S3_REPLICATION_ENABLED=true +elif [ "$S3_SECONDARY_ENABLED" = true ] && [ "$S3_DERIVED_CUSTOM" = false ]; then + log WARN "Secondary bucket configured without a dedicated derived bucket; S3 replication remains disabled." +elif [ "$S3_SECONDARY_ENABLED" = false ] && [ "$S3_DERIVED_CUSTOM" = true ]; then + log WARN "Derived bucket configured without a secondary hot bucket; S3 replication remains disabled." +fi + S3_DCS=() add_s3_dc() { local candidate="$1" @@ -279,11 +426,47 @@ add_s3_dc() { } add_s3_dc "$S3_PRIMARY_DC" -add_s3_dc "$S3_SECONDARY_DC" +if [ "$S3_SECONDARY_ENABLED" = true ]; then + add_s3_dc "$S3_SECONDARY_DC" +fi add_s3_dc "$S3_DERIVED_DC" +write_s3_dc_block() { + local dc="$1" + local key="$2" + local secret="$3" + local endpoint="$4" + local region="$5" + local bucket="$6" + local prefix="$7" + cat >> "$MUSEUM_CONFIG" <> "$MUSEUM_CONFIG" + fi + printf '\n' >> "$MUSEUM_CONFIG" +} + S3_PREFIX_DISPLAY="${S3_PREFIX:-}" log INFO "Resolved S3 configuration: host=$S3_ENDPOINT_HOST region=$S3_REGION pathStyle=$S3_FORCE_PATH_STYLE localBuckets=$S3_ARE_LOCAL_BUCKETS primaryDC=$S3_PRIMARY_DC derivedDC=$S3_DERIVED_DC prefix=$S3_PREFIX_DISPLAY" +if [ "$S3_SECONDARY_ENABLED" = true ]; then + S3_SECONDARY_PREFIX_DISPLAY="${S3_SECONDARY_PREFIX:-}" + log INFO "Secondary replication target: host=$S3_SECONDARY_ENDPOINT_HOST region=$S3_SECONDARY_REGION dc=$S3_SECONDARY_DC prefix=$S3_SECONDARY_PREFIX_DISPLAY" +else + log INFO "Secondary hot-storage bucket not configured; replication disabled." +fi +if [ "$S3_DERIVED_CUSTOM" = true ]; then + S3_DERIVED_PREFIX_DISPLAY="${S3_DERIVED_PREFIX:-}" + log INFO "Derived storage target: host=$S3_DERIVED_ENDPOINT_HOST region=$S3_DERIVED_REGION dc=$S3_DERIVED_DC prefix=$S3_DERIVED_PREFIX_DISPLAY" +else + log INFO "Derived storage reuses the primary bucket." +fi DEFAULT_GIN_TRUSTED_PROXIES="127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" GIN_TRUSTED_PROXIES="${GIN_TRUSTED_PROXIES:-$DEFAULT_GIN_TRUSTED_PROXIES}" @@ -412,6 +595,10 @@ fi # Always regenerate Museum config to pick up S3 changes log INFO "Rendering Museum configuration" +HOT_STORAGE_SECONDARY_LINE="" +if [ "$S3_SECONDARY_ENABLED" = true ]; then + HOT_STORAGE_SECONDARY_LINE=" secondary: ${S3_SECONDARY_DC}" +fi cat > "$MUSEUM_CONFIG" <> "$MUSEUM_CONFIG" <> "$MUSEUM_CONFIG" + if [ "$dc" = "$S3_PRIMARY_DC" ]; then + write_s3_dc_block "$dc" "$S3_ACCESS_KEY" "$S3_SECRET_KEY" "$S3_ENDPOINT_HOST" "$S3_REGION" "$S3_BUCKET" "$S3_PREFIX" + elif [ "$S3_SECONDARY_ENABLED" = true ] && [ "$dc" = "$S3_SECONDARY_DC" ]; then + write_s3_dc_block "$dc" "$S3_SECONDARY_ACCESS_KEY" "$S3_SECONDARY_SECRET_KEY" "$S3_SECONDARY_ENDPOINT_HOST" "$S3_SECONDARY_REGION" "$S3_SECONDARY_BUCKET" "$S3_SECONDARY_PREFIX" + elif [ "$dc" = "$S3_DERIVED_DC" ]; then + if [ "$S3_DERIVED_CUSTOM" = true ]; then + write_s3_dc_block "$dc" "$S3_DERIVED_ACCESS_KEY" "$S3_DERIVED_SECRET_KEY" "$S3_DERIVED_ENDPOINT_HOST" "$S3_DERIVED_REGION" "$S3_DERIVED_BUCKET" "$S3_DERIVED_PREFIX" + else + write_s3_dc_block "$dc" "$S3_ACCESS_KEY" "$S3_SECRET_KEY" "$S3_ENDPOINT_HOST" "$S3_REGION" "$S3_BUCKET" "$S3_PREFIX" + fi fi - printf '\n' >> "$MUSEUM_CONFIG" done +if [ "$S3_REPLICATION_ENABLED" = true ]; then + cat >> "$MUSEUM_CONFIG" <<'EOF_CFG' +replication: + enabled: true +EOF_CFG +else + cat >> "$MUSEUM_CONFIG" <<'EOF_CFG' +replication: + enabled: false +EOF_CFG +fi + cat >> "$MUSEUM_CONFIG" <