Skip to content

feat: add pre-deploy and post-deploy command hooks for applications#4240

Open
Alfredao wants to merge 3 commits intoDokploy:canaryfrom
Alfredao:feat/pre-post-deploy-commands
Open

feat: add pre-deploy and post-deploy command hooks for applications#4240
Alfredao wants to merge 3 commits intoDokploy:canaryfrom
Alfredao:feat/pre-post-deploy-commands

Conversation

@Alfredao
Copy link
Copy Markdown

@Alfredao Alfredao commented Apr 17, 2026

What is this PR about?

Implements pre-deploy and post-deploy command hooks for Applications, closing a long-standing community request (issue #110, open ~2 years, 28+ comments).

Two new optional fields are exposed on the Application Advanced tab under a new Deployment Lifecycle Hooks card:

  • Pre-deploy command — runs inside the previous running container right before the new image replaces it. Skipped on the first deployment (no prior container). Useful for DB backups, cache draining, notifications.
  • Post-deploy command — runs inside the new container after the swarm service reports a running task. Useful for DB migrations, cache warm-ups, webhooks.

Both commands are executed via docker exec <taskContainerId> sh -c "<cmd>" against the resolved swarm task container (local or remote via SSH). A non-zero exit aborts the deployment and marks it as error, reusing the existing failure pipeline (sendBuildErrorNotifications + log-line append). Hook output is streamed into the deployment log. The user's command string itself is never echoed — only a length-only header — so secrets embedded in the command don't leak into logs.

Behaves identically in deployApplication and rebuildApplication. Compose services and preview deployments are out of scope here.

Storage note — single deployHooks column

The pre/post pair is stored in a single text column (deployHooks) as a JSON payload { pre, post }, not two separate columns. Reason: the application table already has 99 columns on canary, and Postgres caps json_build_array() at 100 arguments. Drizzle's relational with query on the project-listing page compiles every column of every nested relation into that call, so adding two columns would push the tree past the 100-arg limit and crash the home page. Stuffing both hooks into one column lands exactly at 100 and avoids the regression. Worth flagging as separate pressure for a future normalization pass on applications — any additional column from any PR will re-trip this.

Changes

  • packages/server/src/db/schema/application.ts — Added deployHooks: text(\"deployHooks\") next to the existing command. Drizzle-zod auto-picks it into apiUpdateApplication (no router change needed).
  • apps/dokploy/drizzle/0166_aromatic_jetstream.sql — Generated migration adding the column.
  • packages/server/src/utils/docker/hooks.ts (new) — parseDeployHooks / serializeDeployHooks for the JSON payload, runDeployHook({ kind, appName, serverId, command, logPath }) that resolves the running task container via the existing getServiceContainer, base64-encodes the user command (mirroring the pattern at utils/docker/utils.ts:720), and dispatches over local execAsync or remote execAsyncRemote. Pre-deploy is silently skipped when no previous container exists. waitForSwarmServiceRunning(appName, serverId, opts) is a remote-aware poll loop that waits for a task with `State === "running"` (default 120s / 2s interval) — needed because the existing checkSwarmServiceRunning at utils/docker/utils.ts:796 is local-only and unexported.
  • packages/server/src/services/application.ts — Calls runDeployHook({ kind: \"pre\" }) between the build step and mechanizeDockerContainer, then (only when a post-deploy command is configured) awaits waitForSwarmServiceRunning + runDeployHook({ kind: \"post\" }) before marking the deployment done. Failures bubble through the existing try/catch that handles error notifications. Same placement inside rebuildApplication.
  • apps/dokploy/components/dashboard/application/advanced/general/deploy-hooks.tsx (new) — New DeployHooks card with two <Textarea> fields. Parses/serializes the JSON payload internally so the API surface stays a single field. Rendered next to the existing Run Command card in the Advanced tab.
  • apps/dokploy/__test__/deploy/application.deploy-hooks.test.ts (new) — 7 Vitest cases covering pre-before-mechanize, post-after-wait, skip-when-empty, pre-fail / post-fail abort deployment, and serverId pass-through.

Notes for reviewers

  • The cloud deploy path (apps/api/src/utils.ts) calls deployApplication from @dokploy/server directly, so no parallel pipeline change was needed.
  • docker exec ... sh -c assumes sh exists in the image (doesn't work on scratch/distroless). This matches the industry pattern (Coolify works the same way).
  • The waitForSwarmServiceRunning helper is technically a remote-aware duplicate of the unexported checkSwarmServiceRunning at utils/docker/utils.ts:796. Happy to consolidate the two in a follow-up if preferred.
  • The 100-column threshold on application is now the binding constraint for any future schema change — flagging so it's on maintainers' radar.

Checklist

Issues related (if applicable)

Closes #110

Screenshots (if applicable)

image

Greptile Summary

This PR adds pre-deploy and post-deploy command hooks for applications, storing them as a JSON payload in a single new deployHooks text column. The implementation uses base64 encoding to safely pass user commands to docker exec, with hook output streamed to the deployment log.

  • P1 — wrong server for hooks when a build server is configured: deployApplication and rebuildApplication compute serverId = buildServerId || serverId and pass it to both runDeployHook and waitForSwarmServiceRunning. However, mechanizeDockerContainer uses application.serverId directly for Swarm operations. When buildServerId is set, hooks would target the build server — where the containers don't exist — causing docker exec to silently skip (pre) or throw (post). Both call sites should use application.serverId instead.
  • P2 — serializeDeployHooks in hooks.ts is dead code: The function is exported but never imported; the frontend component carries its own copy of the same logic.

Confidence Score: 4/5

Safe to merge for the common single-server setup; will silently misbehave for users with a dedicated build server configured.

One P1 defect: hooks use buildServerId || serverId instead of serverId, causing them to target the wrong host when a build server is configured. All other findings are P2 (dead code, minor race condition under non-default Swarm update order). The feature works correctly for the typical case where no separate build server is set.

packages/server/src/services/application.ts — hook serverId argument in both deployApplication and rebuildApplication

Reviews (1): Last reviewed commit: "feat: add pre-deploy and post-deploy com..." | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

@Alfredao Alfredao requested a review from Siumauricio as a code owner April 17, 2026 22:23
@dosubot dosubot bot added size:L This PR changes 100-499 lines, ignoring generated files. enhancement New feature or request labels Apr 17, 2026
Comment thread packages/server/src/services/application.ts
Comment thread packages/server/src/utils/docker/hooks.ts Outdated
Comment thread packages/server/src/utils/docker/hooks.ts
@Alfredao Alfredao force-pushed the feat/pre-post-deploy-commands branch from 14f258f to 16bf43c Compare April 17, 2026 23:31
Alfredao and others added 2 commits April 17, 2026 20:34
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: post/pre deployments commands

1 participant