Skip to content

fix(api/dashboard): import Coolify's runtime env during server migration - #403

Merged
Hydralerne merged 2 commits into
oblien:mainfrom
Rish-it:fix/issue-394-coolify-env-import
Aug 3, 2026
Merged

fix(api/dashboard): import Coolify's runtime env during server migration#403
Hydralerne merged 2 commits into
oblien:mainfrom
Rish-it:fix/issue-394-coolify-env-import

Conversation

@Rish-it

@Rish-it Rish-it commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Migrating a Coolify server imported almost none of its environment variables — usually only the one or two an operator had set to something the image didn't already contain. Scope the image-default subtraction to containers Coolify doesn't manage, and surface the variables Docker genuinely cannot expose instead of leaving the gap silent.

Motivation

toDiscoveredService subtracts any container env entry whose exact KEY=VALUE matches the image's Config.Env, so a base image's dozen defaults don't masquerade as operator config. That reasoning holds for a hand-run docker run. It does not hold for Coolify.

Coolify resolves and re-injects every runtime variable explicitly — service-level, shared {{team.*}}, linked-resource and secret alike — and its Nixpacks builds bake those same resolved values into the image it then runs. The two sets overlap almost completely, so the subtraction discarded real production configuration.

Docker reports image defaults and explicitly-set values in one flat list, with no marker distinguishing them. Against a real daemon (29.6.2), an image that bakes in NODE_ENV / DATABASE_URL / REDIS_URL, run the way Coolify runs one:

container Config.Env : DATABASE_URL, REDIS_URL, APP_SECRET, NODE_ENV, PATH
image     Config.Env : PATH, NODE_ENV, DATABASE_URL, REDIS_URL

Four of the five entries are indistinguishable after the fact, so four of the five were dropped. Only APP_SECRET — the one value that happened not to be baked in — survived.

The label is the way out: Coolify stamps coolify.managed=true on every container it manages, for applications, services and databases alike (bootstrap/helpers/docker.php). Keying off it scopes the change to the platform that needs it — plain Docker and Compose migrations keep the base-image denoising and the test that covers it.

The issue also asks that unmigratable variables be reported. Coolify's build-time variables are passed as build args, and BuildKit secrets are never persisted, so neither survives in a running container's config. Discovery now says so per service. That warning had nowhere to render: DiscoveredService.warnings was computed and sent to the client, but no wizard step displayed it — so bind-mount and dropped-host-port warnings were invisible too.

Related issue

Closes #394

Changes

apps/api

  • modules/migration/docker-reconcile.ts — skip the image-default subtraction for containers carrying the coolify.managed label, reusing envArrayToRecord's existing "no image data, drop nothing" path; emit a warning naming build-time variables and BuildKit secrets as unrecoverable.
  • test/modules/migration/docker-inspect.test.ts — a Coolify-labelled fixture whose image defaults are exactly its runtime env, plus two tests: env survives when it matches the image default, and the unrecoverable-variable warning is reported. The existing "subtracts image-default env" test is untouched and still passes.

apps/dashboard

  • components/migration/ServerMigrationWizard.tsx — render service.warnings on the service config card, beside the env editor an operator would use to fill the gaps, reusing the card's existing warning treatment.

Verification

Unit, at the seam the bug lives in:

# RED — on upstream/main, the new tests reproduce the report exactly:
$ bun run --cwd apps/api test -- test/modules/migration/docker-inspect.test.ts
 × keeps a Coolify container's env even when it matches the image default
   AssertionError: expected {} to deeply equal { NODE_ENV: 'production', …(1) }
 × reports the Coolify variables Docker cannot expose
   AssertionError: expected false to be true
 Tests  2 failed | 8 passed (10)

# GREEN — same tests, with the fix:
 Tests  10 passed (10)

End to end from committed HEAD against a real Docker daemon, driving the actual HTTP route rather than the function directly — a Coolify-labelled container on the host, adopted through the migration wizard:

$ curl -X POST localhost:4000/api/migration/scan -d '{"serverId":"…"}'
# before: env keys ["APP_SECRET"]                                          warnings 0
# after:  env keys ["DATABASE_URL","REDIS_URL","APP_SECRET","NODE_ENV"]    warnings 1

Stripping the coolify.managed label from the same container yields the old, denoised result, so the scoping holds in both directions.

Completing the migration through the wizard persists all four with their values intact — the service row afterwards:

{"NODE_ENV": "production", "REDIS_URL": "redis://cache:6379",
 "APP_SECRET": "coolify-secret-sentinel", "DATABASE_URL": "postgres://db:5432/app"}

Full suites, two separate clean runs, both green:

$ bun run --cwd apps/api test
 Test Files  174 passed | 1 skipped (175)
      Tests  1862 passed | 4 skipped (1866)

$ bun run --cwd apps/dashboard test
 Test Files  19 passed (19)
      Tests  236 passed (236)

$ bun run --cwd apps/api lint                            # tsc --noEmit, clean
$ bun x tsc --noEmit -p apps/dashboard/tsconfig.json     # clean

bun format was not run: all three touched files already fail prettier --check on upstream/main, so --write would reformat several hundred unrelated lines. The additions follow the surrounding style.

Checklist

  • One change per PR — one bug, or one agreed feature, with nothing unrelated bundled in
  • The diff is scoped — no reformatting or lint fixes on lines I wasn't otherwise changing
  • A test fails without this change and passes with it (or I explained above why there isn't one)
  • bun run test and bun run --cwd <workspace> lint pass locally (bun format — see Verification)
  • I understand every line of this diff and can explain it in review

Rish-it added 2 commits August 3, 2026 13:47
Discovery subtracted every container env entry whose exact KEY=VALUE matched the
image's baked-in default, so a migrated service imported only the vars an
operator had provably overridden. That holds for a hand-run `docker run`, but not
for Coolify: it resolves and injects every runtime variable explicitly —
service-level, shared `{{team.*}}`, linked-resource and secret alike — and its
Nixpacks builds bake those same values into the image it then runs. The two sets
overlap almost completely, so the subtraction discarded real production
configuration, frequently all of it.

Measured through `reconcileStack` with a Coolify-labelled container whose
NODE_ENV and DATABASE_URL match its image defaults:

  before -> env {}
  after  -> env { NODE_ENV, DATABASE_URL }   (PATH still denylisted)

Coolify stamps `coolify.managed=true` on every container it manages
(bootstrap/helpers/docker.php), so keying off that label scopes the change to the
platform that needs it — plain Docker and Compose migrations keep the base-image
denoising and the test that covers it.

Build-time variables and BuildKit secrets are genuinely absent from a running
container's config and stay unrecoverable; discovery now says so per service
instead of leaving the gap silent.
`DiscoveredService.warnings` was computed during discovery and sent to the
client, but no step ever rendered it. Everything discovery could not carry over —
bind mounts whose data stays on the host, host ports dropped as duplicates, and
now Coolify's build-time variables — stayed invisible until after adoption.

Render them on the service config card, beside the env editor an operator would
use to fill those gaps, reusing the card's existing warning treatment.
Copilot AI review requested due to automatic review settings August 3, 2026 08:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes Coolify server migration env discovery by preserving runtime environment variables even when they match image defaults (a Coolify/Nixpacks overlap), and makes discovery warnings visible in the dashboard migration wizard so operators can address unmigratable gaps.

Changes:

  • Skip “subtract image-default env” behavior for containers labeled coolify.managed, so Coolify runtime env is retained during discovery.
  • Emit a per-service warning for Coolify about build-time variables / BuildKit secrets not being recoverable from a running container.
  • Render service.warnings in the Server Migration Wizard’s per-service configuration card.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
apps/api/src/modules/migration/docker-reconcile.ts Adjusts env reconciliation for Coolify-managed containers and adds a user-facing warning about unrecoverable variables.
apps/api/test/modules/migration/docker-inspect.test.ts Adds fixtures/tests to verify Coolify-labeled env is preserved and warnings are reported.
apps/dashboard/src/components/migration/ServerMigrationWizard.tsx Displays per-service discovery warnings in the migration UI to prevent silent data loss.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +345 to +349
if (coolifyManaged) {
warnings.push(
"Coolify build-time variables (and any BuildKit secrets) are absent from a running container, so they cannot be imported — re-enter them before rebuilding. Runtime variables, including shared, linked-resource and secret values, were imported.",
);
}
@Hydralerne
Hydralerne merged commit bc206bd into oblien:main Aug 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Coolify migration does not import all environment variables

3 participants