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
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
$imageRef = escapeshellarg("ghcr.io/coollabsio/coolify-helper:{$version}");
|
||||
$imageRef = escapeshellarg(coolifyHelperImage().":{$version}");
|
||||
$buildCommand = "docker build -t {$imageRef} -f docker/coolify-helper/Dockerfile .";
|
||||
|
||||
$activity = remote_process(
|
||||
|
|
|
|||
|
|
@ -4152,11 +4152,12 @@ function coolifyHelperImage(): 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 (isDev() && ! empty($settings->dev_helper_version)) {
|
||||
return $settings->dev_helper_version;
|
||||
if (! empty($devHelperVersion)) {
|
||||
return $devHelperVersion;
|
||||
}
|
||||
}
|
||||
|
||||
return config('constants.coolify.helper_version');
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
# https://hub.docker.com/_/alpine
|
||||
ARG BASE_IMAGE=alpine:3.21
|
||||
# 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
|
||||
ARG DOCKER_COMPOSE_VERSION=2.38.2
|
||||
ARG DOCKER_COMPOSE_VERSION=5.5.0
|
||||
# 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
|
||||
ARG PACK_VERSION=0.38.2
|
||||
# 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')
|
||||
->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