#!/bin/bash ## Do not modify this file. You will lose the ability to autoupdate! CDN="https://cdn.coollabs.io/coolify-nightly" LATEST_IMAGE=${1:-latest} LATEST_HELPER_VERSION=${2:-latest} ENV_FILE="/data/coolify/source/.env" if [ -n "${3+x}" ]; then REGISTRY_URL="$3" elif [ -f "$ENV_FILE" ] && grep -q "^REGISTRY_URL=" "$ENV_FILE"; then REGISTRY_URL=$(grep "^REGISTRY_URL=" "$ENV_FILE" | cut -d '=' -f2- | head -n1) else REGISTRY_URL="docker.io" fi SKIP_BACKUP=${4:-false} STATUS_FILE="/data/coolify/source/.upgrade-status" DATE=$(date +%Y-%m-%d-%H-%M-%S) LOGFILE="/data/coolify/source/upgrade-${DATE}.log" # Helper function to log with timestamp log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >>"$LOGFILE" } # Helper function to log section headers log_section() { echo "" >>"$LOGFILE" echo "============================================================" >>"$LOGFILE" echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >>"$LOGFILE" echo "============================================================" >>"$LOGFILE" } # Helper function to write upgrade status for API polling write_status() { local step="$1" local message="$2" echo "${step}|${message}|$(date -Iseconds)" > "$STATUS_FILE" } # Print the first gateway of a network that Docker reports with a prefix length (fd00::1/64). # Docker Engine 27.0 to 28.0.0 does this for IPv6 until the daemon restarts (https://github.com/moby/moby/pull/49520). # The Docker CLI and Compose in the helper image fail on such a network with a ParseAddr error. network_gateway_with_prefix() { docker network inspect "$1" --format '{{range .IPAM.Config}}{{.Gateway}} {{end}}' 2>&1 | grep -oE '[0-9A-Fa-f:.]+/[0-9]+' | head -n1 } # Make sure the coolify network exists and the helper image can read it. # It is created when starting Coolify with docker compose ensure_coolify_network() { log "Checking Docker network 'coolify'..." if ! docker network inspect coolify >/dev/null 2>&1; then log "Network 'coolify' does not exist, creating..." if ! docker network create --attachable --ipv6 coolify 2>/dev/null; then log "Failed to create network with IPv6, trying without IPv6..." docker network create --attachable coolify 2>/dev/null log "Network 'coolify' created without IPv6" elif [ -n "$(network_gateway_with_prefix coolify)" ]; then # No container uses the new network yet, so it is safe to recreate it. log "Docker reports the IPv6 gateway of network 'coolify' with a prefix length, recreating it without IPv6..." docker network rm coolify >/dev/null 2>&1 docker network create --attachable coolify 2>/dev/null log "Network 'coolify' created without IPv6" else log "Network 'coolify' created with IPv6 support" fi return 0 fi log "Network 'coolify' already exists" local gateway gateway=$(network_gateway_with_prefix coolify) if [ -n "$gateway" ]; then local message="Docker Engine $(docker version --format '{{.Server.Version}}' 2>/dev/null) reports the gateway of the 'coolify' network as ${gateway}, which Coolify cannot read (Docker Engine bug, fixed in 28.0.1). Run 'systemctl restart docker' (this restarts all containers) or upgrade Docker Engine, then upgrade Coolify again." log "ERROR: ${message}" write_status "error" "$message" echo " ERROR: ${message}" return 1 fi } # Pull an image. Retry with backoff, because the registry can rate limit many instances that pull at the same time. pull_image() { local image="$1" local attempt=1 local max_attempts=5 local delay=15 until docker pull "$image" >>"$LOGFILE" 2>&1; do if [ "$attempt" -ge "$max_attempts" ]; then return 1 fi log "Pull of $image failed (attempt $attempt/$max_attempts). Retrying in ${delay}s..." echo " Pull failed (attempt $attempt/$max_attempts). Retrying in ${delay}s..." sleep "$((delay + RANDOM % 10))" attempt=$((attempt + 1)) delay=$((delay * 2)) done } echo "" echo "==========================================" echo " Coolify Upgrade - ${DATE}" echo "==========================================" echo "" # Initialize log file with header echo "============================================================" >>"$LOGFILE" echo "Coolify Upgrade Log" >>"$LOGFILE" echo "Started: $(date '+%Y-%m-%d %H:%M:%S')" >>"$LOGFILE" echo "Target Version: ${LATEST_IMAGE}" >>"$LOGFILE" echo "Helper Version: ${LATEST_HELPER_VERSION}" >>"$LOGFILE" echo "Registry URL: ${REGISTRY_URL}" >>"$LOGFILE" echo "============================================================" >>"$LOGFILE" log_section "Step 1/6: Downloading configuration files" write_status "1" "Downloading configuration files" echo "1/6 Downloading latest configuration files..." log "Downloading docker-compose.yml from ${CDN}/docker-compose.yml" curl -fsSL -L $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml log "Downloading docker-compose.prod.yml from ${CDN}/docker-compose.prod.yml" curl -fsSL -L $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml log "Downloading .env.production from ${CDN}/.env.production" curl -fsSL -L $CDN/.env.production -o /data/coolify/source/.env.production log "Downloading upgrade-postgres.sh from ${CDN}/upgrade-postgres.sh" curl -fsSL -L $CDN/upgrade-postgres.sh -o /data/coolify/source/upgrade-postgres.sh chmod +x /data/coolify/source/upgrade-postgres.sh log "Configuration files downloaded successfully" echo " Done." # Extract all images from docker-compose configuration log "Extracting all images from docker-compose configuration..." COMPOSE_FILES="-f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml" # Check if custom compose file exists if [ -f /data/coolify/source/docker-compose.custom.yml ]; then COMPOSE_FILES="$COMPOSE_FILES -f /data/coolify/source/docker-compose.custom.yml" log "Including custom docker-compose.yml in image extraction" fi # Check if PostgreSQL upgrade override exists if [ -f /data/coolify/source/docker-compose.postgres-upgrade.yml ]; then COMPOSE_FILES="$COMPOSE_FILES -f /data/coolify/source/docker-compose.postgres-upgrade.yml" log "Including PostgreSQL upgrade compose override in image extraction" fi # Get all unique images from docker compose config # LATEST_IMAGE env var is needed for image substitution in compose files IMAGES=$(REGISTRY_URL=${REGISTRY_URL} LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file "$ENV_FILE" $COMPOSE_FILES config --images 2>/dev/null | sort -u) if [ -z "$IMAGES" ]; then log "ERROR: Failed to extract images from docker-compose files" write_status "error" "Failed to parse docker-compose configuration" echo " ERROR: Failed to parse docker-compose configuration. Aborting upgrade." exit 1 fi log "Images to pull:" echo "$IMAGES" | while read img; do log " - $img"; done # Backup existing .env file before making any changes if [ "$SKIP_BACKUP" != "true" ]; then if [ -f "$ENV_FILE" ]; then echo " Creating backup of .env file..." log "Creating backup of .env file to .env-$DATE" cp "$ENV_FILE" "$ENV_FILE-$DATE" log "Backup created: ${ENV_FILE}-${DATE}" else log "WARNING: No existing .env file found to backup" fi fi log_section "Step 2/6: Updating environment configuration" write_status "2" "Updating environment configuration" echo "" echo "2/6 Updating environment configuration..." log "Merging .env.production values into .env" awk -F '=' '!seen[$1]++' "$ENV_FILE" /data/coolify/source/.env.production > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE" log "Environment file merged successfully" update_env_var() { local key="$1" local value="$2" # If variable "key=" exists but has no value, update the value of the existing line if grep -q "^${key}=$" "$ENV_FILE"; then sed -i "s|^${key}=$|${key}=${value}|" "$ENV_FILE" log "Updated ${key} (was empty)" # If variable "key=" doesn't exist, append it to the file with value elif ! grep -q "^${key}=" "$ENV_FILE"; then printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE" log "Added ${key} (was missing)" fi } set_env_var() { local key="$1" local value="$2" if grep -q "^${key}=" "$ENV_FILE"; then sed -i "s|^${key}=.*|${key}=${value}|" "$ENV_FILE" log "Updated ${key}" else printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE" log "Added ${key}" fi } # Quote a value so that Docker Compose and phpdotenv both read it back unchanged (same as install.sh). quote_env_value() { local value="$1" case "$value" in *\'*) value=${value//\\/\\\\} value=${value//\"/\\\"} value=${value//\$/\\\$} printf '"%s"' "$value" ;; *) printf "'%s'" "$value" ;; esac } # Installers before 4.5.1 (and manual edits) wrote root user credentials with spaces without quotes. # phpdotenv cannot parse such a line, so the recreated Coolify container returns HTTP 500 for every page. # Only quote lines that phpdotenv rejects: unquoted values that contain whitespace but no comment. quote_root_user_env_values() { local key value for key in ROOT_USERNAME ROOT_USER_EMAIL ROOT_USER_PASSWORD; do value=$(grep -m1 "^${key}=" "$ENV_FILE" | cut -d '=' -f2-) || continue # phpdotenv ignores whitespace around an unquoted value (also the \r of CRLF line endings) value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" case "$value" in \"* | \'* | *[[:space:]]\#*) continue ;; *[[:space:]]*) ;; *) continue ;; esac # awk reads the value from the environment, so characters such as | & \ in it stay literal KEY="$key" VALUE="$(quote_env_value "$value")" awk '!done && index($0, ENVIRON["KEY"] "=") == 1 { $0 = ENVIRON["KEY"] "=" ENVIRON["VALUE"]; done = 1 } { print }' "$ENV_FILE" >"$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE" log "Quoted ${key} (its value has spaces)" done } log "Checking environment variables..." quote_root_user_env_values set_env_var "REGISTRY_URL" "$REGISTRY_URL" update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)" update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)" update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)" update_env_var "PUSHER_BACKEND_PORT" "6001" # The realtime container no longer exists; point older installs at the bundled Reverb server if grep -q '^PUSHER_BACKEND_HOST=coolify-realtime$' "$ENV_FILE"; then set_env_var "PUSHER_BACKEND_HOST" "127.0.0.1" fi log "Environment variables check complete" echo " Done." ensure_coolify_network || exit 1 mkdir -p /data/coolify/images/{avatars,project-icons} chown -R 9999:root /data/coolify/images chmod -R 700 /data/coolify/images # Fix SSH directory ownership if not owned by container user UID 9999 (fixes #6621) # Only changes owner — preserves existing group to respect custom setups SSH_OWNER=$(stat -c '%u' /data/coolify/ssh 2>/dev/null || echo "unknown") if [ "$SSH_OWNER" != "9999" ]; then log "Fixing SSH directory ownership (was owned by UID $SSH_OWNER)" chown -R 9999 /data/coolify/ssh chmod -R 700 /data/coolify/ssh fi # Check if Docker config file exists DOCKER_CONFIG_MOUNT="" if [ -f /root/.docker/config.json ]; then DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json" log "Docker config mount enabled: /root/.docker/config.json" fi log_section "Step 3/6: Pulling Docker images" write_status "3" "Pulling Docker images" echo "" echo "3/6 Pulling Docker images..." echo " This may take a few minutes depending on your connection." # Also pull the helper image (not in compose files but needed for upgrade) HELPER_IMAGE="${REGISTRY_URL:-docker.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" echo " - Pulling $HELPER_IMAGE..." log "Pulling image: $HELPER_IMAGE" if pull_image "$HELPER_IMAGE"; then log "Successfully pulled $HELPER_IMAGE" else log "ERROR: Failed to pull $HELPER_IMAGE" write_status "error" "Failed to pull $HELPER_IMAGE" echo " ERROR: Failed to pull $HELPER_IMAGE. Aborting upgrade." exit 1 fi # Pull all images from compose config # Using a for loop to avoid subshell issues with exit for IMAGE in $IMAGES; do if [ -n "$IMAGE" ]; then echo " - Pulling $IMAGE..." log "Pulling image: $IMAGE" if pull_image "$IMAGE"; then log "Successfully pulled $IMAGE" else log "ERROR: Failed to pull $IMAGE" write_status "error" "Failed to pull $IMAGE" echo " ERROR: Failed to pull $IMAGE. Aborting upgrade." exit 1 fi fi done log "All images pulled successfully" echo " All images pulled successfully." set_env_var "LATEST_IMAGE" "$LATEST_IMAGE" set_env_var "COOLIFY_VERSION" "$LATEST_IMAGE" log_section "Step 4/6: Stopping and restarting containers" write_status "4" "Stopping containers" echo "" echo "4/6 Stopping containers and starting new ones..." echo " This step will restart all Coolify containers." echo " Check the log file for details: ${LOGFILE}" # From this point forward, we need to ensure the script continues even if # the SSH connection is lost (which happens when coolify container stops) # We use a subshell with nohup to ensure completion log "Starting container restart sequence (detached)..." nohup bash -c " LOGFILE='$LOGFILE' STATUS_FILE='$STATUS_FILE' DOCKER_CONFIG_MOUNT='$DOCKER_CONFIG_MOUNT' REGISTRY_URL='$REGISTRY_URL' LATEST_HELPER_VERSION='$LATEST_HELPER_VERSION' LATEST_IMAGE='$LATEST_IMAGE' log() { echo \"[\$(date '+%Y-%m-%d %H:%M:%S')] \$1\" >>\"\$LOGFILE\" } write_status() { echo \"\$1|\$2|\$(date -Iseconds)\" > \"\$STATUS_FILE\" } # Stop and remove containers # coolify-realtime is kept for upgrades from versions that still ran the separate realtime container for container in coolify coolify-db coolify-redis coolify-realtime; do if docker ps -a --format '{{.Names}}' | grep -q \"^\${container}\$\"; then log \"Stopping container: \${container}\" docker stop \"\$container\" >>\"\$LOGFILE\" 2>&1 || true log \"Removing container: \${container}\" docker rm \"\$container\" >>\"\$LOGFILE\" 2>&1 || true log \"Container \${container} stopped and removed\" else log \"Container \${container} not found (skipping)\" fi done log \"Container cleanup complete\" # Start new containers echo '' >>\"\$LOGFILE\" echo '============================================================' >>\"\$LOGFILE\" log 'Step 5/6: Starting new containers' echo '============================================================' >>\"\$LOGFILE\" write_status '5' 'Starting new containers' COMPOSE_FILES='-f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml' if [ -f /data/coolify/source/docker-compose.custom.yml ]; then log 'Using custom docker-compose.yml' COMPOSE_FILES=\"\$COMPOSE_FILES -f /data/coolify/source/docker-compose.custom.yml\" fi if [ -f /data/coolify/source/docker-compose.postgres-upgrade.yml ]; then log 'Using PostgreSQL upgrade compose override' COMPOSE_FILES=\"\$COMPOSE_FILES -f /data/coolify/source/docker-compose.postgres-upgrade.yml\" fi log 'Running docker compose up...' if docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-docker.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env \${COMPOSE_FILES} up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1; then log 'Docker compose up completed' elif [ \"\$(docker inspect --format '{{.State.Running}}' coolify 2>/dev/null)\" = 'true' ]; then # A slow first start (for example long migrations) can pass the wait timeout. Coolify is still starting. log 'Docker compose up did not finish waiting, but the coolify container is running' else log 'ERROR: Docker compose up failed and the coolify container is not running' write_status 'error' \"Coolify did not start. Check the upgrade log: \$LOGFILE\" exit 1 fi # Final log entry echo '' >>\"\$LOGFILE\" echo '============================================================' >>\"\$LOGFILE\" log 'Step 6/6: Upgrade complete' echo '============================================================' >>\"\$LOGFILE\" write_status '6' 'Upgrade complete' log 'Coolify upgrade completed successfully' log \"Version: \${LATEST_IMAGE}\" echo '' >>\"\$LOGFILE\" echo '============================================================' >>\"\$LOGFILE\" echo \"Upgrade completed: \$(date '+%Y-%m-%d %H:%M:%S')\" >>\"\$LOGFILE\" echo '============================================================' >>\"\$LOGFILE\" # Clean up status file after a short delay to allow frontend to read completion sleep 10 rm -f \"\$STATUS_FILE\" log 'Status file cleaned up' " >>"$LOGFILE" 2>&1 & # Give the background process a moment to start sleep 2 log "Container restart sequence started in background (PID: $!)" echo "" echo "5/6 Containers are being restarted in the background..." echo "6/6 Upgrade process initiated!" echo "" echo "==========================================" echo " Coolify upgrade to ${LATEST_IMAGE} in progress" echo "==========================================" echo "" echo " The upgrade will continue in the background." echo " Coolify will be available again shortly." echo " Log file: ${LOGFILE}"