fix(docker): update helper CLI for registry pushes (#11461)
This commit is contained in:
parent
59cd5b0d72
commit
60eff6fb6e
8 changed files with 368 additions and 8 deletions
|
|
@ -228,6 +228,35 @@ # Development Guides
|
||||||
## Local Development
|
## Local Development
|
||||||
To build and run Coolify locally, see: [Development](./DEVELOPMENT.md)
|
To build and run Coolify locally, see: [Development](./DEVELOPMENT.md)
|
||||||
|
|
||||||
|
### Testing the Coolify Helper Locally
|
||||||
|
|
||||||
|
Use `scripts/dev-helper` to build a local helper image and test it with the running development instance. The script requires the standard local Coolify container and the seeded Dockerfile, Docker Compose, and Nixpacks applications.
|
||||||
|
|
||||||
|
Run the complete workflow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/dev-helper test my-helper-test
|
||||||
|
```
|
||||||
|
|
||||||
|
This builds and selects the helper image, verifies its bundled tools and Docker socket access, runs a Docker Compose smoke test, and deploys all three seeded applications.
|
||||||
|
|
||||||
|
You can also run each step separately:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/dev-helper build my-helper-test
|
||||||
|
./scripts/dev-helper use my-helper-test
|
||||||
|
./scripts/dev-helper verify my-helper-test
|
||||||
|
./scripts/dev-helper deploy my-helper-test
|
||||||
|
```
|
||||||
|
|
||||||
|
Clear the helper override when finished:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/dev-helper reset
|
||||||
|
```
|
||||||
|
|
||||||
|
The default image repository is `docker.io/coollabsio/coolify-helper`. Set `HELPER_IMAGE_REPOSITORY` to test another repository, or `COOLIFY_CONTAINER` if the local Coolify container has a different name.
|
||||||
|
|
||||||
### macOS Development with Lima
|
### macOS Development with Lima
|
||||||
Mac users can use [Lima](https://lima-vm.io/) to run a lightweight Linux virtual machine for local Coolify development. This is useful if you prefer a Linux-based Docker environment on macOS.
|
Mac users can use [Lima](https://lima-vm.io/) to run a lightweight Linux virtual machine for local Coolify development. This is useful if you prefer a Linux-based Docker environment on macOS.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ public function buildHelperImage()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$imageRef = escapeshellarg("ghcr.io/coollabsio/coolify-helper:{$version}");
|
$imageRef = escapeshellarg(coolifyHelperImage().":{$version}");
|
||||||
$buildCommand = "docker build -t {$imageRef} -f docker/coolify-helper/Dockerfile .";
|
$buildCommand = "docker build -t {$imageRef} -f docker/coolify-helper/Dockerfile .";
|
||||||
|
|
||||||
$activity = remote_process(
|
$activity = remote_process(
|
||||||
|
|
|
||||||
|
|
@ -4152,11 +4152,12 @@ function coolifyHelperImage(): string
|
||||||
|
|
||||||
function getHelperVersion(): string
|
function getHelperVersion(): string
|
||||||
{
|
{
|
||||||
$settings = instanceSettings();
|
if (isDev()) {
|
||||||
|
$devHelperVersion = InstanceSettings::query()->whereKey(0)->value('dev_helper_version');
|
||||||
|
|
||||||
// In development mode, use the dev_helper_version if set, otherwise fallback to config
|
if (! empty($devHelperVersion)) {
|
||||||
if (isDev() && ! empty($settings->dev_helper_version)) {
|
return $devHelperVersion;
|
||||||
return $settings->dev_helper_version;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config('constants.coolify.helper_version');
|
return config('constants.coolify.helper_version');
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
# https://hub.docker.com/_/alpine
|
# https://hub.docker.com/_/alpine
|
||||||
ARG BASE_IMAGE=alpine:3.21
|
ARG BASE_IMAGE=alpine:3.21
|
||||||
# https://download.docker.com/linux/static/stable/
|
# https://download.docker.com/linux/static/stable/
|
||||||
ARG DOCKER_VERSION=28.0.0
|
ARG DOCKER_VERSION=29.7.2
|
||||||
# https://github.com/docker/compose/releases
|
# https://github.com/docker/compose/releases
|
||||||
ARG DOCKER_COMPOSE_VERSION=2.38.2
|
ARG DOCKER_COMPOSE_VERSION=5.5.0
|
||||||
# https://github.com/docker/buildx/releases
|
# https://github.com/docker/buildx/releases
|
||||||
ARG DOCKER_BUILDX_VERSION=0.25.0
|
ARG DOCKER_BUILDX_VERSION=0.36.1
|
||||||
# https://github.com/buildpacks/pack/releases
|
# https://github.com/buildpacks/pack/releases
|
||||||
ARG PACK_VERSION=0.38.2
|
ARG PACK_VERSION=0.38.2
|
||||||
# https://github.com/railwayapp/nixpacks/releases
|
# https://github.com/railwayapp/nixpacks/releases
|
||||||
|
|
|
||||||
280
scripts/dev-helper
Executable file
280
scripts/dev-helper
Executable file
|
|
@ -0,0 +1,280 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
REPOSITORY=${HELPER_IMAGE_REPOSITORY:-docker.io/coollabsio/coolify-helper}
|
||||||
|
DEFAULT_TAG="dev-$(git -C "$ROOT_DIR" rev-parse --short HEAD)"
|
||||||
|
COOLIFY_CONTAINER=${COOLIFY_CONTAINER:-coolify}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $0 <command> [tag]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
build [tag] Build the local helper image
|
||||||
|
use [tag] Configure local Coolify to use an existing helper image
|
||||||
|
verify [tag] Verify tools and run a Docker Compose socket smoke test
|
||||||
|
deploy [tag] Deploy seeded Dockerfile, Compose, and Nixpacks applications
|
||||||
|
reset Clear the local helper version override
|
||||||
|
test [tag] Build, select, verify, and run real seeded deployments
|
||||||
|
|
||||||
|
Default tag: $DEFAULT_TAG
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
require_local_coolify() {
|
||||||
|
local is_dev
|
||||||
|
|
||||||
|
if ! docker inspect "$COOLIFY_CONTAINER" >/dev/null 2>&1; then
|
||||||
|
echo "Coolify container '$COOLIFY_CONTAINER' is not running." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
is_dev=$(docker exec "$COOLIFY_CONTAINER" php artisan tinker --execute 'echo isDev() ? "yes" : "no";' 2>/dev/null | tail -1)
|
||||||
|
if [[ $is_dev != yes ]]; then
|
||||||
|
echo "The running Coolify instance is not in development mode." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_tag() {
|
||||||
|
local tag=$1
|
||||||
|
|
||||||
|
if [[ ! $tag =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]]; then
|
||||||
|
echo "Invalid Docker tag: $tag" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
image_for() {
|
||||||
|
echo "${REPOSITORY}:$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_helper() {
|
||||||
|
local tag=$1
|
||||||
|
local image
|
||||||
|
image=$(image_for "$tag")
|
||||||
|
|
||||||
|
echo "Building $image"
|
||||||
|
docker build --progress=plain -f "$ROOT_DIR/docker/coolify-helper/Dockerfile" -t "$image" "$ROOT_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
use_helper() {
|
||||||
|
local tag=$1
|
||||||
|
local image
|
||||||
|
image=$(image_for "$tag")
|
||||||
|
|
||||||
|
require_local_coolify
|
||||||
|
docker image inspect "$image" >/dev/null
|
||||||
|
docker exec -e DEV_HELPER_VERSION="$tag" "$COOLIFY_CONTAINER" php artisan tinker --execute '
|
||||||
|
App\Models\InstanceSettings::findOrFail(0)->update([
|
||||||
|
"dev_helper_version" => getenv("DEV_HELPER_VERSION"),
|
||||||
|
]);
|
||||||
|
' >/dev/null
|
||||||
|
|
||||||
|
local selected
|
||||||
|
selected=$(docker exec "$COOLIFY_CONTAINER" php artisan tinker --execute 'echo getHelperVersion();' 2>/dev/null | tail -1)
|
||||||
|
if [[ $selected != "$tag" ]]; then
|
||||||
|
echo "Coolify selected helper '$selected' instead of '$tag'." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Coolify now uses $image"
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_helper() {
|
||||||
|
local tag=$1
|
||||||
|
local image project compose
|
||||||
|
image=$(image_for "$tag")
|
||||||
|
project="coolify-helper-${tag//[^A-Za-z0-9_-]/-}-smoke"
|
||||||
|
compose=$'services:\n app:\n image: alpine:3.21\n command: ["sh", "-c", "sleep 30"]\n'
|
||||||
|
|
||||||
|
docker image inspect "$image" >/dev/null
|
||||||
|
|
||||||
|
docker run --rm "$image" docker --version
|
||||||
|
docker run --rm "$image" docker compose version
|
||||||
|
docker run --rm "$image" docker buildx version
|
||||||
|
docker run --rm "$image" pack version
|
||||||
|
docker run --rm "$image" nixpacks --version
|
||||||
|
docker run --rm "$image" railpack --version
|
||||||
|
|
||||||
|
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock "$image" \
|
||||||
|
docker version --format 'client={{.Client.Version}} server={{.Server.Version}} api={{.Client.APIVersion}}/{{.Server.APIVersion}}'
|
||||||
|
|
||||||
|
cleanup_compose() {
|
||||||
|
printf '%s' "$compose" | docker run --rm -i \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock "$image" \
|
||||||
|
docker compose -p "$project" -f - down --remove-orphans >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
trap cleanup_compose RETURN
|
||||||
|
|
||||||
|
printf '%s' "$compose" | docker run --rm -i \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock "$image" \
|
||||||
|
docker compose -p "$project" -f - up -d --wait
|
||||||
|
printf '%s' "$compose" | docker run --rm -i \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock "$image" \
|
||||||
|
docker compose -p "$project" -f - ps --format json
|
||||||
|
cleanup_compose
|
||||||
|
trap - RETURN
|
||||||
|
|
||||||
|
if [[ -n $(docker ps -aq --filter "label=com.docker.compose.project=$project") ]]; then
|
||||||
|
echo "Compose smoke-test resources were not removed." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Helper verification passed for $image"
|
||||||
|
}
|
||||||
|
|
||||||
|
queue_deployment() {
|
||||||
|
local application_uuid=$1
|
||||||
|
|
||||||
|
docker exec -e APPLICATION_UUID="$application_uuid" "$COOLIFY_CONTAINER" php artisan tinker --execute '
|
||||||
|
$application = App\Models\Application::query()
|
||||||
|
->where("uuid", getenv("APPLICATION_UUID"))
|
||||||
|
->firstOrFail();
|
||||||
|
$deploymentUuid = (string) Illuminate\Support\Str::uuid();
|
||||||
|
queue_application_deployment(
|
||||||
|
application: $application,
|
||||||
|
deployment_uuid: $deploymentUuid,
|
||||||
|
force_rebuild: true,
|
||||||
|
no_questions_asked: true,
|
||||||
|
);
|
||||||
|
echo "DEPLOYMENT_UUID={$deploymentUuid}";
|
||||||
|
' 2>/dev/null | sed -n 's/^DEPLOYMENT_UUID=//p' | tail -1
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_deployment() {
|
||||||
|
local deployment_uuid=$1
|
||||||
|
local expected_image=$2
|
||||||
|
local status
|
||||||
|
|
||||||
|
for _ in $(seq 1 150); do
|
||||||
|
status=$(docker exec -e DEPLOYMENT_UUID="$deployment_uuid" "$COOLIFY_CONTAINER" php artisan tinker --execute '
|
||||||
|
$deployment = App\Models\ApplicationDeploymentQueue::query()
|
||||||
|
->where("deployment_uuid", getenv("DEPLOYMENT_UUID"))
|
||||||
|
->first();
|
||||||
|
echo $deployment?->status ?? "missing";
|
||||||
|
' 2>/dev/null | tail -1)
|
||||||
|
|
||||||
|
case "$status" in
|
||||||
|
finished)
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
failed|cancelled|missing)
|
||||||
|
echo "Deployment $deployment_uuid ended with status: $status" >&2
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
sleep 4
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $status != finished ]]; then
|
||||||
|
echo "Deployment $deployment_uuid timed out with status: $status" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local used_expected_helper
|
||||||
|
used_expected_helper=$(docker exec \
|
||||||
|
-e DEPLOYMENT_UUID="$deployment_uuid" \
|
||||||
|
-e EXPECTED_HELPER_IMAGE="$expected_image" \
|
||||||
|
"$COOLIFY_CONTAINER" php artisan tinker --execute '
|
||||||
|
$deployment = App\Models\ApplicationDeploymentQueue::query()
|
||||||
|
->where("deployment_uuid", getenv("DEPLOYMENT_UUID"))
|
||||||
|
->firstOrFail();
|
||||||
|
$logs = collect(json_decode($deployment->logs, true))
|
||||||
|
->pluck("output")
|
||||||
|
->implode("\n");
|
||||||
|
echo str_contains(
|
||||||
|
$logs,
|
||||||
|
"Preparing container with helper image: ".getenv("EXPECTED_HELPER_IMAGE"),
|
||||||
|
) ? "yes" : "no";
|
||||||
|
' 2>/dev/null | tail -1)
|
||||||
|
|
||||||
|
if [[ $used_expected_helper != yes ]]; then
|
||||||
|
echo "Deployment $deployment_uuid did not use $expected_image." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy_examples() {
|
||||||
|
local tag=$1
|
||||||
|
local image application_uuid deployment_uuid fqdn
|
||||||
|
image=$(image_for "$tag")
|
||||||
|
|
||||||
|
require_local_coolify
|
||||||
|
use_helper "$tag"
|
||||||
|
|
||||||
|
for application_uuid in dockerfile docker-compose nodejs; do
|
||||||
|
echo "Deploying seeded application: $application_uuid"
|
||||||
|
deployment_uuid=$(queue_deployment "$application_uuid")
|
||||||
|
if [[ -z $deployment_uuid ]]; then
|
||||||
|
echo "Could not queue $application_uuid." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
wait_for_deployment "$deployment_uuid" "$image"
|
||||||
|
|
||||||
|
fqdn=$(docker exec -e APPLICATION_UUID="$application_uuid" "$COOLIFY_CONTAINER" php artisan tinker --execute '
|
||||||
|
echo App\Models\Application::query()
|
||||||
|
->where("uuid", getenv("APPLICATION_UUID"))
|
||||||
|
->value("fqdn") ?? "";
|
||||||
|
' 2>/dev/null | tail -1)
|
||||||
|
|
||||||
|
if [[ -n $fqdn ]]; then
|
||||||
|
curl --fail --silent --show-error --output /dev/null "$fqdn"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deployment passed: $application_uuid ($deployment_uuid)"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_helper() {
|
||||||
|
require_local_coolify
|
||||||
|
docker exec "$COOLIFY_CONTAINER" php artisan tinker --execute '
|
||||||
|
App\Models\InstanceSettings::findOrFail(0)->update([
|
||||||
|
"dev_helper_version" => null,
|
||||||
|
]);
|
||||||
|
' >/dev/null
|
||||||
|
echo "Development helper override cleared."
|
||||||
|
}
|
||||||
|
|
||||||
|
command=${1:-help}
|
||||||
|
tag=${2:-$DEFAULT_TAG}
|
||||||
|
|
||||||
|
case "$command" in
|
||||||
|
build)
|
||||||
|
validate_tag "$tag"
|
||||||
|
build_helper "$tag"
|
||||||
|
;;
|
||||||
|
use)
|
||||||
|
validate_tag "$tag"
|
||||||
|
use_helper "$tag"
|
||||||
|
;;
|
||||||
|
verify)
|
||||||
|
validate_tag "$tag"
|
||||||
|
verify_helper "$tag"
|
||||||
|
;;
|
||||||
|
deploy)
|
||||||
|
validate_tag "$tag"
|
||||||
|
deploy_examples "$tag"
|
||||||
|
;;
|
||||||
|
reset)
|
||||||
|
reset_helper
|
||||||
|
;;
|
||||||
|
test)
|
||||||
|
validate_tag "$tag"
|
||||||
|
build_helper "$tag"
|
||||||
|
use_helper "$tag"
|
||||||
|
verify_helper "$tag"
|
||||||
|
deploy_examples "$tag"
|
||||||
|
;;
|
||||||
|
help|-h|--help)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
@ -88,3 +88,22 @@
|
||||||
->call('buildHelperImage')
|
->call('buildHelperImage')
|
||||||
->assertDispatched('error');
|
->assertDispatched('error');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('development helper version is read fresh for queue workers', function () {
|
||||||
|
config(['app.env' => 'local']);
|
||||||
|
|
||||||
|
InstanceSettings::findOrFail(0)->update(['dev_helper_version' => 'first']);
|
||||||
|
expect(getHelperVersion())->toBe('first');
|
||||||
|
|
||||||
|
InstanceSettings::query()->whereKey(0)->update(['dev_helper_version' => 'second']);
|
||||||
|
|
||||||
|
expect(getHelperVersion())->toBe('second');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('development helper build uses the configured helper repository', function () {
|
||||||
|
$component = file_get_contents(app_path('Livewire/Settings/Index.php'));
|
||||||
|
|
||||||
|
expect($component)
|
||||||
|
->toContain('$imageRef = escapeshellarg(coolifyHelperImage().":{$version}");')
|
||||||
|
->not->toContain('"ghcr.io/coollabsio/coolify-helper:{$version}"');
|
||||||
|
});
|
||||||
|
|
|
||||||
19
tests/Unit/DevHelperScriptTest.php
Normal file
19
tests/Unit/DevHelperScriptTest.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
it('provides a local helper build and deployment workflow', function () {
|
||||||
|
$script = dirname(__DIR__, 2).'/scripts/dev-helper';
|
||||||
|
|
||||||
|
expect($script)->toBeFile()
|
||||||
|
->and(is_executable($script))->toBeTrue();
|
||||||
|
|
||||||
|
exec('bash -n '.escapeshellarg($script), $output, $exitCode);
|
||||||
|
|
||||||
|
expect($exitCode)->toBe(0)
|
||||||
|
->and(file_get_contents($script))
|
||||||
|
->toContain('build)')
|
||||||
|
->toContain('use)')
|
||||||
|
->toContain('verify)')
|
||||||
|
->toContain('deploy)')
|
||||||
|
->toContain('reset)')
|
||||||
|
->toContain('test)');
|
||||||
|
});
|
||||||
12
tests/Unit/DockerCliPackagingTest.php
Normal file
12
tests/Unit/DockerCliPackagingTest.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
it('installs the current Docker CLI in the Coolify helper image', function () {
|
||||||
|
$dockerfile = file_get_contents(dirname(__DIR__, 2).'/docker/coolify-helper/Dockerfile');
|
||||||
|
expect($dockerfile)
|
||||||
|
->toContain('ARG DOCKER_VERSION=29.7.2')
|
||||||
|
->toContain('ARG DOCKER_COMPOSE_VERSION=5.5.0')
|
||||||
|
->toContain('ARG DOCKER_BUILDX_VERSION=0.36.1')
|
||||||
|
->toContain('https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz')
|
||||||
|
->toContain('https://download.docker.com/linux/static/stable/aarch64/docker-${DOCKER_VERSION}.tgz')
|
||||||
|
->toMatch('/chmod \+x [^\n]*\/usr\/bin\/docker/');
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue