From 1efba2461adc6b44cd27f3358e4091220b75263e Mon Sep 17 00:00:00 2001 From: Dasemu Date: Thu, 8 Oct 2026 06:46:28 +0200 Subject: [PATCH] feat: support an explicit start command override per installation Needed to install third-party repos we don't control (e.g. the upstream gitea.com/gitea/gitea-mcp project) where nixpacks detects a working build but the wrong/no start command, and pushing a nixpacks.toml to the source repo isn't an option. Passed through as nixpacks' own --start-cmd flag. Co-Authored-By: Claude Sonnet 5 --- apps/server/src/api/installations.ts | 4 +++- apps/server/src/build/nixpacks.ts | 14 +++++++++++++- apps/server/src/db/index.ts | 1 + apps/server/src/db/installations.ts | 7 +++++++ apps/server/src/service/orchestrator.ts | 8 +++++++- 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/server/src/api/installations.ts b/apps/server/src/api/installations.ts index 2e7b407..4755042 100644 --- a/apps/server/src/api/installations.ts +++ b/apps/server/src/api/installations.ts @@ -58,7 +58,8 @@ installationsRouter.post("/", async (req, res) => { }); installationsRouter.patch("/:id/config", (req, res) => { - const { env, portEnvVar, hostEnvVar, httpPath, internalPort, basicAuthUsername, basicAuthPassword } = req.body ?? {}; + const { env, portEnvVar, hostEnvVar, httpPath, internalPort, basicAuthUsername, basicAuthPassword, startCommand } = + req.body ?? {}; try { const installation = configureInstallation(req.params.id, { env, @@ -68,6 +69,7 @@ installationsRouter.patch("/:id/config", (req, res) => { internalPort, basicAuthUsername, basicAuthPassword, + startCommand, }); res.json(toPublicJson(installation)); } catch (err) { diff --git a/apps/server/src/build/nixpacks.ts b/apps/server/src/build/nixpacks.ts index f08c702..f71121e 100644 --- a/apps/server/src/build/nixpacks.ts +++ b/apps/server/src/build/nixpacks.ts @@ -54,7 +54,12 @@ export function detectNodeVersion(repoDir: string): string | undefined { return DEFAULT_NODE_VERSION; } -export function runNixpacksBuild(slug: string, repoDir: string, imageTag: string): Promise { +export function runNixpacksBuild( + slug: string, + repoDir: string, + imageTag: string, + startCommand?: string | null, +): Promise { return new Promise((resolve) => { const buildId = Date.now().toString(); const logPath = buildLogPath(slug, buildId); @@ -62,6 +67,13 @@ export function runNixpacksBuild(slug: string, repoDir: string, imageTag: string const logStream = fs.createWriteStream(logPath, { flags: "a" }); const args = ["build", repoDir, "--name", imageTag]; + if (startCommand) { + // Lets an installation override the start command without needing a nixpacks.toml in + // the source repo — necessary for third-party repos we don't control (e.g. installing + // gitea.com/gitea/gitea-mcp as-is, which nixpacks builds fine but whose detected start + // command needs CLI flags like `-t http -p ` appended). + args.push("--start-cmd", startCommand); + } const nodeVersion = detectNodeVersion(repoDir); if (nodeVersion) { // nixpacks does NOT read NIXPACKS_* config from the process environment — only from diff --git a/apps/server/src/db/index.ts b/apps/server/src/db/index.ts index 4cf96e8..4b1b980 100644 --- a/apps/server/src/db/index.ts +++ b/apps/server/src/db/index.ts @@ -36,6 +36,7 @@ CREATE TABLE IF NOT EXISTS installations ( for (const ddl of [ "ALTER TABLE installations ADD COLUMN basic_auth_username TEXT", "ALTER TABLE installations ADD COLUMN basic_auth_password_encrypted TEXT", + "ALTER TABLE installations ADD COLUMN start_command TEXT", ]) { try { db.exec(ddl); diff --git a/apps/server/src/db/installations.ts b/apps/server/src/db/installations.ts index 62c428d..dc718f1 100644 --- a/apps/server/src/db/installations.ts +++ b/apps/server/src/db/installations.ts @@ -31,6 +31,7 @@ export interface Installation { bearerToken: string | null; basicAuthUsername: string | null; basicAuthPassword: string | null; + startCommand: string | null; createdAt: string; updatedAt: string; } @@ -54,6 +55,7 @@ interface Row { env_json_encrypted: string | null; basic_auth_username: string | null; basic_auth_password_encrypted: string | null; + start_command: string | null; created_at: string; updated_at: string; } @@ -78,6 +80,7 @@ function rowToInstallation(row: Row): Installation { bearerToken: row.bearer_token_encrypted ? decryptSecret(row.bearer_token_encrypted) : null, basicAuthUsername: row.basic_auth_username, basicAuthPassword: row.basic_auth_password_encrypted ? decryptSecret(row.basic_auth_password_encrypted) : null, + startCommand: row.start_command, createdAt: row.created_at, updatedAt: row.updated_at, }; @@ -140,6 +143,7 @@ export interface InstallationUpdate { bearerToken?: string | null; basicAuthUsername?: string | null; basicAuthPassword?: string | null; + startCommand?: string | null; gitRef?: string | null; } @@ -162,6 +166,7 @@ export function updateInstallation(id: string, patch: InstallationUpdate): Insta patch.basicAuthUsername !== undefined ? patch.basicAuthUsername : current.basicAuthUsername, basicAuthPassword: patch.basicAuthPassword !== undefined ? patch.basicAuthPassword : current.basicAuthPassword, + startCommand: patch.startCommand !== undefined ? patch.startCommand : current.startCommand, gitRef: patch.gitRef !== undefined ? patch.gitRef : current.gitRef, }; @@ -179,6 +184,7 @@ export function updateInstallation(id: string, patch: InstallationUpdate): Insta bearer_token_encrypted = @bearerTokenEncrypted, basic_auth_username = @basicAuthUsername, basic_auth_password_encrypted = @basicAuthPasswordEncrypted, + start_command = @startCommand, git_ref = @gitRef, updated_at = @updatedAt WHERE id = @id`, @@ -196,6 +202,7 @@ export function updateInstallation(id: string, patch: InstallationUpdate): Insta bearerTokenEncrypted: next.bearerToken ? encryptSecret(next.bearerToken) : null, basicAuthUsername: next.basicAuthUsername, basicAuthPasswordEncrypted: next.basicAuthPassword ? encryptSecret(next.basicAuthPassword) : null, + startCommand: next.startCommand, gitRef: next.gitRef, updatedAt: new Date().toISOString(), }); diff --git a/apps/server/src/service/orchestrator.ts b/apps/server/src/service/orchestrator.ts index fd696e5..337ca1f 100644 --- a/apps/server/src/service/orchestrator.ts +++ b/apps/server/src/service/orchestrator.ts @@ -55,6 +55,7 @@ export function configureInstallation( internalPort?: number; basicAuthUsername?: string | null; basicAuthPassword?: string | null; + startCommand?: string | null; }, ): Installation { return updateInstallation(id, patch); @@ -68,7 +69,12 @@ export async function buildInstallation(id: string): Promise { const imageTag = `mcpmgr/${installation.slug}:latest`; const previousImageId = installation.imageTag ? await getImageId(installation.imageTag) : undefined; - const result = await runNixpacksBuild(installation.slug, repoDir(installation.slug), imageTag); + const result = await runNixpacksBuild( + installation.slug, + repoDir(installation.slug), + imageTag, + installation.startCommand, + ); if (!result.success) { return updateInstallation(id, {