mc-manager/src/lib/docker.ts
2026-02-07 12:20:12 -08:00

187 lines
4.6 KiB
TypeScript

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<Docker.Container> {
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<string, object> = { '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<void> {
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<Docker.Container | null> {
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<Docker.Container | null> {
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}`
}