feat(dev): add coold VM dev workflow

Add scripts and Lima config for managing local coold endpoint VMs,
mint Flux dev host JWTs, expose coold host details on the V5 home
page, and document the local development workflow.
This commit is contained in:
Andras Bacsai
2026-06-16 11:39:36 +02:00
parent ed89d83d68
commit 1b1b0726a2
15 changed files with 1661 additions and 5 deletions
+3
View File
@@ -40,3 +40,6 @@ CHANGELOG.md
/.workspaces
tests/Browser/Screenshots
tests/v4/Browser/Screenshots
# Local generated Lima configs
.dev/lima/*.generated.yaml
+19
View File
@@ -30,6 +30,25 @@ You can find the installation script source [here](./scripts/install.sh).
> Please refer to the [docs](https://coolify.io/docs/installation) for more information about the installation.
## Local dev with coold VM
For v5 development that needs a packaged `coold` endpoint VM with Corrosion, use:
```bash
scripts/dev.sh up
```
This starts two Lima endpoint VMs by default, configures a WireGuard mesh between them, starts the Docker stack in the background, mints dev host JWTs with `php artisan flux:dev --caps=coold,builder`, installs them into the VMs, starts each VM `coold` agent service with builder capacity, and follows Coolify + VM agent logs by default. Flux runs with the Coolify container, not inside the VM. Set `COOLIFY_COOLD_VM_COUNT=1` for a single endpoint, `COOLIFY_COOLD_VM_ENABLED=false` to skip the VMs, `COOLIFY_COOLD_VM_BUILDER_CAPACITY=0` to disable builder capability, or `COOLIFY_DEV_FOLLOW_LOGS=false` to leave `up` detached. Use `scripts/dev.sh down` to stop the stack and VM agent; set `COOLIFY_COOLD_VM_STOP_ON_DOWN=true` if you also want the VM stopped. The dev entrypoints are intentionally kept to `scripts/dev.sh` for the full stack and `scripts/coold-vm.sh` for VM-only operations.
Useful Corrosion checks:
```bash
scripts/dev.sh corrosion check
scripts/dev.sh corrosion containers
scripts/dev.sh corrosion config
scripts/dev.sh corrosion sql 'select count(*) from service_endpoints;'
```
## Container roles and Flux
The Coolify image can run different process roles with `COOLIFY_CONTAINER_ROLE`. The value can be a single role or a comma-separated list. If `all` is present anywhere in the list, every role starts.
+75
View File
@@ -0,0 +1,75 @@
<?php
namespace App\Console\Commands;
use Firebase\JWT\JWT;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class FluxDev extends Command
{
protected $signature = 'flux:dev
{host_id=coold-dev : Stable coold host id}
{--caps=coold : Comma-separated host capabilities}
{--ttl=3600 : Token lifetime in seconds}
{--output= : Optional path to write the token with 0600 permissions}
{--force : Allow running outside local/development environments}';
protected $description = 'Run Flux development helpers.';
public function handle(): int
{
if (! app()->environment(['local', 'development', 'testing']) && ! $this->option('force')) {
$this->error('This command is intended for development only. Use --force to override.');
return self::FAILURE;
}
$privateKeyPath = config('flux.jwt_private_key_path');
if (! is_string($privateKeyPath) || $privateKeyPath === '' || ! File::isReadable($privateKeyPath)) {
$this->error("Flux JWT private key not found at {$privateKeyPath}.");
return self::FAILURE;
}
$hostId = (string) $this->argument('host_id');
$ttl = max(60, (int) $this->option('ttl'));
$now = time();
$caps = collect(explode(',', (string) $this->option('caps')))
->map(fn (string $cap) => trim($cap))
->filter()
->unique()
->values()
->all();
if ($caps === []) {
$caps = ['coold'];
}
$token = JWT::encode([
'sub' => $hostId,
'aud' => 'coold',
'caps' => $caps,
'iat' => $now,
'exp' => $now + $ttl,
], File::get($privateKeyPath), 'ES256');
$output = $this->option('output');
if (is_string($output) && $output !== '') {
$outputPath = Str::startsWith($output, '/') ? $output : base_path($output);
File::ensureDirectoryExists(dirname($outputPath));
File::put($outputPath, $token.PHP_EOL);
chmod($outputPath, 0600);
$this->info("Host JWT written to {$outputPath}.");
return self::SUCCESS;
}
$this->line($token);
return self::SUCCESS;
}
}
@@ -21,6 +21,7 @@ class HomeController extends Controller
return Inertia::render('Home', [
'status' => 'v5-ready',
'flux' => $fluxHealth->check(),
'cooldHosts' => $this->cooldHosts(),
'currentTeam' => $currentTeam instanceof Team ? [
'id' => $currentTeam->id,
'name' => $currentTeam->name,
@@ -41,4 +42,29 @@ class HomeController extends Controller
]),
]);
}
/**
* @return array<int, array{id: string, wireguardIp: string|null, capabilities: array<int, string>, builderEnabled: bool, builderCapacity: int}>
*/
private function cooldHosts(): array
{
$baseId = (string) config('coold.dev_host_id');
$count = max(0, (int) config('coold.dev_host_count'));
$builderCapacity = (int) config('coold.dev_builder_capacity');
$builderEnabled = $builderCapacity > 0;
if ($count === 0 || $baseId === '') {
return [];
}
return collect(range(1, $count))
->map(fn (int $index) => [
'id' => $index === 1 ? $baseId : (string) config("coold.dev_host_id_{$index}", "{$baseId}-{$index}"),
'wireguardIp' => (string) config("coold.dev_wireguard_ip_{$index}") ?: null,
'capabilities' => $builderEnabled ? ['coold', 'builder'] : ['coold'],
'builderEnabled' => $builderEnabled,
'builderCapacity' => $builderCapacity,
])
->all();
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
return [
'dev_host_count' => (int) env('COOLIFY_COOLD_VM_COUNT', 2),
'dev_host_id' => env('COOLIFY_COOLD_DEV_HOST_ID', 'coolify-coold-dev'),
'dev_host_id_2' => env('COOLIFY_COOLD_LIMA_INSTANCE_2', env('COOLIFY_COOLD_DEV_HOST_ID', 'coolify-coold-dev').'-2'),
'dev_wireguard_ip_1' => env('COOLIFY_COOLD_VM_WG_IP_1', '100.64.0.10'),
'dev_wireguard_ip_2' => env('COOLIFY_COOLD_VM_WG_IP_2', '100.64.0.11'),
'dev_builder_capacity' => (int) env('COOLIFY_COOLD_VM_BUILDER_CAPACITY', 2),
'dev_builder_enabled' => (int) env('COOLIFY_COOLD_VM_BUILDER_CAPACITY', 2) > 0,
];
+2
View File
@@ -2,5 +2,7 @@
return [
'unix_socket_path' => env('COOLIFY_FLUX_UNIX_SOCKET_PATH', '/run/coolify/flux.sock'),
'jwt_private_key_path' => env('COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH', storage_path('app/flux/jwt.priv')),
'jwt_public_key_path' => env('COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH', storage_path('app/flux/jwt.pub')),
'health_timeout_seconds' => (float) env('COOLIFY_FLUX_HEALTH_TIMEOUT_SECONDS', 1.0),
];
+125
View File
@@ -0,0 +1,125 @@
# Lima VM for testing Coolify v5 against a packaged coold endpoint.
# Start with: scripts/coold-vm.sh up
# Run endpoint: scripts/coold-vm.sh dev
#
# This file is copied into .dev/lima/coold.generated.yaml by the wrapper script,
# with {{COOLIFY_REPO}}, {{COOLIFY_COOLD_VERSION}}, and {{COOLIFY_CORROSION_VERSION}} replaced before Lima sees it.
vmType: "vz"
arch: "default"
cpus: 4
memory: "8GiB"
disk: "100GiB"
containerd:
system: false
user: false
images:
- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img"
arch: "x86_64"
- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img"
arch: "aarch64"
mountType: "virtiofs"
mounts:
- location: "{{COOLIFY_REPO}}"
mountPoint: "/workspace/coolify"
writable: true
portForwards:
- guestPort: 5173
hostPort: 5173
hostIP: "127.0.0.1"
- guestPort: 3000
hostPort: 3000
hostIP: "127.0.0.1"
provision:
- mode: system
script: |
#!/usr/bin/env bash
set -euxo pipefail
export DEBIAN_FRONTEND=noninteractive
echo "[coold-vm] Updating Ubuntu package index..."
apt-get update
echo "[coold-vm] Installing VM dependencies..."
apt-get install -y --no-install-recommends \
bash \
buildah \
ca-certificates \
clang \
curl \
git \
iproute2 \
iptables \
jq \
libssl-dev \
mold \
nftables \
openssl \
perl \
pkg-config \
podman \
protobuf-compiler \
sqlite3 \
sudo \
wireguard-tools
echo "[coold-vm] Enabling Podman socket..."
systemctl enable --now podman.socket
cat >/usr/local/bin/rtk <<'SH'
#!/usr/bin/env bash
exec "$@"
SH
chmod +x /usr/local/bin/rtk
mkdir -p /etc/coolify /var/lib/corrosion /run/corrosion /var/lib/coolify-dev /var/lib/coolify-builder/work
chmod 755 /etc/coolify
chmod 777 /var/lib/corrosion /run/corrosion
install_coolify_binary() {
name="$1"
version="{{COOLIFY_COOLD_VERSION}}"
arch_raw="$(uname -m)"
case "$arch_raw" in
x86_64) arch=amd64 ;;
aarch64) arch=arm64 ;;
*) echo "unsupported arch: $arch_raw" >&2; exit 1 ;;
esac
url="https://github.com/coollabsio/coold/releases/download/${version}/${name}-linux-${arch}.tar.gz"
echo "[coold-vm] Installing ${name} from ${url}..."
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' RETURN
curl -fsSL --retry 3 --max-time 120 -o "$tmpdir/${name}.tar.gz" "$url"
tar -xzf "$tmpdir/${name}.tar.gz" -C "$tmpdir"
test -f "$tmpdir/$name"
install -m 0755 "$tmpdir/$name" "/usr/local/bin/${name}.tmp"
mv "/usr/local/bin/${name}.tmp" "/usr/local/bin/${name}"
echo "$version" > "/usr/local/bin/${name}.version"
}
install_corrosion_binary() {
version="{{COOLIFY_CORROSION_VERSION}}"
arch_raw="$(uname -m)"
case "$arch_raw" in
x86_64) arch=x86_64-unknown-linux-gnu ;;
aarch64) arch=aarch64-unknown-linux-gnu ;;
*) echo "unsupported arch: $arch_raw" >&2; exit 1 ;;
esac
url="https://github.com/superfly/corrosion/releases/download/${version}/corrosion-${arch}.tar.gz"
echo "[coold-vm] Installing corrosion from ${url}..."
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' RETURN
curl -fsSL --retry 3 --max-time 180 -o "$tmpdir/corrosion.tar.gz" "$url"
tar -xzf "$tmpdir/corrosion.tar.gz" -C "$tmpdir"
test -f "$tmpdir/corrosion"
install -m 0755 "$tmpdir/corrosion" /usr/local/bin/corrosion.tmp
mv /usr/local/bin/corrosion.tmp /usr/local/bin/corrosion
echo "$version" > /usr/local/bin/corrosion.version
}
install_coolify_binary coold
install_coolify_binary builder
install_corrosion_binary
echo "[coold-vm] Endpoint binaries installed."
echo "[coold-vm] Provisioning complete."
+2 -2
View File
@@ -12,8 +12,8 @@ services:
- "${APP_PORT:-8000}:8080"
environment:
AUTORUN_ENABLED: false
PUSHER_HOST: "${PUSHER_HOST}"
PUSHER_PORT: "${PUSHER_PORT}"
PUSHER_HOST: "${PUSHER_HOST:-}"
PUSHER_PORT: "${PUSHER_PORT:-}"
PUSHER_SCHEME: "${PUSHER_SCHEME:-http}"
PUSHER_APP_ID: "${PUSHER_APP_ID:-coolify}"
PUSHER_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
+6 -2
View File
@@ -8,14 +8,18 @@ services:
args:
- USER_ID=${USERID:-1000}
- GROUP_ID=${GROUPID:-1000}
- COOLIFY_FLUX_VERSION=${COOLIFY_FLUX_VERSION:-nightly}
ports:
- "${APP_PORT:-8000}:8080"
- "${FORWARD_FLUX_PORT:-6443}:6443"
environment:
AUTORUN_ENABLED: false
COOLIFY_CONTAINER_ROLE: "${COOLIFY_CONTAINER_ROLE:-all}"
PUSHER_HOST: "${PUSHER_HOST}"
PUSHER_PORT: "${PUSHER_PORT}"
COOLIFY_COOLD_VERSION: "${COOLIFY_COOLD_VERSION:-nightly}"
COOLIFY_FLUX_VERSION: "${COOLIFY_FLUX_VERSION:-nightly}"
COOLIFY_CORROSION_VERSION: "${COOLIFY_CORROSION_VERSION:-v1.0.0}"
PUSHER_HOST: "${PUSHER_HOST:-}"
PUSHER_PORT: "${PUSHER_PORT:-}"
PUSHER_SCHEME: "${PUSHER_SCHEME:-http}"
PUSHER_APP_ID: "${PUSHER_APP_ID:-coolify}"
PUSHER_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
+19 -1
View File
@@ -1,6 +1,6 @@
import { Head } from '@inertiajs/react';
export default function Home({ status, currentTeam, teams, flux }) {
export default function Home({ status, currentTeam, teams, flux, cooldHosts }) {
return (
<>
<Head title="V5" />
@@ -27,6 +27,24 @@ export default function Home({ status, currentTeam, teams, flux }) {
{flux.socket ? <p>Socket: {flux.socket}</p> : null}
</section>
<section aria-labelledby="coold-host-heading">
<h2 id="coold-host-heading">coold host</h2>
<ul>
{cooldHosts.map((host) => (
<li key={host.id}>
<strong>{host.id}</strong>
{host.wireguardIp ? ` (${host.wireguardIp})` : ''}:
{' '}
{host.capabilities.join(', ')}; builder{' '}
{host.builderEnabled
? `enabled, capacity ${host.builderCapacity}`
: 'disabled'}
</li>
))}
</ul>
</section>
<h2>Current team</h2>
{currentTeam ? (
+591
View File
@@ -0,0 +1,591 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
read_coolify_env() {
key="$1"
default_value="$2"
current_value="${!key:-}"
if [ -n "$current_value" ]; then
printf '%s\n' "$current_value"
return
fi
if [ -f "$ROOT/.env" ]; then
env_value="$(grep -E "^${key}=" "$ROOT/.env" 2>/dev/null | tail -n1 | cut -d= -f2- | sed "s/^['\"]//; s/['\"]$//")"
if [ -n "$env_value" ]; then
printf '%s\n' "$env_value"
return
fi
fi
printf '%s\n' "$default_value"
}
INSTANCE="$(read_coolify_env COOLIFY_COOLD_LIMA_INSTANCE coold-dev)"
VERSION="$(read_coolify_env COOLIFY_COOLD_VERSION nightly)"
CORROSION_VERSION="$(read_coolify_env COOLIFY_CORROSION_VERSION v1.0.0)"
FLUX_URL="$(read_coolify_env COOLIFY_COOLD_VM_FLUX_URL http://host.lima.internal:6443)"
BUILDER_CAPACITY="$(read_coolify_env COOLIFY_COOLD_VM_BUILDER_CAPACITY 2)"
WG_IP="$(read_coolify_env COOLIFY_COOLD_VM_WG_IP "")"
WG_PEER_IP="$(read_coolify_env COOLIFY_COOLD_VM_WG_PEER_IP "")"
WG_PEER_ENDPOINT="$(read_coolify_env COOLIFY_COOLD_VM_WG_PEER_ENDPOINT "")"
WG_PEER_PUBLIC_KEY="$(read_coolify_env COOLIFY_COOLD_VM_WG_PEER_PUBLIC_KEY "")"
CONTAINER_SUBNET="$(read_coolify_env COOLIFY_COOLD_VM_CONTAINER_SUBNET 10.210.0.0/24)"
CONTAINER_GATEWAY="$(read_coolify_env COOLIFY_COOLD_VM_CONTAINER_GATEWAY 10.210.0.1)"
BUILDER_ENABLED="true"
if [ "$BUILDER_CAPACITY" = "0" ]; then
BUILDER_ENABLED="false"
fi
TEMPLATE="$ROOT/dev/lima/coold.yaml"
GENERATED="$ROOT/.dev/lima/coold.generated.yaml"
GUEST_COOLIFY_ROOT="/workspace/coolify"
usage() {
cat <<USAGE
Usage: scripts/coold-vm.sh <command>
Commands:
up Create/start the Lima VM and install packaged coold endpoint binaries
dev Start packaged coold + Corrosion inside the VM
start-agent
Start production-like coold.service + corrosion.service inside the VM
stop-agent
Stop the VM coold.service + corrosion.service
logs-agent
Follow the VM coold.service + corrosion.service logs
install-host-jwt [token]
Install a Flux host JWT into the VM at /etc/coolify/host-jwt
shell Open a shell inside the VM
status Show Lima instance status
stop Stop the VM
delete Delete the VM and all VM-local runtime state
Environment:
COOLIFY_COOLD_LIMA_INSTANCE Override Lima instance name (default: coold-dev)
COOLIFY_COOLD_VERSION coold release tag to install (default: nightly)
COOLIFY_CORROSION_VERSION corrosion release tag to install (default: v1.0.0)
COOLIFY_COOLD_VM_FLUX_URL Flux gRPC URL visible from the VM (default: http://host.lima.internal:6443)
COOLIFY_COOLD_VM_BUILDER_CAPACITY VM builder capacity to advertise (default: 2; set 0 to disable)
COOLIFY_COOLD_VM_WG_IP Optional WireGuard mgmt IP for this host
COOLIFY_COOLD_VM_CONTAINER_SUBNET Podman mesh subnet for this host
COOLIFY_COOLD_VM_CONTAINER_GATEWAY Podman mesh gateway for this host
Guest mounts:
$ROOT -> $GUEST_COOLIFY_ROOT
Installed from:
https://github.com/coollabsio/coold/releases/tag/$VERSION
https://github.com/superfly/corrosion/releases/tag/$CORROSION_VERSION
USAGE
}
require_lima() {
command -v limactl >/dev/null 2>&1 || {
echo "limactl is required. Install Lima first: brew install lima" >&2
exit 1
}
}
instance_exists() {
limactl list 2>/dev/null | awk 'NR > 1 {print $1}' | grep -qx "$INSTANCE"
}
instance_running() {
limactl list 2>/dev/null | awk -v name="$INSTANCE" 'NR > 1 && $1 == name {print $2}' | grep -qx Running
}
lima_shell() {
(cd /tmp && limactl shell "$INSTANCE" -- "$@")
}
vm_primary_ip() {
lima_shell sh -lc "ip -4 route get 1.1.1.1 | awk '{print \$7; exit}'"
}
wireguard_public_key() {
lima_shell sudo sh -lc 'install -d -m 0700 /etc/wireguard; if [ ! -s /etc/wireguard/privatekey ]; then wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey; chmod 600 /etc/wireguard/privatekey; fi; cat /etc/wireguard/publickey'
}
setup_wireguard() {
local ip="${1:-$WG_IP}"
local peer_ip="${2:-$WG_PEER_IP}"
local peer_endpoint="${3:-$WG_PEER_ENDPOINT}"
local peer_public_key="${4:-$WG_PEER_PUBLIC_KEY}"
local listen_port="${5:-51820}"
local peer_port="${6:-51820}"
local peer_subnet="${7:-}"
if [ -z "$ip" ]; then
echo "ERROR: WireGuard IP is required." >&2
exit 1
fi
wireguard_public_key >/dev/null
if [ -n "$peer_ip" ] && [ -n "$peer_endpoint" ] && [ -n "$peer_public_key" ]; then
lima_shell sudo sh -lc "cat >/etc/wireguard/wg0.conf.tmp <<WG
[Interface]
Address = ${ip}/32
ListenPort = ${listen_port}
PrivateKey = \$(cat /etc/wireguard/privatekey)
[Peer]
PublicKey = ${peer_public_key}
AllowedIPs = ${peer_ip}/32${peer_subnet:+, ${peer_subnet}}
Endpoint = ${peer_endpoint}:${peer_port}
PersistentKeepalive = 5
WG
chmod 600 /etc/wireguard/wg0.conf.tmp && mv /etc/wireguard/wg0.conf.tmp /etc/wireguard/wg0.conf && wg-quick down wg0 >/dev/null 2>&1 || true; wg-quick up wg0"
else
lima_shell sudo sh -lc "cat >/etc/wireguard/wg0.conf.tmp <<WG
[Interface]
Address = ${ip}/32
ListenPort = ${listen_port}
PrivateKey = \$(cat /etc/wireguard/privatekey)
WG
chmod 600 /etc/wireguard/wg0.conf.tmp && mv /etc/wireguard/wg0.conf.tmp /etc/wireguard/wg0.conf && wg-quick down wg0 >/dev/null 2>&1 || true; wg-quick up wg0"
fi
}
install_host_jwt() {
token="${1:-}"
if [ -z "$token" ]; then
token="$(cat)"
fi
if [ -z "$token" ]; then
echo "ERROR: host JWT is empty." >&2
exit 1
fi
printf '%s\n' "$token" | lima_shell sudo sh -c 'install -d -m 0755 /etc/coolify && cat > /tmp/coolify-host-jwt && install -m 0600 /tmp/coolify-host-jwt /etc/coolify/host-jwt && rm -f /tmp/coolify-host-jwt'
}
stop_agent_processes() {
lima_shell sudo systemctl stop coold.service corrosion.service coold-dev-agent.service >/dev/null 2>&1 || true
lima_shell sudo pkill -x coold >/dev/null 2>&1 || true
lima_shell sudo pkill -x corrosion >/dev/null 2>&1 || true
}
ensure_podman_networks() {
local current_subnet
current_subnet="$(lima_shell sudo podman network inspect coolify-default-mesh --format '{{range .Subnets}}{{.Subnet}}{{end}}' 2>/dev/null || true)"
if [ -n "$current_subnet" ] && [ "$current_subnet" != "$CONTAINER_SUBNET" ]; then
lima_shell sudo podman network rm coolify-default-mesh >/dev/null
current_subnet=""
fi
if [ -z "$current_subnet" ]; then
lima_shell sudo podman network create --subnet "$CONTAINER_SUBNET" --gateway "$CONTAINER_GATEWAY" coolify-default-mesh >/dev/null
fi
}
write_runtime_config() {
local gossip_addr="127.0.0.1:8787"
local bootstrap=""
if [ -n "$WG_IP" ]; then
gossip_addr="$WG_IP:8787"
fi
if [ -n "$WG_PEER_IP" ]; then
bootstrap="\"$WG_PEER_IP:8787\""
fi
lima_shell sudo install -d -m 0755 /etc/corrosion/schemas /etc/coolify /run/coolify /var/lib/corrosion /var/run/corrosion /var/lib/coolify-dev /var/lib/coolify-builder/work
lima_shell sudo tee /etc/corrosion/schemas/coolify.sql >/dev/null <<'SQL'
CREATE TABLE service_endpoints (
container_id TEXT NOT NULL DEFAULT '' PRIMARY KEY,
container_name TEXT NOT NULL DEFAULT '',
namespace TEXT NOT NULL DEFAULT '',
host_mgmt_ip TEXT NOT NULL DEFAULT '',
container_ip TEXT NOT NULL DEFAULT '',
state TEXT NOT NULL DEFAULT '',
health TEXT NOT NULL DEFAULT 'unknown',
updated_at INTEGER NOT NULL DEFAULT 0
);
SQL
lima_shell sudo tee /etc/corrosion/config.toml >/dev/null <<TOML
[db]
path = "/var/lib/corrosion/corrosion.db"
schema_paths = ["/etc/corrosion/schemas"]
[gossip]
addr = "$gossip_addr"
bootstrap = [$bootstrap]
plaintext = true
[api]
addr = "127.0.0.1:8080"
[admin]
path = "/var/run/corrosion/admin.sock"
TOML
}
run_foreground() {
stop_agent_processes
write_runtime_config
ensure_podman_networks
install_mesh_firewall
(cd /tmp && limactl shell "$INSTANCE" -- sudo \
env COOLIFY_COOLD_HOST_MGMT_IP="${WG_IP:-127.0.0.1}" \
COOLIFY_COOLD_FLUX_URL="$FLUX_URL" \
COOLIFY_COOLD_BUILDER_ENABLED="$BUILDER_ENABLED" \
COOLIFY_COOLD_BUILDER_CAPACITY="$BUILDER_CAPACITY" \
CONTAINER_GATEWAY="$CONTAINER_GATEWAY" \
bash -s) <<'RUNNER'
set -euo pipefail
echo "coold: $(/usr/local/bin/coold --version)"
echo "builder: $(/usr/local/bin/builder --version)"
echo "corrosion: $(/usr/local/bin/corrosion --version 2>/dev/null || cat /usr/local/bin/corrosion.version)"
echo "starting packaged coold endpoint with corrosion in local mode"
cleanup() {
jobs -pr | xargs -r kill || true
}
trap cleanup EXIT INT TERM
/usr/local/bin/corrosion agent --config /etc/corrosion/config.toml &
COOLIFY_COOLD_HOST_MGMT_IP="${COOLIFY_COOLD_HOST_MGMT_IP:-127.0.0.1}" \
COOLIFY_COOLD_PODMAN_SOCKET="${COOLIFY_COOLD_PODMAN_SOCKET:-/run/podman/podman.sock}" \
COOLIFY_COOLD_CORROSION_URL="${COOLIFY_COOLD_CORROSION_URL:-http://127.0.0.1:8080}" \
COOLIFY_COOLD_NAMESPACES="${COOLIFY_COOLD_NAMESPACES:-default:coolify-default-mesh:$CONTAINER_GATEWAY}" \
COOLIFY_COOLD_DNS_ZONE="${COOLIFY_COOLD_DNS_ZONE:-coolify.internal}" \
COOLIFY_COOLD_API_BIND="${COOLIFY_COOLD_API_BIND:-${WG_IP:-127.0.0.1}:8443}" \
COOLIFY_COOLD_API_TOKEN_FILE="${COOLIFY_COOLD_API_TOKEN_FILE:-/etc/coolify/api-token}" \
COOLIFY_COOLD_FLUX_URL="${COOLIFY_COOLD_FLUX_URL:-http://host.lima.internal:6443}" \
COOLIFY_COOLD_HOST_JWT_PATH="${COOLIFY_COOLD_HOST_JWT_PATH:-/etc/coolify/host-jwt}" \
COOLIFY_COOLD_BUILDER_ENABLED="${COOLIFY_COOLD_BUILDER_ENABLED:-true}" \
COOLIFY_COOLD_BUILDER_CAPACITY="${COOLIFY_COOLD_BUILDER_CAPACITY:-2}" \
COOLIFY_COOLD_BUILDER_BIN="${COOLIFY_COOLD_BUILDER_BIN:-/usr/local/bin/builder}" \
COOLIFY_COOLD_BUILDER_WORK_DIR="${COOLIFY_COOLD_BUILDER_WORK_DIR:-/var/lib/coolify-builder/work}" \
/usr/local/bin/coold &
wait
RUNNER
}
install_mesh_firewall() {
lima_shell sudo install -d -m 0755 /etc/coolify
lima_shell sudo touch /etc/coolify/allow.rules /etc/coolify/allow.nft
lima_shell sudo tee /etc/coolify/bridge-fw.nft >/dev/null <<NFT
add table bridge coolify_bridge
add chain bridge coolify_bridge coolify_allow
flush chain bridge coolify_bridge coolify_allow
add chain bridge coolify_bridge coolify_intra
flush chain bridge coolify_bridge coolify_intra
add rule bridge coolify_bridge coolify_intra jump coolify_allow
add rule bridge coolify_bridge coolify_intra drop
add chain bridge coolify_bridge forward { type filter hook forward priority -200; policy accept; }
flush chain bridge coolify_bridge forward
add rule bridge coolify_bridge forward meta protocol != ip accept
add rule bridge coolify_bridge forward ct state established,related accept
add rule bridge coolify_bridge forward ip saddr { $CONTAINER_SUBNET } jump coolify_intra
add rule bridge coolify_bridge forward ip daddr { $CONTAINER_SUBNET } jump coolify_intra
NFT
lima_shell sudo tee /etc/systemd/system/coolify-mesh-fw.service >/dev/null <<UNIT
[Unit]
Description=Coolify mesh firewall rules
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/sysctl -w net.ipv4.ip_forward=1
ExecStart=/bin/sh -c "/usr/sbin/iptables -t nat -C POSTROUTING -s $CONTAINER_SUBNET -o wg0 -j RETURN 2>/dev/null || /usr/sbin/iptables -t nat -I POSTROUTING -s $CONTAINER_SUBNET -o wg0 -j RETURN"
ExecStart=/bin/sh -c "/usr/sbin/iptables -D FORWARD -s $CONTAINER_SUBNET -j ACCEPT 2>/dev/null || true"
ExecStart=/bin/sh -c "/usr/sbin/iptables -D FORWARD -d $CONTAINER_SUBNET -j ACCEPT 2>/dev/null || true"
ExecStart=/bin/sh -c "/usr/sbin/iptables -N COOLIFY-ALLOW 2>/dev/null || true"
ExecStart=/bin/sh -c "/usr/sbin/iptables -N COOLIFY-INTRA 2>/dev/null || true"
ExecStart=/usr/sbin/iptables -F COOLIFY-ALLOW
ExecStart=/usr/sbin/iptables -F COOLIFY-INTRA
ExecStart=/usr/sbin/iptables -A COOLIFY-INTRA -j COOLIFY-ALLOW
ExecStart=/usr/sbin/iptables -A COOLIFY-INTRA -j DROP
ExecStart=/bin/sh -c "/usr/sbin/iptables -C FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || /usr/sbin/iptables -I FORWARD 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT"
ExecStart=/bin/sh -c "/usr/sbin/iptables -C FORWARD -d $CONTAINER_SUBNET -j COOLIFY-INTRA 2>/dev/null || /usr/sbin/iptables -A FORWARD -d $CONTAINER_SUBNET -j COOLIFY-INTRA"
ExecStart=/bin/sh -c "/usr/sbin/iptables -C FORWARD -s $CONTAINER_SUBNET -j COOLIFY-INTRA 2>/dev/null || /usr/sbin/iptables -A FORWARD -s $CONTAINER_SUBNET -j COOLIFY-INTRA"
ExecStart=/bin/sh -c "nft delete table bridge coolify_bridge 2>/dev/null || true"
ExecStart=/bin/sh -c "nft -f /etc/coolify/bridge-fw.nft"
ExecStart=/bin/sh -c "[ -s /etc/coolify/allow.nft ] && nft -f /etc/coolify/allow.nft || true"
[Install]
WantedBy=multi-user.target
UNIT
lima_shell sudo systemctl daemon-reload
lima_shell sudo systemctl enable --now coolify-mesh-fw.service
lima_shell sudo systemctl restart coolify-mesh-fw.service
}
start_agent() {
stop_agent_processes
write_runtime_config
ensure_podman_networks
install_mesh_firewall
lima_shell sudo sh -c 'if [ ! -s /etc/coolify/api-token ]; then openssl rand -hex 32 > /etc/coolify/api-token.tmp && chmod 600 /etc/coolify/api-token.tmp && mv /etc/coolify/api-token.tmp /etc/coolify/api-token; fi'
lima_shell sudo tee /etc/systemd/system/corrosion.service >/dev/null <<'UNIT'
[Unit]
Description=Corrosion local state store
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/corrosion agent --config /etc/corrosion/config.toml
Restart=on-failure
RestartSec=2s
[Install]
WantedBy=multi-user.target
UNIT
lima_shell sudo tee /etc/systemd/system/coold.service >/dev/null <<UNIT
[Unit]
Description=Coolify host agent
Wants=corrosion.service
After=corrosion.service network-online.target podman.socket coolify-mesh-fw.service
[Service]
Environment=COOLIFY_COOLD_HOST_MGMT_IP=${WG_IP:-127.0.0.1}
Environment=COOLIFY_COOLD_PODMAN_SOCKET=/run/podman/podman.sock
Environment=COOLIFY_COOLD_CORROSION_URL=http://127.0.0.1:8080
Environment=COOLIFY_COOLD_NAMESPACES=default:coolify-default-mesh:$CONTAINER_GATEWAY
Environment=COOLIFY_COOLD_DNS_ZONE=coolify.internal
Environment=COOLIFY_COOLD_API_BIND=${WG_IP:-127.0.0.1}:8443
Environment=COOLIFY_COOLD_API_TOKEN_FILE=/etc/coolify/api-token
Environment=COOLIFY_COOLD_FLUX_URL=$FLUX_URL
Environment=COOLIFY_COOLD_HOST_JWT_PATH=/etc/coolify/host-jwt
Environment=COOLIFY_COOLD_BUILDER_ENABLED=$BUILDER_ENABLED
Environment=COOLIFY_COOLD_BUILDER_CAPACITY=$BUILDER_CAPACITY
Environment=COOLIFY_COOLD_BUILDER_BIN=/usr/local/bin/builder
Environment=COOLIFY_COOLD_BUILDER_WORK_DIR=/var/lib/coolify-builder/work
ExecStart=/usr/local/bin/coold
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN CAP_NET_RAW
Restart=on-failure
RestartSec=2s
[Install]
WantedBy=multi-user.target
UNIT
lima_shell sudo systemctl daemon-reload
lima_shell sudo systemctl enable --now corrosion.service coold.service
lima_shell sudo systemctl restart corrosion.service coold.service
}
generate_yaml() {
mkdir -p "$(dirname "$GENERATED")"
sed \
-e "s#{{COOLIFY_REPO}}#$ROOT#g" \
-e "s#{{COOLIFY_COOLD_VERSION}}#$VERSION#g" \
-e "s#{{COOLIFY_CORROSION_VERSION}}#$CORROSION_VERSION#g" \
"$TEMPLATE" > "$GENERATED"
}
start_vm() {
generate_yaml
if instance_running; then
return
fi
if instance_exists; then
limactl start --tty=false "$INSTANCE"
else
limactl start --tty=false --name="$INSTANCE" "$GENERATED"
fi
}
latest_lima_message() {
log_file="$HOME/.lima/$INSTANCE/ha.stderr.log"
if [ ! -f "$log_file" ]; then
echo "creating Lima instance directory"
return 0
fi
grep -E '"msg":"(Starting VZ|Waiting for|The essential requirement|Executing /mnt/lima|SSH Local Port|Port is available|Attempting|Downloaded|Using the existing instance|The instance)' "$log_file" | tail -n 1 | sed -E 's/^.*"msg":"(.*)","time":.*$/\1/' | sed 's/\\"/"/g' || true
}
wait_for_lima_start() {
start_vm &
start_pid=$!
elapsed=0
while kill -0 "$start_pid" 2>/dev/null; do
if instance_exists && lima_shell true >/dev/null 2>&1; then
status="$(lima_shell cloud-init status 2>/dev/null || true)"
printf '==> [%3ss] Lima start: guest SSH ready, cloud-init %s
' "$elapsed" "${status:-unknown}"
lima_shell sudo sh -c 'test -f /var/log/cloud-init-output.log && tail -n 12 /var/log/cloud-init-output.log || true' 2>/dev/null | awk '{ print "[guest] " $0; fflush(); }' || true
if ! printf '%s' "$status" | grep -q running; then
kill "$start_pid" >/dev/null 2>&1 || true
break
fi
else
message="$(latest_lima_message)"
printf '==> [%3ss] Lima start: %s
' "$elapsed" "${message:-booting}"
fi
sleep 5
elapsed=$((elapsed + 5))
done
wait "$start_pid" 2>/dev/null || true
}
wait_for_guest_provisioning() {
echo "==> Waiting for guest SSH..."
until instance_exists && lima_shell true >/dev/null 2>&1; do
message="$(latest_lima_message)"
printf '==> Waiting for guest SSH: %s
' "${message:-booting}"
sleep 5
done
status="$(lima_shell cloud-init status 2>/dev/null || true)"
if printf '%s' "$status" | grep -q running; then
echo "==> Guest SSH is ready; streaming cloud-init output until provisioning completes..."
(
lima_shell sudo sh -c 'touch /var/log/cloud-init-output.log; tail -n 40 -F /var/log/cloud-init-output.log' 2>/dev/null | awk '{ print "[guest] " $0; fflush(); }'
) &
tail_pid=$!
while true; do
status="$(lima_shell cloud-init status 2>/dev/null || true)"
printf '==> Guest cloud-init: %s
' "${status:-unknown}"
if ! printf '%s' "$status" | grep -q running; then
break
fi
sleep 5
done
kill "$tail_pid" >/dev/null 2>&1 || true
wait "$tail_pid" 2>/dev/null || true
else
printf '==> Guest cloud-init: %s
' "${status:-unknown}"
fi
echo "==> Final guest provisioning status:"
lima_shell bash -lc 'cloud-init status 2>/dev/null || true; if command -v coold >/dev/null; then coold --version; else echo "coold not installed yet"; fi; if command -v corrosion >/dev/null; then echo "corrosion installed"; else echo "corrosion not installed yet"; fi; if command -v builder >/dev/null; then echo "builder installed"; else echo "builder not installed yet"; fi; true' | awk '{ print "[guest] " $0; fflush(); }' || true
if ! lima_shell bash -lc 'command -v coold >/dev/null && command -v corrosion >/dev/null && command -v builder >/dev/null' >/dev/null 2>&1; then
echo "ERROR: VM provisioning finished but coold, corrosion, or builder is missing." >&2
echo "Check with: scripts/coold-vm.sh shell" >&2
exit 1
fi
}
up_with_logs() {
echo "==> Coolify coold VM: $INSTANCE"
echo "==> coold package tag: $VERSION"
echo "==> corrosion package tag: $CORROSION_VERSION"
echo "==> Lima config: $GENERATED"
wait_for_lima_start
wait_for_guest_provisioning
echo "==> VM is ready. Run: scripts/coold-vm.sh dev"
}
cmd="${1:-}"
case "$cmd" in
up)
require_lima
up_with_logs
;;
dev)
require_lima
start_vm >/dev/null
run_foreground
;;
start-agent)
require_lima
start_vm >/dev/null
start_agent
;;
stop-agent)
require_lima
if instance_running; then
stop_agent_processes
fi
;;
logs-agent)
require_lima
start_vm >/dev/null
exec bash -lc "cd /tmp && limactl shell '$INSTANCE' -- sudo journalctl -u coold.service -u corrosion.service -f -n 100"
;;
install-host-jwt)
require_lima
start_vm >/dev/null
install_host_jwt "${2:-}"
;;
wg-public-key)
require_lima
start_vm >/dev/null
wireguard_public_key
;;
vm-ip)
require_lima
start_vm >/dev/null
vm_primary_ip
;;
setup-wireguard)
require_lima
start_vm >/dev/null
setup_wireguard "${2:-}" "${3:-}" "${4:-}" "${5:-}" "${6:-}" "${7:-}" "${8:-}"
;;
shell)
require_lima
start_vm >/dev/null
exec bash -lc "cd /tmp && limactl shell '$INSTANCE' -- env TERM=xterm-256color SYSTEMD_PAGER=cat SYSTEMD_LESS=FRXMK bash -l"
;;
status)
require_lima
if instance_exists; then
exec limactl list "$INSTANCE"
fi
echo "No Lima instance named '$INSTANCE' exists yet."
echo "Run: scripts/coold-vm.sh up"
;;
stop)
require_lima
exec limactl stop "$INSTANCE"
;;
delete|destroy)
require_lima
exec limactl delete --force --tty=false "$INSTANCE"
;;
-h|--help|help|"")
usage
;;
*)
echo "unknown command: $cmd" >&2
usage >&2
exit 1
;;
esac
Executable
+680
View File
@@ -0,0 +1,680 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
read_coolify_env() {
key="$1"
default_value="$2"
current_value="${!key:-}"
if [ -n "$current_value" ]; then
printf '%s\n' "$current_value"
return
fi
if [ -f .env ]; then
env_value="$(grep -E "^${key}=" .env 2>/dev/null | tail -n1 | cut -d= -f2- | sed "s/^['\"]//; s/['\"]$//")"
if [ -n "$env_value" ]; then
printf '%s\n' "$env_value"
return
fi
fi
printf '%s\n' "$default_value"
}
coold_vm_count() {
local count
count="$(read_coolify_env COOLIFY_COOLD_VM_COUNT 2)"
if [ "$count" != "1" ] && [ "$count" != "2" ]; then
echo "ERROR: COOLIFY_COOLD_VM_COUNT supports 1 or 2 for now." >&2
exit 1
fi
printf '%s\n' "$count"
}
coold_vm_instance() {
local index="$1"
local base
base="$(read_coolify_env COOLIFY_COOLD_LIMA_INSTANCE coold-dev)"
if [ "$index" = "1" ]; then
printf '%s\n' "$base"
return
fi
read_coolify_env "COOLIFY_COOLD_LIMA_INSTANCE_${index}" "${base}-${index}"
}
coold_vm_wg_ip() {
local index="$1"
read_coolify_env "COOLIFY_COOLD_VM_WG_IP_${index}" "100.64.0.$((9 + index))"
}
coold_vm_wg_port() {
local index="$1"
read_coolify_env "COOLIFY_COOLD_VM_WG_PORT_${index}" "$((51820 + index))"
}
coold_vm_container_subnet() {
local index="$1"
read_coolify_env "COOLIFY_COOLD_VM_CONTAINER_SUBNET_${index}" "10.210.$((index - 1)).0/24"
}
coold_vm_container_gateway() {
local index="$1"
read_coolify_env "COOLIFY_COOLD_VM_CONTAINER_GATEWAY_${index}" "10.210.$((index - 1)).1"
}
coold_vm() {
local index="$1"
shift
COOLIFY_COOLD_LIMA_INSTANCE="$(coold_vm_instance "$index")" \
COOLIFY_COOLD_VM_WG_IP="$(coold_vm_wg_ip "$index")" \
COOLIFY_COOLD_VM_CONTAINER_SUBNET="$(coold_vm_container_subnet "$index")" \
COOLIFY_COOLD_VM_CONTAINER_GATEWAY="$(coold_vm_container_gateway "$index")" \
scripts/coold-vm.sh "$@"
}
mint_host_jwt_for_host() {
local host_id="$1"
local attempts=60
local output
local caps
local builder_capacity
builder_capacity="$(read_coolify_env COOLIFY_COOLD_VM_BUILDER_CAPACITY 2)"
caps="coold"
if [ "$builder_capacity" != "0" ]; then
caps="coold,builder"
fi
for attempt in $(seq 1 "$attempts"); do
if output="$(spin exec -T coolify php artisan flux:dev "$host_id" --caps="$caps" 2>&1)"; then
printf '%s\n' "$output" | tail -n 1
return 0
fi
echo "==> Waiting for Flux dev JWT key for ${host_id} (${attempt}/${attempts})..." >&2
printf '%s\n' "$output" | tail -n 3 | sed 's/^/[coolify] /' >&2
sleep 2
done
echo "ERROR: Could not mint Flux dev host JWT for ${host_id}." >&2
return 1
}
setup_wireguard_mesh() {
local count="$1"
if [ "$count" -lt 2 ]; then
return
fi
echo "==> Configuring WireGuard mesh for ${count} coold VMs..."
local pub1 pub2 ip1 ip2 port1 port2 subnet1 subnet2
pub1="$(coold_vm 1 wg-public-key)"
pub2="$(coold_vm 2 wg-public-key)"
ip1="$(coold_vm_wg_ip 1)"
ip2="$(coold_vm_wg_ip 2)"
port1="$(coold_vm_wg_port 1)"
port2="$(coold_vm_wg_port 2)"
subnet1="$(coold_vm_container_subnet 1)"
subnet2="$(coold_vm_container_subnet 2)"
coold_vm 1 setup-wireguard "$ip1" "$ip2" "host.lima.internal" "$pub2" "$port1" "$port2" "$subnet2"
coold_vm 2 setup-wireguard "$ip2" "$ip1" "host.lima.internal" "$pub1" "$port2" "$port1" "$subnet1"
}
follow_logs() {
local coold_vm_enabled="$1"
local count
local vm_logs_pids=""
count="$(coold_vm_count)"
echo "==> Following dev logs. Press Ctrl-C to stop the dev environment."
cleanup_logs() {
for pid in $vm_logs_pids; do
kill "$pid" >/dev/null 2>&1 || true
done
}
stop_from_signal() {
trap - INT TERM EXIT
cleanup_logs
echo
echo "==> Ctrl-C received; stopping dev environment..."
down
exit 130
}
trap cleanup_logs EXIT
trap stop_from_signal INT TERM
if [ "$coold_vm_enabled" != "false" ]; then
for index in $(seq 1 "$count"); do
instance="$(coold_vm_instance "$index")"
coold_vm "$index" logs-agent | sed "s/^/[${instance}] /" &
vm_logs_pids="$vm_logs_pids $!"
done
fi
spin logs -f
}
up() {
local coold_vm_enabled
local follow_dev_logs
local count
coold_vm_enabled="$(read_coolify_env COOLIFY_COOLD_VM_ENABLED true)"
follow_dev_logs="$(read_coolify_env COOLIFY_DEV_FOLLOW_LOGS true)"
count="$(coold_vm_count)"
if [ "$coold_vm_enabled" != "false" ]; then
echo "==> Starting ${count} Coolify coold VM(s) before Spin..."
for index in $(seq 1 "$count"); do
coold_vm "$index" up
done
setup_wireguard_mesh "$count"
else
echo "==> COOLIFY_COOLD_VM_ENABLED=false; skipping coold VM."
fi
echo "==> Starting Coolify Docker stack with Spin..."
spin up -d "$@"
if [ "$coold_vm_enabled" != "false" ]; then
for index in $(seq 1 "$count"); do
host_id="$(coold_vm_instance "$index")"
echo "==> Minting Flux dev host JWT for ${host_id}..."
host_jwt="$(mint_host_jwt_for_host "$host_id")"
echo "==> Installing host JWT into ${host_id}..."
coold_vm "$index" install-host-jwt "$host_jwt"
done
for index in $(seq 1 "$count"); do
echo "==> Starting coold VM agent service on $(coold_vm_instance "$index")..."
if [ "$count" -ge 2 ]; then
peer_index=1
if [ "$index" = "1" ]; then
peer_index=2
fi
COOLIFY_COOLD_VM_WG_PEER_IP="$(coold_vm_wg_ip "$peer_index")" coold_vm "$index" start-agent
else
coold_vm "$index" start-agent
fi
done
fi
if [ "$follow_dev_logs" = "false" ]; then
echo "==> Dev environment is ready. Use 'spin logs -f' or 'scripts/coold-vm.sh logs-agent' to follow logs."
return
fi
follow_logs "$coold_vm_enabled"
}
down() {
local coold_vm_enabled
local stop_coold_vm
coold_vm_enabled="$(read_coolify_env COOLIFY_COOLD_VM_ENABLED true)"
stop_coold_vm="$(read_coolify_env COOLIFY_COOLD_VM_STOP_ON_DOWN false)"
if [ "$coold_vm_enabled" != "false" ]; then
for index in $(seq 1 "$(coold_vm_count)"); do
echo "==> Stopping coold VM agent service on $(coold_vm_instance "$index")..."
coold_vm "$index" stop-agent || true
done
fi
echo "==> Stopping Coolify Docker stack with Spin..."
spin down "$@"
if [ "$stop_coold_vm" = "true" ]; then
echo "==> Stopping Coolify coold VM..."
for index in $(seq 1 "$(coold_vm_count)"); do
coold_vm "$index" stop
done
fi
}
corrosion_for_each_vm() {
local label="$1"
shift
local count
local script
count="$(coold_vm_count)"
script="$(cat)"
for index in $(seq 1 "$count"); do
instance="$(coold_vm_instance "$index")"
echo "--- ${instance}: ${label} ---"
printf '%s\n' "$script" | COOLIFY_COOLD_LIMA_INSTANCE="$instance" scripts/coold-vm.sh shell "$@"
done
}
corrosion_check() {
local count
count="$(coold_vm_count)"
for index in $(seq 1 "$count"); do
instance="$(coold_vm_instance "$index")"
peer_index=1
if [ "$index" = "1" ]; then
peer_index=2
fi
peer_ip="$(coold_vm_wg_ip "$peer_index")"
echo "--- ${instance}: check ---"
COOLIFY_COOLD_LIMA_INSTANCE="$instance" scripts/coold-vm.sh shell <<SH
set -e
printf 'services: '
systemctl is-active corrosion.service || true
printf 'coold: '
systemctl is-active coold.service || true
printf 'wireguard: '
sudo wg show wg0 >/dev/null 2>&1 && echo active || echo unavailable
printf 'peer ping (${peer_ip}): '
ping -c 1 -W 2 ${peer_ip} >/dev/null 2>&1 && echo ok || echo failed
printf 'gossip: '
awk '/^\[gossip\]/{section=1; next} /^\[/{section=0} section && /^addr =|^bootstrap =/ {printf "%s ", \$0} END {print ""}' /etc/corrosion/config.toml
printf 'registered containers: '
sudo sqlite3 /var/lib/corrosion/corrosion.db 'select count(*) from service_endpoints;' 2>/dev/null || echo unavailable
SH
done
}
corrosion_containers() {
corrosion_for_each_vm containers <<'SH'
echo '[corrosion service_endpoints]'
sudo sqlite3 -header -column /var/lib/corrosion/corrosion.db \
'select container_id, container_name, namespace, host_mgmt_ip, container_ip, state, health, updated_at from service_endpoints order by updated_at desc;' 2>/dev/null \
|| echo 'service_endpoints table unavailable'
echo
echo '[rootful podman]'
sudo podman ps --format 'table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}' 2>/dev/null \
|| echo 'rootful podman unavailable'
echo
echo '[rootless podman]'
podman ps --format 'table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}' 2>/dev/null \
|| echo 'rootless podman unavailable'
SH
}
corrosion_config() {
corrosion_for_each_vm config <<'SH'
sudo cat /etc/corrosion/config.toml
SH
}
corrosion_logs() {
local index="${1:-1}"
coold_vm "$index" logs-agent
}
corrosion_sql() {
local query="$*"
if [ -z "$query" ]; then
echo "Usage: scripts/dev.sh corrosion sql <query>" >&2
exit 1
fi
corrosion_for_each_vm sql <<SH
sudo sqlite3 -header -column /var/lib/corrosion/corrosion.db $(printf '%q' "$query")
SH
}
corrosion() {
local command="${1:-help}"
if [ $# -gt 0 ]; then
shift
fi
case "$command" in
check)
corrosion_check
;;
containers|registered-containers)
corrosion_containers
;;
config)
corrosion_config
;;
logs)
corrosion_logs "${1:-1}"
;;
sql)
corrosion_sql "$@"
;;
-h|--help|help)
cat <<'USAGE'
Usage: scripts/dev.sh corrosion <command>
Commands:
check Show Corrosion service, WireGuard, gossip, and row-count state
containers List registered service_endpoints rows on each VM
config Print /etc/corrosion/config.toml from each VM
logs [n] Follow coold/corrosion logs for VM n (default: 1)
sql <query> Run a read-only sqlite query against each Corrosion DB
USAGE
;;
*)
echo "unknown corrosion command: $command" >&2
echo "Run: scripts/dev.sh corrosion help" >&2
exit 1
;;
esac
}
example_nginx_name() {
local index="$1"
if [ "$index" = "1" ]; then
printf '%s\n' coolify-example-nginx
return
fi
printf 'coolify-example-nginx-%s\n' "$index"
}
example_nginx_up() {
local count
count="$(coold_vm_count)"
for index in $(seq 1 "$count"); do
instance="$(coold_vm_instance "$index")"
name="$(example_nginx_name "$index")"
gateway="$(coold_vm_container_gateway "$index")"
echo "--- ${instance}: starting ${name} with coold DNS (${gateway}) ---"
COOLIFY_COOLD_LIMA_INSTANCE="$instance" scripts/coold-vm.sh shell <<SH
set -e
sudo podman rm -f ${name} >/dev/null 2>&1 || true
sudo podman run -d \\
--name ${name} \\
--network coolify-default-mesh \\
--dns ${gateway} \\
--dns-search default.coolify.internal \\
docker.io/library/nginx:alpine
SH
done
}
example_nginx_down() {
local count
count="$(coold_vm_count)"
for index in $(seq 1 "$count"); do
instance="$(coold_vm_instance "$index")"
name="$(example_nginx_name "$index")"
echo "--- ${instance}: removing ${name} ---"
COOLIFY_COOLD_LIMA_INSTANCE="$instance" scripts/coold-vm.sh shell <<SH
sudo podman rm -f ${name} >/dev/null 2>&1 || true
SH
done
}
example_nginx_check_dns() {
COOLIFY_COOLD_LIMA_INSTANCE="$(coold_vm_instance 1)" scripts/coold-vm.sh shell <<'SH'
set -e
echo '--- resolv.conf ---'
sudo podman exec coolify-example-nginx cat /etc/resolv.conf
echo
echo '--- coold DNS lookup through search domain ---'
sudo podman exec coolify-example-nginx nslookup coolify-example-nginx-2
echo
echo '--- coold DNS lookup by full name ---'
sudo podman exec coolify-example-nginx nslookup coolify-example-nginx-2.default.coolify.internal
SH
}
example_nginx_help() {
cat <<'USAGE'
Usage: scripts/dev.sh example-nginx <command>
Commands:
up Start one nginx container on each coold VM with coold DNS configured
down Remove the example nginx containers
check-dns Verify host 1 nginx can resolve host 2 nginx through coold DNS
USAGE
}
example_nginx() {
local command="${1:-help}"
if [ $# -gt 0 ]; then
shift
fi
case "$command" in
up)
example_nginx_up
;;
down)
example_nginx_down
;;
check-dns)
example_nginx_check_dns
;;
-h|--help|help)
example_nginx_help
;;
*)
echo "unknown example-nginx command: $command" >&2
echo "Run: scripts/dev.sh example-nginx help" >&2
exit 1
;;
esac
}
firewall_help() {
cat <<'USAGE'
Usage: scripts/dev.sh firewall <command>
Commands:
allow <src> <dst> [proto] [port] Allow traffic on every coold VM (proto/port optional)
revoke [id|src] [dst] [proto] [port]
Remove an allow rule from every coold VM
list List allow rules on every coold VM
reconcile Re-apply firewall snapshot on every coold VM
Examples:
scripts/dev.sh firewall allow 10.210.0.2 10.210.1.2 tcp 80
scripts/dev.sh firewall revoke
scripts/dev.sh firewall revoke 10.210.0.2 10.210.1.2 tcp 80
scripts/dev.sh firewall revoke 3ba6e0c235a6
scripts/dev.sh firewall list
USAGE
}
firewall_api_for_each_vm() {
local label="$1"
local method="$2"
local path="$3"
local body="${4:-}"
local count
count="$(coold_vm_count)"
for index in $(seq 1 "$count"); do
instance="$(coold_vm_instance "$index")"
api_ip="$(coold_vm_wg_ip "$index")"
echo "--- ${instance}: ${label} ---"
COOLIFY_COOLD_LIMA_INSTANCE="$instance" scripts/coold-vm.sh shell <<SH
set -e
token="\$(sudo cat /etc/coolify/api-token)"
curl_args=(-fsS -X "${method}" "http://${api_ip}:8443${path}" -H "Authorization: Bearer \${token}")
if [ -n '${body}' ]; then
curl_args+=(-H 'Content-Type: application/json' -d '${body}')
fi
curl "\${curl_args[@]}"
echo
SH
done
}
firewall_allow() {
local src="${1:-}"
local dst="${2:-}"
local proto="${3:-}"
local port="${4:-}"
local body
if [ -z "$src" ] || [ -z "$dst" ]; then
firewall_help >&2
exit 1
fi
if [ -n "$port" ] && [ -z "$proto" ]; then
echo "ERROR: port requires proto (tcp or udp)." >&2
exit 1
fi
body="$(printf '{"namespace":"default","src":"%s","dst":"%s"' "$src" "$dst")"
if [ -n "$proto" ]; then
body="${body}$(printf ',"proto":"%s"' "$proto")"
fi
if [ -n "$port" ]; then
body="${body}$(printf ',"port":%s' "$port")"
fi
body="${body}}"
firewall_api_for_each_vm "allow ${src} -> ${dst}" POST /api/v1/firewall/allow "$body"
}
firewall_rule_id() {
local src="$1"
local dst="$2"
local proto="${3:-}"
local port="${4:-0}"
if [ -z "$proto" ]; then
port=0
fi
printf 'default|%s|%s|%s|%s' "$src" "$dst" "$proto" "$port" \
| shasum -a 256 \
| awk '{print substr($1, 1, 12)}'
}
firewall_revoke() {
local id_or_src="${1:-}"
local dst="${2:-}"
local proto="${3:-}"
local port="${4:-}"
local id
if [ -z "$id_or_src" ]; then
echo "Current firewall allow rule IDs:"
firewall_list
echo
echo "Revoke one with: scripts/dev.sh firewall revoke <id>"
return
fi
if [ -z "$dst" ]; then
id="$id_or_src"
else
id="$(firewall_rule_id "$id_or_src" "$dst" "$proto" "$port")"
fi
firewall_api_for_each_vm "revoke ${id}" DELETE "/api/v1/firewall/allow/${id}"
}
firewall_list() {
firewall_api_for_each_vm list GET '/api/v1/firewall/allow?namespace=default'
}
firewall_reconcile() {
firewall_api_for_each_vm reconcile POST /api/v1/firewall/reconcile
}
firewall() {
local command="${1:-help}"
if [ $# -gt 0 ]; then
shift
fi
case "$command" in
allow)
firewall_allow "$@"
;;
revoke|remove|delete|deny)
firewall_revoke "$@"
;;
list)
firewall_list
;;
reconcile)
firewall_reconcile
;;
-h|--help|help)
firewall_help
;;
*)
echo "unknown firewall command: $command" >&2
echo "Run: scripts/dev.sh firewall help" >&2
exit 1
;;
esac
}
usage() {
cat <<'USAGE'
Usage: scripts/dev.sh <command> [spin args]
Commands:
up Start the coold VM, Spin stack, and dev coold agent
down Stop the dev coold agent and Spin stack
shell [n] Open a shell inside coold VM n (default: 1)
list Show Lima instances
corrosion <command> Inspect Corrosion state, config, logs, and registered containers
firewall <command> Manage dev coold firewall allow rules
example-nginx <command> Start/check example nginx containers with coold DNS
USAGE
}
cmd="${1:-}"
if [ $# -gt 0 ]; then
shift
fi
case "$cmd" in
up)
up "$@"
;;
down)
down "$@"
;;
shell)
coold_vm "${1:-1}" shell
;;
list)
limactl list
;;
corrosion)
corrosion "$@"
;;
firewall)
firewall "$@"
;;
example-nginx)
example_nginx "$@"
;;
-h|--help|help|"")
usage
;;
*)
echo "unknown command: $cmd" >&2
usage >&2
exit 1
;;
esac
@@ -0,0 +1,31 @@
<?php
it('does not include coold dev tooling defaults in the development env example', function (string $variable) {
$developmentExample = file_get_contents(base_path('.env.development.example'));
expect($developmentExample)->not->toContain($variable.'=');
})->with([
'coold package version default' => 'COOLIFY_COOLD_VERSION',
'flux package version default' => 'COOLIFY_FLUX_VERSION',
'corrosion package version default' => 'COOLIFY_CORROSION_VERSION',
'coold VM count default' => 'COOLIFY_COOLD_VM_COUNT',
'coold VM flux URL default' => 'COOLIFY_COOLD_VM_FLUX_URL',
'coold VM WireGuard IP 1 default' => 'COOLIFY_COOLD_VM_WG_IP_1',
'coold VM WireGuard IP 2 default' => 'COOLIFY_COOLD_VM_WG_IP_2',
'coold VM WireGuard port 1 default' => 'COOLIFY_COOLD_VM_WG_PORT_1',
'coold VM WireGuard port 2 default' => 'COOLIFY_COOLD_VM_WG_PORT_2',
'coold VM builder capacity default' => 'COOLIFY_COOLD_VM_BUILDER_CAPACITY',
'coold VM enabled default' => 'COOLIFY_COOLD_VM_ENABLED',
'coold VM stop on down default' => 'COOLIFY_COOLD_VM_STOP_ON_DOWN',
'dev follow logs default' => 'COOLIFY_DEV_FOLLOW_LOGS',
]);
it('defaults coold dev VM settings in Laravel config', function () {
expect(config('coold.dev_host_count'))->toBe(2)
->and(config('coold.dev_host_id'))->toBe('coolify-coold-dev')
->and(config('coold.dev_host_id_2'))->toBe('coolify-coold-dev-2')
->and(config('coold.dev_wireguard_ip_1'))->toBe('100.64.0.10')
->and(config('coold.dev_wireguard_ip_2'))->toBe('100.64.0.11')
->and(config('coold.dev_builder_capacity'))->toBe(2)
->and(config('coold.dev_builder_enabled'))->toBeTrue();
});
+65
View File
@@ -0,0 +1,65 @@
<?php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Config;
it('mints a host jwt signed by the configured flux private key', function () {
[$privateKeyPath, $publicKeyPath] = createFluxJwtKeypair();
Config::set('flux.jwt_private_key_path', $privateKeyPath);
$exitCode = Artisan::call('flux:dev', [
'host_id' => 'coold-dev',
'--caps' => 'coold,builder',
'--ttl' => '600',
]);
expect($exitCode)->toBe(0);
$token = trim(Artisan::output());
$claims = JWT::decode($token, new Key(file_get_contents($publicKeyPath), 'ES256'));
expect($claims->sub)->toBe('coold-dev')
->and($claims->aud)->toBe('coold')
->and($claims->caps)->toBe(['coold', 'builder'])
->and($claims->exp)->toBeGreaterThan(time());
});
it('writes the host jwt to an output path with owner-only permissions', function () {
[$privateKeyPath] = createFluxJwtKeypair();
$outputPath = storage_path('framework/testing/host-jwt');
Config::set('flux.jwt_private_key_path', $privateKeyPath);
$exitCode = Artisan::call('flux:dev', [
'host_id' => 'coold-dev',
'--output' => $outputPath,
]);
expect($exitCode)->toBe(0);
expect($outputPath)->toBeFile()
->and(substr(sprintf('%o', fileperms($outputPath)), -4))->toBe('0600');
});
/**
* @return array{0: string, 1: string}
*/
function createFluxJwtKeypair(): array
{
$directory = storage_path('framework/testing/flux-keys-'.bin2hex(random_bytes(4)));
mkdir($directory, 0777, true);
$privateKeyPath = $directory.'/jwt.priv';
$publicKeyPath = $directory.'/jwt.pub';
exec('openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out '.escapeshellarg($privateKeyPath), $output, $exitCode);
expect($exitCode)->toBe(0);
exec('openssl pkey -in '.escapeshellarg($privateKeyPath).' -pubout -out '.escapeshellarg($publicKeyPath), $output, $exitCode);
expect($exitCode)->toBe(0);
return [$privateKeyPath, $publicKeyPath];
}
+6
View File
@@ -105,6 +105,12 @@ it('serves the v5 inertia shell', function () {
->assertSee('v5-ready', false)
->assertSee('Running')
->assertSee('Flux is running.')
->assertSee('coolify-coold-dev')
->assertSee('coolify-coold-dev-2')
->assertSee('100.64.0.10')
->assertSee('100.64.0.11')
->assertSee('builder')
->assertSee('builderCapacity')
->assertSee('V5 Shared Team')
->assertSee('Shared team details')
->assertSee('owner')