/** * Dokploy API Client * ------------------- * Manages tenant instances on the Dokploy server: create applications from * a Git repo, deploy, attach domains, create databases, suspend/delete. * * Config (env): * DOKPLOY_URL - e.g. https://cp.project-url.eu * DOKPLOY_API_KEY - bearer token * DOKPLOY_SERVER_ID - the Dokploy server ID to deploy to (default "default") * DOKPLOY_PROJECT_ID - the Dokploy project to put tenant apps under * * NOTE: Dokploy API endpoints should be verified against the current API docs * at implementation time. The endpoints below are based on the Dokploy REST API. * See: https://docs.dokploy.com/docs/api */ export interface DokployConfig { url: string apiKey: string serverId?: string projectId?: string } export interface DokployApp { applicationId: string name: string status?: string url?: string createdAt?: string } export interface DokployDatabase { databaseId: string name: string type: string // postgres|mysql|redis|... host: string port: number database: string username: string password: string externalUrl?: string } export interface DokployDomain { domainId: string hostName: string applicationId: string https: boolean certificateStatus?: string } export interface ProvisionInstanceInput { name: string subdomain: string gitRepo: string // e.g. https://github.com/cetinmustafa83/restov1 gitBranch: string // e.g. main buildPath?: string // e.g. / (repo root, default) envVars: Record // Resource limits (Dokploy applies these as container constraints) cpuLimit?: string // e.g. "0.5" = 50% of one core memoryLimit?: string // e.g. "512m" // Database databaseName?: string // if set, a Postgres DB is created for this instance } export interface ProvisionInstanceResult { app: DokployApp database?: DokployDatabase domain: DokployDomain url: string } class DokployError extends Error { constructor( message: string, public status: number, public endpoint: string, public body?: unknown ) { super(message) this.name = 'DokployError' } } export function createDokployClient(config: DokployConfig) { const baseUrl = config.url.replace(/\/$/, '') const serverId = config.serverId ?? 'default' const headers: Record = { 'Content-Type': 'application/json', Authorization: `Bearer ${config.apiKey}`, } async function call(endpoint: string, body?: Record, method = 'POST'): Promise { const url = `${baseUrl}${endpoint}` const res = await fetch(url, { method, headers, body: body ? JSON.stringify(body) : undefined, }) const text = await res.text() let json: unknown try { json = text ? JSON.parse(text) : null } catch { json = text } if (!res.ok) { throw new DokployError( `Dokploy API ${endpoint} failed: HTTP ${res.status}`, res.status, endpoint, json ) } return json as T } // --- Applications --- async function createApplication(input: { name: string description?: string gitRepo: string gitBranch: string buildPath?: string envVars?: Record cpuLimit?: string memoryLimit?: string }): Promise { const body: Record = { name: input.name, description: input.description ?? `Tenant instance: ${input.name}`, serverId, projectName: config.projectId ?? 'default', sourceType: 'git', gitRepository: input.gitRepo, gitBranch: input.gitBranch, buildPath: input.buildPath ?? '/', // Dokploy expects env vars as a JSON string env: JSON.stringify(input.envVars ?? {}), cpuLimit: input.cpuLimit ?? '0.5', memoryLimit: input.memoryLimit ?? '512m', } const res = await call<{ applicationId?: string; app?: { applicationId: string }; [k: string]: unknown }>('/api/application.create', body) const applicationId = res.applicationId ?? (res.app as { applicationId: string } | undefined)?.applicationId if (!applicationId) { throw new DokployError('Dokploy createApplication returned no applicationId', 200, '/api/application.create', res) } return { applicationId, name: input.name, status: 'created' } } async function deployApplication(applicationId: string): Promise<{ deployId: string; status: string }> { const res = await call<{ deployId?: string; jobId?: string; status?: string }>('/api/application.deploy', { applicationId }) return { deployId: res.deployId ?? res.jobId ?? '', status: res.status ?? 'deploying' } } async function getApplication(applicationId: string): Promise { const res = await call( `/api/application.one`, { applicationId }, ) return { applicationId: res.applicationId, name: res.name, status: res.applicationStatus ?? res.status, url: res.previewDomain ?? res.url, createdAt: res.createdAt, } } async function deleteApplication(applicationId: string): Promise { await call('/api/application.delete', { applicationId, deleteVolumes: true }) } async function stopApplication(applicationId: string): Promise { await call('/api/application.stop', { applicationId }) } async function startApplication(applicationId: string): Promise { await call('/api/application.start', { applicationId }) } async function updateEnv(applicationId: string, envVars: Record): Promise { await call('/api/application.update', { applicationId, env: JSON.stringify(envVars), }) } // --- Databases (Postgres) --- async function createPostgresDatabase(input: { name: string appName: string // the application this DB belongs to description?: string externalUrl?: boolean }): Promise { const body: Record = { name: input.name, appName: input.appName, description: input.description ?? `DB for ${input.appName}`, serverId, projectName: config.projectId ?? 'default', externalUrl: input.externalUrl ?? false, } const res = await call>('/api/database.postgres.create', body) // The response shape varies between Dokploy versions; handle both. const db = (res.database as Record | undefined) ?? res const databaseId = (db.databaseId as string | undefined) ?? (res.databaseId as string | undefined) if (!databaseId) { throw new DokployError('Dokploy createDatabase returned no databaseId', 200, '/api/database.postgres.create', res) } return { databaseId, name: (db.name as string | undefined) ?? input.name, type: 'postgres', host: (db.host as string | undefined) ?? 'postgres', port: (db.port as number | undefined) ?? 5432, database: (db.database as string | undefined) ?? input.name, username: (db.username as string | undefined) ?? 'postgres', password: (db.password as string | undefined) ?? '', } } async function deleteDatabase(databaseId: string): Promise { await call('/api/database.postgres.delete', { databaseId }) } // --- Domains --- async function createDomain(input: { applicationId: string hostName: string path?: string port?: number https?: boolean certificateType?: 'letsencrypt' | 'none' }): Promise { const body: Record = { applicationId: input.applicationId, hostName: input.hostName, path: input.path ?? '/', port: input.port ?? 3000, https: input.https ?? true, certificateType: input.certificateType ?? 'letsencrypt', } const res = await call<{ domainId?: string; id?: string; hostName?: string; https?: boolean }>('/api/domain.create', body) const domainId = res.domainId ?? res.id if (!domainId) { throw new DokployError('Dokploy createDomain returned no domainId', 200, '/api/domain.create', res) } return { domainId, hostName: input.hostName, applicationId: input.applicationId, https: input.https ?? true, } } async function deleteDomain(domainId: string): Promise { await call('/api/domain.delete', { domainId }) } // --- High-level: provision a full instance --- async function provisionInstance(input: ProvisionInstanceInput): Promise { // 1. Create application from Git repo const app = await createApplication({ name: input.name, gitRepo: input.gitRepo, gitBranch: input.gitBranch, buildPath: '/', envVars: input.envVars, cpuLimit: input.cpuLimit, memoryLimit: input.memoryLimit, }) // 2. (Optional) Create Postgres database let database: DokployDatabase | undefined if (input.databaseName) { database = await createPostgresDatabase({ name: input.databaseName, appName: input.name, }) // Inject DATABASE_URL into the app's env const dbUrl = `postgresql://${database.username}:${database.password}@${database.host}:${database.port}/${database.database}` await updateEnv(app.applicationId, { ...input.envVars, DATABASE_URL: dbUrl, }) } // 3. Attach domain (subdomain.platform-domain) const hostName = input.subdomain const domain = await createDomain({ applicationId: app.applicationId, hostName, port: 3000, https: true, }) // 4. Trigger deploy await deployApplication(app.applicationId) return { app, database, domain, url: `https://${hostName}`, } } // --- High-level: teardown an instance (idempotent) --- async function teardownInstance(input: { dokployAppId?: string dokployDbId?: string dnsDomainId?: string // Dokploy domain ID }): Promise<{ steps: string[] }> { const steps: string[] = [] // Remove domain first (stop traffic) if (input.dnsDomainId) { try { await deleteDomain(input.dnsDomainId) steps.push('domain.removed') } catch (e) { if (e instanceof DokployError && e.status !== 404) { steps.push(`domain.error: ${e.message}`) } } } // Delete database if (input.dokployDbId) { try { await deleteDatabase(input.dokployDbId) steps.push('database.removed') } catch (e) { if (e instanceof DokployError && e.status !== 404) { steps.push(`database.error: ${e.message}`) } } } // Delete application (+ volumes) if (input.dokployAppId) { try { await deleteApplication(input.dokployAppId) steps.push('application.removed') } catch (e) { if (e instanceof DokployError && e.status !== 404) { steps.push(`application.error: ${e.message}`) } } } return { steps } } return { // Low-level createApplication, deployApplication, getApplication, deleteApplication, stopApplication, startApplication, updateEnv, createPostgresDatabase, deleteDatabase, createDomain, deleteDomain, call, // High-level provisionInstance, teardownInstance, } } export type DokployClient = ReturnType // --- Factory: create from env --- export function createDokployClientFromEnv(): DokployClient | null { const url = process.env.DOKPLOY_URL const apiKey = process.env.DOKPLOY_API_KEY if (!url || !apiKey) return null return createDokployClient({ url, apiKey, serverId: process.env.DOKPLOY_SERVER_ID, projectId: process.env.DOKPLOY_PROJECT_ID, }) }