feat: add Docker bootstrap for mcpd with auto-migration and seeding
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:
15
.dockerignore
Normal file
15
.dockerignore
Normal 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
61
deploy/Dockerfile.mcpd
Normal 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"]
|
||||||
@@ -27,14 +27,17 @@ services:
|
|||||||
- "3100:3100"
|
- "3100:3100"
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql://mcpctl:mcpctl_dev@postgres:5432/mcpctl
|
DATABASE_URL: postgresql://mcpctl:mcpctl_dev@postgres:5432/mcpctl
|
||||||
PORT: "3100"
|
MCPD_PORT: "3100"
|
||||||
HOST: "0.0.0.0"
|
MCPD_HOST: "0.0.0.0"
|
||||||
LOG_LEVEL: info
|
MCPD_LOG_LEVEL: info
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
volumes:
|
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:
|
networks:
|
||||||
- mcpctl
|
- mcpctl
|
||||||
- mcp-servers
|
- mcp-servers
|
||||||
|
|||||||
11
deploy/entrypoint.sh
Executable file
11
deploy/entrypoint.sh
Executable 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
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"build": "tsc --build",
|
"build": "tsc --build",
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"dev": "tsx watch src/index.ts",
|
"dev": "tsx watch src/index.ts",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/main.js",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:run": "vitest run"
|
"test:run": "vitest run"
|
||||||
},
|
},
|
||||||
|
|||||||
104
src/mcpd/src/main.ts
Normal file
104
src/mcpd/src/main.ts
Normal 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);
|
||||||
|
});
|
||||||
17
src/mcpd/src/seed-runner.ts
Normal file
17
src/mcpd/src/seed-runner.ts
Normal 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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user