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 <preview>.<YYYYMMDD>.<N>
- 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
This commit is contained in:
peaklabs-dev committed 2026-11-01 19:37:39 +01:00
1 parent 2615559b5e
commit e0fb2b3897
29 files changed
+232 -2355

No files matched your search

+18 -2
View File
@@ -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:
+127
View File
@@ -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"
+23 -2
View File
@@ -118,9 +118,29 @@ Every release is tagged `v<version>`, 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 `<base>.<YYYYMMDD>.<N>`:
| 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-<number>`, versioned `<version of the base branch>-pr.<number>`. 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-<number>`, versioned `<version of the base branch>-pr.<number>`. 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
+5
View File
@@ -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
-17
View File
@@ -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
-89
View File
@@ -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
-110
View File
@@ -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
-32
View File
@@ -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
File diff suppressed because it is too large. Load diff
-405
View File
@@ -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 <<EOF
Usage:
$0 <target-major>
$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" <<YAML
services:
postgres:
image: "${image}"
volumes:
- coolify-db:${mount_path}
volumes:
coolify-db:
name: "${volume}"
external: true
YAML
}
write_rollback_file() {
local previous_image="$1"
local previous_volume="$2"
local previous_mount_path="$3"
local previous_override_present="$4"
local upgraded_image="$5"
local upgraded_volume="$6"
cat > "$ROLLBACK_FILE" <<EOF
PREVIOUS_IMAGE='${previous_image}'
PREVIOUS_VOLUME='${previous_volume}'
PREVIOUS_MOUNT_PATH='${previous_mount_path}'
PREVIOUS_OVERRIDE_PRESENT='${previous_override_present}'
UPGRADED_IMAGE='${upgraded_image}'
UPGRADED_VOLUME='${upgraded_volume}'
CREATED_AT='${DATE}'
EOF
chmod 600 "$ROLLBACK_FILE"
}
start_stack() {
local coolify_image_tag="${1:-${LATEST_IMAGE:-latest}}"
local files
files=$(compose_files)
# shellcheck disable=SC2086
LATEST_IMAGE="$coolify_image_tag" docker compose --env-file "$ENV_FILE" $files up -d --remove-orphans --wait --wait-timeout 120
}
print_rollback_instructions() {
cat <<EOF | tee -a "$LOGFILE"
Rollback command if the upgraded database does not work:
${SOURCE_DIR}/upgrade-postgres.sh rollback
Rollback metadata was saved to:
${ROLLBACK_FILE}
The previous active Docker volume was '${PREVIOUS_VOLUME}'.
The new Docker volume is '${TARGET_VOLUME}'.
The dump file is '${DUMP_FILE}'.
EOF
}
validate_common_requirements() {
mkdir -p "$BACKUP_DIR"
touch "$LOGFILE"
chmod 700 "$BACKUP_DIR"
[ -f "$ENV_FILE" ] || fail "Missing ${ENV_FILE}. Run this on a self-hosted Coolify server."
command -v docker >/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 <<EOF | tee -a "$LOGFILE"
Rollback completed.
Current active PostgreSQL volume should be '${PREVIOUS_VOLUME}'.
The upgraded volume '${UPGRADED_VOLUME:-unknown}' was left untouched for inspection or manual cleanup.
EOF
}
upgrade_postgres() {
validate_target_major
TARGET_MOUNT_PATH=$(mount_path_for_major "$TARGET_MAJOR")
validate_common_requirements
log "Starting Coolify internal PostgreSQL major upgrade."
log "Target major: ${TARGET_MAJOR}"
log "Target image: ${TARGET_IMAGE}"
log "Target volume: ${TARGET_VOLUME}"
log "Target mount path: ${TARGET_MOUNT_PATH}"
DB_USERNAME=$(get_env_var DB_USERNAME coolify)
DB_DATABASE=$(get_env_var DB_DATABASE coolify)
if ! docker ps -a --format '{{.Names}}' | grep -qx 'coolify-db'; then
fail "Container 'coolify-db' was not found. Start Coolify before running this script."
fi
if ! docker ps --format '{{.Names}}' | grep -qx 'coolify-db'; then
log "Starting existing coolify-db container for version detection and dump."
docker start coolify-db >>"$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
-427
View File
@@ -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}"
-31
View File
@@ -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"
}
}
+4 -4
View File
@@ -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"
+43 -3
View File
@@ -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() {
@@ -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 () {
@@ -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',
]);
@@ -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'],
]);
@@ -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'],
]);
@@ -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'];
@@ -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);
@@ -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');
});
+1 -1
View File
@@ -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'],
@@ -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'],
]);
@@ -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',
]);
-8
View File
@@ -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 () {
@@ -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']);
@@ -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']);
+2 -2
View File
@@ -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']);
+1 -1
View File
@@ -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", [