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:
On error:
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
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
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
| 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, thendocker 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_launchentry 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
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
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-oldto clear any stale backup, thendocker stop <id>anddocker rename <id> <name>-starkone-old. - Runs
docker run -d ...with the new settings, thendocker rm -f <name>-starkone-oldon success. - On failure, runs
docker rename <name>-starkone-old <name>anddocker 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
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.
| 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, thendocker commit <id> starkone-backup/<name>:<stamp>, creating a new local image. - Creates
/var/backups/starkone-docker/<name>-<stamp>(mode0700) and runstar -czf <archive> -C <parent> <basename>for each host bind mount. - Writes a
docker_backupentry 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
| 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 - anddocker inspect -f ...twice to copy the source environment and restart policy. - Runs
docker run -d --name <new_name> ...from the snapshot, with--restartcopied from the source (falling back tounless-stopped). OnlyKEY=valueenvironment 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_cloneentry 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
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
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
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:
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
| 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
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 toapt-get install -y docker.ioand thendnf 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
csfis present, setsDOCKER = "1"in/etc/csf/csf.conf(mode0600) if it is not already on, and runscsf -r. - When
csfis present, writes a# >>> starkone-docker >>>…# <<< starkone-docker <<<block into/etc/csf/csfpost.sh(mode0700), replacing an existing block or appending a new one, then runscsf -r. The block restarts docker if its iptables chains are missing and insertsACCEPTrules for the docker bridge (docker0, orDOCKER_DEVICEfromcsf.conf) onINPUTandOUTPUT.
Example
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
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
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
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
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>, thendocker start|stop|restart <id>- ordocker rm -f <id>forremove- on each match. - For
remove, also runsdocker 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>-netviadocker network create. If creation fails with an iptables/chain error, runssystemctl restart docker, waits 6 seconds and retries once. - Runs
docker inspect <name>per service, thendocker 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_launchentry 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
Errors
This endpoint does not return error responses. If docker stats fails it returns 200 with an empty object as data.
Example
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:
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