feat: add Docker bootstrap for mcpd with auto-migration and seeding
Some checks are pending
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / test (push) Waiting to run
CI / build (push) Blocked by required conditions

Adds Dockerfile, entrypoint, and server bootstrap so that
`docker compose up` starts postgres, pushes the schema,
seeds default MCP servers, and starts mcpd with all routes wired up.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michal
2026-02-21 13:34:18 +00:00
parent 6da4ae495c
commit 89b2b1b13d
7 changed files with 216 additions and 5 deletions

15
.dockerignore Normal file
View File

@@ -0,0 +1,15 @@
node_modules
*/node_modules
**/node_modules
dist
**/dist
.git
.taskmaster
.claude
*.md
!pnpm-workspace.yaml
.env
.env.*
deploy/docker-compose.yml
src/cli
src/local-proxy

61
deploy/Dockerfile.mcpd Normal file
View File

@@ -0,0 +1,61 @@
# Stage 1: Build TypeScript
FROM node:20-alpine AS builder
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
WORKDIR /app
# Copy workspace config and package manifests
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json tsconfig.base.json ./
COPY src/mcpd/package.json src/mcpd/tsconfig.json src/mcpd/
COPY src/db/package.json src/db/tsconfig.json src/db/
COPY src/shared/package.json src/shared/tsconfig.json src/shared/
# Install all dependencies
RUN pnpm install --frozen-lockfile
# Copy source code
COPY src/mcpd/src/ src/mcpd/src/
COPY src/db/src/ src/db/src/
COPY src/db/prisma/ src/db/prisma/
COPY src/shared/src/ src/shared/src/
# Generate Prisma client and build TypeScript
RUN pnpm -F @mcpctl/db db:generate
RUN pnpm -F @mcpctl/shared build && pnpm -F @mcpctl/db build && pnpm -F @mcpctl/mcpd build
# Stage 2: Production runtime
FROM node:20-alpine
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
WORKDIR /app
# Copy workspace config, manifests, and lockfile
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./
COPY src/mcpd/package.json src/mcpd/
COPY src/db/package.json src/db/
COPY src/shared/package.json src/shared/
# Install all deps (prisma CLI needed at runtime for db push)
RUN pnpm install --frozen-lockfile
# Copy prisma schema and generate client
COPY src/db/prisma/ src/db/prisma/
RUN pnpm -F @mcpctl/db db:generate
# Copy built output from builder
COPY --from=builder /app/src/shared/dist/ src/shared/dist/
COPY --from=builder /app/src/db/dist/ src/db/dist/
COPY --from=builder /app/src/mcpd/dist/ src/mcpd/dist/
# Copy entrypoint
COPY deploy/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 3100
HEALTHCHECK --interval=10s --timeout=5s --retries=3 --start-period=10s \
CMD wget -q --spider http://localhost:3100/healthz || exit 1
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -27,14 +27,17 @@ services:
- "3100:3100"
environment:
DATABASE_URL: postgresql://mcpctl:mcpctl_dev@postgres:5432/mcpctl
PORT: "3100"
HOST: "0.0.0.0"
LOG_LEVEL: info
MCPD_PORT: "3100"
MCPD_HOST: "0.0.0.0"
MCPD_LOG_LEVEL: info
depends_on:
postgres:
condition: service_healthy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# Mount container runtime socket (Docker or Podman)
# For Docker: /var/run/docker.sock
# For Podman: /run/user/<UID>/podman/podman.sock
- ${CONTAINER_SOCK:-/var/run/docker.sock}:/var/run/docker.sock
networks:
- mcpctl
- mcp-servers

11
deploy/entrypoint.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/sh
set -e
echo "mcpd: pushing database schema..."
pnpm -F @mcpctl/db exec prisma db push --schema=prisma/schema.prisma --accept-data-loss 2>&1
echo "mcpd: seeding default data..."
node src/mcpd/dist/seed-runner.js
echo "mcpd: starting server..."
exec node src/mcpd/dist/main.js

View File

@@ -9,7 +9,7 @@
"build": "tsc --build",
"clean": "rimraf dist",
"dev": "tsx watch src/index.ts",
"start": "node dist/index.js",
"start": "node dist/main.js",
"test": "vitest",
"test:run": "vitest run"
},

104
src/mcpd/src/main.ts Normal file
View File

@@ -0,0 +1,104 @@
import { PrismaClient } from '@prisma/client';
import { seedMcpServers } from '@mcpctl/db';
import { loadConfigFromEnv } from './config/index.js';
import { createServer } from './server.js';
import { setupGracefulShutdown } from './utils/index.js';
import {
McpServerRepository,
McpProfileRepository,
McpInstanceRepository,
ProjectRepository,
AuditLogRepository,
} from './repositories/index.js';
import {
McpServerService,
McpProfileService,
InstanceService,
ProjectService,
AuditLogService,
DockerContainerManager,
MetricsCollector,
HealthAggregator,
BackupService,
RestoreService,
} from './services/index.js';
import {
registerMcpServerRoutes,
registerMcpProfileRoutes,
registerInstanceRoutes,
registerProjectRoutes,
registerAuditLogRoutes,
registerHealthMonitoringRoutes,
registerBackupRoutes,
} from './routes/index.js';
async function main(): Promise<void> {
const config = loadConfigFromEnv();
// Database
const prisma = new PrismaClient({
datasources: { db: { url: config.databaseUrl } },
});
await prisma.$connect();
// Seed default servers (upsert, safe to repeat)
await seedMcpServers(prisma);
// Repositories
const serverRepo = new McpServerRepository(prisma);
const profileRepo = new McpProfileRepository(prisma);
const instanceRepo = new McpInstanceRepository(prisma);
const projectRepo = new ProjectRepository(prisma);
const auditLogRepo = new AuditLogRepository(prisma);
// Orchestrator
const orchestrator = new DockerContainerManager();
// Services
const serverService = new McpServerService(serverRepo);
const profileService = new McpProfileService(profileRepo, serverRepo);
const instanceService = new InstanceService(instanceRepo, serverRepo, orchestrator);
const projectService = new ProjectService(projectRepo, profileRepo, serverRepo);
const auditLogService = new AuditLogService(auditLogRepo);
const metricsCollector = new MetricsCollector();
const healthAggregator = new HealthAggregator(metricsCollector, orchestrator);
const backupService = new BackupService(serverRepo, profileRepo, projectRepo);
const restoreService = new RestoreService(serverRepo, profileRepo, projectRepo);
// Server
const app = await createServer(config, {
health: {
checkDb: async () => {
try {
await prisma.$queryRaw`SELECT 1`;
return true;
} catch {
return false;
}
},
},
});
// Routes
registerMcpServerRoutes(app, serverService);
registerMcpProfileRoutes(app, profileService);
registerInstanceRoutes(app, instanceService);
registerProjectRoutes(app, projectService);
registerAuditLogRoutes(app, auditLogService);
registerHealthMonitoringRoutes(app, { healthAggregator, metricsCollector });
registerBackupRoutes(app, { backupService, restoreService });
// Start
await app.listen({ port: config.port, host: config.host });
app.log.info(`mcpd listening on ${config.host}:${config.port}`);
// Graceful shutdown
setupGracefulShutdown(app, {
disconnectDb: () => prisma.$disconnect(),
});
}
main().catch((err) => {
console.error('Failed to start mcpd:', err);
process.exit(1);
});

View File

@@ -0,0 +1,17 @@
import { PrismaClient } from '@prisma/client';
import { seedMcpServers } from '@mcpctl/db';
async function run(): Promise<void> {
const prisma = new PrismaClient();
try {
const count = await seedMcpServers(prisma);
console.log(`Seeded ${count} MCP servers`);
} finally {
await prisma.$disconnect();
}
}
run().catch((err) => {
console.error('Seed failed:', err);
process.exit(1);
});