import { serve } from "@hono/node-server";import { Hono } from "hono";import "dotenv/config";import { zValidator } from "@hono/zod-validator";import { Inngest } from "inngest";import { serve as serveInngest } from "inngest/hono";import { logger } from "./logger.js";import { cancelDeploymentSchema, type DeployJob, deployJobSchema,} from "./schema.js";import { fetchDeploymentJobs } from "./service.js";import { deploy } from "./utils.js"; const app = new Hono(); // Initialize Inngest clientexport const inngest = new Inngest({ id: "dokploy-deployments", name: "Dokploy Deployment Service",}); export const deploymentFunction = inngest.createFunction( { id: "deploy-application", name: "Deploy Application", concurrency: [ { key: "event.data.serverId", limit: 1, }, ], retries: 0, cancelOn: [ { event: "deployment/cancelled", if: "async.data.applicationId == event.data.applicationId || async.data.composeId == event.data.composeId", timeout: "1h", // Allow cancellation for up to 1 hour }, ], }, { event: "deployment/requested" }, async ({ event, step }) => { const jobData = event.data as DeployJob; return await step.run("execute-deployment", async () => { logger.info("Deploying started"); try { const result = await deploy(jobData); logger.info("Deployment finished", result); // Send success event await inngest.send({ name: "deployment/completed", data: { ...jobData, result, status: "success", }, }); return result; } catch (error) { logger.error("Deployment failed", { jobData, error }); // Send failure event await inngest.send({ name: "deployment/failed", data: { ...jobData, error: error instanceof Error ? error.message : String(error), status: "failed", }, }); throw error; } }); },); app.use(async (c, next) => { if (c.req.path === "/health" || c.req.path === "/api/inngest") { return next(); } const authHeader = c.req.header("X-API-Key"); if (process.env.API_KEY !== authHeader) { return c.json({ message: "Invalid API Key" }, 403); } return next();}); app.post("/deploy", zValidator("json", deployJobSchema), async (c) => { const data = c.req.valid("json"); logger.info("Received deployment request", data); try { // Send event to Inngest instead of adding to Redis queue await inngest.send({ name: "deployment/requested", data, }); logger.info("Deployment event sent to Inngest", { serverId: data.serverId, }); return c.json( { message: "Deployment Added to Inngest Queue", serverId: data.serverId, }, 200, ); } catch (error) { logger.error("Failed to send deployment event", error); return c.json( { message: "Failed to queue deployment", error: error instanceof Error ? error.message : String(error), }, 500, ); }}); app.post( "/cancel-deployment", zValidator("json", cancelDeploymentSchema), async (c) => { const data = c.req.valid("json"); logger.info("Received cancel deployment request", data); try { // Send cancellation event to Inngest await inngest.send({ name: "deployment/cancelled", data, }); const identifier = data.applicationType === "application" ? `applicationId: ${data.applicationId}` : `composeId: ${data.composeId}`; logger.info("Deployment cancellation event sent", { ...data, identifier, }); return c.json({ message: "Deployment cancellation requested", applicationType: data.applicationType, }); } catch (error) { logger.error("Failed to send deployment cancellation event", error); return c.json( { message: "Failed to cancel deployment", error: error instanceof Error ? error.message : String(error), }, 500, ); } },); app.get("/health", async (c) => { return c.json({ status: "ok" });}); // List deployment jobs (Inngest runs) for a server - same shape as BullMQ queue for the UIapp.get("/jobs", async (c) => { const serverId = c.req.query("serverId"); if (!serverId) { return c.json({ message: "serverId is required" }, 400); } try { const rows = await fetchDeploymentJobs(serverId); return c.json(rows); } catch (error) { const message = error instanceof Error ? error.message : String(error); if (message.includes("INNGEST_BASE_URL")) { return c.json( { message: "INNGEST_BASE_URL is required to list deployment jobs" }, 503, ); } logger.error("Failed to fetch jobs from Inngest", { serverId, error }); return c.json([], 200); }}); // Serve Inngest functions endpointapp.on( ["GET", "POST", "PUT"], "/api/inngest", serveInngest({ client: inngest, functions: [deploymentFunction], }),); const port = Number.parseInt(process.env.PORT || "3000");logger.info("Starting Deployments Server with Inngest ✅", port);serve({ fetch: app.fetch, port });
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import "dotenv/config";
import { zValidator } from "@hono/zod-validator";
import { Inngest } from "inngest";
import { serve as serveInngest } from "inngest/hono";
import { logger } from "./logger.js";
import {
cancelDeploymentSchema,
type DeployJob,
deployJobSchema,
} from "./schema.js";
import { fetchDeploymentJobs } from "./service.js";
import { deploy } from "./utils.js";
const app = new Hono();
// Initialize Inngest client
export const inngest = new Inngest({
id: "dokploy-deployments",
name: "Dokploy Deployment Service",
});
export const deploymentFunction = inngest.createFunction(
{
id: "deploy-application",
name: "Deploy Application",
concurrency: [
{
key: "event.data.serverId",
limit: 1,
},
],
retries: 0,
cancelOn: [
{
event: "deployment/cancelled",
if: "async.data.applicationId == event.data.applicationId || async.data.composeId == event.data.composeId",
timeout: "1h", // Allow cancellation for up to 1 hour
},
],
},
{ event: "deployment/requested" },
async ({ event, step }) => {
const jobData = event.data as DeployJob;
return await step.run("execute-deployment", async () => {
logger.info("Deploying started");
try {
const result = await deploy(jobData);
logger.info("Deployment finished", result);
// Send success event
await inngest.send({
name: "deployment/completed",
data: {
...jobData,
result,
status: "success",
},
});
return result;
} catch (error) {
logger.error("Deployment failed", { jobData, error });
// Send failure event
await inngest.send({
name: "deployment/failed",
data: {
...jobData,
error: error instanceof Error ? error.message : String(error),
status: "failed",
},
});
throw error;
}
});
},
);
app.use(async (c, next) => {
if (c.req.path === "/health" || c.req.path === "/api/inngest") {
return next();
}
const authHeader = c.req.header("X-API-Key");
if (process.env.API_KEY !== authHeader) {
return c.json({ message: "Invalid API Key" }, 403);
}
return next();
});
app.post("/deploy", zValidator("json", deployJobSchema), async (c) => {
const data = c.req.valid("json");
logger.info("Received deployment request", data);
try {
// Send event to Inngest instead of adding to Redis queue
await inngest.send({
name: "deployment/requested",
data,
});
logger.info("Deployment event sent to Inngest", {
serverId: data.serverId,
});
return c.json(
{
message: "Deployment Added to Inngest Queue",
serverId: data.serverId,
},
200,
);
} catch (error) {
logger.error("Failed to send deployment event", error);
return c.json(
{
message: "Failed to queue deployment",
error: error instanceof Error ? error.message : String(error),
},
500,
);
}
});
app.post(
"/cancel-deployment",
zValidator("json", cancelDeploymentSchema),
async (c) => {
const data = c.req.valid("json");
logger.info("Received cancel deployment request", data);
try {
// Send cancellation event to Inngest
await inngest.send({
name: "deployment/cancelled",
data,
});
const identifier =
data.applicationType === "application"
? `applicationId: ${data.applicationId}`
: `composeId: ${data.composeId}`;
logger.info("Deployment cancellation event sent", {
...data,
identifier,
});
return c.json({
message: "Deployment cancellation requested",
applicationType: data.applicationType,
});
} catch (error) {
logger.error("Failed to send deployment cancellation event", error);
return c.json(
{
message: "Failed to cancel deployment",
error: error instanceof Error ? error.message : String(error),
},
500,
);
}
},
);
app.get("/health", async (c) => {
return c.json({ status: "ok" });
});
// List deployment jobs (Inngest runs) for a server - same shape as BullMQ queue for the UI
app.get("/jobs", async (c) => {
const serverId = c.req.query("serverId");
if (!serverId) {
return c.json({ message: "serverId is required" }, 400);
}
try {
const rows = await fetchDeploymentJobs(serverId);
return c.json(rows);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes("INNGEST_BASE_URL")) {
return c.json(
{ message: "INNGEST_BASE_URL is required to list deployment jobs" },
503,
);
}
logger.error("Failed to fetch jobs from Inngest", { serverId, error });
return c.json([], 200);
}
});
// Serve Inngest functions endpoint
app.on(
["GET", "POST", "PUT"],
"/api/inngest",
serveInngest({
client: inngest,
functions: [deploymentFunction],
}),
);
const port = Number.parseInt(process.env.PORT || "3000");
logger.info("Starting Deployments Server with Inngest ✅", port);
serve({ fetch: app.fetch, port });