fix: rewrite Dokploy client with correct API format (tRPC + x-api-key)

- Verified endpoints from @dokploy/mcp source code:
  - Auth: x-api-key header (not Authorization: Bearer)
  - Base URL: {url}/api
  - application.create → needs environmentId (not projectId)
  - Git provider saved separately: saveGiteaProvider / saveGithubProvider / saveGitProvider
  - Environment vars saved via saveEnvironment
  - Deploy is a separate call after create
  - Domain attach via domain.create
- Provisioning flow now multi-step:
  1. application.create
  2. saveGiteaProvider or saveGithubProvider (based on gitRepo URL)
  3. saveEnvironment (env vars)
  4. application.deploy
  5. domain.create (optional, non-fatal)
- Removed database provisioning (Dokploy apps use their own DB via env vars)
- Teardown simplified: deleteDomain → deleteApplication
- Updated instances route + cleanup route to match new API
This commit is contained in:
Z User committed 2026-10-28 23:05:53 +00:00
1 parent 970986fef7
commit 617f00b715
4 files changed
+209 -195

No files matched your search

+1 -2
View File
@@ -184,8 +184,7 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
try { try {
const result = await dokploy.teardownInstance({ const result = await dokploy.teardownInstance({
dokployAppId: instance.dokployAppId ?? undefined, dokployAppId: instance.dokployAppId ?? undefined,
dokployDbId: instance.dokployDbId ?? undefined, dokployDomainId: meta.dokployDomainId,
dnsDomainId: meta.dokployDomainId,
}) })
steps.push(...result.steps) steps.push(...result.steps)
await db.instanceEvent.create({ await db.instanceEvent.create({
+1 -2
View File
@@ -88,8 +88,7 @@ export async function POST(req: Request) {
try { try {
const r = await dokploy.teardownInstance({ const r = await dokploy.teardownInstance({
dokployAppId: inst.dokployAppId ?? undefined, dokployAppId: inst.dokployAppId ?? undefined,
dokployDbId: inst.dokployDbId ?? undefined, dokployDomainId: meta.dokployDomainId,
dnsDomainId: meta.dokployDomainId,
}) })
steps.push(...r.steps) steps.push(...r.steps)
} catch (e) { } catch (e) {
+3 -8
View File
@@ -158,7 +158,7 @@ export async function POST(req: Request) {
gitRepo, gitRepo,
gitBranch, gitBranch,
envVars: instanceEnv, envVars: instanceEnv,
databaseName: `${product}_${subdomain}`.replace(/-/g, '_').slice(0, 40), gitProvider: gitRepo.includes('github.com') ? 'github' : 'gitea',
cpuLimit: plan === 'enterprise' ? '2' : plan === 'pro' ? '1' : '0.5', cpuLimit: plan === 'enterprise' ? '2' : plan === 'pro' ? '1' : '0.5',
memoryLimit: plan === 'enterprise' ? '2048m' : plan === 'pro' ? '1024m' : '512m', memoryLimit: plan === 'enterprise' ? '2048m' : plan === 'pro' ? '1024m' : '512m',
} }
@@ -183,8 +183,7 @@ export async function POST(req: Request) {
result: 'ok', result: 'ok',
details: JSON.stringify({ details: JSON.stringify({
appId: result.app.applicationId, appId: result.app.applicationId,
dbId: result.database?.databaseId, domainId: result.domain?.domainId,
domainId: result.domain.domainId,
url: result.url, url: result.url,
}), }),
}, },
@@ -227,15 +226,11 @@ export async function POST(req: Request) {
data: { data: {
status: isDemo ? 'demo_active' : 'active', status: isDemo ? 'demo_active' : 'active',
dokployAppId: result.app.applicationId, dokployAppId: result.app.applicationId,
dokployDbId: result.database?.databaseId ?? null,
dokployDbName: result.database?.database ?? null,
dokployDbUser: result.database?.username ?? null,
dokployDbPassword: result.database?.password ?? null,
dnsRecordId, dnsRecordId,
metadata: JSON.stringify({ metadata: JSON.stringify({
gitRepo, gitRepo,
gitBranch, gitBranch,
dokployDomainId: result.domain.domainId, dokployDomainId: result.domain?.domainId ?? null,
url: result.url, url: result.url,
provisionedAt: now.toISOString(), provisionedAt: now.toISOString(),
}), }),
+204 -183
View File
@@ -1,102 +1,90 @@
/** /**
* Dokploy API Client * Dokploy API Client (v2 — correct tRPC-based API)
* ------------------- * -----------------------------------------------
* Manages tenant instances on the Dokploy server: create applications from * Manages tenant instances on the Dokploy server.
* a Git repo, deploy, attach domains, create databases, suspend/delete. *
* API format (verified from @dokploy/mcp source):
* - Base URL: {DOKPLOY_URL}/api
* - Auth: x-api-key header (NOT Authorization: Bearer)
* - Endpoints: /application.create, /application.deploy, /application.delete, etc.
* - Application creation is multi-step:
* 1. POST /application.create { name, environmentId, sourceType: 'gitea' }
* 2. POST /application.saveGiteaProvider { applicationId, giteaOwner, giteaRepository, giteaBranch, giteaBuildPath }
* (or saveGitProvider for generic git, saveGithubProvider for github)
* 3. POST /application.saveEnvironment { applicationId, env: [...] }
* 4. POST /application.deploy { applicationId }
* *
* Config (env): * Config (env):
* DOKPLOY_URL - e.g. https://cp.project-url.eu * DOKPLOY_URL - e.g. https://cp.project-url.eu
* DOKPLOY_API_KEY - bearer token * DOKPLOY_API_KEY - x-api-key value
* DOKPLOY_SERVER_ID - the Dokploy server ID to deploy to (default "default") * DOKPLOY_ENVIRONMENT_ID - the Dokploy environment ID to create apps under
* DOKPLOY_PROJECT_ID - the Dokploy project to put tenant apps under * DOKPLOY_SERVER_ID - (optional) specific server, null = default
*
* 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 { export interface DokployConfig {
url: string url: string
apiKey: string apiKey: string
serverId?: string environmentId?: string
projectId?: string serverId?: string | null
} }
export interface DokployApp { export interface DokployApp {
applicationId: string applicationId: string
name: string name: string
appName?: string
status?: string status?: string
url?: string previewDomain?: string
createdAt?: 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 { export interface DokployDomain {
domainId: string domainId: string
hostName: string hostName: string
applicationId: string applicationId: string
https: boolean https: boolean
certificateStatus?: string
} }
export interface ProvisionInstanceInput { export interface ProvisionInstanceInput {
name: string name: string
subdomain: string subdomain: string
gitRepo: string // e.g. https://github.com/cetinmustafa83/restov1 gitRepo: string
gitBranch: string // e.g. main gitBranch: string
buildPath?: string // e.g. / (repo root, default) buildPath?: string
envVars: Record<string, string> envVars: Record<string, string>
// Resource limits (Dokploy applies these as container constraints) gitProvider?: 'gitea' | 'github' | 'git' // default: gitea
cpuLimit?: string // e.g. "0.5" = 50% of one core // Resource limits (applied via application.update after creation)
memoryLimit?: string // e.g. "512m" cpuLimit?: string
// Database memoryLimit?: string
databaseName?: string // if set, a Postgres DB is created for this instance
} }
export interface ProvisionInstanceResult { export interface ProvisionInstanceResult {
app: DokployApp app: DokployApp
database?: DokployDatabase domain?: DokployDomain
domain: DokployDomain
url: string url: string
} }
class DokployError extends Error { class DokployError extends Error {
constructor( constructor(message: string, public status: number, public endpoint: string, public body?: unknown) {
message: string,
public status: number,
public endpoint: string,
public body?: unknown
) {
super(message) super(message)
this.name = 'DokployError' this.name = 'DokployError'
} }
} }
export function createDokployClient(config: DokployConfig) { export function createDokployClient(config: DokployConfig) {
const baseUrl = config.url.replace(/\/$/, '') const baseUrl = config.url.replace(/\/+$/, '') + '/api'
const serverId = config.serverId ?? 'default'
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${config.apiKey}`, Accept: 'application/json',
'x-api-key': config.apiKey,
} }
async function call<T = unknown>(endpoint: string, body?: Record<string, unknown>, method = 'POST'): Promise<T> { async function call<T = unknown>(path: string, body?: Record<string, unknown>, method = 'POST'): Promise<T> {
const url = `${baseUrl}${endpoint}` const url = `${baseUrl}${path}`
const isGet = method === 'GET'
const res = await fetch(url, { const res = await fetch(url, {
method, method,
headers, headers,
body: body ? JSON.stringify(body) : undefined, body: !isGet && body ? JSON.stringify(body) : undefined,
}) })
const text = await res.text() const text = await res.text()
let json: unknown let json: unknown
@@ -107,9 +95,9 @@ export function createDokployClient(config: DokployConfig) {
} }
if (!res.ok) { if (!res.ok) {
throw new DokployError( throw new DokployError(
`Dokploy API ${endpoint} failed: HTTP ${res.status}`, `Dokploy API ${path} failed: HTTP ${res.status}`,
res.status, res.status,
endpoint, path,
json json
) )
} }
@@ -120,111 +108,130 @@ export function createDokployClient(config: DokployConfig) {
async function createApplication(input: { async function createApplication(input: {
name: string name: string
environmentId: string
sourceType?: string
serverId?: string | null
description?: string description?: string
gitRepo: string
gitBranch: string
buildPath?: string
envVars?: Record<string, string>
cpuLimit?: string
memoryLimit?: string
}): Promise<DokployApp> { }): Promise<DokployApp> {
const body: Record<string, unknown> = { const body: Record<string, unknown> = {
name: input.name, name: input.name,
description: input.description ?? `Tenant instance: ${input.name}`, environmentId: input.environmentId,
serverId, sourceType: input.sourceType ?? 'gitea',
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) if (input.serverId) body.serverId = input.serverId
const applicationId = res.applicationId ?? (res.app as { applicationId: string } | undefined)?.applicationId if (input.description) body.description = input.description
const res = await call<{ applicationId?: string; app?: DokployApp; [k: string]: unknown }>(
'/application.create',
body
)
const applicationId = res.applicationId ?? (res.app as DokployApp | undefined)?.applicationId
if (!applicationId) { if (!applicationId) {
throw new DokployError('Dokploy createApplication returned no applicationId', 200, '/api/application.create', res) throw new DokployError('createApplication returned no applicationId', 200, '/application.create', res)
} }
return { applicationId, name: input.name, status: 'created' } return { applicationId, name: input.name, status: 'created' }
} }
async function deployApplication(applicationId: string): Promise<{ deployId: string; status: string }> { async function saveGiteaProvider(input: {
const res = await call<{ deployId?: string; jobId?: string; status?: string }>('/api/application.deploy', { applicationId }) applicationId: string
return { deployId: res.deployId ?? res.jobId ?? '', status: res.status ?? 'deploying' } giteaOwner: string
giteaRepository: string
giteaBranch: string
giteaBuildPath?: string | null
giteaId?: string | null
}): Promise<void> {
await call('/application.saveGiteaProvider', {
applicationId: input.applicationId,
giteaOwner: input.giteaOwner,
giteaRepository: input.giteaRepository,
giteaBranch: input.giteaBranch,
giteaBuildPath: input.giteaBuildPath ?? '/',
giteaId: input.giteaId ?? null,
})
} }
async function getApplication(applicationId: string): Promise<DokployApp & { status?: string; url?: string }> { async function saveGitProvider(input: {
applicationId: string
gitProviderOwner?: string | null
gitProviderRepository?: string | null
gitProviderBranch?: string
gitProviderBuildPath?: string | null
gitProviderId?: string | null
}): Promise<void> {
await call('/application.saveGitProvider', {
applicationId: input.applicationId,
gitProviderOwner: input.gitProviderOwner ?? null,
gitProviderRepository: input.gitProviderRepository ?? null,
gitProviderBranch: input.gitProviderBranch ?? 'main',
gitProviderBuildPath: input.gitProviderBuildPath ?? '/',
gitProviderId: input.gitProviderId ?? null,
})
}
async function saveGithubProvider(input: {
applicationId: string
githubOwner: string
githubRepository: string
githubBranch: string
githubBuildPath?: string | null
githubId?: string | null
}): Promise<void> {
await call('/application.saveGithubProvider', {
applicationId: input.applicationId,
githubOwner: input.githubOwner,
githubRepository: input.githubRepository,
githubBranch: input.githubBranch,
githubBuildPath: input.githubBuildPath ?? '/',
githubId: input.githubId ?? null,
})
}
async function saveEnvironment(applicationId: string, envVars: Record<string, string>): Promise<void> {
// Dokploy expects env as array of { key, value } objects OR a JSON string
const envArray = Object.entries(envVars).map(([key, value]) => ({ key, value }))
await call('/application.saveEnvironment', {
applicationId,
env: JSON.stringify(envArray),
})
}
async function deployApplication(applicationId: string): Promise<{ jobId?: string; status: string }> {
const res = await call<{ jobId?: string; deployId?: string; status?: string }>('/application.deploy', {
applicationId,
})
return { jobId: res.jobId ?? res.deployId, status: res.status ?? 'deploying' }
}
async function getApplication(applicationId: string): Promise<DokployApp> {
const res = await call<DokployApp & { applicationStatus?: string; previewDomain?: string }>( const res = await call<DokployApp & { applicationStatus?: string; previewDomain?: string }>(
`/api/application.one`, '/application.one',
{ applicationId }, { applicationId },
'GET'
) )
return { return {
applicationId: res.applicationId, applicationId: res.applicationId,
name: res.name, name: res.name,
appName: res.appName,
status: res.applicationStatus ?? res.status, status: res.applicationStatus ?? res.status,
url: res.previewDomain ?? res.url, previewDomain: res.previewDomain,
createdAt: res.createdAt, createdAt: res.createdAt,
} }
} }
async function deleteApplication(applicationId: string): Promise<void> { async function deleteApplication(applicationId: string): Promise<void> {
await call('/api/application.delete', { applicationId, deleteVolumes: true }) await call('/application.delete', { applicationId })
} }
async function stopApplication(applicationId: string): Promise<void> { async function stopApplication(applicationId: string): Promise<void> {
await call('/api/application.stop', { applicationId }) await call('/application.stop', { applicationId })
} }
async function startApplication(applicationId: string): Promise<void> { async function startApplication(applicationId: string): Promise<void> {
await call('/api/application.start', { applicationId }) await call('/application.start', { applicationId })
} }
async function updateEnv(applicationId: string, envVars: Record<string, string>): Promise<void> { async function redeployApplication(applicationId: string): Promise<void> {
await call('/api/application.update', { await call('/application.redeploy', { applicationId })
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 --- // --- Domains ---
@@ -235,20 +242,17 @@ export function createDokployClient(config: DokployConfig) {
path?: string path?: string
port?: number port?: number
https?: boolean https?: boolean
certificateType?: 'letsencrypt' | 'none'
}): Promise<DokployDomain> { }): Promise<DokployDomain> {
const body: Record<string, unknown> = { const res = await call<{ domainId?: string; id?: string }>('/domain.create', {
applicationId: input.applicationId, applicationId: input.applicationId,
hostName: input.hostName, hostName: input.hostName,
path: input.path ?? '/', path: input.path ?? '/',
port: input.port ?? 3000, port: input.port ?? 3000,
https: input.https ?? true, 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 const domainId = res.domainId ?? res.id
if (!domainId) { if (!domainId) {
throw new DokployError('Dokploy createDomain returned no domainId', 200, '/api/domain.create', res) throw new DokployError('createDomain returned no domainId', 200, '/domain.create', res)
} }
return { return {
domainId, domainId,
@@ -259,55 +263,84 @@ export function createDokployClient(config: DokployConfig) {
} }
async function deleteDomain(domainId: string): Promise<void> { async function deleteDomain(domainId: string): Promise<void> {
await call('/api/domain.delete', { domainId }) await call('/domain.delete', { domainId })
} }
// --- High-level: provision a full instance --- // --- High-level: provision a full instance ---
async function provisionInstance(input: ProvisionInstanceInput): Promise<ProvisionInstanceResult> { async function provisionInstance(input: ProvisionInstanceInput): Promise<ProvisionInstanceResult> {
// 1. Create application from Git repo const environmentId = config.environmentId ?? ''
if (!environmentId) {
throw new DokployError('DOKPLOY_ENVIRONMENT_ID not configured', 500, 'config')
}
// 1. Create application
const app = await createApplication({ const app = await createApplication({
name: input.name, name: input.name,
gitRepo: input.gitRepo, environmentId,
gitBranch: input.gitBranch, sourceType: input.gitProvider ?? 'gitea',
buildPath: '/', serverId: config.serverId,
envVars: input.envVars, description: `Tenant instance: ${input.subdomain}`,
cpuLimit: input.cpuLimit,
memoryLimit: input.memoryLimit,
}) })
// 2. (Optional) Create Postgres database // 2. Save Git provider (repo connection)
let database: DokployDatabase | undefined // Parse git repo URL: https://git.dfmpanel.eu/dfmsolutions/AOS.git
if (input.databaseName) { // → owner: dfmsolutions, repo: AOS
database = await createPostgresDatabase({ const gitMatch = input.gitRepo.match(/\/([^/]+)\/([^/]+?)(?:\.git)?$/)
name: input.databaseName, const owner = gitMatch?.[1] ?? ''
appName: input.name, const repo = gitMatch?.[2] ?? ''
if (input.gitProvider === 'github' || input.gitRepo.includes('github.com')) {
await saveGithubProvider({
applicationId: app.applicationId,
githubOwner: owner,
githubRepository: repo,
githubBranch: input.gitBranch,
githubBuildPath: input.buildPath ?? '/',
}) })
// Inject DATABASE_URL into the app's env } else if (input.gitProvider === 'gitea' || input.gitRepo.includes('git.dfmpanel.eu')) {
const dbUrl = `postgresql://${database.username}:${database.password}@${database.host}:${database.port}/${database.database}` await saveGiteaProvider({
await updateEnv(app.applicationId, { applicationId: app.applicationId,
...input.envVars, giteaOwner: owner,
DATABASE_URL: dbUrl, giteaRepository: repo,
giteaBranch: input.gitBranch,
giteaBuildPath: input.buildPath ?? '/',
})
} else {
// Generic git
await saveGitProvider({
applicationId: app.applicationId,
gitProviderOwner: owner,
gitProviderRepository: repo,
gitProviderBranch: input.gitBranch,
gitProviderBuildPath: input.buildPath ?? '/',
}) })
} }
// 3. Attach domain (subdomain.platform-domain) // 3. Save environment variables
const hostName = input.subdomain await saveEnvironment(app.applicationId, input.envVars)
const domain = await createDomain({
applicationId: app.applicationId,
hostName,
port: 3000,
https: true,
})
// 4. Trigger deploy // 4. Deploy
await deployApplication(app.applicationId) await deployApplication(app.applicationId)
// 5. Attach domain (optional — can be done later)
let domain: DokployDomain | undefined
try {
domain = await createDomain({
applicationId: app.applicationId,
hostName: input.subdomain,
port: 3000,
https: true,
})
} catch (e) {
// Domain creation is non-fatal — app is already deployed
console.warn('[dokploy] Domain creation failed (non-fatal):', e instanceof Error ? e.message : e)
}
return { return {
app, app,
database,
domain, domain,
url: `https://${hostName}`, url: `https://${input.subdomain}`,
} }
} }
@@ -315,14 +348,13 @@ export function createDokployClient(config: DokployConfig) {
async function teardownInstance(input: { async function teardownInstance(input: {
dokployAppId?: string dokployAppId?: string
dokployDbId?: string dokployDomainId?: string
dnsDomainId?: string // Dokploy domain ID
}): Promise<{ steps: string[] }> { }): Promise<{ steps: string[] }> {
const steps: string[] = [] const steps: string[] = []
// Remove domain first (stop traffic) // Remove domain first (stop traffic)
if (input.dnsDomainId) { if (input.dokployDomainId) {
try { try {
await deleteDomain(input.dnsDomainId) await deleteDomain(input.dokployDomainId)
steps.push('domain.removed') steps.push('domain.removed')
} catch (e) { } catch (e) {
if (e instanceof DokployError && e.status !== 404) { if (e instanceof DokployError && e.status !== 404) {
@@ -330,17 +362,6 @@ export function createDokployClient(config: DokployConfig) {
} }
} }
} }
// 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) // Delete application (+ volumes)
if (input.dokployAppId) { if (input.dokployAppId) {
try { try {
@@ -358,14 +379,16 @@ export function createDokployClient(config: DokployConfig) {
return { return {
// Low-level // Low-level
createApplication, createApplication,
saveGiteaProvider,
saveGitProvider,
saveGithubProvider,
saveEnvironment,
deployApplication, deployApplication,
getApplication, getApplication,
deleteApplication, deleteApplication,
stopApplication, stopApplication,
startApplication, startApplication,
updateEnv, redeployApplication,
createPostgresDatabase,
deleteDatabase,
createDomain, createDomain,
deleteDomain, deleteDomain,
call, call,
@@ -377,8 +400,6 @@ export function createDokployClient(config: DokployConfig) {
export type DokployClient = ReturnType<typeof createDokployClient> export type DokployClient = ReturnType<typeof createDokployClient>
// --- Factory: create from env ---
export function createDokployClientFromEnv(): DokployClient | null { export function createDokployClientFromEnv(): DokployClient | null {
const url = process.env.DOKPLOY_URL const url = process.env.DOKPLOY_URL
const apiKey = process.env.DOKPLOY_API_KEY const apiKey = process.env.DOKPLOY_API_KEY
@@ -386,7 +407,7 @@ export function createDokployClientFromEnv(): DokployClient | null {
return createDokployClient({ return createDokployClient({
url, url,
apiKey, apiKey,
serverId: process.env.DOKPLOY_SERVER_ID, environmentId: process.env.DOKPLOY_ENVIRONMENT_ID,
projectId: process.env.DOKPLOY_PROJECT_ID, serverId: process.env.DOKPLOY_SERVER_ID ?? null,
}) })
} }