infrastructure-as-coolify

module
v0.1.0-rc.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 29, 2026 License: CC0-1.0

README ΒΆ

infrastructure-as-coolify

CI Release License Go version Go Report Card GHCR cosign SLSA 3

Declarative Infrastructure as Code for Coolify β€” YAML in, plan/apply/destroy out. No HCL, no state file, no magic.

iac-coolify is a single-binary CLI that manages Coolify v4 resources declaratively, the way Terraform manages cloud resources β€” but native to Coolify, with Kubernetes-style YAML (apiVersion/kind/metadata/spec), a stateless-first model, auto-generated docs, and a future Bubble Tea TUI for live exploration.

Status: 🚧 beta β€” Coolify v4.x only. APIs may change before v1.0.0.

The Coolify OpenAPI commit SHA is pinned in testdata/openapi/COMMIT_SHA (currently 5a27427), with a SHA-256 sidecar verified at boot. A nightly CI job watches upstream v4.x for drift.

Supported platforms
OS amd64 arm64
Linux βœ… βœ…
macOS βœ… βœ…
Windows βœ… β€”

Quick start

# Validate your declarative config
iac-coolify validate examples/minimal/

# Preview the changes against live Coolify (Terraform-style diff)
export COOLIFY_API_TOKEN=...                       # plus --coolify-url or COOLIFY_API_URL
iac-coolify plan examples/minimal/                 # runs offline (all-new) if unconfigured
iac-coolify plan examples/minimal/ --output=json --detailed-exitcode

# Apply the changes (creates projects and environments before applications)
iac-coolify apply examples/full-project/ --dry-run # offline preview, mutates nothing
iac-coolify apply examples/full-project/           # interactive confirmation prompt
iac-coolify apply examples/full-project/ --auto-approve   # required in CI / non-interactive

# Tear the resources down (reverse dependency order)
iac-coolify destroy examples/full-project/ --dry-run      # offline preview, deletes nothing
iac-coolify destroy examples/full-project/ --auto-approve # required in CI / non-interactive

# Generate the reference documentation from the resource structs
iac-coolify docs gen

plan resolves Coolify UUIDs from your logical metadata names (never written to YAML), fetches live state, and prints a per-field diff. --detailed-exitcode returns 0 (no changes), 2 (changes pending), or 1 (error). Secret values follow a Notify-only policy: a change is announced (resolved value changed, source ${env:X} unchanged) but never shown. Behind a Cloudflare Access gateway, set CF_ACCESS_CLIENT_ID and CF_ACCESS_CLIENT_SECRET.

apply reconciles the desired state, creating resources in dependency order (project β†’ environment β†’ application). It refuses to run in a non-interactive session unless --auto-approve is given, and exits 0 (success), 1 (error), or 2 (partial: some resources changed before a failure β€” no rollback, the append-only .iac-coolify/audit.log records each applied operation and the sources of any secrets, never their values). Every write carries an Idempotency-Key, so a retried apply cannot create duplicates.

destroy deletes the declared resources in reverse dependency order (applications and services first, then environments, then projects). Only resources that still exist remotely are deleted, so a repeated destroy is a no-op; a 404 on delete is treated as success. Like apply, it refuses a non-interactive session without --auto-approve and shares the same exit codes and audit log.

Install

Download a binary for your platform from the releases page, or pull the container image:

docker pull ghcr.io/rems08/infrastructure-as-coolify:latest
docker run --rm ghcr.io/rems08/infrastructure-as-coolify version

Images are multi-arch (linux/amd64, linux/arm64). Replace :latest with a version such as :0.1.0-rc.2 to pin a release.

Verifying release signatures

Every release is signed with cosign keyless (no long-lived key β€” the signature is bound to this repository's GitHub Actions identity) and ships SLSA build level 3 provenance. Set TAG to the release you downloaded, e.g. TAG=v0.1.0-rc.2.

# Verify a downloaded archive against its cosign bundle
cosign verify-blob \
  --bundle iac-coolify_${TAG#v}_linux_amd64.tar.gz.bundle \
  --certificate-identity-regexp 'https://github.com/Rems08/infrastructure-as-coolify/.github/workflows/release.yml@.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  iac-coolify_${TAG#v}_linux_amd64.tar.gz

# Verify the container image
cosign verify ghcr.io/rems08/infrastructure-as-coolify:${TAG#v} \
  --certificate-identity-regexp 'https://github.com/Rems08/infrastructure-as-coolify/.github/workflows/release.yml@.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

# Verify the SLSA provenance attached to the release
slsa-verifier verify-artifact \
  --provenance-path multiple.intoto.jsonl \
  --source-uri github.com/Rems08/infrastructure-as-coolify \
  --source-tag "$TAG" \
  iac-coolify_${TAG#v}_linux_amd64.tar.gz

Why

iac-coolify coollabsio/coolify-cli SierraJC/terraform-provider-coolify
Model declarative YAML imperative wrapper declarative HCL
plan/apply/destroy βœ… plan + apply + destroy ❌ βœ…
State file stateless-first n/a tfstate required
Native to Coolify βœ… βœ… wraps Terraform

Configuration

Resources are described in a coolify/ directory tree, one file per resource:

coolify/
  coolify.yaml                                   # global config (api_url, required_coolify)
  project.yaml                                   # kind: Project
  environments/<env>/environment.yaml            # kind: Environment
  environments/<env>/applications/<app>.yaml     # kind: Application
  environments/<env>/databases/<db>.yaml         # kind: Database (8 engines)
  environments/<env>/envvars/<set>.yaml          # kind: EnvVar (shared, referenced via env_vars_from)
  services/<svc>.yaml                            # kind: Service (docker-compose stack)

Supported resource kinds: Project, Environment, Application, Service, Database (postgresql, mysql, mariadb, mongodb, redis, keydb, dragonfly, clickhouse), and standalone EnvVar sets that an Application merges in by name (env_vars_from). apply creates projects and environments before the applications and services that reference them.

A Service is a docker-compose stack. It sources its definition from exactly one of:

  • docker_compose_path β€” a relative path to a docker-compose.yml in your repository. The path may not be absolute and may not resolve outside the config tree (a hostile compose path can never read /etc/passwd); the file is base64-encoded and sent to Coolify on apply.
  • type β€” a Coolify one-click template identifier such as gitea-with-mysql.

An Application is built by one of six build packs, each with its own source of truth:

  • dockerimage β€” a prebuilt image (image.name + image.tag).
  • dockerfile β€” either an inline dockerfile (Dockerfile content, no git) or a git source; exactly one of the two.
  • nixpacks, docker-compose, static, railpack β€” a public git source (git_repository + git_branch + ports_exposes).

A git source.git_repository must use an https://, http:// or git@ URL; an inline dockerfile is capped at 1 MB. The IaC build pack docker-compose is sent to Coolify under its upstream spelling dockercompose.

Private git sources: applications from a private GitHub App or deploy key are not yet supported (they need a separate credential resolver). Use a public repository, an inline Dockerfile, or a prebuilt image.

Service domains: Coolify binds domains per docker-compose sub-service, so a Service's fqdn is advisory metadata for now and is not applied on create.

See examples/ and the generated docs/reference/ (project.md, environment.md, application.md, service.md, database.md, envvar.md + JSON schemas).

Selecting environments

plan, apply and destroy operate on every environment found under the config path by default. Use --env <name> to restrict the run to one or more environments, matched against each resource's metadata.environment:

# Plan/apply/destroy a single environment
iac-coolify plan   infra/ --env staging
iac-coolify apply  infra/ --env production --auto-approve

# Several environments at once (repeat the flag; the result is their union)
iac-coolify apply  infra/ --env staging --env preview --auto-approve

# Compose --env with --target to act on one named resource within an environment
iac-coolify apply  infra/ --env staging --target back-office --auto-approve

--env composes with --target (logical resource name): a resource is selected only when it passes both filters. A --env value that no resource declares β€” or a --target/--env combination that matches nothing β€” exits with code 2 so a typo fails loudly instead of silently doing nothing. Projects are cross-environment and are never filtered by --env.

Both folder layouts work β€” the filter reads metadata.environment, not the path:

# Nested per environment (see examples/beenaire)        # Flat (see examples/full-stack)
infra/                                                  infra/
  environments/staging/applications/back-office.yaml      applications/back-office.yaml   # environment: staging
  environments/production/applications/api.yaml            applications/api.yaml           # environment: production

Managing databases

apply creates, updates and deletes managed databases alongside applications and services. A database is declared per engine; the engine selects the Coolify create endpoint:

api_version: iac-coolify/v1
kind: Database
metadata:
  name: pg-core
  project: beenaire
  environment: staging
spec:
  engine: postgresql        # postgresql|mysql|mariadb|mongodb|redis|keydb|dragonfly|clickhouse
  image: postgres:18-alpine
  destination:
    server: localhost
    network: coolify
  password: "${env:PG_PASSWORD}"   # opaque Secret; mapped to the engine's credential field
# Create or update the declared databases (creation is ordered after project + environment)
iac-coolify apply infra/ --env staging --auto-approve

# Restrict to one database by logical name
iac-coolify apply infra/ --target pg-core --auto-approve

# Delete databases (reverse dependency order; a full teardown of configs, volumes and networks)
iac-coolify destroy infra/ --env staging --auto-approve

The password is an opaque Secret: it is mapped to the engine-specific credential field (postgres_password, redis_password, mysql_password, …) and revealed only at the HTTP boundary, never in plan/apply output, errors or the audit log. The apply diff compares image, public/public_port and CPU/memory limits; a credential never field-diffs, so rotating a password is an explicit future operation rather than reconciled drift.

MongoDB passwords on create: the pinned Coolify v4 spec exposes no password field on the MongoDB create endpoint (only mongo_initdb_root_username), so a MongoDB password is not sent at creation time. Set it afterwards through Coolify.

Environment interpolation

Any visible (Param) string field can reference an environment variable with ${env:VAR} β€” metadata names, image.name/image.tag, fqdn, a git source, an inline dockerfile, destination, limits, and plain env_vars values. References are resolved at load time; an unset variable is an error (no silent fallback to "").

metadata:
  environment: "${env:DEPLOY_ENV}"
spec:
  image:
    tag: "${env:IMAGE_TAG}"
  fqdn: "https://${env:PUBLIC_HOST}"

Param fields are visible in plan/apply output once resolved. Never put a secret in a Param field β€” use value_secret (below) for sensitive values.

Secrets management

Secrets are an opaque Secret type: shown as [REDACTED] in every log, plan, apply output, error, and the state cache β€” only their origin (${env:NAME}) is ever displayed. A literal secret in YAML is rejected at parse time. The defences layer up:

Layer What
L0 Opaque Secret type redaction by construction (the value has no visible accessor)
L1 Env interpolation ${env:VAR} in Param fields and value_secret
L2 SOPS + age at rest ${sops:path} decrypted in memory from a colocated secrets.enc.yaml
L4 validate --strict flags a secret-like value mistakenly put in a visible value
L5 Redacted logs/plan guaranteed by L0; no value can reach output
L6 Audit log append-only 0600 log of who/what, with secret origins and a diff hash, never values

Ways to supply a value:

env_vars:
  - name: NODE_ENV
    value: "production"                     # visible literal
  - name: LOG_LEVEL
    value: "${env:LOG_LEVEL}"               # visible, env-resolved
  - name: DATABASE_URL
    value_secret: "${env:DATABASE_URL}"     # secret, env-sourced, REDACTED
  - name: STRIPE_KEY
    value_secret: "${sops:stripe.key}"      # secret, SOPS-decrypted, REDACTED
SOPS + age secrets at rest

A ${sops:path} reference is read from a secrets.enc.yaml colocated with the manifest (the path is never user-supplied, so there is no traversal surface), decrypted in memory, and shown as [REDACTED]. path is a dotted key into the decrypted document, e.g. databases.staging.password. A worked example lives in examples/secrets-sops/.

The committed secrets.enc.yaml is encrypted to a throwaway recipient, so re-encrypt it with your own key before running the example:

# 1. Generate an age key and restrict it (iac-coolify refuses a group/other-readable key).
age-keygen -o ~/.config/sops/age/keys.txt && chmod 600 ~/.config/sops/age/keys.txt

# 2. Put your public key (age1...) in examples/secrets-sops/.sops.yaml, then re-encrypt:
printf 'databases:\n  staging:\n    password: your-secret\n' \
  | sops --encrypt --input-type yaml /dev/stdin > examples/secrets-sops/secrets.enc.yaml

# 3. Validate β€” the SOPS value resolves and stays REDACTED.
iac-coolify validate examples/secrets-sops/coolify.yaml

iac-coolify locates the key via SOPS_AGE_KEY_FILE (or the default ~/.config/sops/age/keys.txt). Never commit a private age key β€” the repo .gitignore excludes *.age.key; only the encrypted file and the public recipient (.sops.yaml) are safe to track.

Viewing secret values

iac-coolify never reveals secret values, by design. To inspect one, go to the source β€” e.g. printenv DATABASE_URL. Secrets stay scoped to the tool that owns them (your shell).

CI integration

Pass COOLIFY_API_TOKEN (and any Cloudflare Access service-token headers) as masked CI secrets, and set IAC_COOLIFY_ACTOR so the audit log attributes the change. When you use SOPS, store the age private key as a masked secret (SOPS_AGE_KEY) and write it to an owner-only file pointed at by SOPS_AGE_KEY_FILE. Run plan on pull/merge requests and apply --auto-approve on the protected default branch. Ready-to-copy workflows: examples/ci/github-actions.yml and examples/ci/gitlab-ci.yml.

License

Apache-2.0. See SECURITY.md for responsible disclosure and CONTRIBUTING.md to get started.

Acknowledgements

Built on the shoulders of coollabsio/coolify, charmbracelet, spf13/cobra, and goreleaser.

Directories ΒΆ

Path Synopsis
cmd
iac-coolify command
Command iac-coolify is the declarative Infrastructure-as-Code CLI for Coolify v4.
Command iac-coolify is the declarative Infrastructure-as-Code CLI for Coolify v4.
internal
apply
Package apply reconciles desired iac-coolify resources with a live Coolify instance.
Package apply reconciles desired iac-coolify resources with a live Coolify instance.
config
Package config is the boundary between user-authored YAML and the typed resource model.
Package config is the boundary between user-authored YAML and the typed resource model.
coolify
Package coolify is the low-level HTTP client for the Coolify v4 API.
Package coolify is the low-level HTTP client for the Coolify v4 API.
docs
Package docs generates the reference documentation from the iac struct tags in internal/resource, keeping docs and code in lock-step (single source of truth).
Package docs generates the reference documentation from the iac struct tags in internal/resource, keeping docs and code in lock-step (single source of truth).
plan
Package plan computes the semantic difference between the desired state (YAML config) and the actual state (live Coolify), rendering it Terraform-style.
Package plan computes the semantic difference between the desired state (YAML config) and the actual state (live Coolify), rendering it Terraform-style.
resource
Package resource holds the user-facing declarative resource types (the iac-coolify YAML schema).
Package resource holds the user-facing declarative resource types (the iac-coolify YAML schema).
secrets
Package secrets provides the opaque Secret type and env-var sourcing used throughout iac-coolify.
Package secrets provides the opaque Secret type and env-var sourcing used throughout iac-coolify.
state
Package state holds the optional UUID resolver cache.
Package state holds the optional UUID resolver cache.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL