import Docker from 'dockerode' import { mkdir, chown } from 'fs/promises' import { existsSync } from 'fs' let dockerClient: Docker | null = null /** * Returns a cached dockerode instance connected to the Docker Engine socket. */ export function getDockerClient(): Docker { if (!dockerClient) { const socketPath = process.env.DOCKER_SOCKET || '/var/run/docker.sock' dockerClient = new Docker({ socketPath }) } return dockerClient } /** * Maps Docker container state to our ServerStatus. */ export function mapContainerState(state: string): 'online' | 'offline' | 'starting' | 'stopping' | 'crashed' { switch (state) { case 'running': return 'online' case 'created': case 'restarting': return 'starting' case 'removing': case 'paused': return 'stopping' case 'exited': return 'offline' case 'dead': return 'crashed' default: return 'offline' } } /** * Maps the itzg/minecraft-server TYPE env var from our server.type. */ export function getItzgServerType(type: string): string { switch (type) { case 'vanilla': return 'VANILLA' case 'bukkit': return 'PAPER' // Default bukkit to Paper — override if needed case 'forge': return 'FORGE' case 'fabric': return 'FABRIC' default: return 'VANILLA' } } interface CreateContainerOptions { containerName: string dockerImage: string serverType: string version: string port: number rconPort: number | null memoryMax: number memoryMin: number jvmArgs: string[] autoRestart: boolean serverPath: string maxPlayers: number } /** * Creates a Docker container for a Minecraft server. */ export async function createServerContainer(options: CreateContainerOptions): Promise { const docker = getDockerClient() const isItzg = options.dockerImage === 'itzg/minecraft-server' const env: string[] = [ `EULA=TRUE`, `MAX_PLAYERS=${options.maxPlayers}`, ] // Only set itzg-specific env vars for the default image if (isItzg) { env.push( `TYPE=${getItzgServerType(options.serverType)}`, `VERSION=${options.version}`, `MEMORY=${options.memoryMax}M`, `JVM_XX_OPTS=${options.jvmArgs.join(' ')}`, 'CREATE_CONSOLE_IN_PIPE=true', ) } const portBindings: Docker.PortMap = { '25565/tcp': [{ HostPort: options.port.toString() }], } if (options.rconPort) { portBindings['25575/tcp'] = [{ HostPort: options.rconPort.toString() }] } const exposedPorts: Record = { '25565/tcp': {} } if (options.rconPort) { exposedPorts['25575/tcp'] = {} } const container = await docker.createContainer({ name: options.containerName, Image: options.dockerImage, Env: env, ExposedPorts: exposedPorts, HostConfig: { PortBindings: portBindings, Binds: [`${options.serverPath}:/data:Z`], Memory: options.memoryMax * 1024 * 1024, RestartPolicy: { Name: options.autoRestart ? 'unless-stopped' : 'no', }, }, }) return container } /** * Ensures the server data directory exists with correct ownership (uid=1000) * for the itzg/minecraft-server container. */ export async function ensureServerDirectory(serverPath: string): Promise { if (!existsSync(serverPath)) { await mkdir(serverPath, { recursive: true }) } // itzg/minecraft-server runs as uid=1000 gid=1000 try { await chown(serverPath, 1000, 1000) } catch (err) { console.warn(`[Docker] Could not chown ${serverPath} to 1000:1000 — container may have permission issues:`, (err as Error).message) } } /** * Gets a container by name. Returns null if not found. */ export async function getContainerByName(name: string): Promise { const docker = getDockerClient() try { const container = docker.getContainer(name) await container.inspect() // Verify it exists return container } catch { return null } } /** * Gets a container by ID. Returns null if not found. */ export async function getContainerById(id: string): Promise { const docker = getDockerClient() try { const container = docker.getContainer(id) await container.inspect() return container } catch { return null } } /** * Gets the base server path for volumes. */ export function getServersBasePath(): string { return process.env.MC_SERVERS_PATH || '/opt/mc-servers' } /** * Gets the full path for a specific server's data directory. */ export function getServerPath(serverId: string): string { return `${getServersBasePath()}/${serverId}` }