feat(v5): use bridged Lima hosts for coold dev

Add v5 architecture docs and update dev bootstrap, seeding, and tests to use bridged Lima .local hosts with a container-accessible SSH key.
This commit is contained in:
Andras Bacsai
2026-06-21 15:13:59 +02:00
parent 1c091e9e36
commit 068a2979fd
17 changed files with 636 additions and 462 deletions
+2 -1
View File
@@ -43,5 +43,6 @@ tests/v4/Browser/Screenshots
# Local generated Lima configs
.dev/bin/
.dev/lima/*.generated.yaml
.dev/lima/ssh.config
.dev/lima/ssh_key
.dev/lima/hosts
+1 -1
View File
@@ -6,5 +6,5 @@ return [
'corrosion_version' => env('COOLIFY_CORROSION_VERSION', 'v1.0.0'),
'dev_builder_capacity' => (int) env('COOLIFY_COOLD_VM_BUILDER_CAPACITY', 2),
'dev_builder_enabled' => (int) env('COOLIFY_COOLD_VM_BUILDER_CAPACITY', 2) > 0,
'dev_ssh_user' => env('COOLIFY_CLI_SSH_USER', get_current_user()),
'dev_ssh_user' => env('COOLIFY_CLI_SSH_USER', 'coolify'),
];
+7 -7
View File
@@ -31,7 +31,7 @@ class V5DevLimaSeeder extends Seeder
->first();
$builderCapacity = max(0, (int) config('coold.dev_builder_capacity', 2));
$sshUser = (string) config('coold.dev_ssh_user', get_current_user());
$sshUser = (string) config('coold.dev_ssh_user', 'coolify');
$servers = collect($this->servers())
->map(fn (array $server): array => [
...$server,
@@ -57,19 +57,19 @@ class V5DevLimaSeeder extends Seeder
return [
[
'name' => 'coold-dev',
'host' => 'host.docker.internal',
'ssh_port' => 60001,
'host' => 'coold-dev.local',
'ssh_port' => 22,
'wireguard_management_ip' => '100.64.0.1',
'wireguard_listen_port_override' => 51821,
'wireguard_endpoint_override' => 'host.lima.internal:51821',
'wireguard_endpoint_override' => 'coold-dev.local:51821',
],
[
'name' => 'coold-dev-2',
'host' => 'host.docker.internal',
'ssh_port' => 60002,
'host' => 'coold-dev-2.local',
'ssh_port' => 22,
'wireguard_management_ip' => '100.64.0.2',
'wireguard_listen_port_override' => 51822,
'wireguard_endpoint_override' => 'host.lima.internal:51822',
'wireguard_endpoint_override' => 'coold-dev-2.local:51822',
],
];
}
+17 -13
View File
@@ -49,20 +49,24 @@ scripts/dev.sh coolify bootstrap-command
scripts/dev.sh coolify run <args>
```
On macOS, the helper builds `coolify` from the local coold repo because the
nightly release currently publishes Linux binaries. On Linux, it downloads the
nightly release artifact.
The helper runs the `coolify` CLI from the Coolify development container. Lima
VMs are addressed by their bridged mDNS names (`<vm>.local`), so the same
addresses are used by bootstrap and by the v5 dev server seeder.
The generated bootstrap command uses Lima's forwarded SSH ports and dev
WireGuard endpoint overrides, for example:
Because Docker Desktop does not reliably pass mDNS multicast into containers,
`scripts/dev.sh up` resolves each `<vm>.local` name on the host and writes the
resolved records into the Coolify container `/etc/hosts` before bootstrapping.
The generated bootstrap command uses the container CLI, the repo-local copy of
the Lima SSH key, and dev WireGuard endpoint overrides, for example:
```bash
.dev/bin/coolify init bootstrap \
--nodes "127.0.0.1:<ssh1>,127.0.0.1:<ssh2>" \
--ssh-key "$HOME/.lima/_config/user" \
--ssh-user "$USER" \
--wg-listen-port-overrides "127.0.0.1:<ssh1>=51821,127.0.0.1:<ssh2>=51822" \
--wg-endpoint-overrides "127.0.0.1:<ssh1>=host.lima.internal:51821,127.0.0.1:<ssh2>=host.lima.internal:51822" \
spin exec -T coolify /usr/local/bin/coolify init bootstrap \
--nodes "coold-dev.local,coold-dev-2.local" \
--ssh-key "/var/www/html/.dev/lima/ssh_key" \
--ssh-user "coolify" \
--wg-listen-port-overrides "coold-dev.local=51821,coold-dev-2.local=51822" \
--wg-endpoint-overrides "coold-dev.local=coold-dev.local:51821,coold-dev-2.local=coold-dev-2.local:51822" \
--coold-version "nightly" \
--corrosion-version "v1.0.0" \
--yes
@@ -78,8 +82,8 @@ After `coolify init bootstrap`, defaults are:
| VM | WireGuard IP | WireGuard endpoint | Podman subnet | Gateway |
| --- | --- | --- | --- | --- |
| `coold-dev` | `100.64.0.1` | `host.lima.internal:51821` | `10.210.0.0/24` | `10.210.0.1` |
| `coold-dev-2` | `100.64.0.2` | `host.lima.internal:51822` | `10.210.1.0/24` | `10.210.1.1` |
| `coold-dev` | `100.64.0.1` | `coold-dev.local:51821` | `10.210.0.0/24` | `10.210.0.1` |
| `coold-dev-2` | `100.64.0.2` | `coold-dev-2.local:51822` | `10.210.1.0/24` | `10.210.1.1` |
## Checking state
+36 -62
View File
@@ -1,79 +1,53 @@
# Lima VM for testing Coolify v5 against coolify-provisioned hosts.
# Start with: scripts/coold-vm.sh up
#
# 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"
user:
name: coolify
cpus: 4
memory: "8GiB"
disk: "100GiB"
memory: "2GiB"
disk: "20GiB"
containerd:
system: false
user: false
ssh:
localPort: {{COOLIFY_COOLD_VM_SSH_PORT}}
images:
- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img"
- location: "https://cloud-images.ubuntu.com/releases/26.04/release/ubuntu-26.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"
- location: "https://cloud-images.ubuntu.com/releases/26.04/release/ubuntu-26.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"
networks:
- lima: bridged
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 minimal VM dependencies..."
apt-get install -y --no-install-recommends \
bash \
ca-certificates \
curl \
iproute2 \
jq \
openssl \
sqlite3 \
sudo
cat >/usr/local/bin/rtk <<'SH'
#!/usr/bin/env bash
exec "$@"
SH
chmod +x /usr/local/bin/rtk
mkdir -p /etc/coolify /var/lib/coolify-dev
chmod 755 /etc/coolify
coolify_test_public_key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuGmoeGq/pojrsyP1pszcNVuZx9iFkCELtxrh31QJ68 sail@76ff66d2e2dd"
install -d -m 700 /root/.ssh
touch /root/.ssh/authorized_keys
grep -qxF "$coolify_test_public_key" /root/.ssh/authorized_keys || echo "$coolify_test_public_key" >>/root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
target_home="$(getent passwd 501 | cut -d: -f6 || true)"
if [ -n "$target_home" ] && [ -d "$target_home" ]; then
owner="$(stat -c '%u:%g' "$target_home")"
install -d -m 700 -o "${owner%:*}" -g "${owner#*:}" "$target_home/.ssh"
touch "$target_home/.ssh/authorized_keys"
grep -qxF "$coolify_test_public_key" "$target_home/.ssh/authorized_keys" || echo "$coolify_test_public_key" >>"$target_home/.ssh/authorized_keys"
chown "$owner" "$target_home/.ssh/authorized_keys"
chmod 600 "$target_home/.ssh/authorized_keys"
fi
echo "[coold-vm] Minimal provisioning complete. coolify bootstrap will install WireGuard, Podman, Corrosion, coold, and builder."
echo "[coold-vm] Installing and configuring mDNS."
apt-get update
apt-get install -y avahi-daemon
hostnamectl set-hostname "{{.Name}}"
systemctl enable --now avahi-daemon
echo "[coold-vm] Installing SSH keys."
install_public_key() {
user="$1"
home="$(getent passwd "$user" | cut -d: -f6 || true)"
if [ -z "$home" ] || [ ! -d "$home" ]; then
return
fi
owner="$(stat -c '%u:%g' "$home")"
authorized_keys="$home/.ssh/authorized_keys"
install -d -m 700 -o "${owner%:*}" -g "${owner#*:}" "$home/.ssh"
touch "$authorized_keys"
chown "$owner" "$authorized_keys"
chmod 600 "$authorized_keys"
grep -qxF "$coolify_test_public_key" "$authorized_keys" || echo "$coolify_test_public_key" >>"$authorized_keys"
chown "$owner" "$authorized_keys"
}
install_public_key root
install_public_key coolify
+34
View File
@@ -0,0 +1,34 @@
# Coolify v5 Architecture
This directory is the canonical architecture reference for Coolify v5 from the
Laravel control-plane point of view.
v5 has three major building blocks:
```text
User / API / Git webhook
Coolify Laravel control plane
↓ HTTP over /run/coolify/flux.sock
Flux
↓ outbound gRPC Agent.Stream
coold on each host
Podman / networks / firewall / DNS / Corrosion / builder
```
## Documents
| Document | Purpose |
| --- | --- |
| [Overview](overview.md) | High-level mental model and data flow. |
| [Responsibility split](responsibility-split.md) | What belongs in Coolify, Flux, and coold. |
| [Primitives](primitives.md) | Canonical host primitive surface Coolify may dispatch. |
| [Deploy flows](deploy-flows.md) | User-functionality flows, starting with `nginx:alpine`. |
| [ADR 0001](adr/0001-control-plane-flux-coold-split.md) | Decision record for the v5 split. |
## Core rule
Coolify owns user intent and application state. Flux routes requests. coold
executes explicit host primitives. coold must not grow app, team, RBAC,
deployment, billing, or audit concepts.
@@ -0,0 +1,75 @@
# ADR 0001: Split v5 into Coolify control plane, Flux broker, and coold host agent
## Status
Accepted.
## Context
Coolify v5 needs to manage many user resources across many user-owned hosts,
including hosts behind NAT or restrictive firewalls. The Laravel application
must own product behavior and durable state, but it should not hold thousands of
long-lived agent streams or directly expose host runtime sockets.
Host operations also need a narrow privileged boundary. Podman, firewall, DNS,
Corrosion, and build supervision require local host privileges that should not
be spread across the Laravel app or arbitrary scripts.
## Decision
Coolify v5 uses three distinct building blocks:
1. **Coolify Laravel control plane** owns user intent, durable product state,
RBAC, deployment state machines, placement decisions, secrets, proxy config
rendering, notifications, and audit.
2. **Flux** owns long-lived agent connectivity. Laravel talks to Flux over a
Unix socket. coold agents dial Flux over outbound gRPC. Flux routes typed
primitive requests to connected hosts and resolves pending responses.
3. **coold** runs once per host. It exposes a closed set of host primitives and
owns privileged local execution through Podman, firewall, DNS, Corrosion,
host facts, and builder supervision.
coold must not expose raw Podman passthrough. Every supported operation must be
an explicit primitive with validation and a stable protocol shape.
## Consequences
### Positive
- Coolify keeps product complexity in one durable control plane.
- Hosts can live behind NAT because coold dials out to Flux.
- Privileged host access is isolated to coold.
- Flux can scale connection handling separately from Laravel request workers.
- The primitive surface can be tested independently from Coolify's app model.
### Negative
- More moving parts than a direct SSH/Docker model.
- Protocol changes require coordination across Coolify, Flux, and coold.
- Some deploy features require new primitives before the v5 path is complete.
## Boundary rules
- Coolify may say: "deploy this nginx resource for this team to host H1." coold
may not.
- Coolify may send: `images.pull`, `containers.create`, `containers.start`,
`services.register`, `firewall.allow`. coold may execute those.
- Flux may route requests but may not make product decisions.
- coold may reject dangerous host operations even when Coolify requested them.
## First reference flow
The first validation flow is deploying a Docker image app using `nginx:alpine`.
That flow requires no builder and exercises the minimum runtime path:
```text
Coolify state machine
→ Flux dispatch
→ coold image pull
→ coold container create/start
→ coold status
→ coold service registration
→ coold firewall
→ Coolify proxy config/reload
→ Coolify marks deployment running
```
+76
View File
@@ -0,0 +1,76 @@
# v5 Deploy Flows
These flows are written from the user-functionality view. Coolify owns the
state machine; Flux routes primitive requests; coold executes host operations.
## Flow: deploy Docker image app `nginx:alpine`
User intent:
```text
Run nginx:alpine on host H1, expose port 80, and route nginx.example.com to it.
```
### Steps
| Step | Coolify | Flux | coold |
| --- | --- | --- | --- |
| Create app | Stores app, source type `docker_image`, image `nginx:alpine`, target host, port, domain. | No action. | No action. |
| Start deploy | Creates deployment record and enters deploy state machine. | No action until dispatch. | No action. |
| Pull image | Sends `images.pull` to H1. | Routes request to H1's stream. | Pulls image through Podman. |
| Prepare network/volume | Sends required `networks.create` / `volumes.create` operations. | Routes requests. | Creates idempotent host resources. |
| Create container | Sends `containers.create` with image, name, env, labels, network, port, health check, mounts, DNS. | Routes request. | Applies deny filter and creates container through Podman. |
| Start container | Sends `containers.start`. | Routes request. | Starts container. |
| Check status | Sends `containers.inspect` or reads status stream. | Routes request. | Reads local Podman state. |
| Register endpoint | Sends `services.register`. | Routes request. | Writes this host's endpoint row to Corrosion. |
| Allow traffic | Sends `firewall.allow` for proxy-to-container traffic when required. | Routes request. | Writes iptables and nft allow rules. |
| Configure ingress | Renders proxy config from Coolify domain/resource state. If reload is needed, sends `containers.exec` or equivalent primitive for the proxy container. | Routes runtime reload request. | Executes the reload primitive only. |
| Mark running | Stores final status, deployment result, and user-facing logs. | Resolves final responses. | Keeps container running and reports future state. |
### Expected data locations
| Data | Owner |
| --- | --- |
| App name, team, project, env, domain, image ref, desired port | Coolify database |
| Deployment state and history | Coolify database |
| Open host stream and pending request IDs | Flux memory |
| Pulled image and running container | Host Podman storage |
| Endpoint rows for this host | Corrosion via coold |
| Firewall tuples and snapshots | Host kernel + `/etc/coolify` via coold |
### No build path
`nginx:alpine` is a prebuilt image. This flow does not use Git clone,
Dockerfile, buildpacks, Railpack, or the builder subprocess.
## Flow: delete the nginx app
| Step | Coolify | Flux | coold |
| --- | --- | --- | --- |
| User deletes resource | Validates permission and starts cleanup state machine. | No action until dispatch. | No action. |
| Stop container | Sends `containers.stop`. | Routes request. | Stops container. |
| Remove container | Sends `containers.delete`. | Routes request. | Deletes container. |
| Remove endpoint | Sends `services.unregister`. | Routes request. | Removes service endpoint row. |
| Remove firewall rule | Sends `firewall.revoke`. | Routes request. | Removes iptables and nft allow rules. |
| Update proxy | Removes rendered route and reloads proxy if needed. | Routes reload primitive. | Executes proxy reload primitive. |
| Finish cleanup | Marks resource deleted and records outcome. | Resolves responses. | No product state retained. |
## Flow: Git app with build
A Git app adds a build phase before container creation.
```text
Coolify resolves source + secrets + build config
Flux routes build request to a host with builder capability
coold supervises builder subprocess
builder writes image/result
Coolify deploy state machine continues with image/container primitives
```
coold supervises the builder process. The builder owns build implementation.
Coolify owns the decision to build, the build configuration, and the deployment
state transitions around the build.
+38
View File
@@ -0,0 +1,38 @@
# v5 Architecture Overview
Coolify v5 separates product intent from host execution.
- **Coolify Laravel** is the control plane. It owns users, teams, projects,
environments, resources, deployments, domains, secrets, RBAC, audit-worthy
state, and the deployment state machines.
- **Flux** is the connection broker. Laravel talks to Flux over a Unix socket.
coold agents dial Flux over outbound gRPC streams. Flux maps host IDs to
connected streams and maps request IDs to pending responses.
- **coold** is the per-host executor. It owns local runtime access: Podman,
host networks, DNS, firewall mutations, service-discovery sync, host facts,
and builder subprocess supervision.
## Data flow
```text
1. A user/API/webhook asks Coolify to do something.
2. Coolify validates permissions and stores desired state in its database.
3. Coolify's state machine turns that intent into ordered host primitives.
4. Coolify sends each primitive to Flux over /run/coolify/flux.sock.
5. Flux forwards the primitive to the target host's open coold stream.
6. coold executes the primitive locally and returns a typed result.
7. Flux resolves the pending request.
8. Coolify records the result and moves the deployment/resource state forward.
```
## Non-goals for Flux and coold
Flux and coold are not product layers. They do not decide which app to deploy,
which user is allowed to deploy, how to roll back, what domains mean, or where
business audit belongs. They only handle routing and host execution.
## Current implementation note
This directory describes the target v5 architecture. Some primitives are not
implemented in coold yet. Until implemented, docs should label them as target
primitives and code should not pretend they exist.
+106
View File
@@ -0,0 +1,106 @@
# v5 Host Primitives
Coolify deploy logic must send explicit primitives to coold through Flux. There
must be no raw Podman passthrough.
This file is the target primitive catalog. Implementations may lag behind this
catalog; code should only call primitives that exist in the current protocol.
## Images
| Primitive | Purpose |
| --- | --- |
| `images.pull` | Pull an OCI image ref onto a host. |
| `images.list` | List local images. |
| `images.delete` | Remove a local image when safe. |
## Containers
| Primitive | Purpose |
| --- | --- |
| `containers.create` | Create a container from an explicit spec. |
| `containers.start` | Start an existing container. |
| `containers.stop` | Stop a container with an optional timeout. |
| `containers.restart` | Restart a container. |
| `containers.delete` | Delete a stopped or forced container. |
| `containers.inspect` | Return detailed runtime state. |
| `containers.list` | Return host container summaries. |
| `containers.logs` | Stream or read container logs. |
| `containers.exec` | Run a command inside a container. |
| `containers.healthcheck.run` | Trigger or read a runtime health check. |
`containers.create` must enforce a deny filter for dangerous host options such
as privileged mode, unsafe host mounts, host networking, and disallowed
capabilities unless the host is explicitly configured to allow them.
## Volumes
| Primitive | Purpose |
| --- | --- |
| `volumes.create` | Create an idempotent named volume. |
| `volumes.inspect` | Inspect a host volume. |
| `volumes.delete` | Delete an unused volume. |
## Networks
| Primitive | Purpose |
| --- | --- |
| `networks.create` | Create an idempotent Podman network. |
| `networks.list` | List host networks. |
| `networks.delete` | Delete an unused network. |
Bootstrap-created mesh namespace networks are managed by the v5 cluster init
flow. Per-resource or compose networks are runtime primitives.
## Firewall
| Primitive | Purpose |
| --- | --- |
| `firewall.allow` | Add an allow tuple and persist it. |
| `firewall.revoke` | Remove an allow tuple by ID. |
| `firewall.list` | List active/persisted allow rules. |
| `firewall.reconcile` | Flush and restore firewall state from snapshots. |
coold is the sole writer for both firewall planes: iptables for cross-host
traffic and nft bridge rules for same-bridge traffic.
## Service discovery and DNS
| Primitive | Purpose |
| --- | --- |
| `services.register` | Register this host's container endpoint in Corrosion. |
| `services.unregister` | Remove an endpoint from Corrosion. |
| `services.endpoints` | Read known endpoints for diagnostics. |
| `dns.lookup` | Diagnose internal DNS resolution. |
| `dns.stats` | Return DNS server status. |
## Host facts
| Primitive | Purpose |
| --- | --- |
| `host.info` | Return host/runtime facts for scheduling and debugging. |
| `host.stats` | Return CPU, memory, disk, and container stats snapshot. |
| `host.containers` | Return host container summaries. |
## Builder
Builds are routed through Flux to a host with builder capability. coold should
supervise the builder process but should not implement build logic inline.
| Primitive | Purpose |
| --- | --- |
| `build.dispatch` | Start a builder request on a capable host. |
| `build.cancel` | Cancel a running build request. |
| `build.result` | Return or long-poll build result. |
## Not primitives
These belong in Coolify, not coold:
- `deploy.application`
- `rollback.deployment`
- `create.preview`
- `configure.domain`
- `authorize.user`
- `send.notification`
- `render.proxy.config`
@@ -0,0 +1,60 @@
# v5 Responsibility Split
Use this as the first check when deciding where a new feature belongs.
## Rule of thumb
| Question | Owner |
| --- | --- |
| What should happen for this user, team, app, database, or deployment? | Coolify |
| Which connected host should receive this request? | Coolify chooses, Flux routes |
| How do we get a command to a NATed host? | Flux |
| Do this concrete operation on this host. | coold |
| Is this allowed for this Coolify user/team? | Coolify |
| Is this host operation dangerous even if Coolify asked for it? | coold deny filter |
## Coolify owns
- Users, teams, roles, RBAC, API tokens, sessions, SSO/OAuth.
- Projects, environments, applications, services, databases, servers.
- Resource configuration: source, image, build settings, env vars, domains,
ports, health checks, resource limits, volumes, schedules, webhooks.
- Deployment state machines: pending, building, pulling, creating, starting,
health waiting, cutover, running, failed, rollback, cleanup.
- Placement and scheduling decisions.
- Proxy/ingress configuration rendering and TLS intent.
- Secret storage, encryption, resolution, and injection at deploy time.
- Business audit, event history, deployment logs, and user-facing status.
- Notifications, billing/subscriptions, cloud-provider integration.
## Flux owns
- Long-lived coold stream registry keyed by host ID.
- Pending request registry keyed by request ID.
- Request routing from Laravel's Unix-socket lane to the selected coold stream.
- Timeouts, disconnected-host responses, pending-cap protection, and late result
handling.
- Host-agent authentication for inbound coold streams.
Flux does not inspect product meaning. `containers.start` is just a frame to a
host; Flux does not know it is part of an nginx deployment.
## coold owns
- Local Podman access.
- Host runtime primitives: images, containers, volumes, networks, logs, exec,
health checks, host facts.
- Firewall mutation and reconciliation as the sole kernel firewall writer.
- Embedded DNS and service-discovery sync.
- Writing this host's service endpoint rows to Corrosion.
- Builder subprocess supervision when the host advertises builder capability.
- Host-level safety checks such as container-create deny filters.
coold does not store Coolify secrets, users, teams, app ownership, deployment
history, billing data, or business audit.
## Boundary test
If a different orchestrator could reuse the same operation with its own app
model, it probably belongs in coold. If the operation only makes sense because
of Coolify's product model, it belongs in Coolify.
+15 -22
View File
@@ -30,7 +30,6 @@ 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)"
START_TIMEOUT="$(read_coolify_env COOLIFY_COOLD_VM_START_TIMEOUT 300)"
SSH_PORT="$(read_coolify_env COOLIFY_COOLD_VM_SSH_PORT 60002)"
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 "")"
@@ -42,7 +41,6 @@ if [ "$BUILDER_CAPACITY" = "0" ]; then
BUILDER_ENABLED="false"
fi
TEMPLATE="$ROOT/dev/lima/coold.yaml"
GENERATED="$ROOT/.dev/lima/${INSTANCE}.generated.yaml"
GUEST_COOLIFY_ROOT="/workspace/coolify"
usage() {
@@ -72,7 +70,6 @@ Environment:
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_START_TIMEOUT Seconds to wait for Lima SSH/provisioning (default: 300)
COOLIFY_COOLD_VM_SSH_PORT Host SSH port forwarded to this VM (default: 60002)
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
@@ -341,8 +338,6 @@ COOLIFY_COOLD_PODMAN_SOCKET="${COOLIFY_COOLD_PODMAN_SOCKET:-/run/podman/podman.s
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}" \
@@ -417,7 +412,6 @@ start_agent() {
configure_system_resolved
ensure_mesh_dns_anchor
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]
@@ -446,8 +440,6 @@ 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
@@ -468,19 +460,7 @@ UNIT
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" \
-e "s#{{COOLIFY_COOLD_VM_SSH_PORT}}#$SSH_PORT#g" \
"$TEMPLATE" > "$GENERATED"
}
start_vm() {
generate_yaml
if instance_running; then
return
fi
@@ -491,7 +471,7 @@ start_vm() {
if instance_exists; then
limactl start --tty=false "$INSTANCE"
else
limactl start --tty=false --name="$INSTANCE" "$GENERATED"
limactl start --tty=false --name="$INSTANCE" "$TEMPLATE"
fi
}
@@ -604,14 +584,27 @@ wait_for_guest_provisioning() {
| awk '{ print "[guest] " $0; fflush(); }' || true
}
ensure_mdns_hostname() {
local current_hostname
current_hostname="$(lima_shell hostname 2>/dev/null || true)"
if [ "$current_hostname" != "$INSTANCE" ]; then
echo "==> Setting guest hostname to ${INSTANCE} for ${INSTANCE}.local mDNS..."
lima_shell sudo hostnamectl set-hostname "$INSTANCE"
fi
lima_shell sudo systemctl restart avahi-daemon.service
}
up_with_logs() {
echo "==> Coolify coold VM: $INSTANCE"
echo "==> coold package tag: $VERSION"
echo "==> corrosion package tag: $CORROSION_VERSION"
echo "==> Lima config: $GENERATED"
echo "==> Lima config: $TEMPLATE"
wait_for_lima_start
wait_for_guest_provisioning
ensure_mdns_hostname
echo "==> VM is ready. Run coolify bootstrap via: scripts/dev.sh up"
}
+104 -315
View File
@@ -60,9 +60,71 @@ coold_vm_wg_port() {
read_coolify_env "COOLIFY_COOLD_VM_WG_PORT_${index}" "$((51820 + index))"
}
coold_vm_ssh_port() {
coold_vm_dns_name() {
local index="$1"
read_coolify_env "COOLIFY_COOLD_VM_SSH_PORT_${index}" "6000${index}"
printf '%s.local\n' "$(coold_vm_instance "$index")"
}
resolve_lima_dns_name() {
local name="$1"
local ip=""
if command -v dscacheutil >/dev/null 2>&1; then
ip="$(dscacheutil -q host -a name "$name" 2>/dev/null | awk '/ip_address:/ && $2 ~ /^[0-9.]+$/ { print $2; exit }')"
fi
if [ -z "$ip" ] && command -v getent >/dev/null 2>&1; then
ip="$(getent ahostsv4 "$name" 2>/dev/null | awk '{ print $1; exit }')"
fi
if [ -z "$ip" ]; then
ip="$(ping -c 1 -W 1 "$name" 2>/dev/null | sed -n 's/^PING .* (\([0-9.]*\)).*$/\1/p' | head -n 1)"
fi
if [ -z "$ip" ]; then
echo "ERROR: Could not resolve ${name} from the host." >&2
echo "Check that the Lima VM is running, bridged networking is enabled, and Avahi/mDNS finished provisioning." >&2
return 1
fi
printf '%s\n' "$ip"
}
sync_lima_hosts_into_coolify_container() {
local count
local hosts_file="$ROOT/.dev/lima/hosts"
local name
local ip
count="$(coold_vm_count)"
mkdir -p "$(dirname "$hosts_file")"
: > "$hosts_file"
for index in $(seq 1 "$count"); do
name="$(coold_vm_dns_name "$index")"
ip="$(resolve_lima_dns_name "$name")" || return 1
printf '%s %s\n' "$ip" "$name" >> "$hosts_file"
done
echo "==> Syncing Lima .local host records into the Coolify container..."
spin exec -T -u root coolify sh -lc '
set -e
records=/tmp/coolify-lima-hosts
next=/tmp/coolify-hosts-next
cat > "$records"
cp /etc/hosts "$next"
while read -r ip name; do
if [ -z "$ip" ] || [ -z "$name" ]; then
continue
fi
awk -v name="$name" '\''$0 !~ ("(^|[[:space:]])" name "([[:space:]]|$)") { print }'\'' "$next" > "${next}.filtered"
printf "%s %s\n" "$ip" "$name" >> "${next}.filtered"
mv "${next}.filtered" "$next"
done < "$records"
cat "$next" > /etc/hosts
rm -f "$records" "$next" "${next}.filtered"
' < "$hosts_file"
}
coold_vm_container_subnet() {
@@ -75,124 +137,13 @@ coold_vm_container_gateway() {
read_coolify_env "COOLIFY_COOLD_VM_CONTAINER_GATEWAY_${index}" "10.210.$((index - 1)).1"
}
host_arch() {
case "$(uname -m)" in
x86_64|amd64)
printf '%s\n' amd64
;;
arm64|aarch64)
printf '%s\n' arm64
;;
*)
echo "ERROR: unsupported host architecture: $(uname -m)" >&2
exit 1
;;
esac
}
host_os() {
case "$(uname -s)" in
Darwin)
printf '%s\n' darwin
;;
Linux)
printf '%s\n' linux
;;
*)
echo "ERROR: unsupported host OS: $(uname -s)" >&2
exit 1
;;
esac
}
coolify_cli_bin() {
printf '%s\n' "$ROOT/.dev/bin/coolify"
}
ensure_coolify() {
local bin
local version
local arch
local os
local url
local tmpdir
bin="$(coolify_cli_bin)"
version="$(read_coolify_env COOLIFY_CLI_VERSION "$(read_coolify_env COOLIFY_COOLD_VERSION nightly)")"
arch="$(host_arch)"
os="$(host_os)"
url="https://github.com/coollabsio/coold/releases/download/${version}/coolify-${os}-${arch}.tar.gz"
echo "==> Installing coolify from ${url}"
mkdir -p "$(dirname "$bin")"
tmpdir="$(mktemp -d)"
if ! curl -fsSL --retry 3 --max-time 120 -o "$tmpdir/coolify.tar.gz" "$url"; then
rm -rf "$tmpdir"
return 1
fi
tar -xzf "$tmpdir/coolify.tar.gz" -C "$tmpdir"
install -m 0755 "$tmpdir/coolify" "$bin"
rm -rf "$tmpdir"
printf '%s\n' '/usr/local/bin/coolify'
}
lima_ssh_target() {
local index="$1"
local instance
instance="$(coold_vm_instance "$index")"
if [ ! -f "$HOME/.lima/${instance}/ssh.config" ]; then
echo "ERROR: Lima SSH config for ${instance} was not found. Start it first with scripts/dev.sh up." >&2
exit 1
fi
printf 'lima-%s\n' "$instance"
}
lima_ssh_config() {
local count
local config="$ROOT/.dev/lima/ssh.config"
local instance
count="$(coold_vm_count)"
mkdir -p "$(dirname "$config")"
: > "$config"
for index in $(seq 1 "$count"); do
instance="$(coold_vm_instance "$index")"
if [ ! -f "$HOME/.lima/${instance}/ssh.config" ]; then
echo "ERROR: Lima SSH config for ${instance} was not found. Start it first with scripts/dev.sh up." >&2
exit 1
fi
cat "$HOME/.lima/${instance}/ssh.config" >> "$config"
printf '\n' >> "$config"
done
printf '%s\n' "$config"
}
lima_ssh_port() {
local index="$1"
local instance
local port
instance="$(coold_vm_instance "$index")"
if [ ! -f "$HOME/.lima/${instance}/ssh.config" ]; then
echo "ERROR: Lima SSH config for ${instance} was not found. Start it first with scripts/dev.sh up." >&2
exit 1
fi
port="$(awk 'tolower($1) == "port" { print $2; exit }' "$HOME/.lima/${instance}/ssh.config")"
if [ -z "$port" ]; then
echo "ERROR: Lima SSH port for ${instance} was not found in $HOME/.lima/${instance}/ssh.config." >&2
exit 1
fi
printf '%s\n' "$port"
coold_vm_dns_name "$index"
}
coolify_nodes_arg() {
@@ -240,18 +191,37 @@ coolify_wg_endpoint_overrides_arg() {
if [ -n "$overrides" ]; then
overrides="${overrides},"
fi
overrides="${overrides}${node}=host.lima.internal:$(coold_vm_wg_port "$index")"
overrides="${overrides}${node}=$(coold_vm_dns_name "$index"):$(coold_vm_wg_port "$index")"
done
printf '%s\n' "$overrides"
}
coolify_ssh_key() {
read_coolify_env COOLIFY_CLI_SSH_KEY "$HOME/.lima/_config/user"
read_coolify_env COOLIFY_CLI_SSH_KEY "/var/www/html/.dev/lima/ssh_key"
}
coolify_host_ssh_key_source() {
read_coolify_env COOLIFY_CLI_HOST_SSH_KEY "$HOME/.lima/_config/user"
}
ensure_coolify_container_ssh_key() {
local source_key
local target_key="$ROOT/.dev/lima/ssh_key"
source_key="$(coolify_host_ssh_key_source)"
if [ ! -f "$source_key" ]; then
echo "ERROR: SSH key for dev Lima VMs was not found at ${source_key}." >&2
echo "Set COOLIFY_CLI_HOST_SSH_KEY to the host-side private key that Lima authorized." >&2
exit 1
fi
mkdir -p "$(dirname "$target_key")"
install -m 0600 "$source_key" "$target_key"
}
coolify_ssh_user() {
read_coolify_env COOLIFY_CLI_SSH_USER "$USER"
read_coolify_env COOLIFY_CLI_SSH_USER coolify
}
coolify_bootstrap_concurrency() {
@@ -264,20 +234,17 @@ coolify_bootstrap_ssh_timeout() {
coolify_bootstrap_command() {
local nodes
local ssh_config
local listen_overrides
local endpoint_overrides
ensure_coolify
nodes="$(coolify_nodes_arg)" || return 1
ssh_config="$(lima_ssh_config)" || return 1
listen_overrides="$(coolify_wg_listen_overrides_arg)" || return 1
endpoint_overrides="$(coolify_wg_endpoint_overrides_arg)" || return 1
cat <<CMD
$(coolify_cli_bin) init bootstrap \\
spin exec -T coolify $(coolify_cli_bin) init bootstrap \\
--nodes "${nodes}" \\
--ssh-config "${ssh_config}" \\
--ssh-key "$(coolify_ssh_key)" \\
--ssh-user "$(coolify_ssh_user)" \\
--concurrency "$(coolify_bootstrap_concurrency)" \\
--ssh-timeout "$(coolify_bootstrap_ssh_timeout)" \\
@@ -292,19 +259,17 @@ CMD
coolify_bootstrap() {
local nodes
local ssh_config
local listen_overrides
local endpoint_overrides
ensure_coolify
ensure_coolify_container_ssh_key
nodes="$(coolify_nodes_arg)" || return 1
ssh_config="$(lima_ssh_config)" || return 1
listen_overrides="$(coolify_wg_listen_overrides_arg)" || return 1
endpoint_overrides="$(coolify_wg_endpoint_overrides_arg)" || return 1
"$(coolify_cli_bin)" init bootstrap \
spin exec -T coolify "$(coolify_cli_bin)" init bootstrap \
--nodes "$nodes" \
--ssh-config "$ssh_config" \
--ssh-key "$(coolify_ssh_key)" \
--ssh-user "$(coolify_ssh_user)" \
--concurrency "$(coolify_bootstrap_concurrency)" \
--ssh-timeout "$(coolify_bootstrap_ssh_timeout)" \
@@ -373,27 +338,25 @@ coolify_dev() {
case "$command" in
install)
ensure_coolify
"$(coolify_cli_bin)" --version
echo "==> coolify CLI is provided by the Coolify dev container."
spin exec -T coolify "$(coolify_cli_bin)" --version
;;
path)
ensure_coolify
coolify_cli_bin
;;
bootstrap-command)
coolify_bootstrap_command
;;
run)
ensure_coolify
exec "$(coolify_cli_bin)" "$@"
exec spin exec -T coolify "$(coolify_cli_bin)" "$@"
;;
-h|--help|help)
cat <<'USAGE'
Usage: scripts/dev.sh coolify <command>
Commands:
install Download/install the nightly coolify dev binary
path Print the local coolify path
install Print the coolify CLI version from the dev container
path Print the coolify CLI path inside the dev container
bootstrap-command Print the dev Lima bootstrap command without running it
run <args> Run coolify with arbitrary args
@@ -414,7 +377,6 @@ coold_vm() {
shift
COOLIFY_COOLD_LIMA_INSTANCE="$(coold_vm_instance "$index")" \
COOLIFY_COOLD_VM_WG_IP="$(coold_vm_wg_ip "$index")" \
COOLIFY_COOLD_VM_SSH_PORT="$(coold_vm_ssh_port "$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 "$@"
@@ -524,8 +486,7 @@ sync_v5_dev_lima_servers() {
for index in $(seq 1 "$count"); do
instance="$(coold_vm_instance "$index")"
ssh_port="$(lima_ssh_port "$index")"
server_args+=(--server="${instance}|host.docker.internal|${ssh_user}|${ssh_port}|$(coold_vm_wg_ip "$index")")
server_args+=(--server="${instance}|$(coold_vm_dns_name "$index")|${ssh_user}|22|$(coold_vm_wg_ip "$index")")
done
echo "==> Running pending migrations before syncing v5 dev Lima state..."
@@ -605,6 +566,10 @@ up() {
COOLIFY_CLI_SSH_USER="$(coolify_ssh_user)" spin up -d
fi
if [ "$coold_vm_enabled" != "false" ]; then
sync_lima_hosts_into_coolify_container
fi
if [ "$naked" = "true" ]; then
echo "==> --naked enabled. Skipping coolify bootstrap and Flux VM wiring. Use /v5 to bootstrap hosts from the UI."
return
@@ -940,7 +905,7 @@ SH
example_nginx_require_pair() {
if [ "$(coold_vm_count)" = "1" ]; then
echo "ERROR: example-nginx ping/firewall commands require COOLIFY_COOLD_VM_COUNT=2." >&2
echo "ERROR: example-nginx ping command requires COOLIFY_COOLD_VM_COUNT=2." >&2
exit 1
fi
}
@@ -966,28 +931,6 @@ echo 'ok: coolify-example-nginx can reach coolify-example-nginx-2 on tcp/80'
SH
}
example_nginx_firewall_up() {
local src
local dst
example_nginx_require_pair
src="$(example_nginx_container_ip 1)"
dst="$(example_nginx_container_ip 2)"
scripts/dev.sh firewall allow "$src" "$dst" tcp 80
}
example_nginx_firewall_down() {
local src
local dst
example_nginx_require_pair
src="$(example_nginx_container_ip 1)"
dst="$(example_nginx_container_ip 2)"
scripts/dev.sh firewall revoke "$src" "$dst" tcp 80
}
example_nginx_help() {
cat <<'USAGE'
Usage: scripts/dev.sh example-nginx <command>
@@ -997,8 +940,6 @@ Commands:
down Remove the example nginx containers
check-dns Verify host 1 nginx can resolve host 2 nginx through coold DNS
ping Verify host 1 nginx can reach host 2 nginx on tcp/80
firewall up Allow host 1 nginx to reach host 2 nginx through the coolify CLI
firewall down Revoke the example nginx tcp/80 allow rule through the coolify CLI
USAGE
}
@@ -1021,21 +962,6 @@ example_nginx() {
ping)
example_nginx_ping
;;
firewall)
case "${1:-help}" in
up)
example_nginx_firewall_up
;;
down)
example_nginx_firewall_down
;;
*)
echo "unknown example-nginx firewall command: ${1:-help}" >&2
echo "Run: scripts/dev.sh example-nginx help" >&2
exit 1
;;
esac
;;
-h|--help|help)
example_nginx_help
;;
@@ -1047,139 +973,6 @@ example_nginx() {
esac
}
firewall_help() {
cat <<'USAGE'
Usage: scripts/dev.sh firewall <command>
Commands:
allow <src> <dst> [proto] [port] Allow traffic through the coolify CLI (proto/port optional)
revoke [id|src] [dst] [proto] [port]
Remove an allow rule through the coolify CLI
list List allow rules through the coolify CLI
containers List registered containers through the coolify CLI
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
}
coolify_firewall() {
local command="$1"
shift
local nodes
local ssh_config
ensure_coolify
nodes="$(coolify_nodes_arg)" || return 1
ssh_config="$(lima_ssh_config)" || return 1
"$(coolify_cli_bin)" firewall "$command" \
--nodes "$nodes" \
--ssh-config "$ssh_config" \
--ssh-user "$(coolify_ssh_user)" \
"$@"
}
firewall_allow() {
local src="${1:-}"
local dst="${2:-}"
local proto="${3:-}"
local port="${4:-}"
local args=()
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
args+=(--from "$src" --to "$dst")
if [ -n "$proto" ]; then
args+=(--proto "$proto")
fi
if [ -n "$port" ]; then
args+=(--port "$port")
fi
coolify_firewall allow "${args[@]}"
}
firewall_revoke() {
local id_or_src="${1:-}"
local dst="${2:-}"
local proto="${3:-}"
local port="${4:-}"
local args=()
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
args+=(--id "$id_or_src")
else
args+=(--from "$id_or_src" --to "$dst")
if [ -n "$proto" ]; then
args+=(--proto "$proto")
fi
if [ -n "$port" ]; then
args+=(--port "$port")
fi
fi
coolify_firewall revoke "${args[@]}"
}
firewall_list() {
coolify_firewall list "$@"
}
firewall_containers() {
coolify_firewall containers "$@"
}
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 "$@"
;;
containers)
firewall_containers "$@"
;;
-h|--help|help)
firewall_help
;;
*)
echo "unknown firewall command: $command" >&2
echo "Run: scripts/dev.sh firewall help" >&2
exit 1
;;
esac
}
refresh_test_host_key() {
echo "==> Refreshing /tmp/testhostkey inside coolify..."
@@ -1258,7 +1051,6 @@ Commands:
clean-vms Delete the coold Lima VMs and all VM-local runtime state (alias for down --cleanup)
naked-vm Recreate the naked Lima VM used for bootstrap testing
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
coolify <command> Install/run the released coolify dev helper
USAGE
@@ -1294,9 +1086,6 @@ case "$cmd" in
corrosion)
corrosion "$@"
;;
firewall)
firewall "$@"
;;
example-nginx)
example_nginx "$@"
;;
+4 -5
View File
@@ -21,11 +21,10 @@ it('does not include coold dev tooling defaults in the development env example',
]);
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')
expect(config('coold.coolify_cli_bin'))->toBe('/usr/local/bin/coolify')
->and(config('coold.coold_version'))->toBe('nightly')
->and(config('coold.corrosion_version'))->toBe('v1.0.0')
->and(config('coold.dev_ssh_user'))->toBe('coolify')
->and(config('coold.dev_builder_capacity'))->toBe(2)
->and(config('coold.dev_builder_enabled'))->toBeTrue();
});
+6 -6
View File
@@ -2,22 +2,22 @@
use Symfony\Component\Process\Process;
it('documents example nginx connectivity and firewall commands', function () {
it('documents example nginx connectivity commands', function () {
$process = new Process(['bash', base_path('scripts/dev.sh'), 'example-nginx', 'help'], base_path());
$process->run();
expect($process->isSuccessful())->toBeTrue()
->and($process->getOutput())->toContain('ping')
->and($process->getOutput())->toContain('firewall up')
->and($process->getOutput())->toContain('firewall down')
->and($process->getOutput())->not->toContain('firewall up')
->and($process->getOutput())->not->toContain('firewall down')
->and($process->getOutput())->not->toContain('firewall-up')
->and($process->getOutput())->not->toContain('firewall-down');
});
it('uses the coolify firewall wrapper for example nginx firewall changes', function () {
it('does not wire example nginx to removed firewall CLI commands', function () {
$script = file_get_contents(base_path('scripts/dev.sh'));
expect($script)->toContain('scripts/dev.sh firewall allow "$src" "$dst" tcp 80')
->and($script)->toContain('scripts/dev.sh firewall revoke "$src" "$dst" tcp 80');
expect($script)->not->toContain('scripts/dev.sh firewall allow "$src" "$dst" tcp 80')
->and($script)->not->toContain('scripts/dev.sh firewall revoke "$src" "$dst" tcp 80');
});
@@ -1,12 +1,11 @@
<?php
it('delegates dev firewall commands to the coolify CLI instead of calling coold APIs directly', function () {
it('does not expose removed dev firewall commands', function () {
$script = file_get_contents(base_path('scripts/dev.sh'));
expect($script)->toContain('coolify_firewall()')
->and($script)->toContain('"$(coolify_cli_bin)" firewall "$command"')
->and($script)->toContain('--nodes "$nodes"')
->and($script)->toContain('--ssh-config "$ssh_config"')
expect($script)->not->toContain('coolify_firewall()')
->and($script)->not->toContain('scripts/dev.sh firewall')
->and($script)->not->toContain('firewall <command>')
->and($script)->not->toContain('/api/v1/firewall/allow')
->and($script)->not->toContain('firewall_api_for_each_vm');
});
@@ -22,13 +21,28 @@ it('installs the coolify CLI in both application container images', function (st
'production image' => 'docker/production/Dockerfile',
]);
it('refreshes the host coolify CLI on every dev script run', function () {
it('runs the coolify CLI from the development application container', function () {
$script = file_get_contents(base_path('scripts/dev.sh'));
expect($script)->toContain('url="https://github.com/coollabsio/coold/releases/download/${version}/coolify-${os}-${arch}.tar.gz"')
->and($script)->toContain('==> Installing coolify from ${url}')
->and($script)->not->toContain('COOLIFY_CLI_FORCE_DOWNLOAD')
->and($script)->not->toContain('if [ -x "$bin" ] && "$bin" --version >/dev/null 2>&1');
expect($script)->toContain("printf '%s\\n' '/usr/local/bin/coolify'")
->and($script)->toContain('spin exec -T coolify "$(coolify_cli_bin)" init bootstrap')
->and($script)->toContain('spin exec -T coolify "$(coolify_cli_bin)" "$@"')
->and($script)->toContain('ensure_coolify_container_ssh_key')
->and($script)->not->toContain('.dev/bin/coolify')
->and($script)->not->toContain('coolify-${os}-${arch}.tar.gz');
});
it('syncs host-resolved Lima local names into the Coolify container hosts file', function () {
$script = file_get_contents(base_path('scripts/dev.sh'));
expect($script)->toContain('resolve_lima_dns_name()')
->and($script)->toContain('dscacheutil -q host -a name "$name"')
->and($script)->toContain('getent ahostsv4 "$name"')
->and($script)->toContain('sync_lima_hosts_into_coolify_container()')
->and($script)->toContain('spin exec -T -u root coolify sh -lc')
->and($script)->toContain('cat "$next" > /etc/hosts')
->and($script)->toContain('sync_lima_hosts_into_coolify_container')
->and($script)->toContain('if [ "$naked" = "true" ]; then');
});
it('does not require predefined UI node environment variables in the development app container', function () {
@@ -81,28 +95,38 @@ it('seeds bootstrapped Lima VMs into v5 development server state', function () {
expect($script)->toContain('sync_v5_dev_lima_servers()')
->and($script)->toContain('COOLIFY_CLI_SSH_USER="$ssh_user"')
->and($script)->toContain('COOLIFY_CLI_SSH_USER="$(coolify_ssh_user)" spin up -d')
->and($script)->toContain('lima_ssh_port "$index"')
->and($script)->toContain('host.docker.internal')
->and($script)->toContain('coold_vm_dns_name()')
->and($script)->toContain('$(coold_vm_dns_name "$index")')
->and($script)->toContain('v5:sync-dev-lima-servers')
->and($script)->toContain('--server="${instance}|host.docker.internal|${ssh_user}|${ssh_port}|$(coold_vm_wg_ip "$index")"')
->and($script)->toContain('--server="${instance}|$(coold_vm_dns_name "$index")|${ssh_user}|22|$(coold_vm_wg_ip "$index")"')
->and($compose)->toContain('COOLIFY_CLI_SSH_USER: "${COOLIFY_CLI_SSH_USER:-}"')
->and($script)->not->toContain('db:seed --class=V5DevLimaSeeder --force')
->and($script)->not->toContain('--server "${instance}|${node}|$(coolify_ssh_user)|22"');
->and($script)->not->toContain('host.docker.internal|${ssh_user}');
});
it('configures predictable Lima SSH local ports for dev VMs', function () {
it('uses the shared Lima template directly instead of generated per-VM YAML', function () {
$script = file_get_contents(base_path('scripts/dev.sh'));
$vmScript = file_get_contents(base_path('scripts/coold-vm.sh'));
$template = file_get_contents(base_path('dev/lima/coold.yaml'));
expect($script)->toContain('coold_vm_ssh_port()')
->and($script)->toContain('COOLIFY_COOLD_VM_SSH_PORT_')
->and($script)->toContain('6000${index}')
->and($script)->toContain('COOLIFY_COOLD_VM_SSH_PORT="$(coold_vm_ssh_port "$index")"')
->and($vmScript)->toContain('SSH_PORT="$(read_coolify_env COOLIFY_COOLD_VM_SSH_PORT 60002)"')
->and($vmScript)->toContain('{{COOLIFY_COOLD_VM_SSH_PORT}}')
->and($template)->toContain('ssh:')
->and($template)->toContain('localPort: {{COOLIFY_COOLD_VM_SSH_PORT}}');
expect($script)->toContain('coold_vm_dns_name()')
->and($script)->not->toContain('COOLIFY_COOLD_VM_SSH_PORT="$(coold_vm_ssh_port "$index")"')
->and($vmScript)->toContain('limactl start --tty=false --name="$INSTANCE" "$TEMPLATE"')
->and($vmScript)->not->toContain('GENERATED=')
->and($vmScript)->not->toContain('generate_yaml()')
->and($template)->not->toContain('{{COOLIFY_COOLD_VM_SSH_PORT}}');
});
it('sets the Lima guest hostname to the instance name for predictable mDNS', function () {
$vmScript = file_get_contents(base_path('scripts/coold-vm.sh'));
$template = file_get_contents(base_path('dev/lima/coold.yaml'));
expect($vmScript)->toContain('ensure_mdns_hostname()')
->and($vmScript)->toContain('hostnamectl set-hostname "$INSTANCE"')
->and($vmScript)->toContain('systemctl restart avahi-daemon.service')
->and($vmScript)->toContain('ensure_mdns_hostname')
->and($template)->toContain('hostnamectl set-hostname "{{.Name}}"')
->and($template)->not->toContain('hostnamectl set-hostname myvm');
});
it('authorizes the seeded testing host key in dev Lima VMs', function () {
@@ -112,10 +136,11 @@ it('authorizes the seeded testing host key in dev Lima VMs', function () {
->and($template)->toContain('ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuGmoeGq/pojrsyP1pszcNVuZx9iFkCELtxrh31QJ68')
->and($template)->toContain('authorized_keys')
->and($template)->toContain('grep -qxF "$coolify_test_public_key"')
->and($template)->toContain('install -d -m 700 /root/.ssh')
->and($template)->toContain('/root/.ssh/authorized_keys')
->and($template)->toContain('target_home="$(getent passwd 501 | cut -d: -f6 || true)"')
->and($template)->toContain('owner="$(stat -c \'%u:%g\' "$target_home")"')
->and($template)->toContain('install_public_key root')
->and($template)->toContain('install_public_key coolify')
->and($template)->toContain('owner="$(stat -c \'%u:%g\' "$home")"')
->and($template)->toContain('hostnamectl set-hostname "{{.Name}}"')
->and($template)->not->toContain('hostnamectl set-hostname myvm')
->and($template)->not->toContain('mode: user')
->and($template)->not->toContain('user="$(basename "$home")"');
});
+3 -3
View File
@@ -4710,8 +4710,8 @@ it('seeds dev Lima VMs into v5 clusters and servers idempotently', function () {
->and($cluster->created_by_user_id)->toBe($user->id)
->and($cluster->description)->toBe('Local Lima development cluster managed by scripts/dev.sh.')
->and(V5Server::query()->count())->toBe(2)
->and(V5Server::query()->where('name', 'coold-dev')->where('host', 'host.docker.internal')->where('ssh_user', get_current_user())->where('ssh_port', 60001)->exists())->toBeTrue()
->and(V5Server::query()->where('name', 'coold-dev-2')->where('host', 'host.docker.internal')->where('ssh_user', get_current_user())->where('ssh_port', 60002)->exists())->toBeTrue()
->and(V5Server::query()->where('name', 'coold-dev')->where('host', 'coold-dev.local')->where('ssh_user', 'coolify')->where('ssh_port', 22)->exists())->toBeTrue()
->and(V5Server::query()->where('name', 'coold-dev-2')->where('host', 'coold-dev-2.local')->where('ssh_user', 'coolify')->where('ssh_port', 22)->exists())->toBeTrue()
->and(V5Server::query()->where('name', 'coold-dev')->where('node_address', '100.64.0.1')->where('wireguard_management_ip', '100.64.0.1')->exists())->toBeTrue()
->and(V5Server::query()->where('name', 'coold-dev-2')->where('node_address', '100.64.0.2')->where('wireguard_management_ip', '100.64.0.2')->exists())->toBeTrue()
->and(V5Server::query()->where('status', 'installed')->count())->toBe(2)
@@ -4747,7 +4747,7 @@ it('seeds dev Lima VMs by updating existing named servers', function () {
(new V5DevLimaSeeder)->run();
expect(V5Server::query()->where('name', 'coold-dev')->count())->toBe(1)
->and(V5Server::query()->where('name', 'coold-dev')->where('host', 'host.docker.internal')->where('ssh_port', 60001)->exists())->toBeTrue()
->and(V5Server::query()->where('name', 'coold-dev')->where('host', 'coold-dev.local')->where('ssh_port', 22)->exists())->toBeTrue()
->and(V5Server::query()->count())->toBe(2);
});