Commit Graph

849 Commits

Author SHA1 Message Date
Mauricio Siu 49b8214e5d feat(compose): add pull latest images on deploy toggle 2026-09-09 17:13:24 -06:00
Mauricio Siu 9dcdb53aa1 feat(transfer): move services between servers with their volumes, mounts and config
Adds a Transfer action on every service page that moves an application,
compose or database to another server without S3: volumes, bind mounts
and deployment logs are streamed through the panel (ssh2/spawn pipe),
file mounts and Traefik config are recreated on the target, the source
is cleaned up and the service is deployed on the target. Failures before
cleanup roll back to the source server.
2026-09-09 02:50:29 -06:00
Artur Spatari 5611662351 feat(vault): address an Infisical folder from the reference
An Infisical provider is pinned to one non-recursive `secretPath`, so reading
two folders means two providers, two machine identities and two sets of
credentials to rotate. This lets a reference name the folder instead:

    ${{vault.my-provider.external/sentry:SENTRY_DSN}}

`<path>:<KEY>` mirrors the HashiCorp client in this directory, which already
documents that exact format. A relative path resolves against the provider's
`secretPath`, a leading slash is absolute, and a ref without a colon keeps its
current meaning — the whole ref is the secret name at the provider's own path.

A dot cannot be the separator here: Infisical accepts dots inside secret names
(`A.B.C` is a valid key), so `provider.a.b.C` cannot be split unambiguously
and would silently break anyone using such a name.

Refs are grouped by resolved path so each path is listed once, and the login
happens once per batch rather than once per path.

Tests cover the bare ref, relative and absolute paths, a provider at `/`,
grouping with a single login, the error naming the path, and a malformed ref.
2026-09-08 21:38:24 +03:00
Narciso E. Núñez Arias 887a457f15 Merge pull request #5374 from aspatari/fix/infisical-expand-secret-references
fix(vault): expand Infisical secret references when listing secrets
2026-09-08 14:08:52 -04:00
Yash Kumar be128d5a4a Merge remote-tracking branch 'upstream/canary' into fix/dns-upsert-preserve-existing-records
# Conflicts:
#	packages/server/src/utils/dns/infomaniak.ts
2026-09-08 22:54:00 +05:30
Narciso E. Núñez Arias 467c80a73d Merge pull request #5356 from mitc-gjuge/feat/infomaniak-record-filter
perf(dns): filter Infomaniak records server-side when upserting
2026-09-08 13:13:43 -04:00
Yash Kumar 8291165259 Update packages/server/src/utils/dns/porkbun.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-09-08 17:32:11 +05:30
autofix-ci[bot] e701644fa5 [autofix.ci] apply automated fixes 2026-09-08 11:54:43 +00:00
Yash Kumar 94cd5035ef fix(dns): extract and normalize prio for porkbun MX/SRV records to prevent duplicates 2026-09-08 17:24:17 +05:30
Yash Kumar ea1c6fc3fe fix(dns): fallback to data fields comparison for cloudflare structured SRV/CAA records 2026-09-08 17:20:59 +05:30
Yash Kumar df0986b4e1 fix(dns): apply content normalization in upsert match logic to prevent duplicates 2026-09-08 17:13:58 +05:30
Yash Kumar 91db4e60c4 fix(dns): match record content in upsertRecord to prevent overwriting existing records
Previously, upsertRecord matched only by type and name, silently
overwriting any existing record of the same type/name with different
content. This caused data loss when users had multiple records of the
same type (e.g. multiple TXT records for SPF + site verification).

Now upsertRecord also checks the record content/target before deciding
to update. If the content differs, a new record is created alongside
the existing one instead of replacing it.

Affected providers: Cloudflare, Porkbun, Infomaniak, OVH.
Tests updated accordingly.
2026-09-08 16:59:16 +05:30
Artur Spatari 6e760da93b fix(vault): expand Infisical secret references when listing secrets
A secret in Infisical can reference another one (`${env.folder.KEY}`), which
is the only way to keep a value in one place and read it from several folders.
The Infisical client fetches `/api/v3/secrets/raw` without
`expandSecretReferences`, and that endpoint leaves references untouched by
default. The referencing secret therefore arrives as the literal `${...}`
string, is written to the generated `.env`, and the deployment still reports
success — the service just receives a placeholder instead of its value.

This is easy to miss because the single-secret endpoint (`/raw/{name}`) does
expand by default, so the Infisical UI and API show the resolved value while
only Dokploy sees the literal.

Adds the parameter and the first tests for this client: the list request now
asserts the flag, a missing secret and an auth failure. Without the fix the
new test fails with `expected null to be 'true'`.
2026-09-07 22:47:43 +03:00
Mauricio Siu 383438e4fc Merge branch 'canary' into codex/5265-aws-parameter-store 2026-09-07 11:33:45 -06:00
Guillaume Juge 8409b8ce60 perf(dns): filter Infomaniak records server-side when upserting
upsertRecord pulled every record of a zone just to find the one it was about to
write. The API can filter, so ask it to.

The match is still verified locally, because filter[source] matches substrings
rather than exact values. Verified against a live account:

    filter[source]=autoconfig  -> [autoconfig]
    filter[source]=auto        -> [autoconfig, autodiscover]
    filter[source]=disco       -> [autodiscover]

Letting the filter decide would therefore have upserted "auto" onto the existing
"autoconfig" record. The filter narrows what is transferred; the exact
comparison still decides. Covered by a test that fails if the first candidate is
trusted.

The apex was the other thing worth checking, since Infomaniak writes it as ".":
filter[source]=. does return the apex records, and an upsert on an existing apex
TXT returns its id rather than creating a second one.

Suggested by @narcisonunez on #5257.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 11:48:44 +02:00
Guillaume Juge 7e9fdbb5ff Merge canary and regenerate the migration after Infomaniak landed
#5257 merged, so canary now carries the Infomaniak provider and its
0191_cool_christian_walker. This branch's 0191_elite_ultragirl collided on the
number, and the conflicts were the usual "infomaniak and ovh added at the same
spot" across the enum, the config union, the client registry, SENSITIVE_FIELDS,
both providerLabels maps, the icons and the provider form — all resolved by
keeping both.

Regenerated as 0192_light_lake with a current timestamp rather than renaming the
file, so drizzle still applies it on a database that has already run canary's
0191.
2026-09-05 11:21:50 +02:00
Guillaume Juge 2311f346d4 fix(dns): do not report a restored OVH record as lost
restoreRecord had the restore POST and its zone refresh inside one catch. That
was harmless while refreshZone swallowed failures, but the previous commit made
it throw, which brought a new case into that catch: the restore succeeds and
only the publication fails.

The message then told the user the record "has been deleted" and to recreate it
by hand. It exists at OVH, just unpublished, so following that advice duplicates
it as soon as the zone is refreshed.

The two failures are now reported separately. A failed POST still means the
record is really gone and prints what to recreate. A failed refresh after a
successful restore says the record is back but not served yet, and explicitly
says not to recreate it. Either way the original replacement error is kept, so
the user still learns why the type change failed.

Reported by Greptile on #5258.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 11:19:26 +02:00
Guillaume Juge 28f71e727a fix(dns): report an OVH zone refresh failure for what it is
refreshZone runs after the record write has already succeeded, so a failure
there means the change exists at the provider but is not being served yet. It
was left unguarded at all six call sites, so the caller saw a bare transport
error and would reasonably read it as "nothing was applied" — while a type
change had already deleted the old record and created its replacement.

Rolling the write back would destroy correct state over a publish failure, and
re-deleting after an uncertain network error is how records get lost. The error
now says what actually happened and how it resolves: the next successful change
to the zone publishes it, or the user can refresh the zone from the OVH manager.

Guarding the helper covers upsertRecord and deleteRecord too, not just the type
change Greptile pointed at.

Reported by Greptile on #5258.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 20:23:43 +02:00
Guillaume Juge 7ae566f4de Merge remote-tracking branch 'origin/canary' into feat/ovh-dns-provider 2026-09-03 10:29:31 +02:00
Guillaume Juge ea9a466c01 Merge remote-tracking branch 'origin/canary' into feat/infomaniak-dns-provider 2026-09-03 10:28:56 +02:00
autofix-ci[bot] c901260d81 [autofix.ci] apply automated fixes 2026-09-02 11:35:17 +00:00
Yash Kumar 8c9e473b4e test: fix CI failures and improve local test robustness 2026-09-02 16:59:57 +05:30
Mauricio Siu b2ed974422 Merge branch 'canary' into codex/5265-aws-parameter-store 2026-09-02 01:57:43 -06:00
Guillaume Juge 4746cfc1ca Merge canary and regenerate the migration at a free number
canary landed its own 0190 (0190_nappy_anita_blake), which collides with this
branch's 0190_even_mastermind. Renaming would not have been enough: drizzle
applies migrations by comparing timestamps against the last applied one, so this
branch's older `when` would have been silently skipped on any deployment that
had already run canary's 0190, leaving the enum without its new value.

Regenerated as 0191_elite_ultragirl with a current timestamp.
2026-09-02 08:58:09 +02:00
Guillaume Juge 18e617eb14 Merge canary and regenerate the migration at a free number
canary landed its own 0190 (0190_nappy_anita_blake), which collides with this
branch's 0190_perpetual_red_skull. Renaming would not have been enough: drizzle
applies migrations by comparing timestamps against the last applied one, so this
branch's older `when` would have been silently skipped on any deployment that
had already run canary's 0190, leaving the enum without its new value.

Regenerated as 0191_cool_christian_walker with a current timestamp.
2026-09-02 08:57:29 +02:00
Mauricio Siu e67d9efb38 Merge pull request #5231 from drago1520/fix/docker-context-path-default
fix/docker-context-path-default: Placeholder with name attribute "doc…
2026-09-02 00:29:37 -06:00
Felix 1d26fdd027 feat(vault): add AWS Parameter Store provider 2026-09-02 04:36:21 +02:00
Guillaume Juge f83097b80f fix(dns): spell out the OVH right that lists zones
OVH matches access rules per exact path: a `GET /domain/zone/*` rule grants the
subtree but not the bare `GET /domain/zone` that listZones and testConnection
call. Verified against a live account with a consumer key carrying that single
wildcard rule:

    GET /domain/zone                     -> 403 This call has not been granted
    GET /domain/zone/                    -> 200
    GET /domain/zone/{zone}/record       -> 200

The form only asked for rights on `/domain/zone/*`, so a token created by
following it could not list zones at all, and the failure surfaced as a bare
"This call has not been granted" that points nowhere.

The hint now lists the five rights verbatim, and a token missing the root one
gets an error that names it instead of echoing OVH's message.

Reported by @narcisonunez on #5258.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 21:30:07 +02:00
Guillaume Juge 59a74849a8 fix(dns): paginate the documented Infomaniak products endpoint
listZones used /1/product, which lego uses but the API docs don't: it is the
legacy singular route, it returns no pagination metadata, and it 401s for at
least some tokens. The documented /1/products returns `total`, `pages`,
`items_per_page` and `page`, and defaults to 15 domains per page, so an account
with more domains than that would silently lose zones.

Switch to the plural endpoint and walk every page. Verified against a live
account: forcing per_page=2 collects all 5 domains across 3 pages with no
duplicates.

Also inline the single-use createdId helper.

Both reported by @narcisonunez on #5257.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 21:02:54 +02:00
Guillaume Juge 80a6baf58e fix(dns): restore the original OVH record when a type change fails
Changing a record's type deletes the record then recreates it with the new type,
because OVH's update payload carries no fieldType. If the creation failed the
name was left with nothing and no rollback.

The delete still has to come first, since OVH rejects a CNAME that would sit
alongside other data on the same name. So on a failed creation the original
record is put back from the copy already fetched before the delete, and the
original error is rethrown. If the restore fails too, the error names the record
that has to be recreated by hand.

Reported by Greptile on #5258.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 16:29:07 +02:00
Guillaume Juge be4a5da405 fix(dns): match apex records stored under any apex spelling
toSource always writes the apex as ".", but listRecords already accepted "" and
"@" as apex spellings on read. The upsert lookup compared sources strictly, so a
record stored under one of the other spellings would not have matched and the
upsert would have created a duplicate apex record instead of updating it.

Normalize the candidate's source before comparing, so read and match agree.

Reported by Greptile on #5257.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 16:28:04 +02:00
Guillaume Juge 2bc22c520d fix(dns): represent the Infomaniak apex as a root dot
Verified against a live Infomaniak account: the API returns `source: "."` for
apex records, not "" as assumed.

Reading them back produced a doubled dot ("..example.com"), and writing "" meant
an apex upsert never matched the existing record, so it would have created a
duplicate apex record instead of updating it.

toSource now emits "." for the apex and toFqdn accepts ".", "" and "@" so a
hand-written record still round-trips.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 15:58:31 +02:00
Guillaume Juge e629248dea feat: add OVHcloud DNS provider support
Adds OVHcloud alongside Cloudflare, AWS Route53 and Porkbun, following the
existing DnsClient interface in packages/server/src/utils/dns/.

- ovh.ts implements listZones, listRecords, upsertRecord, updateRecord,
  deleteRecord and testConnection against the /domain/zone endpoints of the
  OVHcloud API, on any of its seven regional endpoints (ovh-eu/ca/us, kimsufi
  and soyoustart).
- A new `ovh` value was added to the DnsProviderType enum along with an
  ovhDnsConfigSchema (endpoint, applicationKey, applicationSecret, consumerKey)
  in the discriminated union, plus the Drizzle migration for the enum change.
- The application secret and the consumer key are masked/merged like the other
  providers' secrets in services/dns-provider.ts.
- UI: OVHcloud icon, an endpoint selector and the three credential fields in the
  DNS provider dialog, plus registration in the provider selector.

Three OVH-specific behaviours are handled explicitly:

- Requests are signed with `$1$` + sha1(applicationSecret+consumerKey+method+
  url+body+timestamp). The timestamp comes from the API's own clock via an
  unauthenticated GET /auth/time, since a host clock a few seconds off would get
  every call rejected; the measured drift is cached per endpoint for an hour.
- OVH only applies zone changes once the zone is explicitly refreshed, so every
  successful create, update and delete is followed by POST /domain/zone/{zone}
  /refresh.
- The record update payload carries no fieldType, so changing a record's type
  replaces the record (DELETE then POST) and returns the new id.

The record listing endpoint returns ids only, so each record is fetched
individually with the fan-out capped at 8 concurrent requests.

Also fills in the missing Porkbun label in show-dns-providers.tsx, which fell
back to displaying the raw enum value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 15:29:01 +02:00
Guillaume Juge ff3ebefc70 feat: add Infomaniak DNS provider support
Adds Infomaniak alongside Cloudflare, AWS Route53 and Porkbun, following the
existing DnsClient interface in packages/server/src/utils/dns/.

- infomaniak.ts implements listZones, listRecords, upsertRecord, updateRecord,
  deleteRecord and testConnection against the Infomaniak API. Zones come from
  /1/product?service_name=domain and records from the v2 /2/zones/{zone}/records
  endpoints, which are keyed by zone name rather than by product id.
- A new `infomaniak` value was added to the DnsProviderType enum along with an
  infomaniakDnsConfigSchema (apiToken) in the discriminated union, plus the
  Drizzle migration for the enum change.
- The token is masked/merged like the other providers in services/dns-provider.ts.
- UI: Infomaniak icon and API Token field in the DNS provider dialog, plus
  registration in the provider selector.

Infomaniak's `source` is relative to the zone (empty for the apex), so record
names are translated between Dokploy's fully-qualified format and Infomaniak's
subdomain-only format internally, with the trailing dot handled. TXT targets are
stored quoted by the API and are unquoted on read / quoted on write so that
editing a record does not stack quotes on every save.

Also fills in the missing Porkbun label in show-dns-providers.tsx, which fell
back to displaying the raw enum value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 15:25:03 +02:00
Mauricio Siu 8cd2ba80f2 Merge branch 'canary' into feat/dns-records-management 2026-09-01 02:41:18 -06:00
Mauricio Siu 9c444ebca3 Merge branch 'canary' into feat/porkbun-dns-provider 2026-09-01 02:37:36 -06:00
Mauricio Siu f1a4f4317a refactor: remove DnsPageTransition component and update loading indicators in DNS-related components 2026-09-01 02:36:41 -06:00
Mauricio Siu 1ab4a8a70a Merge branch 'canary' into feat/dns-records-management 2026-09-01 02:28:22 -06:00
autofix-ci[bot] 43178b6442 [autofix.ci] apply automated fixes 2026-09-01 08:23:38 +00:00
Mauricio Siu 50479f20ff Merge branch 'canary' into feat/vault-phase-provider 2026-09-01 01:58:35 -06:00
Mauricio Siu 5624dff5c1 fix: verify swarm task convergence before marking db deploys done 2026-09-01 01:56:58 -06:00
Mauricio Siu 68a05551d0 fix: write remote traefik config via SFTP instead of exec+base64
Embedding the full YAML (base64-encoded) into a single SSH exec command
silently fails once the payload passes the SSH transport's packet size
ceiling (~90-100KB), truncating the dynamic config with no error
surfaced anywhere. Switch to SFTP, which has no such limit.
2026-09-01 01:53:45 -06:00
Mauricio Siu 47308070a8 fix: persist serverThreshold for gotify/ntfy and guard teams dispatch 2026-09-01 01:11:31 -06:00
Mauricio Siu ec8e645758 fix: only pin --project-directory when compose has mounts
--project-directory was added unconditionally in 87b914996 to fix
relative bind mount resolution for git-based compose deploys with a
nested composePath (#5181). It also moves where build.context and the
generated .env resolve, breaking any compose file in a subdirectory
that has context: . alongside its Dockerfile (#5230), or interpolates
env vars (#5242).

Only pin --project-directory when the compose actually has mounts
configured; otherwise build.context and .env resolve against the
compose file's own directory like plain docker compose. Also pass
--env-file explicitly pointing at the generated .env next to the
compose file, so it's found even when --project-directory is pinned
(credit: tonnenpinguin, PR #5235).

Fixes #5230
Fixes #5242
2026-09-01 00:50:05 -06:00
drago1520 f1e2467bb9 fix/docker-context-path-default: Placeholder with name attribute "dockerContextPath" lied that the default path is ".", while being the Dockerfile directory. 2026-08-31 09:39:00 +03:00
Lucas Manchine abc2439ec4 fix: apply watch path negations as a pattern set 2026-08-28 14:43:18 -03:00
Mauricio Siu 032e804114 fix: remove empty routers/services traefik config instead of writing invalid yaml
Attaching Basic Auth or a Redirect to an application with no domains
wrote an empty routers/services block to the app's dynamic config
file. Traefik's file provider rejects that as invalid and aborts its
watcher, blocking config updates for every other application.

Fixes #5189
2026-08-27 09:37:14 -06:00
Mauricio Siu ca3da4dc5d Merge pull request #5201 from Dokploy/fix/compose-git-mounts-5181
fix: resolve relative bind mounts against code dir for git-based compose deploys
2026-08-27 09:15:38 -06:00
Mauricio Siu 87b914996f fix: resolve relative bind mounts against code dir for git-based compose deploys
Compose deployments using a git provider (GitHub/GitLab/Gitea/Bitbucket/
custom Git) clone the repo verbatim, so composePath can point into a
subdirectory (e.g. deploy/docker-compose.yml). Docker Compose resolves
relative bind mounts (../files) against the compose file's own directory,
not against the code/ dir where file mounts are actually written, so the
mount silently binds an empty directory instead of the configured content.

Pin --project-directory to code/ when building the docker compose command
so relative mounts always resolve the same way raw deployments already do.

Fixes #5181
2026-08-27 09:15:06 -06:00
Mauricio Siu 8f3230e2ed Merge pull request #5021 from Souvik-Cyclic/fix/railpack-deploy-sudo-5007
fix: run Railpack install with sudo during remote deploy
2026-08-27 09:10:47 -06:00