diff --git a/apps/server/src/api/installations.ts b/apps/server/src/api/installations.ts index 58bfb93..fad765a 100644 --- a/apps/server/src/api/installations.ts +++ b/apps/server/src/api/installations.ts @@ -68,6 +68,7 @@ installationsRouter.patch("/:id/config", (req, res) => { basicAuthPassword, startCommand, publishHostPort, + hostVolumes, } = req.body ?? {}; try { const installation = configureInstallation(req.params.id, { @@ -80,6 +81,7 @@ installationsRouter.patch("/:id/config", (req, res) => { basicAuthPassword, startCommand, publishHostPort, + hostVolumes, }); res.json(toPublicJson(installation)); } catch (err) { diff --git a/apps/server/src/db/index.ts b/apps/server/src/db/index.ts index ed741f6..d394ad8 100644 --- a/apps/server/src/db/index.ts +++ b/apps/server/src/db/index.ts @@ -38,6 +38,7 @@ for (const ddl of [ "ALTER TABLE installations ADD COLUMN basic_auth_password_encrypted TEXT", "ALTER TABLE installations ADD COLUMN start_command TEXT", "ALTER TABLE installations ADD COLUMN publish_host_port INTEGER", + "ALTER TABLE installations ADD COLUMN host_volumes_json TEXT", ]) { try { db.exec(ddl); diff --git a/apps/server/src/db/installations.ts b/apps/server/src/db/installations.ts index 73048b1..929991a 100644 --- a/apps/server/src/db/installations.ts +++ b/apps/server/src/db/installations.ts @@ -12,6 +12,11 @@ export type InstallationStatus = | "stopped" | "error"; +export interface HostVolume { + hostPath: string; + containerPath: string; +} + export interface Installation { id: string; slug: string; @@ -33,6 +38,7 @@ export interface Installation { basicAuthPassword: string | null; startCommand: string | null; publishHostPort: number | null; + hostVolumes: HostVolume[]; createdAt: string; updatedAt: string; } @@ -58,6 +64,7 @@ interface Row { basic_auth_password_encrypted: string | null; start_command: string | null; publish_host_port: number | null; + host_volumes_json: string | null; created_at: string; updated_at: string; } @@ -84,6 +91,7 @@ function rowToInstallation(row: Row): Installation { basicAuthPassword: row.basic_auth_password_encrypted ? decryptSecret(row.basic_auth_password_encrypted) : null, startCommand: row.start_command, publishHostPort: row.publish_host_port, + hostVolumes: row.host_volumes_json ? JSON.parse(row.host_volumes_json) : [], createdAt: row.created_at, updatedAt: row.updated_at, }; @@ -148,6 +156,7 @@ export interface InstallationUpdate { basicAuthPassword?: string | null; startCommand?: string | null; publishHostPort?: number | null; + hostVolumes?: HostVolume[]; gitRef?: string | null; } @@ -172,6 +181,7 @@ export function updateInstallation(id: string, patch: InstallationUpdate): Insta patch.basicAuthPassword !== undefined ? patch.basicAuthPassword : current.basicAuthPassword, startCommand: patch.startCommand !== undefined ? patch.startCommand : current.startCommand, publishHostPort: patch.publishHostPort !== undefined ? patch.publishHostPort : current.publishHostPort, + hostVolumes: patch.hostVolumes !== undefined ? patch.hostVolumes : current.hostVolumes, gitRef: patch.gitRef !== undefined ? patch.gitRef : current.gitRef, }; @@ -191,6 +201,7 @@ export function updateInstallation(id: string, patch: InstallationUpdate): Insta basic_auth_password_encrypted = @basicAuthPasswordEncrypted, start_command = @startCommand, publish_host_port = @publishHostPort, + host_volumes_json = @hostVolumesJson, git_ref = @gitRef, updated_at = @updatedAt WHERE id = @id`, @@ -210,6 +221,7 @@ export function updateInstallation(id: string, patch: InstallationUpdate): Insta basicAuthPasswordEncrypted: next.basicAuthPassword ? encryptSecret(next.basicAuthPassword) : null, startCommand: next.startCommand, publishHostPort: next.publishHostPort, + hostVolumesJson: JSON.stringify(next.hostVolumes), gitRef: next.gitRef, updatedAt: new Date().toISOString(), }); diff --git a/apps/server/src/docker/containers.ts b/apps/server/src/docker/containers.ts index 20b2a74..45da4cf 100644 --- a/apps/server/src/docker/containers.ts +++ b/apps/server/src/docker/containers.ts @@ -40,6 +40,10 @@ export async function runInstallationContainer(installation: Installation): Prom ? { [`${installation.internalPort}/tcp`]: [{ HostIp: "127.0.0.1", HostPort: String(installation.publishHostPort) }] } : undefined; + // Bind-mounts a host directory into the container — for state that must survive rebuilds + // and that nothing else manages (e.g. a browser profile with an existing logged-in session). + const binds = installation.hostVolumes.map((v) => `${v.hostPath}:${v.containerPath}`); + const container = await docker.createContainer({ name: installation.containerName, Image: installation.imageTag, @@ -55,6 +59,7 @@ export async function runInstallationContainer(installation: Installation): Prom ...(config.containerMemoryMb > 0 ? { Memory: config.containerMemoryMb * 1024 * 1024 } : {}), ...(config.containerCpus > 0 ? { NanoCpus: Math.round(config.containerCpus * 1e9) } : {}), ...(portBinding ? { PortBindings: portBinding } : {}), + ...(binds.length > 0 ? { Binds: binds } : {}), }, }); diff --git a/apps/server/src/service/orchestrator.ts b/apps/server/src/service/orchestrator.ts index 7215b90..456ff34 100644 --- a/apps/server/src/service/orchestrator.ts +++ b/apps/server/src/service/orchestrator.ts @@ -1,5 +1,6 @@ import { type Installation, + type HostVolume, createInstallation, updateInstallation, getInstallationById, @@ -57,6 +58,7 @@ export function configureInstallation( basicAuthPassword?: string | null; startCommand?: string | null; publishHostPort?: number | null; + hostVolumes?: HostVolume[]; }, ): Installation { return updateInstallation(id, patch);