mirror of
https://github.com/rmoren97/mc-manager.git
synced 2026-02-10 17:40:30 -08:00
187 lines
4.6 KiB
TypeScript
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}`
|
|
}
|