393 lines
12 KiB
TypeScript
393 lines
12 KiB
TypeScript
/**
|
|
* 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<string, string>
|
|
// 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<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${config.apiKey}`,
|
|
}
|
|
|
|
async function call<T = unknown>(endpoint: string, body?: Record<string, unknown>, method = 'POST'): Promise<T> {
|
|
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<string, string>
|
|
cpuLimit?: string
|
|
memoryLimit?: string
|
|
}): Promise<DokployApp> {
|
|
const body: Record<string, unknown> = {
|
|
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<DokployApp & { status?: string; url?: string }> {
|
|
const res = await call<DokployApp & { applicationStatus?: string; previewDomain?: string }>(
|
|
`/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<void> {
|
|
await call('/api/application.delete', { applicationId, deleteVolumes: true })
|
|
}
|
|
|
|
async function stopApplication(applicationId: string): Promise<void> {
|
|
await call('/api/application.stop', { applicationId })
|
|
}
|
|
|
|
async function startApplication(applicationId: string): Promise<void> {
|
|
await call('/api/application.start', { applicationId })
|
|
}
|
|
|
|
async function updateEnv(applicationId: string, envVars: Record<string, string>): Promise<void> {
|
|
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<DokployDatabase> {
|
|
const body: Record<string, unknown> = {
|
|
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<Record<string, unknown>>('/api/database.postgres.create', body)
|
|
// The response shape varies between Dokploy versions; handle both.
|
|
const db = (res.database as Record<string, unknown> | 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<void> {
|
|
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<DokployDomain> {
|
|
const body: Record<string, unknown> = {
|
|
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<void> {
|
|
await call('/api/domain.delete', { domainId })
|
|
}
|
|
|
|
// --- High-level: provision a full instance ---
|
|
|
|
async function provisionInstance(input: ProvisionInstanceInput): Promise<ProvisionInstanceResult> {
|
|
// 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<typeof createDokployClient>
|
|
|
|
// --- 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,
|
|
})
|
|
}
|