- FastAPI backend with REST API endpoints - SQLite database for container metadata - Docker/Podman SDK integration with label filtering - Frontend: Server creation form and management page - Container operations: create, list, start, stop, delete - Single container deployment (nginx + Python + supervisor) - Support for Docker and Podman (rootless) - Volume management for persistent data
116 lines
4 KiB
Python
116 lines
4 KiB
Python
import docker
|
|
import os
|
|
import json
|
|
|
|
LABEL_MANAGED = "stronghold.managed"
|
|
LABEL_VALUE = "true"
|
|
|
|
def get_docker_client():
|
|
"""Get Docker client, automatically detecting Docker or Podman"""
|
|
try:
|
|
# Try Docker socket first
|
|
if os.path.exists('/var/run/docker.sock'):
|
|
return docker.DockerClient(base_url='unix://var/run/docker.sock')
|
|
|
|
# Try Podman rootful
|
|
if os.path.exists('/run/podman/podman.sock'):
|
|
return docker.DockerClient(base_url='unix://run/podman/podman.sock')
|
|
|
|
# Try Podman rootless
|
|
xdg_runtime = os.environ.get('XDG_RUNTIME_DIR', '')
|
|
podman_sock = f'{xdg_runtime}/podman/podman.sock'
|
|
if xdg_runtime and os.path.exists(podman_sock):
|
|
return docker.DockerClient(base_url=f'unix://{podman_sock}')
|
|
|
|
# Fallback to default
|
|
return docker.DockerClient(base_url='unix://var/run/docker.sock')
|
|
except Exception as e:
|
|
raise Exception(f"Failed to connect to Docker/Podman: {e}")
|
|
|
|
def list_containers(client):
|
|
"""List all Stronghold-managed containers"""
|
|
filters = {"label": f"{LABEL_MANAGED}={LABEL_VALUE}"}
|
|
return client.containers.list(all=True, filters=filters)
|
|
|
|
def create_container(client, name, config):
|
|
"""Create a new container with Stronghold label"""
|
|
# Build environment variables from config
|
|
environment = {
|
|
"EULA": "TRUE" if config.get("acceptEULA") else "FALSE",
|
|
"TYPE": config.get("serverType", "PAPER"),
|
|
}
|
|
|
|
# Add optional environment variables
|
|
if config.get("version") and config.get("version") != "LATEST":
|
|
environment["VERSION"] = config["version"]
|
|
if config.get("memory"):
|
|
environment["MEMORY"] = config["memory"]
|
|
if config.get("difficulty"):
|
|
environment["DIFFICULTY"] = config["difficulty"]
|
|
if config.get("gamemode"):
|
|
environment["MODE"] = config["gamemode"]
|
|
if config.get("levelType"):
|
|
environment["LEVEL_TYPE"] = config["levelType"]
|
|
if config.get("motd"):
|
|
environment["MOTD"] = config["motd"]
|
|
if config.get("maxPlayers"):
|
|
environment["MAX_PLAYERS"] = str(config["maxPlayers"])
|
|
if config.get("viewDistance"):
|
|
environment["VIEW_DISTANCE"] = str(config["viewDistance"])
|
|
if not config.get("pvpEnabled", True):
|
|
environment["PVP"] = "false"
|
|
if config.get("allowFlight"):
|
|
environment["ALLOW_FLIGHT"] = "true"
|
|
if config.get("enableRCON"):
|
|
environment["ENABLE_RCON"] = "true"
|
|
if config.get("rconPassword"):
|
|
environment["RCON_PASSWORD"] = config["rconPassword"]
|
|
|
|
# Port bindings (host_port:container_port)
|
|
port = config.get("port", "25565")
|
|
port_bindings = {f"{25565}/tcp": int(port)}
|
|
if config.get("enableRCON"):
|
|
rcon_port = config.get("rconPort", "25575")
|
|
port_bindings[f"{25575}/tcp"] = int(rcon_port)
|
|
|
|
# Create or get volume
|
|
volume_name = f"{name}_data"
|
|
try:
|
|
client.volumes.get(volume_name)
|
|
except:
|
|
client.volumes.create(name=volume_name, labels={LABEL_MANAGED: LABEL_VALUE})
|
|
|
|
# Create container with proper volume mount
|
|
restart_policy = config.get("restartPolicy", "unless-stopped")
|
|
|
|
container = client.containers.create(
|
|
image="itzg/minecraft-server",
|
|
name=name,
|
|
environment=environment,
|
|
ports=port_bindings,
|
|
volumes=[f"{volume_name}:/data"],
|
|
labels={LABEL_MANAGED: LABEL_VALUE},
|
|
restart_policy={"Name": restart_policy} if restart_policy != "no" else None,
|
|
detach=True
|
|
)
|
|
|
|
return container
|
|
|
|
def start_container(client, container_id):
|
|
"""Start a container"""
|
|
container = client.containers.get(container_id)
|
|
container.start()
|
|
return container
|
|
|
|
def stop_container(client, container_id):
|
|
"""Stop a container"""
|
|
container = client.containers.get(container_id)
|
|
container.stop()
|
|
return container
|
|
|
|
def remove_container(client, container_id):
|
|
"""Remove a container"""
|
|
container = client.containers.get(container_id)
|
|
container.remove(force=True)
|
|
return container
|
|
|