Skip to content

Docker Manager

Superuser-only container management backed by the docker CLI: engine status and install, containers, images, live and historical usage, backups and clones, and a curated app/stack catalog. Every docker command is exec'd as an argument slice - never through a shell - and every user-supplied identifier, image reference, port mapping, environment variable and volume is regex-validated so nothing can be smuggled in as a docker CLI flag.

All endpoints below are mounted under /api. Authentication: Authorization: Bearer <token> unless noted as public.

Endpoint count: 24

Endpoints

Method Path Handler Guards
GET /api/docker/backups listDockerBackups RequireSuperuser(), RequirePlugin(db, "docker")
GET /api/docker/catalog listDockerCatalog RequireSuperuser(), RequirePlugin(db, "docker")
POST /api/docker/catalog/launch launchCatalogApp RequireSuperuser(), RequirePlugin(db, "docker")
GET /api/docker/containers listDockerContainers RequireSuperuser(), RequirePlugin(db, "docker")
POST /api/docker/containers runDockerContainer RequireSuperuser(), RequirePlugin(db, "docker")
DELETE /api/docker/containers/:id removeDockerContainer RequireSuperuser(), RequirePlugin(db, "docker")
PUT /api/docker/containers/:id updateDockerContainer RequireSuperuser(), RequirePlugin(db, "docker")
POST /api/docker/containers/:id/:action dockerContainerAction RequireSuperuser(), RequirePlugin(db, "docker")
POST /api/docker/containers/:id/backup backupDockerContainer RequireSuperuser(), RequirePlugin(db, "docker")
POST /api/docker/containers/:id/clone cloneDockerContainer RequireSuperuser(), RequirePlugin(db, "docker")
GET /api/docker/containers/:id/config dockerContainerConfig RequireSuperuser(), RequirePlugin(db, "docker")
GET /api/docker/containers/:id/history dockerContainerHistory RequireSuperuser(), RequirePlugin(db, "docker")
GET /api/docker/containers/:id/inspect dockerContainerInspect RequireSuperuser(), RequirePlugin(db, "docker")
GET /api/docker/containers/:id/logs dockerContainerLogs RequireSuperuser(), RequirePlugin(db, "docker")
GET /api/docker/images listDockerImages RequireSuperuser(), RequirePlugin(db, "docker")
DELETE /api/docker/images/:id removeDockerImage RequireSuperuser(), RequirePlugin(db, "docker")
POST /api/docker/images/pull pullDockerImage RequireSuperuser(), RequirePlugin(db, "docker")
POST /api/docker/install dockerInstallHandler RequireSuperuser(), RequirePlugin(db, "docker")
POST /api/docker/prune dockerPrune RequireSuperuser(), RequirePlugin(db, "docker")
GET /api/docker/stacks listDockerStackInstances RequireSuperuser(), RequirePlugin(db, "docker")
POST /api/docker/stacks/:instance/:action stackAction RequireSuperuser(), RequirePlugin(db, "docker")
POST /api/docker/stacks/launch launchDockerStack RequireSuperuser(), RequirePlugin(db, "docker")
GET /api/docker/stats dockerStatsHandler RequireSuperuser(), RequirePlugin(db, "docker")
GET /api/docker/status dockerStatus RequireSuperuser(), RequirePlugin(db, "docker")

Source

Handlers for this section live in api/routes/handlers_*.go in the StarkOne source. Each handler binds its request body with gin.ShouldBindJSON(&req) and responds with the standard envelope:

{ "data": { ... } }

On error:

{ "error": { "code": "string", "message": "string" } }

Reference

GET /api/docker/backups

Lists container snapshots taken by the backup endpoint: local images under the starkone-backup/* repository prefix, plus any volume archive folders stored under /var/backups/starkone-docker.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Response - 200 OK

{
  "data": {
    "images": [
      {
        "image": "starkone-backup/web:20260818-120000",
        "size": "412MB",
        "created": "2 hours ago",
        "id": "a1b2c3d4e5f6"
      }
    ],
    "volume_archives": [
      {
        "folder": "/var/backups/starkone-docker/web-20260818-120000",
        "name": "web-20260818-120000"
      }
    ]
  }
}

Errors

This endpoint does not return error responses. If docker images fails, images is an empty array; if /var/backups/starkone-docker cannot be read, volume_archives is an empty array.

Example

curl https://panel.example.com:2087/api/docker/backups \
  -H "Authorization: Bearer $TOKEN"

GET /api/docker/catalog

Serves the curated app gallery: single-container apps, multi-container stacks, and the list of categories they fall into. Environment values containing a {{SECRET:name}} template are masked as •••••• (auto-generated) so the browser never receives a usable credential - the real values are generated server-side at launch time.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Response - 200 OK

{
  "data": {
    "apps": [
      {
        "id": "postgres",
        "name": "PostgreSQL",
        "icon": "🐘",
        "category": "Databases",
        "description": "The reliable open-source relational database.",
        "image": "postgres:16",
        "ports": ["5434:5432"],
        "env": ["POSTGRES_PASSWORD=•••••• (auto-generated)"],
        "volumes": ["/srv/docker/postgres:/var/lib/postgresql/data"]
      }
    ],
    "stacks": [
      {
        "id": "wordpress",
        "name": "WordPress + Database",
        "icon": "📝",
        "category": "Websites",
        "description": "A complete WordPress site: the WordPress app plus its own MariaDB database, wired together automatically.",
        "services": [
          {
            "key": "db",
            "name": "Database (MariaDB)",
            "image": "mariadb:11",
            "network_alias": "db",
            "ports": null,
            "env": ["MARIADB_DATABASE=wordpress", "MARIADB_PASSWORD=•••••• (auto-generated)"],
            "volumes": ["/srv/docker/{{INSTANCE}}/db:/var/lib/mysql"],
            "description": "Stores the site's content. Runs internally; not exposed to the internet.",
            "standalone": false,
            "default_off": false
          }
        ]
      }
    ],
    "categories": ["Websites", "Databases", "Automation", "Monitoring", "Tools"]
  }
}

An app tile carries an optional note string; a stack tile carries an optional note and a services array. Within a service, standalone marks a service that makes sense to run on its own and default_off marks a service that is listed but not ticked by default. Volume paths containing {{INSTANCE}} are substituted with the chosen instance name at launch.

Errors

This endpoint does not return error responses. The catalog is a compiled-in list and does not touch Docker.

Example

curl https://panel.example.com:2087/api/docker/catalog \
  -H "Authorization: Bearer $TOKEN"

POST /api/docker/catalog/launch

Starts a single-container catalog app. Secrets referenced by the entry's environment are generated server-side, substituted into the container's environment, and returned once in credentials so the operator can store them. Published host ports are auto-opened in the firewall and reported as opened_ports.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Request body

{
  "app_id": "string (required)",
  "name": "string (optional)",
  "ports": "[]string (optional)"
}
Field Type Required Description
app_id string yes Catalog app id, e.g. postgres, nginx, gitea.
name string no Container name. Defaults to the app id. Must match ^[a-zA-Z0-9][a-zA-Z0-9_.-]*$.
ports array of string no Port mappings overriding the catalog defaults. Each must match [ip:]host:container[/tcp\|udp], e.g. 8080:80.

The container is always created with --restart unless-stopped and the catalog entry's own image and volumes.

Response - 200 OK

{
  "data": {
    "id": "9f8e7d6c5b4a3928170615243342516071829304a5b6c7d8e9f0a1b2c3d4e5f6",
    "name": "postgres",
    "opened_ports": ["5434/tcp"],
    "credentials": { "pgpass": "generated-password" },
    "ports": ["5434:5432"]
  }
}

credentials is empty for apps that reference no secrets.

Errors

Status Code Meaning
400 bad_request Body could not be parsed or app_id was missing; the container name is invalid; or a port mapping, environment variable or volume failed validation.
404 not_found No catalog app with that app_id.
409 name_taken A container with the chosen name already exists.
500 docker_error docker run failed; the message carries the command's stderr.

Side effects

  • Runs docker inspect <name> to check for a name collision, then docker run -d ... with the catalog image, ports, generated environment and volumes.
  • Creates the host bind-mount directories the entry declares (typically under /srv/docker/<name>).
  • Opens each published host port inbound in the firewall (firewall.OpenPort).
  • Writes a docker_app_launch entry to the user action log.

Example

curl -X POST https://panel.example.com:2087/api/docker/catalog/launch \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"app_id": "postgres", "name": "pg-main", "ports": ["5434:5432"]}'

GET /api/docker/containers

Lists every container on the host, running or not, from docker ps -a --no-trunc.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Response - 200 OK

{
  "data": [
    {
      "id": "9f8e7d6c5b4a3928170615243342516071829304a5b6c7d8e9f0a1b2c3d4e5f6",
      "name": "web",
      "image": "nginx:alpine",
      "state": "running",
      "status": "Up 2 hours",
      "ports": "0.0.0.0:8086->80/tcp",
      "created": "2 hours ago"
    }
  ]
}

state is one of running, exited, created or paused; status is Docker's human text; created is Docker's RunningFor field.

Errors

Status Code Meaning
500 docker_error docker ps -a failed (engine down or docker not installed); the message carries the command output.

Example

curl https://panel.example.com:2087/api/docker/containers \
  -H "Authorization: Bearer $TOKEN"

POST /api/docker/containers

Creates and starts a container from the run form. Every field is validated before it reaches the CLI, and the arguments are passed as a slice so no value can act as a docker flag. Published host ports are auto-opened in the firewall and reported as opened_ports.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Request body

{
  "image": "string (required)",
  "name": "string (optional)",
  "ports": "[]string (optional)",
  "env": "[]string (optional)",
  "volumes": "[]string (optional)",
  "restart_policy": "string (optional)",
  "command": "string (optional)"
}
Field Type Required Description
image string yes Image reference. Must match ^[a-zA-Z0-9][a-zA-Z0-9._/:@-]*$.
name string no Container name. Must match ^[a-zA-Z0-9][a-zA-Z0-9_.-]*$.
ports array of string no Port mappings [ip:]host:container[/tcp\|udp], e.g. 8080:80, 127.0.0.1:8443:443/tcp. Blank entries are skipped.
env array of string no Environment variables KEY=value; the key must match [A-Za-z_][A-Za-z0-9_]*.
volumes array of string no Mounts /host/path:/container/path or named-vol:/container/path, with an optional third segment of ro or rw. The container path must be absolute.
restart_policy string no One of no (or empty, the default), always, unless-stopped, on-failure. Anything else is rejected.
command string no Start command, split on whitespace and appended after the image.

Response - 200 OK

{
  "data": {
    "id": "9f8e7d6c5b4a3928170615243342516071829304a5b6c7d8e9f0a1b2c3d4e5f6",
    "opened_ports": ["8080/tcp"]
  }
}

opened_ports lists only the ports the firewall actually accepted, formatted port/proto.

Errors

Status Code Meaning
400 bad_request Body could not be parsed or image was missing; or the image reference, container name, a port mapping, an environment variable, a volume, or the restart policy failed validation. The message names the offending value.
500 docker_error docker run -d failed (image not found, port already bound, …); the message carries the command's stderr.

Side effects

  • Runs docker run -d ..., which pulls the image if it is not present locally.
  • Opens each published host port inbound in the firewall (firewall.OpenPort).

Example

curl -X POST https://panel.example.com:2087/api/docker/containers \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"image": "nginx:alpine", "name": "web", "ports": ["8080:80"], "env": ["TZ=UTC"], "volumes": ["/srv/docker/web:/usr/share/nginx/html:ro"], "restart_policy": "unless-stopped"}'

DELETE /api/docker/containers/:id

Removes a container. A running container is only removed when force=1 is supplied.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Path parameters

Name Type Description
id string Container id or name. Must match ^[a-zA-Z0-9][a-zA-Z0-9._/:@-]*$, so it can never begin with -.

Query parameters

Name Type Optional Description
force string yes Set to 1 to add -f, removing the container even if it is running. Any other value is ignored.

Response - 200 OK

{ "data": { "success": true } }

Errors

Status Code Meaning
400 bad_request The identifier failed validation.
500 docker_error docker rm failed (for example, the container is running and force was not set); the message carries the command output.

Side effects

  • Runs docker rm [-f] <id>. Firewall rules opened for the container's published ports are not removed.

Example

curl -X DELETE "https://panel.example.com:2087/api/docker/containers/web?force=1" \
  -H "Authorization: Bearer $TOKEN"

PUT /api/docker/containers/:id

Applies new settings to an existing container. Docker cannot change ports, environment or mounts on a live container, so the container is recreated: the current one is stopped and parked under the temporary name <name>-starkone-old, a replacement is created with the same name and the new settings, and the old one is removed only once the new one is running. If creation fails the original is renamed back and restarted, so a failed edit never loses the container.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Path parameters

Name Type Description
id string Container id or name to recreate. Must match ^[a-zA-Z0-9][a-zA-Z0-9._/:@-]*$.

Request body

Identical to POST /api/docker/containers (dockerRunReq).

{
  "image": "string (required)",
  "name": "string (optional)",
  "ports": "[]string (optional)",
  "env": "[]string (optional)",
  "volumes": "[]string (optional)",
  "restart_policy": "string (optional)",
  "command": "string (optional)"
}
Field Type Required Description
image string yes Image reference for the replacement container.
name string no New container name. Defaults to the :id path value, so the container keeps its current name unless one is supplied.
ports array of string no Full replacement set of port mappings - anything omitted is dropped.
env array of string no Full replacement set of KEY=value environment variables.
volumes array of string no Full replacement set of mounts.
restart_policy string no no, always, unless-stopped or on-failure.
command string no Start command, split on whitespace.

The body is the complete new definition, not a patch: fetch GET /api/docker/containers/:id/config first and send it back with your changes applied.

Response - 200 OK

{
  "data": {
    "id": "1a2b3c4d5e6f7809a1b2c3d4e5f60718293a4b5c6d7e8f9012345678abcdef90",
    "opened_ports": ["8080/tcp"]
  }
}

Errors

Status Code Meaning
400 bad_request The identifier failed validation, the body could not be parsed or image was missing, the container name is invalid, or a port/env/volume/restart-policy value failed validation.
500 docker_error docker rename could not park the original (the original is started again), or docker run failed for the replacement (the original is renamed back and restarted). The message carries the command's stderr.

Side effects

  • Runs docker rm -f <name>-starkone-old to clear any stale backup, then docker stop <id> and docker rename <id> <name>-starkone-old.
  • Runs docker run -d ... with the new settings, then docker rm -f <name>-starkone-old on success.
  • On failure, runs docker rename <name>-starkone-old <name> and docker start <name>.
  • Opens each published host port inbound in the firewall (firewall.OpenPort).

Example

curl -X PUT https://panel.example.com:2087/api/docker/containers/web \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"image": "nginx:alpine", "name": "web", "ports": ["8080:80"], "env": ["TZ=UTC"], "restart_policy": "unless-stopped"}'

POST /api/docker/containers/:id/:action

Runs a lifecycle action against one container. The action is restricted to a fixed allow-list and passed straight to docker <action> <id>.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Path parameters

Name Type Description
id string Container id or name. Must match ^[a-zA-Z0-9][a-zA-Z0-9._/:@-]*$.
action string One of start, stop, restart. Anything else is rejected.

The literal sub-paths backup and clone are registered separately and take priority over this wildcard, so they never reach this handler.

Response - 200 OK

{ "data": { "success": true } }

Errors

Status Code Meaning
400 bad_request The identifier failed validation, or action was not start, stop or restart.
500 docker_error The docker command failed (no such container, container already in that state, …); the message carries the command output.

Side effects

  • Runs docker start|stop|restart <id>.

Example

curl -X POST https://panel.example.com:2087/api/docker/containers/web/restart \
  -H "Authorization: Bearer $TOKEN"

POST /api/docker/containers/:id/backup

Snapshots a container into a local image with docker commit and, unless asked not to, archives the host folders it bind-mounts. A committed image captures only the container's own filesystem - Docker deliberately excludes mounted data - so archiving the bind mounts is what makes this a real backup rather than half of one.

The snapshot is tagged starkone-backup/<lowercased container name>:<YYYYMMDD-HHMMSS>. Volume archives are written to /var/backups/starkone-docker/<name>-<stamp>/volume-N.tar.gz. Named volumes and /var/run/docker.sock are skipped - only real host directories are archived.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Path parameters

Name Type Description
id string Container id or name to snapshot. Must match ^[a-zA-Z0-9][a-zA-Z0-9._/:@-]*$.

Request body

The body is optional; a bind failure is ignored and the defaults apply.

{
  "include_volumes": "bool (optional)"
}
Field Type Required Description
include_volumes bool no Whether to tar the container's host bind-mount folders. Defaults to true when omitted or null; send false to commit the image only.

Response - 200 OK

{
  "data": {
    "image": "starkone-backup/web:20260818-120000",
    "container": "web",
    "volumes": [
      {
        "path": "/srv/docker/web",
        "archive": "/var/backups/starkone-docker/web-20260818-120000/volume-1.tar.gz",
        "size_mb": "412.5"
      }
    ]
  }
}

Each volumes entry always carries path and archive. A successful archive adds size_mb; a failed one adds error with the tar output instead. volumes is an empty array when include_volumes is false, when the container has no host bind mounts, or when the backup directory could not be created.

Errors

Status Code Meaning
400 bad_request The identifier failed validation.
500 docker_error docker commit failed; the message carries the command output.

Archive failures do not fail the request - they are reported per volume in the response.

Side effects

  • Runs docker inspect -f {{.Name}} <id> to resolve the container name, then docker commit <id> starkone-backup/<name>:<stamp>, creating a new local image.
  • Creates /var/backups/starkone-docker/<name>-<stamp> (mode 0700) and runs tar -czf <archive> -C <parent> <basename> for each host bind mount.
  • Writes a docker_backup entry to the user action log.

Example

curl -X POST https://panel.example.com:2087/api/docker/containers/web/backup \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"include_volumes": true}'

POST /api/docker/containers/:id/clone

Starts a second container from a snapshot of an existing one: docker commit produces a private image, then a new container is run from it under a new name. The source container's environment and restart policy are carried over. Mounts are deliberately not reused - two containers writing the same host folder would corrupt each other's data - so the copy starts with the snapshot's contents and its own empty state. Published host ports are auto-opened in the firewall and reported as opened_ports.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Path parameters

Name Type Description
id string Container id or name to clone from. Must match ^[a-zA-Z0-9][a-zA-Z0-9._/:@-]*$.

Request body

{
  "new_name": "string (required)",
  "ports": "[]string (optional)",
  "start": "bool (optional)"
}
Field Type Required Description
new_name string yes Name for the clone. Must be non-empty and match ^[a-zA-Z0-9][a-zA-Z0-9_.-]*$.
ports array of string no Port mappings for the clone. The source container's mappings are not reused, so pick free host ports here.
start bool no Defaults to true. When explicitly false, the clone is created, started, then immediately stopped with docker stop.

Response - 200 OK

{
  "data": {
    "id": "1a2b3c4d5e6f7809a1b2c3d4e5f60718293a4b5c6d7e8f9012345678abcdef90",
    "name": "web-copy",
    "snapshot": "starkone-clone/web-copy:20260818-120000",
    "opened_ports": ["8090/tcp"]
  }
}

Errors

Status Code Meaning
400 bad_request The identifier failed validation, the body could not be parsed, new_name was empty or invalid, or a port mapping failed validation.
409 name_taken A container with new_name already exists.
500 docker_error docker commit failed, or docker run failed for the clone (the snapshot image is deleted again in that case). The message carries the command output.

Side effects

  • Runs docker inspect <new_name> for a collision check, docker commit <id> starkone-clone/<new_name>:<stamp> - leaving a new local image behind - and docker inspect -f ... twice to copy the source environment and restart policy.
  • Runs docker run -d --name <new_name> ... from the snapshot, with --restart copied from the source (falling back to unless-stopped). Only KEY=value environment entries survive the copy.
  • On run failure, runs docker rmi <snapshot> so no orphan image is left.
  • Opens each published host port inbound in the firewall (firewall.OpenPort).
  • Writes a docker_clone entry to the user action log.

Example

curl -X POST https://panel.example.com:2087/api/docker/containers/web/clone \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"new_name": "web-copy", "ports": ["8090:80"], "start": true}'

GET /api/docker/containers/:id/config

Returns a live container's settings in the same shape the run form uses, so the edit dialog can be pre-filled. Port bindings are flattened back into host:container[/proto] strings and sorted.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Path parameters

Name Type Description
id string Container id or name. Must match ^[a-zA-Z0-9][a-zA-Z0-9._/:@-]*$.

Response - 200 OK

{
  "data": {
    "name": "web",
    "image": "nginx:alpine",
    "env": ["PATH=/usr/local/sbin:/usr/local/bin", "TZ=UTC"],
    "volumes": ["/srv/docker/web:/usr/share/nginx/html:ro"],
    "ports": ["8080:80"],
    "restart_policy": "unless-stopped",
    "command": "nginx -g daemon off;"
  }
}

name has the leading / stripped. volumes is the raw HostConfig.Binds list. restart_policy falls back to no when Docker reports an empty policy. command is Config.Cmd joined with spaces.

Errors

Status Code Meaning
400 bad_request The identifier failed validation.
404 not_found docker inspect failed - no such container.
500 docker_error The inspect output could not be parsed or was empty.

Example

curl https://panel.example.com:2087/api/docker/containers/web/config \
  -H "Authorization: Bearer $TOKEN"

GET /api/docker/containers/:id/history

Returns the stored CPU and memory samples for one container, for the usage graph. Samples are collected by a background sampler in internal/dockerstats, which runs docker stats --no-stream every 30 seconds and keeps one hour of history in the docker_stats SQLite table. Samples are keyed by container name, so the supplied id is resolved to a name via docker inspect first.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Path parameters

Name Type Description
id string Container id or name. Must match ^[a-zA-Z0-9][a-zA-Z0-9._/:@-]*$.

Query parameters

Name Type Optional Description
minutes int yes Look-back window in minutes. Defaults to 60. Non-numeric values fall back to 60; values <= 0 or > 60 are clamped to 60, since only one hour is retained.

Response - 200 OK

{
  "data": {
    "name": "web",
    "points": [
      { "ts": 1776499200, "cpu": 3.4, "mem_mb": 128.5, "mem_pct": 1.6 }
    ]
  }
}

ts is a Unix timestamp in seconds; points are ordered oldest first. points is an empty array when the container has no samples yet - the sampler only runs while the docker plugin is enabled and the docker binary is present.

Errors

Status Code Meaning
400 bad_request The identifier failed validation.
500 history_error The docker_stats table could not be created or queried.

Example

curl "https://panel.example.com:2087/api/docker/containers/web/history?minutes=60" \
  -H "Authorization: Bearer $TOKEN"

GET /api/docker/containers/:id/inspect

Returns the raw docker inspect document for a container, for the details view. The first (and only) element of Docker's array is unwrapped and returned as-is.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Path parameters

Name Type Description
id string Container id or name. Must match ^[a-zA-Z0-9][a-zA-Z0-9._/:@-]*$.

Response - 200 OK

{
  "data": {
    "Id": "9f8e7d6c5b4a3928170615243342516071829304a5b6c7d8e9f0a1b2c3d4e5f6",
    "Name": "/web",
    "State": { "Status": "running", "Running": true },
    "Config": { "Image": "nginx:alpine", "Env": ["TZ=UTC"] },
    "HostConfig": { "Binds": ["/srv/docker/web:/usr/share/nginx/html:ro"] },
    "NetworkSettings": { "Ports": { "80/tcp": [{ "HostIp": "0.0.0.0", "HostPort": "8080" }] } }
  }
}

The document is passed through unchanged, so its exact shape follows the installed Docker engine version.

Errors

Status Code Meaning
400 bad_request The identifier failed validation.
404 not_found docker inspect failed - no such container.
500 docker_error The inspect output could not be parsed or was empty.

Example

curl https://panel.example.com:2087/api/docker/containers/web/inspect \
  -H "Authorization: Bearer $TOKEN"

GET /api/docker/containers/:id/logs

Tails a container's combined stdout and stderr as one string.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Path parameters

Name Type Description
id string Container id or name. Must match ^[a-zA-Z0-9][a-zA-Z0-9._/:@-]*$.

Query parameters

Name Type Optional Description
tail int yes Number of trailing lines to return. Defaults to 200. Non-numeric values, or values below 1 or above 5000, fall back to 200.

Response - 200 OK

{ "data": { "logs": "2026/08/18 12:00:00 [notice] 1#1: start worker processes\n" } }

Errors

Status Code Meaning
400 bad_request The identifier failed validation.

A failing docker logs is not treated as an error: its output (including any error text) is returned in logs with status 200.

Example

curl "https://panel.example.com:2087/api/docker/containers/web/logs?tail=500" \
  -H "Authorization: Bearer $TOKEN"

GET /api/docker/images

Lists the images present on the host, from docker images.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Response - 200 OK

{
  "data": [
    {
      "id": "a1b2c3d4e5f6",
      "repository": "nginx",
      "tag": "alpine",
      "size": "48.2MB",
      "created": "3 weeks ago"
    }
  ]
}

created is Docker's CreatedSince field.

Errors

Status Code Meaning
500 docker_error docker images failed; the message carries the command output.

Example

curl https://panel.example.com:2087/api/docker/images \
  -H "Authorization: Bearer $TOKEN"

DELETE /api/docker/images/:id

Deletes a local image with docker rmi.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Path parameters

Name Type Description
id string Image id (sha256:... or short id) or repository:tag. Must match ^[a-zA-Z0-9][a-zA-Z0-9._/:@-]*$.

Response - 200 OK

{ "data": { "success": true } }

Errors

Status Code Meaning
400 bad_request The identifier failed validation.
409 docker_error docker rmi failed - most often because a container still references the image. The message carries the command output.

Side effects

  • Runs docker rmi <id>. No -f, so images in use are refused rather than force-removed.

Example

curl -X DELETE https://panel.example.com:2087/api/docker/images/nginx:alpine \
  -H "Authorization: Bearer $TOKEN"

POST /api/docker/images/pull

Pulls an image and streams progress as Server-Sent Events rather than returning JSON. On success the response is 200 with Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive and X-Accel-Buffering: no.

Each line of docker pull output (stderr is interleaved into the same stream) is emitted as one event:

data: {"line":"alpine: Pulling from library/nginx"}

data: {"line":"✅ Pull complete"}

data: [DONE]

The stream always ends with data: [DONE]. Failures after the stream has started are delivered as ordinary line events beginning with ERROR: - the HTTP status is already 200 at that point, so clients must inspect the lines, not the status code.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Request body

{
  "image": "string (required)"
}
Field Type Required Description
image string yes Image reference to pull. Must match ^[a-zA-Z0-9][a-zA-Z0-9._/:@-]*$.

Response - 200 OK

An SSE stream, not a JSON envelope. Example body:

data: {"line":"alpine: Pulling from library/nginx"}

data: {"line":"Status: Downloaded newer image for nginx:alpine"}

data: {"line":"✅ Pull complete"}

data: [DONE]

Errors

Status Code Meaning
400 bad_request The body could not be parsed, image was missing, or the image reference failed validation. This is the only JSON error response; it is sent before the stream headers.

Once the stream has begun, errors are reported as data: {"line":"ERROR: ..."} events followed by data: [DONE].

Side effects

  • Runs docker pull <image>, adding the image to the local store.

Example

curl -N -X POST https://panel.example.com:2087/api/docker/images/pull \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"image": "nginx:alpine"}'

POST /api/docker/install

Installs the Docker engine so the plugin can be used on a host where docker is missing, then enables and starts the service and puts the CSF firewall integration in place. Calling it when docker is already installed is safe: it re-asserts the service state and the firewall rules, which a csf -r can wipe.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Response - 200 OK

{ "data": { "success": true } }

Errors

Status Code Meaning
500 install_error Every install path failed, or the docker binary was still absent afterwards. The message carries the install script output or docker binary not found after installation.

Side effects

  • When docker is absent: runs the official convenience script (curl -fsSL https://get.docker.com | bash), falling back to apt-get install -y docker.io and then dnf install -y docker. This is the one place the plugin uses a shell, and only with a fixed built-in command string - no request data reaches it.
  • Runs systemctl enable --now docker.
  • When csf is present, sets DOCKER = "1" in /etc/csf/csf.conf (mode 0600) if it is not already on, and runs csf -r.
  • When csf is present, writes a # >>> starkone-docker >>> … # <<< starkone-docker <<< block into /etc/csf/csfpost.sh (mode 0700), replacing an existing block or appending a new one, then runs csf -r. The block restarts docker if its iptables chains are missing and inserts ACCEPT rules for the docker bridge (docker0, or DOCKER_DEVICE from csf.conf) on INPUT and OUTPUT.

Example

curl -X POST https://panel.example.com:2087/api/docker/install \
  -H "Authorization: Bearer $TOKEN"

POST /api/docker/prune

Reclaims disk space with docker system prune -f: stopped containers, dangling images, unused networks and the build cache. Returns the final line of the command output, which is Docker's "Total reclaimed space" summary.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Response - 200 OK

{ "data": { "result": "Total reclaimed space: 1.2GB" } }

Errors

Status Code Meaning
500 docker_error docker system prune -f failed; the message carries the command output.

Side effects

  • Runs docker system prune -f, which permanently deletes stopped containers, dangling images, unused networks and build cache. It is not interactive - there is no confirmation step.

Example

curl -X POST https://panel.example.com:2087/api/docker/prune \
  -H "Authorization: Bearer $TOKEN"

GET /api/docker/stacks

Lists launched stack instances by grouping containers that carry the starkone.stack label, so the UI can show a multi-container stack as one unit. Containers without the label are ignored.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Response - 200 OK

{
  "data": [
    {
      "instance": "myblog",
      "running": 2,
      "total": 2,
      "services": [
        {
          "name": "myblog-db",
          "state": "running",
          "status": "Up 5 minutes",
          "ports": ""
        },
        {
          "name": "myblog-app",
          "state": "running",
          "status": "Up 5 minutes",
          "ports": "0.0.0.0:8081->80/tcp"
        }
      ]
    }
  ]
}

Instances are collected into a map, so the order of the array is not stable between calls. running counts services in state running; total counts every labelled container, stopped ones included.

Errors

This endpoint does not return error responses. If docker ps fails it returns 200 with an empty data array.

Example

curl https://panel.example.com:2087/api/docker/stacks \
  -H "Authorization: Bearer $TOKEN"

POST /api/docker/stacks/:instance/:action

Applies one action to every container of a stack instance, found by the starkone.stack=<instance> label. remove force-removes the containers and then deletes the instance's docker network.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Path parameters

Name Type Description
instance string Stack instance name. Must match ^[a-zA-Z0-9][a-zA-Z0-9_.-]*$.
action string One of start, stop, restart, remove. Anything else is rejected.

The literal sub-path launch is registered separately and takes priority over this wildcard, so POST /api/docker/stacks/launch never reaches this handler.

Response - 200 OK

{ "data": { "success": true, "containers": 2 } }

containers is the number of labelled containers the action was applied to. Individual command failures are not reported - the handler ignores their exit status.

Errors

Status Code Meaning
400 bad_request The instance name failed validation, or action was not start, stop, restart or remove.
404 not_found The label filter matched no containers for this instance.

Side effects

  • Runs docker ps -aq --filter label=starkone.stack=<instance>, then docker start|stop|restart <id> - or docker rm -f <id> for remove - on each match.
  • For remove, also runs docker network rm <instance>-net. The instance's data under /srv/docker/<instance> is left in place, and firewall rules opened at launch are not removed.
  • Writes a docker_stack_<action> entry to the user action log.

Example

curl -X POST https://panel.example.com:2087/api/docker/stacks/myblog/restart \
  -H "Authorization: Bearer $TOKEN"

POST /api/docker/stacks/launch

Launches a multi-container catalog stack as one unit: a dedicated docker network, one container per selected service named <instance>-<key> with a stable network alias so the services reach each other by name, and one shared set of generated secrets. Every container is labelled starkone.stack=<instance> and starkone.stack.service=<key> so the stack can be managed as a group, and each is created with --restart unless-stopped.

{{SECRET:name}} templates across the whole stack are resolved once, so services referencing the same name (for example the WordPress app and its database) receive the same generated password. Values are returned once in credentials. For the supabase stack, anon_key and service_key JWTs derived from the generated jwt secret are added to that map. Volume paths containing {{INSTANCE}} are substituted with the instance name. Published host ports are auto-opened in the firewall and reported as opened_ports.

Relaunching over existing data is refused: a database keeps the password baked in at first initialisation, so freshly generated secrets would not match and every service would fail to connect.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Request body

{
  "stack_id": "string (required)",
  "instance": "string (optional)",
  "services": "[]string (optional)"
}
Field Type Required Description
stack_id string yes Catalog stack id, e.g. wordpress or supabase.
instance string no Installation name; container names, the network (<instance>-net), the data folder (/srv/docker/<instance>) and the group label are all derived from it. Defaults to stack_id. Must match ^[a-zA-Z0-9][a-zA-Z0-9_.-]*$.
services array of string no Service keys to start, e.g. ["db", "app"]. An empty or omitted list starts every service in the stack, including ones the catalog marks default_off.

Response - 200 OK

{
  "data": {
    "instance": "myblog",
    "network": "myblog-net",
    "started": [
      { "service": "db", "name": "myblog-db", "id": "1a2b3c4d5e6f", "ports": null },
      { "service": "app", "name": "myblog-app", "id": "9f8e7d6c5b4a", "ports": ["8081:80"] }
    ],
    "failed": [],
    "opened_ports": ["8081/tcp"],
    "credentials": {
      "wpdb": "generated-password",
      "wproot": "generated-password"
    }
  }
}

The launch is per-service best-effort: a service that already exists, fails validation, or fails to run is recorded in failed ({"service": "...", "name": "...", "error": "..."}) while the rest continue. id in started is the short 12-character container id.

Errors

Status Code Meaning
400 bad_request The body could not be parsed or stack_id was missing, the instance name failed validation, or the services filter selected nothing.
404 not_found No catalog stack with that stack_id.
409 data_exists /srv/docker/<instance> already exists and is non-empty. The message names the folder and suggests a different installation name, or deleting the folder to start fresh.
500 network_error The instance network could not be created, even after restarting docker to rebuild its iptables chains.

If every selected service fails, the response status is 500 but the body is still the full data envelope above, with started empty and the reasons in failed.

Side effects

  • Checks /srv/docker/<instance> and refuses when it already holds data.
  • Creates the docker network <instance>-net via docker network create. If creation fails with an iptables/chain error, runs systemctl restart docker, waits 6 seconds and retries once.
  • Runs docker inspect <name> per service, then docker run -d --network <instance>-net --network-alias <alias> --label starkone.stack=<instance> --label starkone.stack.service=<key> ....
  • Creates the host bind-mount directories under /srv/docker/<instance>/....
  • Opens each published host port inbound in the firewall (firewall.OpenPort).
  • Writes a docker_stack_launch entry to the user action log.

Example

curl -X POST https://panel.example.com:2087/api/docker/stacks/launch \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"stack_id": "wordpress", "instance": "myblog", "services": ["db", "app"]}'

GET /api/docker/stats

Returns a one-shot live CPU and memory reading per running container, from docker stats --no-stream. The values are Docker's own formatted strings, keyed by container id.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Response - 200 OK

{
  "data": {
    "9f8e7d6c5b4a": { "cpu": "3.40%", "mem": "128MiB / 7.7GiB", "mem_pct": "1.62%" }
  }
}

Errors

This endpoint does not return error responses. If docker stats fails it returns 200 with an empty object as data.

Example

curl https://panel.example.com:2087/api/docker/stats \
  -H "Authorization: Bearer $TOKEN"

GET /api/docker/status

Reports engine state, container and image counts, and disk usage in one call - the endpoint the Docker page polls to decide whether to show the manager or the install prompt.

Guards: RequireSuperuser(), RequirePlugin(db, "docker")

Response - 200 OK

{
  "data": {
    "installed": true,
    "running": true,
    "version": "27.1.1",
    "counts": { "running": 3, "total": 5, "images": 9 },
    "disk": [
      { "type": "Images", "size": "1.2GB", "reclaimable": "400MB (33%)", "count": "9" }
    ]
  }
}

When the docker binary is not on PATH the response is the short form:

{ "data": { "installed": false, "running": false } }

running is true only when docker version returns a server version, so a present binary with a stopped daemon reports installed: true, running: false, an empty version, zeroed counts and disk: null. disk mirrors the rows of docker system df.

Errors

This endpoint does not return error responses. Every underlying command failure degrades to a zero value rather than an error status.

Example

curl https://panel.example.com:2087/api/docker/status \
  -H "Authorization: Bearer $TOKEN"