From e0fb2b38979758f3d5140bb8e8df02d1c353ac49 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:25:24 +0100 Subject: [PATCH] ci(release): build and release a nightly twice a day - Remove other/nightly, the nightly channel uses the production files, so there are no longer two copies to keep in sync - sync-cdn.sh --nightly syncs the files of the commit the nightly is built from to /coolify/nightly on the CDN, with the nightly version in its versions.json and in /coolify/release.json, and pushes them straight to main of coollabs-cdn - version.sh picks the branch the nightly comes from, the release branch of the next version after the newest stable one or next, and names the version .. - The image is labeled with its version and commit, the nightly workflow reads both from the image nightly points at to count up N and to skip a run without a new commit --- .github/workflows/build.yaml | 20 +- .github/workflows/nightly.yaml | 127 ++ RELEASE.md | 25 +- docker/production/Dockerfile | 5 + other/nightly/.env.production | 17 - other/nightly/docker-compose.prod.yml | 89 -- other/nightly/docker-compose.windows.yml | 110 -- other/nightly/docker-compose.yml | 32 - other/nightly/install.sh | 1179 ----------------- other/nightly/upgrade-postgres.sh | 405 ------ other/nightly/upgrade.sh | 427 ------ other/nightly/versions.json | 31 - scripts/sync-cdn.sh | 8 +- scripts/version.sh | 46 +- tests/Feature/PersistentImageStorageTest.php | 1 - tests/Feature/PostgresUpgradeScriptTest.php | 4 - .../ReverbAndTerminalPackagingTest.php | 20 - .../Unit/InstallScriptAuthorizedKeysTest.php | 1 - .../InstallScriptDirectoryPermissionsTest.php | 1 - .../InstallScriptDockerDaemonConfigTest.php | 2 +- .../Unit/InstallScriptRockyDockerRepoTest.php | 4 - tests/Unit/InstallScriptRootUserEnvTest.php | 2 +- tests/Unit/InstallScriptTerminalUiTest.php | 2 - .../Unit/ProductionComposeHealthcheckTest.php | 1 - tests/Unit/UpgradePostgresScriptTest.php | 8 - .../Unit/UpgradeScriptContainerStartTest.php | 6 +- .../Unit/UpgradeScriptCoolifyNetworkTest.php | 8 +- tests/Unit/UpgradeScriptPullRetryTest.php | 4 +- tests/Unit/UpgradeScriptRootUserEnvTest.php | 2 +- 29 files changed, 232 insertions(+), 2355 deletions(-) create mode 100644 .github/workflows/nightly.yaml delete mode 100644 other/nightly/.env.production delete mode 100644 other/nightly/docker-compose.prod.yml delete mode 100644 other/nightly/docker-compose.windows.yml delete mode 100644 other/nightly/docker-compose.yml delete mode 100755 other/nightly/install.sh delete mode 100755 other/nightly/upgrade-postgres.sh delete mode 100644 other/nightly/upgrade.sh delete mode 100644 other/nightly/versions.json diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c68945fac7..6c3fb93ea6 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -5,6 +5,15 @@ on: - cron: "50 7 * * 1" workflow_dispatch: workflow_call: + inputs: + ref: + description: The commit to build, the commit of the run when empty + type: string + default: "" + coolify: + description: The Coolify version to build, the version release.json sets for the branch when empty + type: string + default: "" permissions: {} @@ -26,10 +35,11 @@ jobs: runs-on: ubuntu-26.04 # https://github.com/actions/runner-images timeout-minutes: 10 outputs: - coolify: ${{ steps.version.outputs.coolify }} + coolify: ${{ inputs.coolify || steps.version.outputs.coolify }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # https://github.com/actions/checkout/releases/tag/v7.0.1 with: + ref: ${{ inputs.ref }} persist-credentials: false - id: version @@ -60,11 +70,16 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # https://github.com/actions/checkout/releases/tag/v7.0.1 with: + ref: ${{ inputs.ref }} persist-credentials: false - name: Set the build arguments id: args - run: echo "epoch=$(git log -1 --format=%ct)" >> "$GITHUB_OUTPUT" + run: | + { + echo "epoch=$(git log -1 --format=%ct)" + echo "sha=$(git rev-parse HEAD)" + } >> "$GITHUB_OUTPUT" - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # https://github.com/docker/setup-buildx-action/releases/tag/v4.4.1 with: @@ -80,6 +95,7 @@ jobs: build-args: | COOLIFY_VERSION=${{ needs.version.outputs.coolify }} SOURCE_DATE_EPOCH=${{ steps.args.outputs.epoch }} + GIT_SHA=${{ steps.args.outputs.sha }} - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # https://github.com/actions/upload-artifact/releases/tag/v7.0.2 with: diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml new file mode 100644 index 0000000000..3dd8f8d088 --- /dev/null +++ b/.github/workflows/nightly.yaml @@ -0,0 +1,127 @@ +name: Nightly + +on: + schedule: + - cron: "10 9,21 * * *" + workflow_dispatch: + +permissions: {} + +cache-mode: none + +concurrency: + group: nightly + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + version: + name: Select the nightly + if: ${{ github.repository == 'coollabsio/coolify' }} + permissions: + contents: read + runs-on: ubuntu-26.04 # https://github.com/actions/runner-images + timeout-minutes: 10 + outputs: + nightly: ${{ steps.version.outputs.nightly }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # https://github.com/actions/checkout/releases/tag/v7.0.1 + with: + ref: next + fetch-depth: 0 + persist-credentials: false + + - id: version + env: + BRANCH: next + run: | + labels="$(docker buildx imagetools inspect ghcr.io/coollabsio/coolify:nightly --format '{{ json (index .Image "linux/amd64").Config.Labels }}')" || labels='{}' + NIGHTLY_VERSION="$(jq -r '.["org.opencontainers.image.version"] // empty' <<< "$labels")" + NIGHTLY_REVISION="$(jq -r '.["org.opencontainers.image.revision"] // empty' <<< "$labels")" + export NIGHTLY_VERSION NIGHTLY_REVISION + nightly="$(scripts/version.sh --nightly)" + echo "nightly=$nightly" | tee -a "$GITHUB_OUTPUT" + + build: + name: Build + needs: version + if: ${{ needs.version.outputs.nightly != '' }} + permissions: + contents: read + uses: $/.github/workflows/build.yaml + with: + ref: ${{ fromJSON(needs.version.outputs.nightly).revision }} + coolify: ${{ fromJSON(needs.version.outputs.nightly).image }} + + publish: + name: Publish ${{ matrix.line.tag }} + needs: [version, build] + permissions: + packages: write # push the built nightly to ghcr.io and move the nightly tag + environment: + name: release + deployment: false + runs-on: ubuntu-26.04 # https://github.com/actions/runner-images + timeout-minutes: 30 + strategy: + matrix: + line: ${{ fromJSON(needs.version.outputs.nightly).lines }} + steps: + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # https://github.com/actions/download-artifact/releases/tag/v8.0.2 + with: + pattern: image-* + path: ${{ runner.temp }}/images + + - uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # https://github.com/docker/login-action/releases/tag/v4.7.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # https://github.com/docker/login-action/releases/tag/v4.7.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Push the built nightly + env: + TAG: ${{ matrix.line.tag }} + LINE: ${{ matrix.line.name }} + run: | + sources=() + for index in "$RUNNER_TEMP"/images/*/index.json; do + sources+=("oci-layout://${index%/index.json}@$(jq -r '.manifests[0].digest' "$index")") + done + for repository in ghcr.io/coollabsio/coolify docker.io/coollabsio/coolify; do + tags=(--tag "$repository:${TAG#v}") + [ "$LINE" != latest ] || tags+=(--tag "$repository:nightly") + docker buildx imagetools create "${tags[@]}" "${sources[@]}" + done + + cdn: + name: Sync the nightly channel of the CDN + needs: [version, publish] + permissions: + contents: read + environment: + name: release + deployment: false + runs-on: ubuntu-26.04 # https://github.com/actions/runner-images + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # https://github.com/actions/checkout/releases/tag/v7.0.1 + with: + ref: ${{ fromJSON(needs.version.outputs.nightly).revision }} + persist-credentials: false + + - env: + GH_TOKEN: ${{ secrets.CDN_TOKEN }} + IMAGE_VERSION: ${{ fromJSON(needs.version.outputs.nightly).image }} + run: | + gh auth setup-git + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + scripts/sync-cdn.sh --nightly "$IMAGE_VERSION" diff --git a/RELEASE.md b/RELEASE.md index e9725e4364..5aeef84b6b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -118,9 +118,29 @@ Every release is tagged `v`, such as `v4.6.0-beta.1`, and publishes the > [!IMPORTANT] > Cut `v4.x` from `main`, or bring an existing one up to date with `main`, before `release/v5.0` is merged into `main`. Afterwards `main` is v5 and v4 lives on `v4.x` alone. +## Nightlies + +The nightly workflow publishes a nightly twice a day when its branch has a new commit, versioned `..`: + +| State | Nightly | +| ------------------------------ | -------------------------- | +| nothing bumped since 5.0.4 | `5.1.0-alpha.0.20261014.1` | +| after 5.1.0-alpha.1 | `5.1.0-alpha.1.20261014.1` | +| after 5.1.0-beta.1 | `5.1.0-beta.1.20261014.1` | +| second nightly on the same day | `5.1.0-beta.1.20261014.2` | +| after 5.1.0-rc.1 | `5.1.0-rc.1.20261015.1` | +| after 5.1.0 | `5.2.0-alpha.0.20261016.1` | + +- It comes from `next`, or from the lowest release branch while one is in progress: `release/v5.1` takes it over once it is cut and hands it back once `5.1.0` is released +- The base is the prerelease `preview` of that branch sets in `release.json`, else its next version with `-alpha.0`, which is never released +- N counts up per day and base +- No git tag and no GitHub release, only its image tag, the moving `nightly` tag and the nightly channel of the CDN + +Print what the next run publishes with `scripts/version.sh --nightly`. + ## Pull requests -Every push to a pull request from a branch of this repository publishes `ghcr.io/coollabsio/coolify:pr-`, versioned `-pr.`. Pull request 1234 into `next` is `pr-1234` with the version `4.6.0-beta.1-pr.1234`, which sorts after `4.6.0-beta.1` and before `4.6.0-beta.2`. +Every push to a pull request from a branch of this repository publishes `ghcr.io/coollabsio/coolify:pr-`, versioned `-pr.`. Pull request 1234 into `next` is `pr-1234` with the version `4.6.0-beta.1-pr.1234`, which sorts after `4.6.0-beta.1` and its nightlies and before `4.6.0-beta.2`. - Pull requests from forks and Dependabot are built, not published - Pull request builds are published to ghcr.io only, as the Docker Hub credentials are in the `release` environment, which no pull request run can use @@ -134,8 +154,9 @@ Every push to a pull request from a branch of this repository publishes `ghcr.io | --------- | ------------------- | --------------------------------------------------------- | | `stable` | `/coolify/` | the release workflow when `latest` moves, a pull request | | `preview` | `/coolify/preview/` | the release workflow when `preview` moves, pushed to main | +| `nightly` | `/coolify/nightly/` | the nightly workflow after every nightly, pushed to main | -- The preview channel takes betas and release candidates, no alphas +- The preview channel takes betas and release candidates, alphas reach the nightly channel only - `CDN_TOKEN` in the environment `release` is a fine-grained token for `coollabsio/coollabs-cdn` alone, with read and write access to contents and pull requests - `versions.json` of a channel names its Coolify version and the helper, Sentinel and Traefik versions of `upstream.json` - `/coolify/release.json` lists the Coolify version of every channel, each sync sets the version of its channel diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile index 0ab9774927..018dca1d18 100644 --- a/docker/production/Dockerfile +++ b/docker/production/Dockerfile @@ -220,5 +220,10 @@ RUN mkdir -p /etc/nginx/conf.d && \ COPY --from=minio-client /usr/bin/mc /usr/bin/mc RUN chmod +x /usr/bin/mc +ARG GIT_SHA + +LABEL org.opencontainers.image.version="${COOLIFY_VERSION}" \ + org.opencontainers.image.revision="${GIT_SHA}" + # Switch to non-root user USER www-data diff --git a/other/nightly/.env.production b/other/nightly/.env.production deleted file mode 100644 index c6990a51d4..0000000000 --- a/other/nightly/.env.production +++ /dev/null @@ -1,17 +0,0 @@ -APP_ID= -APP_NAME=Coolify -APP_KEY= - -DB_USERNAME=coolify -DB_PASSWORD= - -REDIS_PASSWORD= - -PUSHER_APP_ID= -PUSHER_APP_KEY= -PUSHER_APP_SECRET= -ROOT_USERNAME= -ROOT_USER_EMAIL= -ROOT_USER_PASSWORD= - -REGISTRY_URL=docker.io diff --git a/other/nightly/docker-compose.prod.yml b/other/nightly/docker-compose.prod.yml deleted file mode 100644 index 8e214b385e..0000000000 --- a/other/nightly/docker-compose.prod.yml +++ /dev/null @@ -1,89 +0,0 @@ -services: - # Placeholder for "soketi" overrides in docker-compose.custom.yml from versions before 4.4. - # Realtime runs inside the coolify container now. Without this entry, such an override is an - # incomplete service and every upgrade fails. Zero replicas means it never starts, and - # "docker compose up" removes the old coolify-realtime container (an inactive profile keeps it). - # It uses the coolify image, so install and upgrade do not pull another image. - soketi: - image: "${REGISTRY_URL:-docker.io}/coollabsio/coolify:${LATEST_IMAGE:-latest}" - deploy: - replicas: 0 - coolify: - image: "${REGISTRY_URL:-docker.io}/coollabsio/coolify:${LATEST_IMAGE:-latest}" - volumes: - - type: bind - source: /data/coolify/source/.env - target: /var/www/html/.env - read_only: true - - /data/coolify/ssh:/var/www/html/storage/app/ssh - - /data/coolify/applications:/var/www/html/storage/app/applications - - /data/coolify/databases:/var/www/html/storage/app/databases - - /data/coolify/services:/var/www/html/storage/app/services - - /data/coolify/backups:/var/www/html/storage/app/backups - - /data/coolify/images:/var/www/html/storage/app/images - environment: - - APP_ENV=${APP_ENV:-production} - - PHP_MEMORY_LIMIT=${PHP_MEMORY_LIMIT:-256M} - - PHP_FPM_PM_CONTROL=${PHP_FPM_PM_CONTROL:-dynamic} - - PHP_FPM_PM_START_SERVERS=${PHP_FPM_PM_START_SERVERS:-1} - - PHP_FPM_PM_MIN_SPARE_SERVERS=${PHP_FPM_PM_MIN_SPARE_SERVERS:-1} - - PHP_FPM_PM_MAX_SPARE_SERVERS=${PHP_FPM_PM_MAX_SPARE_SERVERS:-10} - - PUSHER_BACKEND_PORT=${PUSHER_BACKEND_PORT:-6001} - env_file: - - /data/coolify/source/.env - ports: - - "${APP_PORT:-8000}:8080" - - "${REVERB_PORT:-${SOKETI_PORT:-6001}}:6001" - - "6002:6002" - expose: - - "${APP_PORT:-8000}" - - "6001" - - "6002" - networks: - coolify: - # Realtime ran in the coolify-realtime container before 4.4; keep the name for existing proxy upstreams. - aliases: - - coolify-realtime - healthcheck: - test: curl --fail http://127.0.0.1:8080/api/health && curl --fail http://127.0.0.1:${PUSHER_BACKEND_PORT:-6001}/up || exit 1 - interval: 5s - retries: 24 - start_period: 1m - timeout: 2s - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - postgres: - volumes: - - coolify-db:/var/lib/postgresql/data - environment: - POSTGRES_USER: "${DB_USERNAME}" - POSTGRES_PASSWORD: "${DB_PASSWORD}" - POSTGRES_DB: "${DB_DATABASE:-coolify}" - healthcheck: - test: [ "CMD-SHELL", "pg_isready -U ${DB_USERNAME}", "-d", "${DB_DATABASE:-coolify}" ] - interval: 5s - retries: 10 - timeout: 2s - redis: - command: redis-server --save 20 1 --loglevel warning --requirepass ${REDIS_PASSWORD} - environment: - REDIS_PASSWORD: "${REDIS_PASSWORD}" - volumes: - - coolify-redis:/data - healthcheck: - test: redis-cli ping - interval: 5s - retries: 10 - timeout: 2s -volumes: - coolify-db: - name: coolify-db - coolify-redis: - name: coolify-redis - -networks: - coolify: - external: true diff --git a/other/nightly/docker-compose.windows.yml b/other/nightly/docker-compose.windows.yml deleted file mode 100644 index 347495424f..0000000000 --- a/other/nightly/docker-compose.windows.yml +++ /dev/null @@ -1,110 +0,0 @@ -services: - coolify-testing-host: - init: true - image: "docker.io/coollabsio/coolify-testing-host:latest" - pull_policy: always - container_name: coolify-testing-host - volumes: - - //var/run/docker.sock://var/run/docker.sock - - ./:/data/coolify - coolify: - image: "docker.io/coollabsio/coolify:latest" - pull_policy: always - container_name: coolify - restart: always - working_dir: /var/www/html - extra_hosts: - - 'host.docker.internal:host-gateway' - volumes: - - type: bind - source: .env - target: /var/www/html/.env - read_only: true - - ./ssh:/var/www/html/storage/app/ssh - - ./applications:/var/www/html/storage/app/applications - - ./databases:/var/www/html/storage/app/databases - - ./services:/var/www/html/storage/app/services - - ./backups:/var/www/html/storage/app/backups - - ./images:/var/www/html/storage/app/images - env_file: - - .env - environment: - - APP_ID - - APP_ENV=production - - APP_NAME - - APP_KEY - - DB_PASSWORD - - REDIS_PASSWORD - - SSL_MODE=off - - PHP_PM_CONTROL=dynamic - - PHP_PM_START_SERVERS=1 - - PHP_PM_MIN_SPARE_SERVERS=1 - - PHP_PM_MAX_SPARE_SERVERS=10 - - PUSHER_APP_ID - - PUSHER_APP_KEY - - PUSHER_APP_SECRET - - PUSHER_PORT - - PUSHER_BACKEND_PORT - - AUTOUPDATE=true - - SELF_HOSTED=true - - SSH_MUX_ENABLED=false - - IS_WINDOWS_DOCKER_DESKTOP=true - ports: - - "${APP_PORT:-8000}:8080" - - "${REVERB_PORT:-${SOKETI_PORT:-6001}}:6001" - - "6002:6002" - expose: - - "${APP_PORT:-8000}" - - "6001" - - "6002" - healthcheck: - test: curl --fail http://localhost:8080/api/health && curl --fail http://localhost:${PUSHER_BACKEND_PORT:-6001}/up || exit 1 - interval: 5s - retries: 10 - timeout: 2s - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - postgres: - image: postgres:15-alpine - pull_policy: always - container_name: coolify-db - restart: always - env_file: - - .env - volumes: - - coolify-db:/var/lib/postgresql/data - environment: - POSTGRES_USER: "${DB_USERNAME}" - POSTGRES_PASSWORD: "${DB_PASSWORD}" - POSTGRES_DB: "${DB_DATABASE:-coolify}" - healthcheck: - test: [ "CMD-SHELL", "pg_isready -U ${DB_USERNAME}", "-d", "${DB_DATABASE:-coolify}" ] - interval: 5s - retries: 10 - timeout: 2s - redis: - image: redis:7-alpine - pull_policy: always - container_name: coolify-redis - restart: always - command: redis-server --save 20 1 --loglevel warning --requirepass ${REDIS_PASSWORD} - env_file: - - .env - environment: - REDIS_PASSWORD: "${REDIS_PASSWORD}" - volumes: - - coolify-redis:/data - healthcheck: - test: redis-cli ping - interval: 5s - retries: 10 - timeout: 2s - -volumes: - coolify-db: - name: coolify-db - coolify-redis: - name: coolify-redis diff --git a/other/nightly/docker-compose.yml b/other/nightly/docker-compose.yml deleted file mode 100644 index c4fc1233a6..0000000000 --- a/other/nightly/docker-compose.yml +++ /dev/null @@ -1,32 +0,0 @@ -services: - coolify: - container_name: coolify - restart: always - working_dir: /var/www/html - extra_hosts: - - host.docker.internal:host-gateway - expose: - - "6001" - - "6002" - networks: - - coolify - depends_on: - - postgres - - redis - postgres: - image: postgres:15-alpine - container_name: coolify-db - restart: always - networks: - - coolify - redis: - image: redis:7-alpine - container_name: coolify-redis - restart: always - networks: - - coolify -networks: - coolify: - name: coolify - driver: bridge - external: false diff --git a/other/nightly/install.sh b/other/nightly/install.sh deleted file mode 100755 index 7a1783082a..0000000000 --- a/other/nightly/install.sh +++ /dev/null @@ -1,1179 +0,0 @@ -#!/bin/bash -## Do not modify this file. You will lose the ability to install and auto-update! - -## Environment variables that can be set: -## ROOT_USERNAME - Predefined root username -## ROOT_USER_EMAIL - Predefined root user email -## ROOT_USER_PASSWORD - Predefined root user password -## DOCKER_ADDRESS_POOL_BASE - Custom Docker address pool base (default: 10.0.0.0/8) -## DOCKER_ADDRESS_POOL_SIZE - Custom Docker address pool size (default: 24) -## DOCKER_POOL_FORCE_OVERRIDE - Force override Docker address pool configuration, also when Docker must restart with running containers (default: false) -## AUTOUPDATE - Set to "false" to disable auto-updates -## REGISTRY_URL - Custom registry URL for Docker images (default: docker.io) - -set -e # Exit immediately if a command exits with a non-zero status -## $1 could be empty, so we need to disable this check -#set -u # Treat unset variables as an error and exit -set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status -CDN="https://cdn.coollabs.io/coolify-nightly" -DATE=$(date +"%Y%m%d-%H%M%S") - -OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"') -ENV_FILE="/data/coolify/source/.env" -DOCKER_VERSION="latest" -# TODO: Ask for a user -CURRENT_USER=$USER - -if [ $EUID != 0 ]; then - echo "Please run this script as root or with sudo" - exit 1 -fi - -# Resource directories hold data whose owner and mode the resource images or a non-root SSH user need. -# Set their owner and mode only when they are created, never on an upgrade (#11945). -create_data_directory() { - if [ ! -d "$1" ]; then - mkdir -p "$1" - chown 9999:root "$1" - chmod 700 "$1" - fi -} - -# Run only after the checks and downloads succeed, so a failed run does not change the data directories (#11973). -create_coolify_directories() { - for COOLIFY_DIRECTORY in /data/coolify/{applications,databases,backups,services,proxy,proxy/dynamic,sentinel}; do - create_data_directory "$COOLIFY_DIRECTORY" - done - mkdir -p /data/coolify/images /data/coolify/ssh/{keys,mux} -} - -# Coolify's own directories, used by the coolify container (UID 9999) -set_coolify_directory_permissions() { - chown -R 9999:root /data/coolify/{source,ssh,images} - chmod -R 700 /data/coolify/{source,ssh,images} -} - -# The installation log and the downloaded files go to /data/coolify/source, so only it is created before the checks. -create_data_directory /data/coolify -mkdir -p /data/coolify/source - -INSTALLATION_LOG_WITH_DATE="/data/coolify/source/installation-${DATE}.log" - -# Terminal UI: the terminal only shows the step list, everything else goes to the log file -TOTAL_STEPS=9 -UI_STEP=0 -UI_STEP_TITLE="" -UI_STEP_DETAIL="" -UI_STEP_STARTED=0 -UI_SPINNER_PID="" -UI_WARNINGS=() - -if [ -t 1 ] && [ "${TERM:-dumb}" != "dumb" ]; then - UI_TTY=true -else - UI_TTY=false -fi - -if [ "$UI_TTY" = true ] && [ -z "${NO_COLOR:-}" ]; then - C_RESET=$'\033[0m' - C_BOLD=$'\033[1m' - C_DIM=$'\033[2m' - C_PURPLE=$'\033[38;5;135m' - C_GREEN=$'\033[32m' - C_RED=$'\033[31m' - C_YELLOW=$'\033[33m' -else - C_RESET="" C_BOLD="" C_DIM="" C_PURPLE="" C_GREEN="" C_RED="" C_YELLOW="" -fi - -UI_WIDTH=$(tput cols 2>/dev/null || true) -if ! [[ $UI_WIDTH =~ ^[0-9]+$ ]]; then - UI_WIDTH=80 -fi -UI_WIDTH=$((UI_WIDTH > 100 ? 100 : UI_WIDTH < 60 ? 60 : UI_WIDTH)) - -# Current time in milliseconds -ui_now() { - if [ -n "${EPOCHREALTIME:-}" ]; then - local now=${EPOCHREALTIME/[.,]/} - echo $((now / 1000)) - else - echo $(($(date +%s) * 1000)) - fi -} - -ui_duration() { - local ms=$1 - if [ "$ms" -lt 60000 ]; then - printf '%d.%ds' $((ms / 1000)) $((ms % 1000 / 100)) - else - printf '%dm %ds' $((ms / 60000)) $((ms % 60000 / 1000)) - fi -} - -ui_repeat() { - local out="" i - for ((i = 0; i < $2; i++)); do - out+="$1" - done - printf '%s' "$out" -} - -# Prints the current step line without the trailing status symbol -ui_render_step() { - local right="$1" - local title="$UI_STEP_TITLE" - local detail="$UI_STEP_DETAIL" - local plain_detail="${detail//·/.}" - local left=$((7 + ${#title})) - local right_width=$((${#right} + 2)) - - if [ -n "$detail" ] && [ $((left + 2 + ${#plain_detail} + right_width + 2)) -le "$UI_WIDTH" ]; then - left=$((left + 2 + ${#plain_detail})) - else - detail="" - fi - - local pad=$((UI_WIDTH - left - right_width)) - if [ "$pad" -lt 2 ]; then - pad=2 - fi - printf ' %s%-5s%s%s%s%s' "$C_DIM" "${UI_STEP}/${TOTAL_STEPS}" "$C_RESET" "$C_BOLD" "$title" "$C_RESET" - if [ -n "$detail" ]; then - printf ' %s%s%s' "$C_DIM" "$detail" "$C_RESET" - fi - printf '%*s%s%s%s ' "$pad" "" "$C_DIM" "$right" "$C_RESET" -} - -ui_bar() { - local percent=$1 label=$2 - local cells=$((UI_WIDTH - 30 > 40 ? 40 : UI_WIDTH - 30)) - local filled=$((percent * cells / 100)) - printf ' %s%s%s%s%s %s%3d%%%s %s%s%s' \ - "$C_PURPLE" "$(ui_repeat ■ "$filled")" "$C_DIM" "$(ui_repeat ■ $((cells - filled)))" "$C_RESET" \ - "$C_BOLD" "$percent" "$C_RESET" "$C_DIM" "$label" "$C_RESET" -} - -# Runs in the background and animates the current step line and the progress bar below it -ui_spinner() { - set +x - local frames=(◜ ◠ ◝ ◞ ◡ ◟) i=0 stop=false - local line bar - trap 'stop=true' TERM - line=$(ui_render_step "") - bar=$(ui_bar $(((UI_STEP - 1) * 100 / TOTAL_STEPS)) "${UI_STEP_TITLE,,}…") - while [ "$stop" = false ]; do - printf '\r\033[2K%s%s%s%s\n\033[2K%s\033[1A\r' "$line" "$C_PURPLE" "${frames[i]}" "$C_RESET" "$bar" >&3 - i=$(((i + 1) % ${#frames[@]})) - sleep 0.12 - done -} - -ui_stop_spinner() { - if [ -n "$UI_SPINNER_PID" ]; then - kill "$UI_SPINNER_PID" 2>/dev/null || true - wait "$UI_SPINNER_PID" 2>/dev/null || true - UI_SPINNER_PID="" - fi -} - -# Shows a warning below the current step (or right away when no step is running) -warn() { - echo "WARNING: $*" - UI_WARNINGS+=("$*") -} - -ui_flush_warnings() { - local warning - for warning in "${UI_WARNINGS[@]}"; do - printf ' %s! %s%s\n' "$C_YELLOW" "$warning" "$C_RESET" >&3 - done - UI_WARNINGS=() -} - -step_start() { - UI_STEP=$((UI_STEP + 1)) - UI_STEP_TITLE="$1" - UI_STEP_DETAIL="${2:-}" - UI_STEP_STARTED=$(ui_now) - log_section "Step ${UI_STEP}/${TOTAL_STEPS}: $1" - if [ "$UI_TTY" = true ]; then - ui_spinner & - UI_SPINNER_PID=$! - fi -} - -# Usage: step_done [detail] - optionally replaces the detail text shown next to the step title -step_done() { - if [ -n "${1:-}" ]; then - UI_STEP_DETAIL="$1" - fi - ui_stop_spinner - local elapsed - elapsed=$(ui_duration $(($(ui_now) - UI_STEP_STARTED))) - log "Step ${UI_STEP}/${TOTAL_STEPS} completed in ${elapsed}" - if [ "$UI_TTY" = true ]; then - printf '\r\033[2K' >&3 - fi - printf '%s%s✓%s\n' "$(ui_render_step "$elapsed")" "$C_GREEN" "$C_RESET" >&3 - if [ "$UI_TTY" = true ]; then - printf '\033[2K' >&3 - fi - ui_flush_warnings - UI_STEP_TITLE="" -} - -ui_on_exit() { - local code=$? - ui_stop_spinner - if [ "$code" -ne 0 ]; then - if [ "$UI_TTY" = true ]; then - printf '\r\033[2K' >&3 - fi - if [ -n "$UI_STEP_TITLE" ]; then - printf '%s%s✗%s\n' "$(ui_render_step "$(ui_duration $(($(ui_now) - UI_STEP_STARTED)))")" "$C_RED" "$C_RESET" >&3 - fi - if [ "$UI_TTY" = true ]; then - printf '\033[2K' >&3 - fi - ui_flush_warnings - printf '\n %s%sInstallation failed.%s Last lines of the log:\n\n' "$C_BOLD" "$C_RED" "$C_RESET" >&3 - tail -n 15 "$INSTALLATION_LOG_WITH_DATE" 2>/dev/null | sed 's/^/ /' >&3 - printf '\n %sFull log: %s%s\n\n' "$C_DIM" "$INSTALLATION_LOG_WITH_DATE" "$C_RESET" >&3 - fi - if [ "$UI_TTY" = true ]; then - printf '\033[?25h' >&3 - fi -} - -# fd 3 is the terminal, stdout and stderr go to the log file -exec 3>&1 -exec >>"$INSTALLATION_LOG_WITH_DATE" 2>&1 -trap ui_on_exit EXIT - -if [ "$UI_TTY" = true ]; then - printf '\033[?25l' >&3 -fi -printf '\n %sWelcome to Coolify Installer!%s\n' "$C_BOLD" "$C_RESET" >&3 -printf ' %sThis script will install everything for you. Sit back and relax.%s\n' "$C_DIM" "$C_RESET" >&3 -printf ' %sLog file: %s%s\n\n' "$C_DIM" "$INSTALLATION_LOG_WITH_DATE" "$C_RESET" >&3 -UI_INSTALL_STARTED=$(ui_now) - -echo "==========================================" -echo " Coolify Installation - ${DATE}" -echo "==========================================" -echo "Source code: https://github.com/coollabsio/coolify/blob/v4.x/scripts/install.sh" - -# Predefined root user -ROOT_USERNAME=${ROOT_USERNAME:-} -ROOT_USER_EMAIL=${ROOT_USER_EMAIL:-} -ROOT_USER_PASSWORD=${ROOT_USER_PASSWORD:-} - -if [ -n "${REGISTRY_URL+x}" ]; then - echo "Using registry URL from environment variable: $REGISTRY_URL" -else - if [ -f "$ENV_FILE" ] && grep -q "^REGISTRY_URL=" "$ENV_FILE"; then - REGISTRY_URL=$(grep "^REGISTRY_URL=" "$ENV_FILE" | cut -d '=' -f2) - echo "Using registry URL from .env: $REGISTRY_URL" - else - REGISTRY_URL="docker.io" - echo "Using default registry URL: $REGISTRY_URL" - fi -fi - -# Docker address pool configuration defaults -DOCKER_ADDRESS_POOL_BASE_DEFAULT="10.0.0.0/8" -DOCKER_ADDRESS_POOL_SIZE_DEFAULT=24 - -# Check if environment variables were explicitly provided -DOCKER_POOL_BASE_PROVIDED=false -DOCKER_POOL_SIZE_PROVIDED=false -DOCKER_POOL_FORCE_OVERRIDE=${DOCKER_POOL_FORCE_OVERRIDE:-false} - -if [ -n "${DOCKER_ADDRESS_POOL_BASE+x}" ]; then - DOCKER_POOL_BASE_PROVIDED=true -fi - -if [ -n "${DOCKER_ADDRESS_POOL_SIZE+x}" ]; then - DOCKER_POOL_SIZE_PROVIDED=true -fi - -# OpenRC (Alpine) returns from "service docker start" before the daemon accepts connections -wait_for_docker_daemon() { - for _ in $(seq 1 30); do - if docker info >/dev/null 2>&1; then - return 0 - fi - sleep 1 - done - return 1 -} - -restart_docker_service() { - # Check if systemctl is available - if command -v systemctl >/dev/null 2>&1; then - if systemctl restart docker && wait_for_docker_daemon; then - echo " - Docker daemon restarted successfully" - else - echo " - Failed to restart Docker daemon" - return 1 - fi - # Check if service command is available - elif command -v service >/dev/null 2>&1; then - if service docker restart && wait_for_docker_daemon; then - echo " - Docker daemon restarted successfully" - else - echo " - Failed to restart Docker daemon" - return 1 - fi - # If neither systemctl nor service is available - else - echo " - Error: No service management system found" - return 1 - fi -} - -# Function to compare address pools -compare_address_pools() { - local base1="$1" - local size1="$2" - local base2="$3" - local size2="$4" - - # Normalize CIDR notation for comparison - local ip1=$(echo "$base1" | cut -d'/' -f1) - local prefix1=$(echo "$base1" | cut -d'/' -f2) - local ip2=$(echo "$base2" | cut -d'/' -f1) - local prefix2=$(echo "$base2" | cut -d'/' -f2) - - # Compare IPs and prefixes - if [ "$ip1" = "$ip2" ] && [ "$prefix1" = "$prefix2" ] && [ "$size1" = "$size2" ]; then - return 0 # Pools are the same - else - return 1 # Pools are different - fi -} - -# Docker address pool configuration -DOCKER_ADDRESS_POOL_BASE=${DOCKER_ADDRESS_POOL_BASE:-"$DOCKER_ADDRESS_POOL_BASE_DEFAULT"} -DOCKER_ADDRESS_POOL_SIZE=${DOCKER_ADDRESS_POOL_SIZE:-$DOCKER_ADDRESS_POOL_SIZE_DEFAULT} - -# Load Docker address pool configuration from .env file if it exists and environment variables were not provided -if [ -f "/data/coolify/source/.env" ] && [ "$DOCKER_POOL_BASE_PROVIDED" = false ] && [ "$DOCKER_POOL_SIZE_PROVIDED" = false ]; then - ENV_DOCKER_ADDRESS_POOL_BASE=$(grep -E "^DOCKER_ADDRESS_POOL_BASE=" /data/coolify/source/.env | cut -d '=' -f2 || true) - ENV_DOCKER_ADDRESS_POOL_SIZE=$(grep -E "^DOCKER_ADDRESS_POOL_SIZE=" /data/coolify/source/.env | cut -d '=' -f2 || true) - - if [ -n "$ENV_DOCKER_ADDRESS_POOL_BASE" ]; then - DOCKER_ADDRESS_POOL_BASE="$ENV_DOCKER_ADDRESS_POOL_BASE" - fi - - if [ -n "$ENV_DOCKER_ADDRESS_POOL_SIZE" ]; then - DOCKER_ADDRESS_POOL_SIZE="$ENV_DOCKER_ADDRESS_POOL_SIZE" - fi -fi - -# Check if daemon.json exists and extract existing address pool configuration -EXISTING_POOL_CONFIGURED=false -if [ -f /etc/docker/daemon.json ]; then - if jq -e '.["default-address-pools"]' /etc/docker/daemon.json >/dev/null 2>&1; then - EXISTING_POOL_BASE=$(jq -r '.["default-address-pools"][0].base' /etc/docker/daemon.json 2>/dev/null || true) - EXISTING_POOL_SIZE=$(jq -r '.["default-address-pools"][0].size' /etc/docker/daemon.json 2>/dev/null || true) - - if [ -n "$EXISTING_POOL_BASE" ] && [ -n "$EXISTING_POOL_SIZE" ] && [ "$EXISTING_POOL_BASE" != "null" ] && [ "$EXISTING_POOL_SIZE" != "null" ]; then - echo "Found existing Docker network pool: $EXISTING_POOL_BASE/$EXISTING_POOL_SIZE" - EXISTING_POOL_CONFIGURED=true - - # Check if environment variables were explicitly provided - if [ "$DOCKER_POOL_BASE_PROVIDED" = false ] && [ "$DOCKER_POOL_SIZE_PROVIDED" = false ]; then - DOCKER_ADDRESS_POOL_BASE="$EXISTING_POOL_BASE" - DOCKER_ADDRESS_POOL_SIZE="$EXISTING_POOL_SIZE" - else - # Check if force override is enabled - if [ "$DOCKER_POOL_FORCE_OVERRIDE" = true ]; then - echo "Force override enabled - network pool will be updated with $DOCKER_ADDRESS_POOL_BASE/$DOCKER_ADDRESS_POOL_SIZE." - else - warn "Custom Docker network pool ignored, using the existing $EXISTING_POOL_BASE/$EXISTING_POOL_SIZE. Set DOCKER_POOL_FORCE_OVERRIDE=true to override it (existing networks are not changed)." - DOCKER_ADDRESS_POOL_BASE="$EXISTING_POOL_BASE" - DOCKER_ADDRESS_POOL_SIZE="$EXISTING_POOL_SIZE" - DOCKER_POOL_BASE_PROVIDED=false - DOCKER_POOL_SIZE_PROVIDED=false - fi - fi - fi - fi -fi - -# Validate Docker address pool configuration -if ! [[ $DOCKER_ADDRESS_POOL_BASE =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]]; then - warn "Invalid Docker network pool base: $DOCKER_ADDRESS_POOL_BASE" - if [ "$EXISTING_POOL_CONFIGURED" = true ]; then - echo "Using existing configuration: $EXISTING_POOL_BASE" - DOCKER_ADDRESS_POOL_BASE="$EXISTING_POOL_BASE" - else - echo "Using default configuration: $DOCKER_ADDRESS_POOL_BASE_DEFAULT" - DOCKER_ADDRESS_POOL_BASE="$DOCKER_ADDRESS_POOL_BASE_DEFAULT" - fi -fi - -if ! [[ $DOCKER_ADDRESS_POOL_SIZE =~ ^[0-9]+$ ]] || [ "$DOCKER_ADDRESS_POOL_SIZE" -lt 16 ] || [ "$DOCKER_ADDRESS_POOL_SIZE" -gt 28 ]; then - warn "Invalid Docker network pool size: $DOCKER_ADDRESS_POOL_SIZE (must be 16-28)" - if [ "$EXISTING_POOL_CONFIGURED" = true ]; then - echo "Using existing configuration: $EXISTING_POOL_SIZE" - DOCKER_ADDRESS_POOL_SIZE="$EXISTING_POOL_SIZE" - else - echo "Using default configuration: $DOCKER_ADDRESS_POOL_SIZE_DEFAULT" - DOCKER_ADDRESS_POOL_SIZE=$DOCKER_ADDRESS_POOL_SIZE_DEFAULT - fi -fi - -TOTAL_SPACE=$(df -BG / | awk 'NR==2 {print $2}' | sed 's/G//') -AVAILABLE_SPACE=$(df -BG / | awk 'NR==2 {print $4}' | sed 's/G//') -REQUIRED_TOTAL_SPACE=30 -REQUIRED_AVAILABLE_SPACE=20 -WARNING_SPACE=false - -if [ "$TOTAL_SPACE" -lt "$REQUIRED_TOTAL_SPACE" ]; then - WARNING_SPACE=true - warn "Insufficient total disk space: ${TOTAL_SPACE}GB (required: ${REQUIRED_TOTAL_SPACE}GB)" -fi - -if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_AVAILABLE_SPACE" ]; then - warn "Insufficient available disk space: ${AVAILABLE_SPACE}GB (required: ${REQUIRED_AVAILABLE_SPACE}GB)" - WARNING_SPACE=true -fi - -if [ "$WARNING_SPACE" = true ]; then - ui_flush_warnings - echo "Sleeping for 5 seconds." - sleep 5 -fi - -# Helper function to log with timestamp -log() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" -} - -# Helper function to log section headers -log_section() { - echo "" - echo "============================================================" - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" - echo "============================================================" -} - -# Helper function to check if all required packages are installed -all_packages_installed() { - for pkg in curl wget git jq openssl; do - if ! command -v "$pkg" >/dev/null 2>&1; then - return 1 - fi - done - return 0 -} - -# Check if the OS is manjaro, if so, change it to arch -if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then - OS_TYPE="arch" -fi - -# Check if the OS is Endeavour OS, if so, change it to arch -if [ "$OS_TYPE" = "endeavouros" ]; then - OS_TYPE="arch" -fi - -# Check if the OS is Cachy OS, if so, change it to arch -if [ "$OS_TYPE" = "cachyos" ]; then - OS_TYPE="arch" -fi - -# Check if the OS is Asahi Linux, if so, change it to fedora -if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then - OS_TYPE="fedora" -fi - -# Check if the OS is popOS, if so, change it to ubuntu -if [ "$OS_TYPE" = "pop" ]; then - OS_TYPE="ubuntu" -fi - -# Check if the OS is linuxmint, if so, change it to ubuntu -if [ "$OS_TYPE" = "linuxmint" ]; then - OS_TYPE="ubuntu" -fi - -#Check if the OS is zorin, if so, change it to ubuntu -if [ "$OS_TYPE" = "zorin" ]; then - OS_TYPE="ubuntu" -fi - -if [ "$OS_TYPE" = "arch" ] || [ "$OS_TYPE" = "archarm" ]; then - OS_VERSION="rolling" -else - OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"') -fi - -# Install xargs on Amazon Linux 2023 - lol -if [ "$OS_TYPE" = 'amzn' ]; then - dnf install -y findutils >/dev/null -fi - -# Fetch versions.json once and parse all values from it -VERSIONS_JSON=$(curl -L --silent $CDN/versions.json) -LATEST_VERSION=$(echo "$VERSIONS_JSON" | grep -i version | xargs | awk '{print $2}' | tr -d ',') -LATEST_HELPER_VERSION=$(echo "$VERSIONS_JSON" | grep -i version | xargs | awk '{print $6}' | tr -d ',') - -if [ -z "$LATEST_HELPER_VERSION" ]; then - LATEST_HELPER_VERSION=latest -fi - -case "$OS_TYPE" in -arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine | postmarketos | tencentos) ;; -*) - echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now." - exit 1 - ;; -esac - -# Overwrite LATEST_VERSION if user pass a version number -if [ "$1" != "" ]; then - LATEST_VERSION=$1 - LATEST_VERSION="${LATEST_VERSION,,}" - LATEST_VERSION="${LATEST_VERSION#v}" -fi - -echo "---------------------------------------------" -echo "| Operating System | $OS_TYPE $OS_VERSION" -echo "| Docker | $DOCKER_VERSION" -echo "| Coolify | $LATEST_VERSION" -echo "| Helper | $LATEST_HELPER_VERSION" -echo "| Docker Pool | $DOCKER_ADDRESS_POOL_BASE (size $DOCKER_ADDRESS_POOL_SIZE)" -echo "| Registry URL | $REGISTRY_URL" -echo "---------------------------------------------" - -ui_flush_warnings -step_start "Installing required packages" "curl wget git jq openssl" - -# Track if apt-get update was run to avoid redundant calls later -APT_UPDATED=false - -if all_packages_installed; then - log "All required packages already installed, skipping installation" - echo " - All required packages already installed." - UI_STEP_DETAIL="already installed" -else - case "$OS_TYPE" in - arch) - pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true - ;; - alpine | postmarketos) - sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories - apk update >/dev/null - apk add curl wget git jq openssl >/dev/null - ;; - ubuntu | debian | raspbian) - apt-get update -y >/dev/null - APT_UPDATED=true - apt-get install -y curl wget git jq openssl >/dev/null - ;; - centos | fedora | rhel | ol | rocky | almalinux | amzn | tencentos) - if [ "$OS_TYPE" = "amzn" ]; then - dnf install -y wget git jq openssl >/dev/null - else - if ! command -v dnf >/dev/null; then - yum install -y dnf >/dev/null - fi - if ! command -v curl >/dev/null; then - dnf install -y curl >/dev/null - fi - dnf install -y wget git jq openssl >/dev/null - fi - ;; - sles | opensuse-leap | opensuse-tumbleweed) - zypper refresh >/dev/null - zypper install -y curl wget git jq openssl >/dev/null - ;; - *) - echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now." - exit 1 - ;; - esac - log "Required packages installed successfully" -fi -step_done - -step_start "Checking OpenSSH server configuration" - -# Detect OpenSSH server -SSH_DETECTED=false -if [ -x "$(command -v systemctl)" ]; then - if systemctl status sshd >/dev/null 2>&1; then - echo " - OpenSSH server is installed." - SSH_DETECTED=true - elif systemctl status ssh >/dev/null 2>&1; then - echo " - OpenSSH server is installed." - SSH_DETECTED=true - fi -elif [ -x "$(command -v service)" ]; then - if service sshd status >/dev/null 2>&1; then - echo " - OpenSSH server is installed." - SSH_DETECTED=true - elif service ssh status >/dev/null 2>&1; then - echo " - OpenSSH server is installed." - SSH_DETECTED=true - fi -fi - -if [ "$SSH_DETECTED" = "false" ]; then - echo " - OpenSSH server not detected. Installing OpenSSH server." - case "$OS_TYPE" in - arch) - pacman -Sy --noconfirm openssh >/dev/null - systemctl enable sshd >/dev/null 2>&1 - systemctl start sshd >/dev/null 2>&1 - ;; - alpine | postmarketos) - apk add openssh >/dev/null - rc-update add sshd default >/dev/null 2>&1 - service sshd start >/dev/null 2>&1 - ;; - ubuntu | debian | raspbian) - if [ "$APT_UPDATED" = false ]; then - apt-get update -y >/dev/null - APT_UPDATED=true - fi - apt-get install -y openssh-server >/dev/null - systemctl enable ssh >/dev/null 2>&1 - systemctl start ssh >/dev/null 2>&1 - ;; - centos | fedora | rhel | ol | rocky | almalinux | amzn | tencentos) - if [ "$OS_TYPE" = "amzn" ]; then - dnf install -y openssh-server >/dev/null - else - dnf install -y openssh-server >/dev/null - fi - systemctl enable sshd >/dev/null 2>&1 - systemctl start sshd >/dev/null 2>&1 - ;; - sles | opensuse-leap | opensuse-tumbleweed) - zypper install -y openssh >/dev/null - systemctl enable sshd >/dev/null 2>&1 - systemctl start sshd >/dev/null 2>&1 - ;; - *) - echo "###############################################################################" - echo "WARNING: Could not detect and install OpenSSH server - this does not mean that it is not installed or not running, just that we could not detect it." - echo -e "Please make sure it is installed and running, otherwise Coolify cannot connect to the host system. \n" - echo "###############################################################################" - exit 1 - ;; - esac - echo " - OpenSSH server installed successfully." - SSH_DETECTED=true -fi - -# Detect SSH PermitRootLogin -SSH_PERMIT_ROOT_LOGIN=$(sshd -T | grep -i "permitrootlogin" | awk '{print $2}') || true -if [ "$SSH_PERMIT_ROOT_LOGIN" = "yes" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "without-password" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "prohibit-password" ]; then - echo " - SSH PermitRootLogin is enabled." -else - warn "SSH PermitRootLogin is disabled. If you have problems with SSH, read https://coolify.io/docs/knowledge-base/server/openssh" -fi - -# Detect if docker is installed via snap -if [ -x "$(command -v snap)" ]; then - SNAP_DOCKER_INSTALLED=$(snap list docker >/dev/null 2>&1 && echo "true" || echo "false") - if [ "$SNAP_DOCKER_INSTALLED" = "true" ]; then - echo "Docker is installed via snap." - echo " Please note that Coolify does not support Docker installed via snap." - echo " Please remove Docker with snap (snap remove docker) and reexecute this script." - exit 1 - fi -fi - -install_docker() { - set +e - curl -fsSL https://get.docker.com | sh 2>&1 || true - if ! [ -x "$(command -v docker)" ]; then - echo "Automated Docker installation failed. Trying manual installation." - install_docker_manually - fi - set -e -} - -install_docker_manually() { - case "$OS_TYPE" in - "ubuntu" | "debian" | "raspbian") - if [ "$APT_UPDATED" = false ]; then - apt-get update - APT_UPDATED=true - fi - apt-get install -y ca-certificates curl - install -m 0755 -d /etc/apt/keyrings - curl -fsSL https://download.docker.com/linux/$OS_TYPE/gpg -o /etc/apt/keyrings/docker.asc - chmod a+r /etc/apt/keyrings/docker.asc - - # Add the repository to Apt sources - echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$OS_TYPE \ - $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | - tee /etc/apt/sources.list.d/docker.list - apt-get update - apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - ;; - *) - exit 1 - ;; - esac - if ! [ -x "$(command -v docker)" ]; then - echo "Docker installation failed." - echo " Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." - exit 1 - else - echo "Docker installed successfully." - fi -} - -install_docker_from_rhel_repo() { - echo " - Installing Docker from the RHEL repository for Rocky Linux..." - rm -f /etc/yum.repos.d/docker-ce.repo /etc/yum.repos.d/docker-ce-staging.repo - dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo - dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - systemctl --now enable docker -} - -step_done - -if ! [ -x "$(command -v docker)" ]; then - step_start "Installing Docker Engine" - echo " - Docker is not installed. Installing Docker. It may take a while." - case "$OS_TYPE" in - "alpine" | "postmarketos") - apk add docker docker-cli-compose >/dev/null 2>&1 - rc-update add docker default >/dev/null 2>&1 - service docker start >/dev/null 2>&1 - if ! [ -x "$(command -v docker)" ]; then - echo " - Failed to install Docker with apk. Try to install it manually." - echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information." - exit 1 - fi - ;; - "arch") - pacman -Syu --noconfirm --needed docker docker-compose >/dev/null 2>&1 - systemctl enable docker.service >/dev/null 2>&1 - systemctl start docker.service >/dev/null 2>&1 - if ! [ -x "$(command -v docker)" ]; then - echo " - Failed to install Docker with pacman. Try to install it manually." - echo " Please visit https://wiki.archlinux.org/title/docker for more information." - exit 1 - fi - ;; - "amzn") - dnf install docker -y >/dev/null 2>&1 - DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker} - mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1 - curl -fsSL "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1 - chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1 - systemctl start docker >/dev/null 2>&1 - systemctl enable docker >/dev/null 2>&1 - if ! [ -x "$(command -v docker)" ]; then - echo " - Failed to install Docker with dnf. Try to install it manually." - echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information." - exit 1 - fi - ;; - "rocky") - install_docker_from_rhel_repo - if ! [ -x "$(command -v docker)" ]; then - echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." - exit 1 - fi - ;; - "almalinux" | "tencentos") - dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1 - dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1 - systemctl start docker >/dev/null 2>&1 - systemctl enable docker >/dev/null 2>&1 - if ! [ -x "$(command -v docker)" ]; then - echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." - exit 1 - fi - ;; - "ubuntu" | "debian" | "raspbian" | "centos" | "fedora" | "rhel" | "sles") - install_docker - if ! [ -x "$(command -v docker)" ]; then - echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." - exit 1 - fi - ;; - *) - install_docker - if ! [ -x "$(command -v docker)" ]; then - echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." - exit 1 - fi - ;; - esac - echo " - Docker installed successfully." -else - step_start "Checking Docker Engine" - echo " - Docker is installed." -fi - -# Verify minimum Docker version -MIN_DOCKER_VERSION=24 -wait_for_docker_daemon || true -INSTALLED_DOCKER_VERSION=$(docker version --format '{{.Server.Version}}' 2>/dev/null | cut -d. -f1 || true) -if [ -z "$INSTALLED_DOCKER_VERSION" ]; then - warn "Could not determine Docker version. Please ensure Docker $MIN_DOCKER_VERSION+ is installed." -elif [ "$INSTALLED_DOCKER_VERSION" -lt "$MIN_DOCKER_VERSION" ]; then - echo " - ERROR: Docker version $INSTALLED_DOCKER_VERSION is too old. Coolify requires Docker $MIN_DOCKER_VERSION or newer." - echo " Please upgrade Docker: https://docs.docker.com/engine/install/" - exit 1 -else - echo " - Docker version $(docker version --format '{{.Server.Version}}' 2>/dev/null) meets minimum requirement ($MIN_DOCKER_VERSION+)." -fi -DOCKER_SERVER_VERSION=$(docker version --format '{{.Server.Version}}' 2>/dev/null || true) -step_done "${DOCKER_SERVER_VERSION:+v$DOCKER_SERVER_VERSION}" - -step_start "Configuring Docker daemon" "log rotation · address pools" - -# Changing daemon.json needs a Docker restart, which stops every running container. -# Coolify sets log rotation and the network pool only when no pool is configured or DOCKER_POOL_FORCE_OVERRIDE=true, -# keeps all other keys, and does not restart a Docker daemon with running containers unless DOCKER_POOL_FORCE_OVERRIDE=true. -if [ "$DOCKER_POOL_FORCE_OVERRIDE" = true ] || [ "$EXISTING_POOL_CONFIGURED" = false ]; then - mkdir -p /etc/docker - CURRENT_DAEMON_CONFIG='{}' - if [ -s /etc/docker/daemon.json ] && ! CURRENT_DAEMON_CONFIG=$(jq -S . /etc/docker/daemon.json 2>/dev/null); then - CURRENT_DAEMON_CONFIG="" - fi - if [ -z "$CURRENT_DAEMON_CONFIG" ]; then - warn "Docker daemon configuration not changed: /etc/docker/daemon.json is not valid JSON." - else - NEW_DAEMON_CONFIG=$(jq -S --arg base "$DOCKER_ADDRESS_POOL_BASE" --argjson size "$DOCKER_ADDRESS_POOL_SIZE" \ - '. * {"log-driver": "json-file", "log-opts": {"max-size": "10m", "max-file": "3"}, "default-address-pools": [{"base": $base, "size": $size}]}' <<<"$CURRENT_DAEMON_CONFIG") - RUNNING_CONTAINERS=$({ docker ps -q 2>/dev/null || true; } | wc -l) - if [ "$NEW_DAEMON_CONFIG" = "$CURRENT_DAEMON_CONFIG" ]; then - echo " - Configuration is up to date" - elif [ "$RUNNING_CONTAINERS" -gt 0 ] && [ "$DOCKER_POOL_FORCE_OVERRIDE" != true ]; then - warn "Docker daemon configuration not changed: log rotation and the ${DOCKER_ADDRESS_POOL_BASE} (size ${DOCKER_ADDRESS_POOL_SIZE}) network pool need a Docker restart, which stops ${RUNNING_CONTAINERS} running container(s) and can change the docker0 bridge IP. To apply it, run the installer again with DOCKER_POOL_FORCE_OVERRIDE=true." - UI_STEP_DETAIL="skipped · containers running" - else - if [ -f /etc/docker/daemon.json ]; then - cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE" - fi - printf '%s\n' "$NEW_DAEMON_CONFIG" >/etc/docker/daemon.json - echo " - Network pool configuration: ${DOCKER_ADDRESS_POOL_BASE} (size ${DOCKER_ADDRESS_POOL_SIZE})" - echo " - Configuration updated - restarting Docker daemon..." - restart_docker_service - fi - fi -else - echo " - Using the existing network pool ${DOCKER_ADDRESS_POOL_BASE} (size ${DOCKER_ADDRESS_POOL_SIZE})" - echo " - To override it: DOCKER_POOL_FORCE_OVERRIDE=true" -fi - -step_done - -step_start "Downloading required files from CDN" -log "Downloading configuration files in parallel..." - -# Download files in parallel for faster installation -curl -fsSL -L $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml & -PID1=$! -curl -fsSL -L $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml & -PID2=$! -curl -fsSL -L $CDN/.env.production -o /data/coolify/source/.env.production & -PID3=$! -curl -fsSL -L $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh & -PID4=$! -curl -fsSL -L $CDN/upgrade-postgres.sh -o /data/coolify/source/upgrade-postgres.sh & -PID5=$! - -# Wait for all downloads to complete and check for errors -DOWNLOAD_FAILED=false -for PID in $PID1 $PID2 $PID3 $PID4 $PID5; do - if ! wait $PID; then - DOWNLOAD_FAILED=true - fi -done - -if [ "$DOWNLOAD_FAILED" = true ]; then - echo " - ERROR: One or more downloads failed. Please check your network connection." - exit 1 -fi - -chmod +x /data/coolify/source/upgrade.sh /data/coolify/source/upgrade-postgres.sh -log "All configuration files downloaded successfully" -step_done - -step_start "Setting up environment variables" - -if [ -f "$ENV_FILE" ]; then - # If .env exists, create backup - echo " - Creating backup of existing .env file to .env-$DATE" - cp "$ENV_FILE" "$ENV_FILE-$DATE" - # Merge .env.production values into .env - echo " - 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" - echo " - .env file merged successfully" -else - # If no .env exists, copy .env.production to .env - echo " - No .env file found, copying .env.production to .env" - cp "/data/coolify/source/.env.production" "$ENV_FILE" -fi -log "Environment file setup completed" - -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 - # awk reads the value from the environment, so characters such as | & \ in it stay literal - KEY="$key" VALUE="$value" awk '$0 == ENVIRON["KEY"] "=" { $0 = ENVIRON["KEY"] "=" ENVIRON["VALUE"] } { print }' "$ENV_FILE" >"$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE" - echo " - Updated value of ${key} as the current value 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" - echo " - Added ${key} and its value as the variable was missing" - fi -} - -# Quote a user-supplied value so that Docker Compose and phpdotenv both read it back unchanged -# (spaces, #, $ and quotes otherwise break parsing or change the value). -quote_env_value() { - local value="$1" - case "$value" in - *\'*) - value=${value//\\/\\\\} - value=${value//\"/\\\"} - value=${value//\$/\\\$} - printf '"%s"' "$value" - ;; - *) - printf "'%s'" "$value" - ;; - esac -} - -update_env_var "APP_ID" "$(openssl rand -hex 16)" -update_env_var "APP_KEY" "base64:$(openssl rand -base64 32)" -# update_env_var "DB_USERNAME" "$(openssl rand -hex 16)" # Causes issues: database "random-user" does not exist -update_env_var "DB_PASSWORD" "$(openssl rand -base64 32)" -update_env_var "REDIS_PASSWORD" "$(openssl rand -base64 32)" -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" - -# Add default root user credentials from environment variables (ROOT_USERNAME is optional) -if [ -n "$ROOT_USER_EMAIL" ] && [ -n "$ROOT_USER_PASSWORD" ]; then - echo " - Setting predefined root user credentials from environment" - if [ -n "$ROOT_USERNAME" ]; then - update_env_var "ROOT_USERNAME" "$(quote_env_value "$ROOT_USERNAME")" - fi - update_env_var "ROOT_USER_EMAIL" "$(quote_env_value "$ROOT_USER_EMAIL")" - update_env_var "ROOT_USER_PASSWORD" "$(quote_env_value "$ROOT_USER_PASSWORD")" -elif [ -n "$ROOT_USERNAME" ] || [ -n "$ROOT_USER_EMAIL" ] || [ -n "$ROOT_USER_PASSWORD" ]; then - warn "Predefined root user skipped: set both ROOT_USER_EMAIL and ROOT_USER_PASSWORD." -fi - -if [ -n "${REGISTRY_URL+x}" ]; then - # Only update if REGISTRY_URL was explicitly provided - update_env_var "REGISTRY_URL" "$REGISTRY_URL" -fi - -if [ "$AUTOUPDATE" = "false" ]; then - update_env_var "AUTOUPDATE" "false" -fi - -if [ "$DOCKER_POOL_BASE_PROVIDED" = true ]; then - update_env_var "DOCKER_ADDRESS_POOL_BASE" "$DOCKER_ADDRESS_POOL_BASE" -else - # Add with default value if missing - if ! grep -q "^DOCKER_ADDRESS_POOL_BASE=" "$ENV_FILE"; then - update_env_var "DOCKER_ADDRESS_POOL_BASE" "$DOCKER_ADDRESS_POOL_BASE" - fi -fi - -if [ "$DOCKER_POOL_SIZE_PROVIDED" = true ]; then - update_env_var "DOCKER_ADDRESS_POOL_SIZE" "$DOCKER_ADDRESS_POOL_SIZE" -else - # Add with default value if missing - if ! grep -q "^DOCKER_ADDRESS_POOL_SIZE=" "$ENV_FILE"; then - update_env_var "DOCKER_ADDRESS_POOL_SIZE" "$DOCKER_ADDRESS_POOL_SIZE" - fi -fi -log "Environment variables check completed" -step_done - -step_start "Checking SSH key for localhost access" -SSH_KEY_DETAIL="existing key" -create_coolify_directories -if [ ! -f ~/.ssh/authorized_keys ]; then - mkdir -p ~/.ssh - chmod 700 ~/.ssh - touch ~/.ssh/authorized_keys - chmod 600 ~/.ssh/authorized_keys -fi - -set +e -IS_COOLIFY_VOLUME_EXISTS=$(docker volume ls | grep coolify-db | wc -l) -set -e - -if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then - echo " - Generating SSH key." - SSH_KEY_DETAIL="generated new key" - test -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal && rm -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal - test -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub && rm -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub - ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal -q -N "" -C coolify - chown 9999 /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal - # Remove only the key from a previous install (comment "coolify", or "root@coolify" before September 2024), not other keys that contain "coolify" - sed -i -E "/[[:space:]](root@)?coolify[[:space:]]*$/d" ~/.ssh/authorized_keys - cat /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub >>~/.ssh/authorized_keys - rm -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub -fi - -set_coolify_directory_permissions -log "SSH key check completed" -step_done "$SSH_KEY_DETAIL" - -step_start "Pulling images" "coolify · postgres · redis · helper" -# fd 3 is closed so the detached container restart in upgrade.sh does not hold the terminal open -if [[ $- == *x* ]]; then - bash -x /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-docker.io}" "true" 3>&- -else - bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-docker.io}" "true" 3>&- -fi -step_done - -step_start "Starting Coolify" "v${LATEST_VERSION}" -echo " - Waiting for Coolify to be ready..." - -# Wait for upgrade.sh background process to complete -# upgrade.sh writes status to /data/coolify/source/.upgrade-status -# Status file format: step|message|timestamp -# Step 6 = "Upgrade complete", file deleted 10 seconds after -UPGRADE_STATUS_FILE="/data/coolify/source/.upgrade-status" -MAX_WAIT=180 -WAITED=0 -SEEN_STATUS_FILE=false - -while [ $WAITED -lt $MAX_WAIT ]; do - if [ -f "$UPGRADE_STATUS_FILE" ]; then - SEEN_STATUS_FILE=true - STATUS=$(cat "$UPGRADE_STATUS_FILE" 2>/dev/null | cut -d'|' -f1) - MESSAGE=$(cat "$UPGRADE_STATUS_FILE" 2>/dev/null | cut -d'|' -f2) - if [ "$STATUS" = "6" ]; then - log "Upgrade completed: $MESSAGE" - echo " - Upgrade complete!" - break - elif [ "$STATUS" = "error" ]; then - echo " - ERROR: Upgrade failed: $MESSAGE" - echo " - Please check the upgrade logs: /data/coolify/source/upgrade-*.log" - exit 1 - else - if [ $((WAITED % 10)) -eq 0 ]; then - echo " - Upgrade in progress: $MESSAGE (${WAITED}s)" - fi - fi - else - # Status file doesn't exist - if [ "$SEEN_STATUS_FILE" = true ]; then - # We saw the file before, now it's gone = upgrade completed and cleaned up - log "Upgrade status file cleaned up - upgrade complete" - echo " - Upgrade complete!" - break - fi - # Haven't seen status file yet - either very early or upgrade.sh hasn't started - if [ $((WAITED % 10)) -eq 0 ] && [ $WAITED -gt 0 ]; then - echo " - Waiting for upgrade process to start... (${WAITED}s)" - fi - fi - sleep 2 - WAITED=$((WAITED + 2)) -done - -if [ $WAITED -ge $MAX_WAIT ]; then - if [ "$SEEN_STATUS_FILE" = false ]; then - # Never saw status file - fallback to old behavior (wait 20s + health check) - log "Status file not found, using fallback wait" - echo " - Status file not found, waiting 20 seconds..." - sleep 20 - else - echo " - ERROR: Upgrade timed out after ${MAX_WAIT}s" - echo " - Please check the upgrade logs: /data/coolify/source/upgrade-*.log" - exit 1 - fi -fi - -# Final health verification - wait for container to be healthy -echo " - Verifying Coolify is healthy..." -HEALTH_WAIT=60 -HEALTH_WAITED=0 -while [ $HEALTH_WAITED -lt $HEALTH_WAIT ]; do - HEALTH=$(docker inspect --format='{{.State.Health.Status}}' coolify 2>/dev/null || echo "unknown") - if [ "$HEALTH" = "healthy" ]; then - log "Coolify container is healthy" - echo " - Coolify is ready!" - break - fi - sleep 2 - HEALTH_WAITED=$((HEALTH_WAITED + 2)) -done - -if [ "$HEALTH" != "healthy" ]; then - echo " - ERROR: Coolify container is not healthy after ${HEALTH_WAIT}s. Status: $HEALTH" - echo " - Please check: docker logs coolify" - exit 1 -fi -step_done - -if [ "$UI_TTY" = true ]; then - printf '%s\n' "$(ui_bar 100 "done")" >&3 -fi - -# Fetch public IPs in parallel for faster completion -IPV4_TMP=$(mktemp) -IPV6_TMP=$(mktemp) -curl -4s --max-time 5 https://ifconfig.io > "$IPV4_TMP" 2>/dev/null & -IPV4_PID=$! -curl -6s --max-time 5 https://ifconfig.io > "$IPV6_TMP" 2>/dev/null & -IPV6_PID=$! -wait $IPV4_PID 2>/dev/null || true -wait $IPV6_PID 2>/dev/null || true -IPV4_PUBLIC_IP=$(cat "$IPV4_TMP" 2>/dev/null || true) -IPV6_PUBLIC_IP=$(cat "$IPV6_TMP" 2>/dev/null || true) -rm -f "$IPV4_TMP" "$IPV6_TMP" - -set +e -DEFAULT_PRIVATE_IP=$(ip route get 1 | sed -n 's/^.*src \([0-9.]*\) .*$/\1/p') -PRIVATE_IPS=$(hostname -I 2>/dev/null || ip -o addr show scope global | awk '{print $4}' | cut -d/ -f1) -set -e - -printf '\n %s%sCoolify is ready!%s %sInstalled in %s%s\n\n' "$C_BOLD" "$C_PURPLE" "$C_RESET" "$C_DIM" "$(ui_duration $(($(ui_now) - UI_INSTALL_STARTED)))" "$C_RESET" >&3 -if [ -n "$IPV4_PUBLIC_IP" ]; then - printf ' Public IPv4 %shttp://%s:8000%s\n' "$C_BOLD" "$IPV4_PUBLIC_IP" "$C_RESET" >&3 -fi -if [ -n "$IPV6_PUBLIC_IP" ]; then - printf ' Public IPv6 %shttp://[%s]:8000%s\n' "$C_BOLD" "$IPV6_PUBLIC_IP" "$C_RESET" >&3 -fi - -PRIVATE_IP_LABEL="Private IP " -for IP in $PRIVATE_IPS; do - if [ "$IP" != "$DEFAULT_PRIVATE_IP" ]; then - printf ' %s %shttp://%s:8000%s\n' "$PRIVATE_IP_LABEL" "$C_DIM" "$IP" "$C_RESET" >&3 - PRIVATE_IP_LABEL=" " - fi -done - -if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ] && { [ -z "$ROOT_USER_EMAIL" ] || [ -z "$ROOT_USER_PASSWORD" ]; }; then - printf '\n %s! Create the root account within 10 minutes. If the time ends, run %sdocker restart coolify%s%s and try again.%s\n' "$C_YELLOW" "$C_BOLD" "$C_RESET" "$C_YELLOW" "$C_RESET" >&3 -fi -printf '\n %s! Back up %s/data/coolify/source/.env%s%s to a safe place outside this server (e.g. a password manager).%s\n' "$C_YELLOW" "$C_BOLD" "$C_RESET" "$C_YELLOW" "$C_RESET" >&3 -printf ' %sLog file: %s%s\n\n' "$C_DIM" "$INSTALLATION_LOG_WITH_DATE" "$C_RESET" >&3 - -log_section "Installation Complete" -log "Coolify installation completed successfully" -log "Version: ${LATEST_VERSION}" -log "Log file: ${INSTALLATION_LOG_WITH_DATE}" diff --git a/other/nightly/upgrade-postgres.sh b/other/nightly/upgrade-postgres.sh deleted file mode 100755 index 09065875e5..0000000000 --- a/other/nightly/upgrade-postgres.sh +++ /dev/null @@ -1,405 +0,0 @@ -#!/bin/bash -## Explicit Coolify internal PostgreSQL major-version migrator. -## This script is intentionally not run by upgrade.sh automatically. - -set -Eeuo pipefail - -SOURCE_DIR="/data/coolify/source" -ENV_FILE="${SOURCE_DIR}/.env" -BACKUP_DIR="/data/coolify/backups/internal-postgres" -OVERRIDE_FILE="${SOURCE_DIR}/docker-compose.postgres-upgrade.yml" -ROLLBACK_FILE="${SOURCE_DIR}/postgres-upgrade-rollback.env" -DATE=$(date +%Y-%m-%d-%H-%M-%S) -LOGFILE="${SOURCE_DIR}/postgres-upgrade-${DATE}.log" -COMMAND="${1:-upgrade}" -TARGET_MAJOR="${1:-18}" - -if [ "$COMMAND" = "rollback" ]; then - TARGET_MAJOR="" -else - COMMAND="upgrade" -fi - -TARGET_IMAGE="${COOLIFY_POSTGRES_TARGET_IMAGE:-postgres:${TARGET_MAJOR}-alpine}" -TARGET_VOLUME="${COOLIFY_POSTGRES_TARGET_VOLUME:-coolify-db-pg${TARGET_MAJOR}}" -TEMP_CONTAINER="coolify-db-pg${TARGET_MAJOR:-rollback}-restore-${DATE}" -DUMP_FILE="${BACKUP_DIR}/postgres-upgrade-${DATE}.sql.gz" - -log() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOGFILE" -} - -fail() { - log "ERROR: $1" - exit 1 -} - -usage() { - cat < - $0 rollback - -Examples: - $0 18 - $0 rollback - -Environment overrides: - COOLIFY_POSTGRES_TARGET_IMAGE=postgres:18-alpine - COOLIFY_POSTGRES_TARGET_VOLUME=coolify-db-pg18 -EOF -} - -cleanup() { - docker rm -f "$TEMP_CONTAINER" >/dev/null 2>&1 || true -} -trap cleanup EXIT - -get_env_var() { - local key="$1" - local fallback="${2:-}" - local value - - value=$(grep -E "^${key}=" "$ENV_FILE" 2>/dev/null | tail -n 1 | cut -d '=' -f 2- || true) - value="${value%\"}" - value="${value#\"}" - value="${value%\'}" - value="${value#\'}" - - if [ -z "$value" ]; then - printf '%s' "$fallback" - else - printf '%s' "$value" - fi -} - -wait_for_postgres() { - local container="$1" - local user="$2" - local database="$3" - local attempts=60 - - for _ in $(seq 1 "$attempts"); do - if docker exec "$container" pg_isready -U "$user" -d "$database" >/dev/null 2>&1; then - return 0 - fi - sleep 2 - done - - return 1 -} - -compose_files() { - printf -- '-f %s/docker-compose.yml -f %s/docker-compose.prod.yml ' "$SOURCE_DIR" "$SOURCE_DIR" - - if [ -f "${SOURCE_DIR}/docker-compose.custom.yml" ]; then - printf -- '-f %s/docker-compose.custom.yml ' "$SOURCE_DIR" - fi - - if [ -f "$OVERRIDE_FILE" ]; then - printf -- '-f %s ' "$OVERRIDE_FILE" - fi -} - -validate_target_major() { - case "$TARGET_MAJOR" in - ''|*[!0-9]*) - usage - fail "Target major version must be numeric. Example: $0 18" - ;; - esac - - if [ "$TARGET_MAJOR" -lt 10 ]; then - fail "Target major version must be 10 or higher." - fi -} - -mount_path_for_major() { - local major="$1" - - if [ "$major" -ge 18 ]; then - printf '%s' '/var/lib/postgresql' - else - printf '%s' '/var/lib/postgresql/data' - fi -} - -current_postgres_mount_name() { - docker inspect coolify-db --format '{{range .Mounts}}{{if or (eq .Destination "/var/lib/postgresql/data") (eq .Destination "/var/lib/postgresql")}}{{.Name}}{{end}}{{end}}' 2>/dev/null -} - -current_postgres_mount_path() { - docker inspect coolify-db --format '{{range .Mounts}}{{if or (eq .Destination "/var/lib/postgresql/data") (eq .Destination "/var/lib/postgresql")}}{{.Destination}}{{end}}{{end}}' 2>/dev/null -} - -current_postgres_image() { - docker inspect coolify-db --format '{{.Config.Image}}' 2>/dev/null -} - -current_coolify_image_tag() { - local image - local image_without_digest - local last_segment - - image=$(docker inspect coolify --format '{{.Config.Image}}' 2>/dev/null || true) - image_without_digest="${image%@*}" - last_segment="${image_without_digest##*/}" - - if [[ "$last_segment" == *:* ]]; then - printf '%s' "${last_segment##*:}" - fi -} - -write_override_file() { - local image="$1" - local volume="$2" - local mount_path="$3" - - cat > "$OVERRIDE_FILE" < "$ROLLBACK_FILE" </dev/null 2>&1 || fail "Docker is required." - docker info >/dev/null 2>&1 || fail "Docker daemon is not reachable." -} - -rollback_postgres() { - validate_common_requirements - - [ -f "$ROLLBACK_FILE" ] || fail "Missing rollback metadata file: ${ROLLBACK_FILE}" - - # shellcheck disable=SC1090 - . "$ROLLBACK_FILE" - - [ -n "${PREVIOUS_IMAGE:-}" ] || fail "Rollback metadata is missing PREVIOUS_IMAGE." - [ -n "${PREVIOUS_VOLUME:-}" ] || fail "Rollback metadata is missing PREVIOUS_VOLUME." - [ -n "${PREVIOUS_MOUNT_PATH:-}" ] || fail "Rollback metadata is missing PREVIOUS_MOUNT_PATH." - [ -n "${PREVIOUS_OVERRIDE_PRESENT:-}" ] || fail "Rollback metadata is missing PREVIOUS_OVERRIDE_PRESENT." - - CURRENT_COOLIFY_IMAGE_TAG=$(current_coolify_image_tag) - - log "Rolling back Coolify internal PostgreSQL." - log "Previous image: ${PREVIOUS_IMAGE}" - log "Previous volume: ${PREVIOUS_VOLUME}" - log "Previous mount path: ${PREVIOUS_MOUNT_PATH}" - log "Current Coolify image tag: ${CURRENT_COOLIFY_IMAGE_TAG:-latest}" - - docker volume inspect "$PREVIOUS_VOLUME" >/dev/null 2>&1 || fail "Previous volume '${PREVIOUS_VOLUME}' does not exist." - - log "Stopping Coolify application container before rollback." - docker stop coolify >>"$LOGFILE" 2>&1 || true - - log "Removing current coolify-db container. Current upgraded volume is kept untouched." - docker rm -f coolify-db >>"$LOGFILE" 2>&1 || true - - if [ "$PREVIOUS_OVERRIDE_PRESENT" = "true" ]; then - log "Restoring previous PostgreSQL compose override." - write_override_file "$PREVIOUS_IMAGE" "$PREVIOUS_VOLUME" "$PREVIOUS_MOUNT_PATH" - else - log "Removing PostgreSQL compose override to restore base compose configuration." - rm -f "$OVERRIDE_FILE" - fi - - log "Starting Coolify stack with rollback database volume." - start_stack "$CURRENT_COOLIFY_IMAGE_TAG" >>"$LOGFILE" 2>&1 || fail "Could not start Coolify stack after rollback. See ${LOGFILE}." - - log "Rollback completed successfully." - cat <>"$LOGFILE" 2>&1 || fail "Could not start coolify-db." - fi - - wait_for_postgres coolify-db "$DB_USERNAME" "$DB_DATABASE" || fail "Existing coolify-db is not ready." - - SERVER_VERSION_NUM=$(docker exec coolify-db psql -U "$DB_USERNAME" -d "$DB_DATABASE" -Atc 'SHOW server_version_num;' | tr -d '[:space:]') - CURRENT_MAJOR=$((SERVER_VERSION_NUM / 10000)) - PREVIOUS_VOLUME=$(current_postgres_mount_name) - PREVIOUS_MOUNT_PATH=$(current_postgres_mount_path) - PREVIOUS_IMAGE=$(current_postgres_image) - CURRENT_COOLIFY_IMAGE_TAG=$(current_coolify_image_tag) - - [ -n "$PREVIOUS_VOLUME" ] || fail "Could not detect current PostgreSQL Docker volume." - [ -n "$PREVIOUS_MOUNT_PATH" ] || fail "Could not detect current PostgreSQL mount path." - [ -n "$PREVIOUS_IMAGE" ] || fail "Could not detect current PostgreSQL image." - - if [ -f "$OVERRIDE_FILE" ]; then - PREVIOUS_OVERRIDE_PRESENT=true - else - PREVIOUS_OVERRIDE_PRESENT=false - fi - - log "Current PostgreSQL major: ${CURRENT_MAJOR}" - log "Current active volume: ${PREVIOUS_VOLUME}" - log "Current image: ${PREVIOUS_IMAGE}" - log "Current mount path: ${PREVIOUS_MOUNT_PATH}" - log "Current Coolify image tag: ${CURRENT_COOLIFY_IMAGE_TAG:-latest}" - - if [ "$CURRENT_MAJOR" -eq "$TARGET_MAJOR" ]; then - log "PostgreSQL is already on major ${TARGET_MAJOR}. Nothing to do." - exit 0 - fi - - if [ "$CURRENT_MAJOR" -gt "$TARGET_MAJOR" ]; then - fail "Downgrade from ${CURRENT_MAJOR} to ${TARGET_MAJOR} is not supported. Use '$0 rollback' to restore the previous upgrade state." - fi - - if docker volume inspect "$TARGET_VOLUME" >/dev/null 2>&1; then - fail "Target volume '${TARGET_VOLUME}' already exists. Set COOLIFY_POSTGRES_TARGET_VOLUME to a new name or remove the old failed target volume." - fi - - log "Stopping Coolify application container to prevent writes during dump." - docker stop coolify >>"$LOGFILE" 2>&1 || true - - log "Creating compressed dump at ${DUMP_FILE}." - docker exec coolify-db pg_dumpall -U "$DB_USERNAME" | gzip -c > "$DUMP_FILE" - chmod 600 "$DUMP_FILE" - - if [ ! -s "$DUMP_FILE" ]; then - fail "Dump file is empty. Aborting." - fi - - log "Creating target Docker volume '${TARGET_VOLUME}'." - docker volume create "$TARGET_VOLUME" >>"$LOGFILE" 2>&1 - - log "Pulling ${TARGET_IMAGE}." - docker pull "$TARGET_IMAGE" >>"$LOGFILE" 2>&1 - - log "Starting temporary PostgreSQL ${TARGET_MAJOR} container." - docker run -d \ - --name "$TEMP_CONTAINER" \ - --network coolify \ - -e POSTGRES_HOST_AUTH_METHOD=trust \ - -v "${TARGET_VOLUME}:${TARGET_MOUNT_PATH}" \ - "$TARGET_IMAGE" >>"$LOGFILE" 2>&1 - - wait_for_postgres "$TEMP_CONTAINER" postgres postgres || fail "Temporary PostgreSQL ${TARGET_MAJOR} container did not become ready." - - log "Restoring dump into target volume." - gunzip -c "$DUMP_FILE" | docker exec -i "$TEMP_CONTAINER" psql -U postgres -d postgres >>"$LOGFILE" 2>&1 - - log "Smoke-checking restored Coolify database." - docker exec "$TEMP_CONTAINER" psql -U "$DB_USERNAME" -d "$DB_DATABASE" -Atc 'SELECT 1;' | grep -qx '1' || fail "Restored database smoke check failed." - - log "Saving rollback metadata to ${ROLLBACK_FILE}." - write_rollback_file "$PREVIOUS_IMAGE" "$PREVIOUS_VOLUME" "$PREVIOUS_MOUNT_PATH" "$PREVIOUS_OVERRIDE_PRESENT" "$TARGET_IMAGE" "$TARGET_VOLUME" - - log "Writing Docker Compose override to ${OVERRIDE_FILE}." - write_override_file "$TARGET_IMAGE" "$TARGET_VOLUME" "$TARGET_MOUNT_PATH" - - log "Stopping temporary restore container." - docker rm -f "$TEMP_CONTAINER" >>"$LOGFILE" 2>&1 || true - - log "Stopping old coolify-db container. Previous volume '${PREVIOUS_VOLUME}' will be kept for rollback." - docker rm -f coolify-db >>"$LOGFILE" 2>&1 || true - - log "Starting Coolify stack with PostgreSQL ${TARGET_MAJOR}." - start_stack "$CURRENT_COOLIFY_IMAGE_TAG" >>"$LOGFILE" 2>&1 || fail "Could not start Coolify stack with upgraded PostgreSQL. See ${LOGFILE}." - - log "Coolify internal PostgreSQL upgrade completed successfully." - print_rollback_instructions -} - -if [ "${COOLIFY_POSTGRES_UPGRADE_SOURCE_ONLY:-false}" = "true" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then - return 0 -fi - -case "$COMMAND" in - rollback) - rollback_postgres - ;; - upgrade) - upgrade_postgres - ;; - -h|--help|help) - usage - ;; - *) - usage - fail "Unknown command: ${COMMAND}" - ;; -esac diff --git a/other/nightly/upgrade.sh b/other/nightly/upgrade.sh deleted file mode 100644 index fea3c360c7..0000000000 --- a/other/nightly/upgrade.sh +++ /dev/null @@ -1,427 +0,0 @@ -#!/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}" diff --git a/other/nightly/versions.json b/other/nightly/versions.json deleted file mode 100644 index c2fec20f13..0000000000 --- a/other/nightly/versions.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "coolify": { - "v4": { - "version": "4.5.4" - }, - "nightly": { - "version": "4.6-rc.1" - }, - "helper": { - "version": "1.0.19" - }, - "sentinel": { - "version": "1.1.3" - }, - "sentinel-host": { - "version": "1.1.3", - "digest": "sha256:bca77d22355c9005185b438dad406747b4b776018215a0a4a75c88618d681ff7" - } - }, - "traefik": { - "v3.7": "3.7.16", - "v3.6": "3.6.25", - "v3.5": "3.5.6", - "v3.4": "3.4.5", - "v3.3": "3.3.7", - "v3.2": "3.2.5", - "v3.1": "3.1.7", - "v3.0": "3.0.4", - "v2.11": "2.11.60" - } -} diff --git a/scripts/sync-cdn.sh b/scripts/sync-cdn.sh index 1ac3c1cf7f..067c428520 100755 --- a/scripts/sync-cdn.sh +++ b/scripts/sync-cdn.sh @@ -22,8 +22,8 @@ cd "$(dirname "$0")/.." trap 'rm -rf "$WORK_DIR"' EXIT main() { - [[ "$OPTION" =~ ^(--stable [0-9]+\.[0-9]+\.[0-9]+|--preview [0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[0-9]+)?|--purge)$ ]] \ - || die "'$OPTION' is not an option, expected --stable or --preview with the version of the channel, or --purge" + [[ "$OPTION" =~ ^(--stable [0-9]+\.[0-9]+\.[0-9]+|--preview [0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[0-9]+)?|--nightly [0-9]+\.[0-9]+\.[0-9]+-(alpha|beta|rc)\.[0-9]+\.[0-9]{8}\.[0-9]+|--purge)$ ]] \ + || die "'$OPTION' is not an option, expected --stable, --preview or --nightly with the version of the channel, or --purge" if [ "$OPTION" = --purge ]; then purge_cache return @@ -44,7 +44,7 @@ select_channel() { channel_path+="/$channel" channel_url+="/$channel" fi if [[ "$channel:$VERSION" == preview:*-alpha.* ]]; then - info "the preview channel takes no alphas, Coolify $VERSION is not synced" + info "the preview channel takes no alphas, Coolify $VERSION reaches the nightly channel only" exit 0 fi info "syncing $channel_url to Coolify $BOLD$VERSION$RESET" @@ -68,7 +68,7 @@ clone_cdn() { write_release() { local release="$WORK_DIR/cdn/json/coolify/release.json" [ -f "$release" ] || echo '{}' > "$release" - jq --tab --arg channel "$channel" --arg version "$VERSION" '.[$channel].coolify = $version | {stable, preview} | with_entries(select(.value))' \ + jq --tab --arg channel "$channel" --arg version "$VERSION" '.[$channel].coolify = $version | {stable, preview, nightly} | with_entries(select(.value))' \ "$release" > "$WORK_DIR/release.json" cp "$WORK_DIR/release.json" "$release" ok "release.json sets the $channel version $BOLD$VERSION$RESET" diff --git a/scripts/version.sh b/scripts/version.sh index e9b10bebb4..309117da11 100755 --- a/scripts/version.sh +++ b/scripts/version.sh @@ -4,6 +4,8 @@ set -euo pipefail OPTION="$*" BRANCH="${BRANCH:-}" PULL_REQUEST="${PULL_REQUEST:-}" +NIGHTLY_VERSION="${NIGHTLY_VERSION:-}" +NIGHTLY_REVISION="${NIGHTLY_REVISION:-}" VERSION_FILE=release.json ARTIFACT=coolify UPSTREAM_FILE=upstream.json @@ -22,10 +24,11 @@ die() { cd "$(dirname "$0")/.." main() { - [[ "$OPTION" =~ ^(--build|--release|--tags)?$ ]] || die "'$OPTION' is not an option, expected --build, --release, --tags or none" + [[ "$OPTION" =~ ^(--build|--release|--tags|--nightly)?$ ]] || die "'$OPTION' is not an option, expected --build, --release, --tags, --nightly or none" read_version fetch_origin print_tags + print_next_nightly check_branch check_version select_releases @@ -66,6 +69,43 @@ print_tags() { exit 0 } +print_next_nightly() { + [ "$OPTION" = --nightly ] || return 0 + select_nightly_branch + [ "$revision" != "$NIGHTLY_REVISION" ] || exit 0 + select_nightly_version + upstream_lines <(git show "$revision:$UPSTREAM_FILE") | while read -r name upstream; do + jq -nc --arg name "$name" --arg version "$upstream" --arg tag "${upstream:+$upstream-}v$nightly" '{$name, $version, $tag}' + done | jq -sc --arg branch "$branch" --arg revision "$revision" --arg image "$nightly" '{$branch, $revision, $image, lines: .}' + exit 0 +} + +select_nightly_branch() { + stable="$(tag_versions <<< "$tags" | awk '!/-/' | newest)" + stable="${stable:-$(artifact_version stable)}" + line="$(git for-each-ref --format='%(refname:lstrip=4)' 'refs/remotes/origin/release/v*' | sed -n 's/^v\([0-9]*\.[0-9]*\)$/\1/p' | sort -V \ + | while read -r release; do ! newer "$release.0" "$stable" || { echo "$release" && break; }; done)" + branch="${line:+release/v$line}" + branch="${branch:-next}" + revision="$(git rev-parse "origin/$branch")" +} + +select_nightly_version() { + local base number=1 + version="$(artifact_version preview <(git show "$revision:$VERSION_FILE"))" + if [ -n "$line" ]; then + base="$line.0-alpha.0" + [[ "$version" != "$line.0-"* ]] || base="$version" + elif [[ "$version" == *-* ]] && newer "$version" "$stable"; then + base="$version" + else + base="$(next_version "$stable" minor)-alpha.0" + fi + nightly="$base.$(date -u +%Y%m%d)" + [[ "$NIGHTLY_VERSION" != "$nightly".* ]] || number=$((${NIGHTLY_VERSION##*.} + 1)) + nightly+=".$number" +} + check_branch() { [[ "$branch" =~ ^(main|next|v([0-9]+(\.[0-9]+)?)\.x|release/v([0-9]+\.[0-9]+))$ ]] || die "${branch:-a detached HEAD} releases nothing" line="${BASH_REMATCH[2]}${BASH_REMATCH[4]}" @@ -176,7 +216,7 @@ print_releases() { } artifact_version() { - jq -r --arg channel "$1" --arg artifact "$ARTIFACT" '.[$channel][$artifact] // empty' "$VERSION_FILE" + jq -r --arg channel "$1" --arg artifact "$ARTIFACT" '.[$channel][$artifact] // empty' "${2:-$VERSION_FILE}" } upstream_lines() { @@ -184,7 +224,7 @@ upstream_lines() { echo latest return fi - jq -r --arg upstream "$UPSTREAM" '.[$upstream] | to_entries[] | "\(.key) \(.value.version)"' "$UPSTREAM_FILE" + jq -r --arg upstream "$UPSTREAM" '.[$upstream] | to_entries[] | "\(.key) \(.value.version)"' "${1:-$UPSTREAM_FILE}" } next_version() { diff --git a/tests/Feature/PersistentImageStorageTest.php b/tests/Feature/PersistentImageStorageTest.php index 59d88e3f0b..1439f00302 100644 --- a/tests/Feature/PersistentImageStorageTest.php +++ b/tests/Feature/PersistentImageStorageTest.php @@ -14,7 +14,6 @@ it('persists images in stable and nightly production compose files', function (s ->toContain('/data/coolify/images:/var/www/html/storage/app/images'); })->with([ 'stable' => 'docker-compose.prod.yml', - 'nightly' => 'other/nightly/docker-compose.prod.yml', ]); it('persists images in the Windows compose file', function () { diff --git a/tests/Feature/PostgresUpgradeScriptTest.php b/tests/Feature/PostgresUpgradeScriptTest.php index bdb6f7c4e4..79b3c7e315 100644 --- a/tests/Feature/PostgresUpgradeScriptTest.php +++ b/tests/Feature/PostgresUpgradeScriptTest.php @@ -49,9 +49,6 @@ it('detects the current Coolify image tag from the running container image', fun expect($exitCode)->toBe(0) ->and($output)->toBe($expectedTag); })->with([ - 'nightly standard registry image' => ['other/nightly/upgrade-postgres.sh', 'ghcr.io/coollabsio/coolify:4.0.0-beta.420', '4.0.0-beta.420'], - 'nightly registry with port' => ['other/nightly/upgrade-postgres.sh', 'registry.example.com:5000/coollabsio/coolify:4.0.1', '4.0.1'], - 'nightly digest suffix' => ['other/nightly/upgrade-postgres.sh', 'ghcr.io/coollabsio/coolify:4.0.2@sha256:abcdef', '4.0.2'], 'scripts standard registry image' => ['scripts/upgrade-postgres.sh', 'ghcr.io/coollabsio/coolify:4.0.0-beta.420', '4.0.0-beta.420'], 'scripts registry with port' => ['scripts/upgrade-postgres.sh', 'registry.example.com:5000/coollabsio/coolify:4.0.1', '4.0.1'], 'scripts digest suffix' => ['scripts/upgrade-postgres.sh', 'ghcr.io/coollabsio/coolify:4.0.2@sha256:abcdef', '4.0.2'], @@ -66,6 +63,5 @@ it('passes the preserved Coolify image tag to docker compose when starting the s ->and($output)->toBe('') ->and($latestImage)->toBe('4.0.0-beta.420'); })->with([ - 'nightly script' => 'other/nightly/upgrade-postgres.sh', 'release script' => 'scripts/upgrade-postgres.sh', ]); diff --git a/tests/Feature/ReverbAndTerminalPackagingTest.php b/tests/Feature/ReverbAndTerminalPackagingTest.php index ea4662a84b..ef8f39684c 100644 --- a/tests/Feature/ReverbAndTerminalPackagingTest.php +++ b/tests/Feature/ReverbAndTerminalPackagingTest.php @@ -70,7 +70,6 @@ it('rewrites the legacy realtime backend host during upgrades', function (string ->toContain('set_env_var "PUSHER_BACKEND_HOST" "127.0.0.1"'); })->with([ 'upgrade script' => ['scripts/upgrade.sh'], - 'nightly upgrade script' => ['other/nightly/upgrade.sh'], ]); it('includes Reverb but not the terminal server in the Coolify container healthcheck', function (string $composeFile) { @@ -82,9 +81,7 @@ it('includes Reverb but not the terminal server in the Coolify container healthc ->not->toContain('6002/ready'); })->with([ 'production compose' => ['docker-compose.prod.yml'], - 'nightly production compose' => ['other/nightly/docker-compose.prod.yml'], 'windows compose' => ['docker-compose.windows.yml'], - 'nightly windows compose' => ['other/nightly/docker-compose.windows.yml'], ]); it('removes the legacy realtime container during upgrades', function (string $script) { @@ -92,7 +89,6 @@ it('removes the legacy realtime container during upgrades', function (string $sc ->toContain('for container in coolify coolify-db coolify-redis coolify-realtime; do'); })->with([ 'upgrade script' => ['scripts/upgrade.sh'], - 'nightly upgrade script' => ['other/nightly/upgrade.sh'], ]); it('runs Reverb and terminal websocket services inside the Coolify containers', function (string $dockerfile, string $dependencyService) { @@ -195,11 +191,6 @@ it('uses Pusher environment keys for self-hosted Reverb compatibility', function 'docker-compose.windows.yml', 'scripts/install.sh', 'scripts/upgrade.sh', - 'other/nightly/.env.production', - 'other/nightly/docker-compose.prod.yml', - 'other/nightly/docker-compose.windows.yml', - 'other/nightly/install.sh', - 'other/nightly/upgrade.sh', ]; foreach ($files as $file) { @@ -230,9 +221,7 @@ it('does not use the browser websocket port as the Docker host port', function ( ->not->toContain('"${PUSHER_PORT:-6001}:6001"'); })->with([ 'production compose' => ['docker-compose.prod.yml'], - 'nightly production compose' => ['other/nightly/docker-compose.prod.yml'], 'windows compose' => ['docker-compose.windows.yml'], - 'nightly windows compose' => ['other/nightly/docker-compose.windows.yml'], ]); it('preserves existing public websocket port overrides during install and upgrade', function (string $script) { @@ -244,13 +233,10 @@ it('preserves existing public websocket port overrides during install and upgrad })->with([ 'install script' => ['scripts/install.sh'], 'upgrade script' => ['scripts/upgrade.sh'], - 'nightly install script' => ['other/nightly/install.sh'], - 'nightly upgrade script' => ['other/nightly/upgrade.sh'], ]); it('stops publishing or preserving the obsolete realtime image', function () { $productionInstallScript = file_get_contents(base_path('scripts/install.sh')); - $nightlyInstallScript = file_get_contents(base_path('other/nightly/install.sh')); expect(file_get_contents(config_path('constants.php'))) ->not->toContain('realtime_version') @@ -263,8 +249,6 @@ it('stops publishing or preserving the obsolete realtime image', function () { ->and(file_exists(base_path('.github/workflows/coolify-realtime.yml')))->toBeFalse() ->and(file_exists(base_path('.github/workflows/coolify-realtime-next.yml')))->toBeFalse() ->and($productionInstallScript)->not->toContain('LATEST_REALTIME_VERSION') - ->not->toContain('| Realtime') - ->and($nightlyInstallScript)->not->toContain('LATEST_REALTIME_VERSION') ->not->toContain('| Realtime'); }); @@ -278,7 +262,6 @@ it('keeps 4.3.23 realtime setups working with the production compose files', fun ->and(implode("\n", [...$coolify['ports'], ...$coolify['expose']]))->not->toContain('TERMINAL_PORT'); })->with([ 'production compose' => ['docker-compose.prod.yml'], - 'nightly production compose' => ['other/nightly/docker-compose.prod.yml'], ]); it('keeps TERMINAL_PORT browser-only in the Windows compose files', function (string $composeFile) { @@ -288,7 +271,6 @@ it('keeps TERMINAL_PORT browser-only in the Windows compose files', function (st ->and(implode("\n", [...$coolify['ports'], ...$coolify['expose']]))->not->toContain('TERMINAL_PORT'); })->with([ 'windows compose' => ['docker-compose.windows.yml'], - 'nightly windows compose' => ['other/nightly/docker-compose.windows.yml'], ]); it('resolves the browser websocket port like 4.3.23', function (string $pageUrl, ?string $pusherPort, ?string $expectedPort) { @@ -335,7 +317,6 @@ it('accepts a soketi override from 4.3.23 in docker-compose.custom.yml with zero })->with([ 'production compose, partial override' => ['docker-compose.prod.yml', "services:\n soketi:\n environment:\n SOKETI_DEBUG: 'true'\n ports:\n - '6001:6001'\n"], 'production compose, full service' => ['docker-compose.prod.yml', "services:\n soketi:\n image: quay.io/soketi/soketi:1.6-16-alpine\n container_name: coolify-realtime\n ports:\n - '6001:6001'\n"], - 'nightly production compose, partial override' => ['other/nightly/docker-compose.prod.yml', "services:\n soketi:\n environment:\n SOKETI_DEBUG: 'true'\n"], ]); it('does not pull an extra image for the soketi placeholder', function (string $composeFile) { @@ -362,5 +343,4 @@ it('does not pull an extra image for the soketi placeholder', function (string $ ->toBe(['postgres:15-alpine', 'redis:7-alpine', 'registry.example.com/coollabsio/coolify:4.5.1']); })->with([ 'production compose' => ['docker-compose.prod.yml'], - 'nightly production compose' => ['other/nightly/docker-compose.prod.yml'], ]); diff --git a/tests/Unit/InstallScriptAuthorizedKeysTest.php b/tests/Unit/InstallScriptAuthorizedKeysTest.php index a105cea8b0..3caaa213ce 100644 --- a/tests/Unit/InstallScriptAuthorizedKeysTest.php +++ b/tests/Unit/InstallScriptAuthorizedKeysTest.php @@ -38,5 +38,4 @@ it('removes only previous Coolify keys from authorized_keys', function (string $ exec('rm -rf '.escapeshellarg($home)); })->with([ 'stable install script' => ['scripts/install.sh'], - 'nightly install script' => ['other/nightly/install.sh'], ]); diff --git a/tests/Unit/InstallScriptDirectoryPermissionsTest.php b/tests/Unit/InstallScriptDirectoryPermissionsTest.php index e43e0ccff2..259ad41d13 100644 --- a/tests/Unit/InstallScriptDirectoryPermissionsTest.php +++ b/tests/Unit/InstallScriptDirectoryPermissionsTest.php @@ -46,7 +46,6 @@ function installScriptFileMode(string $path): string $installScripts = [ 'stable install' => ['scripts/install.sh', ['source', 'ssh', 'images']], - 'nightly install' => ['other/nightly/install.sh', ['source', 'ssh', 'images']], ]; $resourceDirectories = ['applications', 'databases', 'backups', 'services', 'proxy', 'sentinel']; diff --git a/tests/Unit/InstallScriptDockerDaemonConfigTest.php b/tests/Unit/InstallScriptDockerDaemonConfigTest.php index 2dab67a70d..f25bb21136 100644 --- a/tests/Unit/InstallScriptDockerDaemonConfigTest.php +++ b/tests/Unit/InstallScriptDockerDaemonConfigTest.php @@ -56,7 +56,7 @@ function runInstallScriptDockerDaemonStep(string $scriptPath, ?string $daemonJso return ['config' => $config, 'restarted' => str_contains($output, 'RESTARTED'), 'output' => $output]; } -$installScripts = ['stable install' => 'scripts/install.sh', 'nightly install' => 'other/nightly/install.sh']; +$installScripts = ['stable install' => 'scripts/install.sh']; it('does not create daemon.json or restart Docker when containers are running', function (string $scriptPath) { $result = runInstallScriptDockerDaemonStep($scriptPath, null, runningContainers: 30); diff --git a/tests/Unit/InstallScriptRockyDockerRepoTest.php b/tests/Unit/InstallScriptRockyDockerRepoTest.php index 0e0a92806d..926bc14df3 100644 --- a/tests/Unit/InstallScriptRockyDockerRepoTest.php +++ b/tests/Unit/InstallScriptRockyDockerRepoTest.php @@ -22,7 +22,3 @@ function expectRockyInstallScriptToUseRhelRepo(string $path): void it('uses the rocky linux documented docker install flow in the stable install script', function () { expectRockyInstallScriptToUseRhelRepo('scripts/install.sh'); }); - -it('uses the rocky linux documented docker install flow in the nightly install script', function () { - expectRockyInstallScriptToUseRhelRepo('other/nightly/install.sh'); -}); diff --git a/tests/Unit/InstallScriptRootUserEnvTest.php b/tests/Unit/InstallScriptRootUserEnvTest.php index aab987d738..a067e688d7 100644 --- a/tests/Unit/InstallScriptRootUserEnvTest.php +++ b/tests/Unit/InstallScriptRootUserEnvTest.php @@ -41,7 +41,7 @@ function writeRootUserCredentialsWithInstallScript(string $scriptPath, array $cr return Dotenv::parse($contents); } -$installScripts = ['stable install' => 'scripts/install.sh', 'nightly install' => 'other/nightly/install.sh']; +$installScripts = ['stable install' => 'scripts/install.sh']; $credentials = [ 'username with a space' => ['ROOT_USERNAME', 'Coolify Lab'], diff --git a/tests/Unit/InstallScriptTerminalUiTest.php b/tests/Unit/InstallScriptTerminalUiTest.php index b5cc1628ff..4dc5454a59 100644 --- a/tests/Unit/InstallScriptTerminalUiTest.php +++ b/tests/Unit/InstallScriptTerminalUiTest.php @@ -25,7 +25,6 @@ it('shows every installer step with the terminal UI and sends command output to ->and(preg_match_all('/^step_done(?!\()/m', $script))->toBe(9); })->with([ 'stable install script' => ['scripts/install.sh'], - 'nightly install script' => ['other/nightly/install.sh'], ]); it('renders aligned step lines, warnings and failures without a terminal', function (string $path) { @@ -68,5 +67,4 @@ it('renders aligned step lines, warnings and failures without a terminal', funct ->and(array_map(fn (string $line) => mb_strlen($line), $stepLines))->each->toBe(80); })->with([ 'stable install script' => ['scripts/install.sh'], - 'nightly install script' => ['other/nightly/install.sh'], ]); diff --git a/tests/Unit/ProductionComposeHealthcheckTest.php b/tests/Unit/ProductionComposeHealthcheckTest.php index 6bbe5280e5..31f97f5b2c 100644 --- a/tests/Unit/ProductionComposeHealthcheckTest.php +++ b/tests/Unit/ProductionComposeHealthcheckTest.php @@ -11,5 +11,4 @@ it('allows Coolify enough time to start before counting healthcheck failures', f ->retries->toBe(24); })->with([ 'stable' => 'docker-compose.prod.yml', - 'nightly' => 'other/nightly/docker-compose.prod.yml', ]); diff --git a/tests/Unit/UpgradePostgresScriptTest.php b/tests/Unit/UpgradePostgresScriptTest.php index 677a8fec3e..6e0d44dd05 100644 --- a/tests/Unit/UpgradePostgresScriptTest.php +++ b/tests/Unit/UpgradePostgresScriptTest.php @@ -26,7 +26,6 @@ function assertBashSyntaxIsValid(string $path): void it('ships postgres upgrade scripts with valid bash syntax', function () { assertBashSyntaxIsValid('scripts/upgrade-postgres.sh'); - assertBashSyntaxIsValid('other/nightly/upgrade-postgres.sh'); }); it('downloads postgres upgrade script during install and upgrade without auto-running it', function (string $path) { @@ -39,9 +38,7 @@ it('downloads postgres upgrade script during install and upgrade without auto-ru ->not->toContain('bash /data/coolify/source/upgrade-postgres.sh'); })->with([ 'stable install' => 'scripts/install.sh', - 'nightly install' => 'other/nightly/install.sh', 'stable upgrade' => 'scripts/upgrade.sh', - 'nightly upgrade' => 'other/nightly/upgrade.sh', ]); it('uses the selected registry url when extracting upgrade images', function (string $path) { @@ -50,7 +47,6 @@ it('uses the selected registry url when extracting upgrade images', function (st expect($script)->toContain('IMAGES=$(REGISTRY_URL=${REGISTRY_URL} LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file "$ENV_FILE" $COMPOSE_FILES config --images'); })->with([ 'stable upgrade' => 'scripts/upgrade.sh', - 'nightly upgrade' => 'other/nightly/upgrade.sh', ]); it('persists the selected registry url during upgrades', function (string $path) { @@ -59,7 +55,6 @@ it('persists the selected registry url during upgrades', function (string $path) expect($script)->toContain('set_env_var "REGISTRY_URL" "$REGISTRY_URL"'); })->with([ 'stable upgrade' => 'scripts/upgrade.sh', - 'nightly upgrade' => 'other/nightly/upgrade.sh', ]); it('persists the target image and runtime version before recreating containers', function (string $path) { @@ -88,7 +83,6 @@ it('persists the target image and runtime version before recreating containers', ->and($coolifyVersionPosition)->toBeLessThan($composeUpPosition); })->with([ 'stable upgrade' => 'scripts/upgrade.sh', - 'nightly upgrade' => 'other/nightly/upgrade.sh', ]); it('uses the existing env registry url when old callers do not pass a registry argument', function (string $path) { @@ -102,7 +96,6 @@ it('uses the existing env registry url when old callers do not pass a registry a ->toContain('REGISTRY_URL="docker.io"'); })->with([ 'stable upgrade' => 'scripts/upgrade.sh', - 'nightly upgrade' => 'other/nightly/upgrade.sh', ]); it('keeps postgres upgrade compose override in future upgrade compose commands', function (string $path) { @@ -114,7 +107,6 @@ it('keeps postgres upgrade compose override in future upgrade compose commands', ->toContain('Using PostgreSQL upgrade compose override'); })->with([ 'stable upgrade' => 'scripts/upgrade.sh', - 'nightly upgrade' => 'other/nightly/upgrade.sh', ]); it('uses postgres 18 compatible mount path in generated override and restore container', function () { diff --git a/tests/Unit/UpgradeScriptContainerStartTest.php b/tests/Unit/UpgradeScriptContainerStartTest.php index efc16ad510..e543c53bef 100644 --- a/tests/Unit/UpgradeScriptContainerStartTest.php +++ b/tests/Unit/UpgradeScriptContainerStartTest.php @@ -46,18 +46,18 @@ it('reports an error when the new Coolify containers do not start', function (st expect($result['exitCode'])->toBe(1) ->and($result['status'])->toStartWith('error|') ->toContain('upgrade log'); -})->with(['scripts/upgrade.sh', 'other/nightly/upgrade.sh']); +})->with(['scripts/upgrade.sh']); it('completes the upgrade when the coolify container runs but is not healthy within the wait timeout', function (string $script) { $result = runUpgradeScriptContainerStart($script, composeExitCode: 1, coolifyRunning: true); expect($result['exitCode'])->toBe(0) ->and($result['status'])->toBe(''); -})->with(['scripts/upgrade.sh', 'other/nightly/upgrade.sh']); +})->with(['scripts/upgrade.sh']); it('completes the upgrade when docker compose up succeeds', function (string $script) { $result = runUpgradeScriptContainerStart($script, composeExitCode: 0, coolifyRunning: true); expect($result['exitCode'])->toBe(0) ->and($result['status'])->toBe(''); -})->with(['scripts/upgrade.sh', 'other/nightly/upgrade.sh']); +})->with(['scripts/upgrade.sh']); diff --git a/tests/Unit/UpgradeScriptCoolifyNetworkTest.php b/tests/Unit/UpgradeScriptCoolifyNetworkTest.php index ab3d497730..7a4b89fe3c 100644 --- a/tests/Unit/UpgradeScriptCoolifyNetworkTest.php +++ b/tests/Unit/UpgradeScriptCoolifyNetworkTest.php @@ -64,19 +64,19 @@ it('stops the upgrade before any container changes when the coolify network has ->and($result['status'])->toStartWith('error|') ->toContain('fd00:1::1/64') ->toContain('systemctl restart docker'); -})->with(['scripts/upgrade.sh', 'other/nightly/upgrade.sh']); +})->with(['scripts/upgrade.sh']); it('keeps a readable coolify network', function (string $script) { $result = runUpgradeScriptEnsureCoolifyNetwork($script, existingGateways: '172.19.0.1 fd00:1::1 ', createdIpv6Gateway: 'unused'); expect($result)->toBe(['exitCode' => 0, 'calls' => [], 'status' => '']); -})->with(['scripts/upgrade.sh', 'other/nightly/upgrade.sh']); +})->with(['scripts/upgrade.sh']); it('creates the coolify network with IPv6', function (string $script) { $result = runUpgradeScriptEnsureCoolifyNetwork($script, existingGateways: null, createdIpv6Gateway: 'fd00:1::1'); expect($result)->toBe(['exitCode' => 0, 'calls' => ['network create --attachable --ipv6 coolify'], 'status' => '']); -})->with(['scripts/upgrade.sh', 'other/nightly/upgrade.sh']); +})->with(['scripts/upgrade.sh']); it('recreates a new coolify network without IPv6 when Docker reports its gateway with a prefix length', function (string $script) { $result = runUpgradeScriptEnsureCoolifyNetwork($script, existingGateways: null, createdIpv6Gateway: 'fd00:1::1/64'); @@ -90,4 +90,4 @@ it('recreates a new coolify network without IPv6 when Docker reports its gateway ], 'status' => '', ]); -})->with(['scripts/upgrade.sh', 'other/nightly/upgrade.sh']); +})->with(['scripts/upgrade.sh']); diff --git a/tests/Unit/UpgradeScriptPullRetryTest.php b/tests/Unit/UpgradeScriptPullRetryTest.php index d30514a965..34e9a07d3a 100644 --- a/tests/Unit/UpgradeScriptPullRetryTest.php +++ b/tests/Unit/UpgradeScriptPullRetryTest.php @@ -35,8 +35,8 @@ function runUpgradeScriptPullImage(string $script, int $failures): array it('retries a rate limited image pull until it succeeds', function (string $script) { expect(runUpgradeScriptPullImage($script, failures: 2))->toBe(['exitCode' => 0, 'pulls' => 3]); -})->with(['scripts/upgrade.sh', 'other/nightly/upgrade.sh']); +})->with(['scripts/upgrade.sh']); it('fails the image pull after five attempts', function (string $script) { expect(runUpgradeScriptPullImage($script, failures: 10))->toBe(['exitCode' => 1, 'pulls' => 5]); -})->with(['scripts/upgrade.sh', 'other/nightly/upgrade.sh']); +})->with(['scripts/upgrade.sh']); diff --git a/tests/Unit/UpgradeScriptRootUserEnvTest.php b/tests/Unit/UpgradeScriptRootUserEnvTest.php index 6829e17155..ac5962544f 100644 --- a/tests/Unit/UpgradeScriptRootUserEnvTest.php +++ b/tests/Unit/UpgradeScriptRootUserEnvTest.php @@ -33,7 +33,7 @@ function repairRootUserEnvWithUpgradeScript(string $scriptPath, string $initialE return $contents; } -$upgradeScripts = ['stable upgrade' => 'scripts/upgrade.sh', 'nightly upgrade' => 'other/nightly/upgrade.sh']; +$upgradeScripts = ['stable upgrade' => 'scripts/upgrade.sh']; it('quotes unquoted root user credentials with spaces so that .env parses again', function (string $scriptPath) { $env = repairRootUserEnvWithUpgradeScript($scriptPath, implode("\n", [