Docker Integration

Overview

Plugins can interact with Docker to manage containers, inspect images, and monitor container status. Unraid runs Docker natively, making container integration a powerful option for plugins.

Unraid Docker Labels

Unraid uses specific Docker labels to integrate containers with the WebUI. Containers with these labels appear in the Docker tab with icons, web UI links, and shell access.

Standard Labels

Label Description Example
net.unraid.docker.managed Indicates management source composeman, dockerman
net.unraid.docker.icon URL to container icon https://example.com/icon.png
net.unraid.docker.webui URL to container’s web interface http://[IP]:[PORT:8080]/
net.unraid.docker.shell Shell command for console access bash, sh

Usage in Docker Compose

Add Unraid labels to your compose file’s labels section. The [IP] and [PORT:xxxx] placeholders are replaced by Unraid with the actual container IP and mapped port when displaying the WebUI link.

services:
  myapp:
    image: myapp:latest
    labels:
      net.unraid.docker.managed: "composeman"
      net.unraid.docker.icon: "https://raw.githubusercontent.com/user/repo/main/icon.png"
      net.unraid.docker.webui: "http://[IP]:[PORT:8080]/"
      net.unraid.docker.shell: "bash"

Reading Labels in PHP

Retrieve container labels using docker inspect and decode the JSON output. Labels are stored in the container’s Config section. Use null coalescing (??) to provide fallback values when labels aren’t present.

<?php
require_once("/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerClient.php");

// Define label constants
$docker_label_managed = "net.unraid.docker.managed";
$docker_label_icon = "net.unraid.docker.icon";
$docker_label_webui = "net.unraid.docker.webui";
$docker_label_shell = "net.unraid.docker.shell";

// Get container info with labels
exec("docker inspect mycontainer", $output);
$info = json_decode(implode('', $output), true);
$labels = $info[0]['Config']['Labels'] ?? [];

// Read specific labels
$icon = $labels[$docker_label_icon] ?? '';
$webui = $labels[$docker_label_webui] ?? '';
?>

Docker API Access

Via Command Line

The simplest way to interact with Docker from PHP is using the exec() function to run Docker CLI commands. Use the --format flag with Go templates to get structured output that’s easy to parse.

<?
// List running containers
exec("docker ps --format ''", $containers);

// Get container info as JSON
$containerName = "mycontainer";
exec("docker inspect $containerName", $output);
$info = json_decode(implode('', $output), true);
?>

Via Docker Socket

For more advanced use cases, you can communicate directly with the Docker daemon via its Unix socket. This gives access to the full Docker API but requires more complex request handling.

<?
// Direct socket communication (advanced)
$socket = "/var/run/docker.sock";

// Using curl with Unix socket
$cmd = "curl -s --unix-socket $socket http://localhost/containers/json";
exec($cmd, $output);
$containers = json_decode(implode('', $output), true);
?>

Common Operations

List Containers

Retrieve all containers (running and stopped) as JSON objects. The --format '' flag outputs one JSON object per line, which you can parse into an array.

<?
// Get all containers (including stopped)
exec("docker ps -a --format ''", $output);

$containers = [];
foreach ($output as $line) {
    $containers[] = json_decode($line, true);
}
?>

Start/Stop Containers

Basic container lifecycle management functions. Always use escapeshellarg() to sanitize container names and check the return value to confirm success.

<?
function startContainer($name) {
    exec("docker start " . escapeshellarg($name), $output, $retval);
    return $retval === 0;
}

function stopContainer($name) {
    exec("docker stop " . escapeshellarg($name), $output, $retval);
    return $retval === 0;
}

function restartContainer($name) {
    exec("docker restart " . escapeshellarg($name), $output, $retval);
    return $retval === 0;
}
?>

Get Container Logs

Fetch recent log output from a container. The --tail flag limits output to the most recent lines. Note that 2>&1 redirects stderr (where Docker sends some log output) to stdout.

<?
function getContainerLogs($name, $lines = 100) {
    exec("docker logs --tail " . intval($lines) . " " . escapeshellarg($name) . " 2>&1", $output);
    return implode("\n", $output);
}
?>

Check Container Status

Use docker inspect with a Go template to query specific container properties. This is more efficient than parsing full JSON output when you only need one value.

<?
function isContainerRunning($name) {
    exec("docker inspect -f '' " . escapeshellarg($name) . " 2>/dev/null", $output);
    return isset($output[0]) && $output[0] === 'true';
}
?>

Container Stats

Get real-time resource usage for a container. The --no-stream flag returns a single snapshot instead of continuously updating output. The JSON format includes CPU percentage, memory usage, network I/O, and block I/O statistics.

<?
// Get resource usage
exec("docker stats --no-stream --format '' mycontainer", $output);
$stats = json_decode($output[0], true);

// $stats contains: CPUPerc, MemUsage, NetIO, BlockIO, etc.
?>

Working with Images

Common image operations including listing, pulling, and removing. When pulling images, redirect stderr to capture progress output. Always check return values for error handling.

<?
// List images
exec("docker images --format ''", $output);

// Pull an image
exec("docker pull myimage:latest 2>&1", $output, $retval);

// Remove an image
exec("docker rmi myimage:latest 2>&1", $output, $retval);
?>

Update Checking

Unraid’s Docker Manager provides a built-in system for checking if container images have updates available. This works by comparing the local image digest (SHA256 hash) with the remote registry’s current digest for the same tag.

How Unraid Update Checking Works

  1. Local digest: Extracted from the image’s RepoDigests field via docker inspect
  2. Remote digest: Fetched from the container registry’s API (Docker Hub, GHCR, etc.)
  3. Comparison: If digests differ, an update is available
<?php
require_once("/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerClient.php");

$DockerUpdate = new DockerUpdate();
$image = "library/nginx:latest";

// Force a fresh check (fetches from registry)
$DockerUpdate->reloadUpdateStatus($image);

// Get the status: true = up-to-date, false = update available, null = unknown
$status = $DockerUpdate->getUpdateStatus($image);

if ($status === null) {
    echo "Could not check for updates";
} elseif ($status === true) {
    echo "Image is up-to-date";
} else {
    echo "Update available!";
}
?>

Update Status Storage

Unraid stores update check results in a JSON file to avoid repeated registry queries:

/var/lib/docker/unraid-update-status.json

Structure:

{
  "library/nginx:latest": {
    "local": "sha256:abc123...",
    "remote": "sha256:def456...",
    "status": "false"
  }
}

Reading SHA Values

To display SHA digests in your UI (like showing which version will be updated):

<?php
$dockerManPaths = [
    'update-status' => "/var/lib/docker/unraid-update-status.json"
];

$updateStatusData = DockerUtil::loadJSON($dockerManPaths['update-status']);
$image = "library/nginx:latest";

if (isset($updateStatusData[$image])) {
    $localSha = $updateStatusData[$image]['local'] ?? '';
    $remoteSha = $updateStatusData[$image]['remote'] ?? '';
    
    // Shorten for display (first 12 chars after "sha256:")
    if ($localSha && strpos($localSha, 'sha256:') === 0) {
        $localSha = substr($localSha, 7, 12);
    }
    if ($remoteSha && strpos($remoteSha, 'sha256:') === 0) {
        $remoteSha = substr($remoteSha, 7, 12);
    }
    
    echo "Local: $localSha → Remote: $remoteSha";
}
?>

Handling Pinned Images (SHA256 Digests)

Some users pin images to specific versions using SHA256 digests in their compose files:

services:
  redis:
    image: redis:6.2-alpine@sha256:abc123def456...

These pinned images should not be checked for updates because:

  • The user explicitly wants that exact version
  • Registry checks return the latest tag digest, not the pinned digest
  • Comparing would always show a false “update available”

Detect and handle pinned images:

<?php
/**
 * Check if an image is pinned with a SHA256 digest.
 * Returns array with image/digest info if pinned, false otherwise.
 */
function isImagePinned($image) {
    // Strip docker.io/ prefix first
    if (strpos($image, 'docker.io/') === 0) {
        $image = substr($image, 10);
    }
    
    // Check for @sha256: digest suffix
    if (($digestPos = strpos($image, '@sha256:')) !== false) {
        $baseImage = substr($image, 0, $digestPos);
        $digest = substr($image, $digestPos + 1);
        return [
            'image' => $baseImage,
            'digest' => $digest,
            'shortDigest' => substr($digest, 7, 12)  // First 12 chars
        ];
    }
    
    return false;
}

// Usage
$image = "docker.io/redis:6.2-alpine@sha256:abc123def456...";
$pinned = isImagePinned($image);

if ($pinned) {
    // Show "pinned to abc123def456" instead of checking updates
    echo "Pinned to: " . $pinned['shortDigest'];
} else {
    // Normal update check
    $DockerUpdate->reloadUpdateStatus($image);
}
?>

Common Update Check Issues

| Issue | Cause | Solution | |——-|——-|———-| | Always shows “update available” after pull | Cached local SHA is stale | Clear local field before reloadUpdateStatus() | | Image not found in status file | Image name format mismatch | Use DockerUtil::ensureImageTag() to normalize | | Pinned image shows “not checked” | @sha256: suffix confuses the check | Detect pinned images and skip update check | | Official images not matching | Missing library/ prefix | ensureImageTag() adds it automatically |


## Unraid Docker Integration

### Reading Docker Configuration

Unraid stores Docker configuration in specific locations:

/boot/config/docker.cfg # Docker settings /var/lib/docker/ # Docker data (if on array) /boot/config/plugins/dockerMan/ # Docker templates


### Docker Templates

Docker templates allow Unraid to store container configurations for easy recreation.

### DockerClient.php

Unraid includes a built-in PHP class that provides helper functions for Docker operations. The `DockerUtil::ensureImageTag()` function normalizes image names to match Unraid's internal format (adding `library/` prefix for official images and ensuring a tag is present).

```php
<?php
require_once("/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerClient.php");

// Use DockerUtil for image normalization
$normalizedImage = DockerUtil::ensureImageTag("nginx");
// Returns: library/nginx:latest

// Check if Docker is running
exec('/etc/rc.d/rc.docker status | grep -v "not"', $output, $retval);
$dockerRunning = ($retval === 0);
?>

Docker Compose Integration

Wrapper Script Pattern

A wrapper script provides a clean interface for compose operations from PHP. It handles argument parsing, environment file loading, and command execution. Setting HOME=/root ensures Docker can find its configuration. Using getopts makes the script easy to call with different parameters.

#!/bin/bash
# scripts/compose.sh
export HOME=/root

# Parse arguments
while getopts "e:c:f:p:d:" opt; do
  case $opt in
    e) envFile="--env-file $OPTARG" ;;
    c) command="$OPTARG" ;;
    f) files="$files -f $OPTARG" ;;
    p) name="$OPTARG" ;;
    d) project_dir="$OPTARG" ;;
  esac
done

case $command in
  up)
    docker compose $envFile $files -p "$name" up -d
    ;;
  down)
    docker compose $envFile $files -p "$name" down
    ;;
  pull)
    docker compose $envFile $files -p "$name" pull
    ;;
esac

Stack State Detection

To determine if a compose stack is running, first check if any containers exist for the project, then verify at least one is actually running. A stack with containers that are all stopped should return ‘stopped’ rather than appearing active.

<?php
function getStackState($projectName) {
    exec("docker compose -p " . escapeshellarg($projectName) . " ps -q", $output);
    if (empty($output)) {
        return 'stopped';
    }
    
    // Check if any containers are running
    exec("docker ps -q --filter name=" . escapeshellarg($projectName), $running);
    return empty($running) ? 'stopped' : 'running';
}
?>

Event Monitoring

# Monitor Docker events
docker events --filter 'type=container'

Security Considerations

Input Sanitization

Always sanitize user input before passing to shell commands or storing in files. Use escapeshellarg() for command arguments, filter_var() for URLs, and whitelist validation for action parameters.

<?php
// Always escape shell arguments
$containerName = escapeshellarg($_POST['container']);
exec("docker stop $containerName");

// Validate URLs before storing
$iconUrl = $_POST['iconUrl'];
if (!filter_var($iconUrl, FILTER_VALIDATE_URL) || 
    (strpos($iconUrl, 'http://') !== 0 && strpos($iconUrl, 'https://') !== 0)) {
    echo json_encode(['result' => 'error', 'message' => 'Invalid URL']);
    exit;
}

// Whitelist allowed actions
$allowedActions = ['start', 'stop', 'restart', 'pause', 'unpause'];
if (!in_array($action, $allowedActions)) {
    echo json_encode(['result' => 'error', 'message' => 'Invalid action']);
    exit;
}
?>

XSS Prevention in JavaScript

When displaying error messages or user-provided content, never insert it directly into the DOM using .html(). Instead, use .text() or createTextNode() to ensure special characters are escaped and cannot be interpreted as HTML or JavaScript.

// BAD - vulnerable to XSS
$('#error-display').html(errorMessage);

// GOOD - safe text insertion
var textNode = document.createTextNode(errorMessage);
$('#error-display').empty().append(textNode);

Best Practices

Async Loading Pattern

Docker commands can take several seconds to execute, especially when listing containers or checking update status. Instead of running these commands synchronously (which blocks the page from rendering), load the page shell immediately and fetch the data via AJAX. This dramatically improves perceived performance.

The pattern below shows a delayed spinner that only appears if the AJAX request takes more than 500ms, avoiding a flash of spinner on fast responses:

// compose_manager_main.php - Page loads instantly
<?php
// Note: Stack list is loaded asynchronously via AJAX
?>
<div id="compose_list">Loading...</div>
<script>
// Load data after page renders
function loadlist() {
  composeTimers.load = setTimeout(function(){
    $('div.spinner.fixed').show('slow');
  }, 500);
  
  $.get('/plugins/myplugin/php/list.php', function(data) {
    clearTimeout(composeTimers.load);
    $('#compose_list').html(data);
    $('div.spinner.fixed').hide('slow');
  });
}
$(loadlist);
</script>

Namespace Your Timers

Unraid’s web UI uses a global timers object for its own interval/timeout management. If your plugin creates a variable with the same name, you’ll overwrite Unraid’s timers and break core functionality. Always use a plugin-specific namespace for your timers.

// BAD - may conflict with Unraid's timers
var timers = {};

// GOOD - plugin-specific namespace
var composeTimers = {};
composeTimers.load = setTimeout(...);
composeTimers.check = setInterval(...);

Handle Stale Cache

Unraid caches Docker image SHA hashes in /boot/config/plugins/dynamix.docker.manager/update-status.json for update checking. After running docker compose pull, the local image has changed but Unraid’s cache still has the old SHA. You must clear the cached value to force Unraid to re-inspect the image and detect the update correctly.

<?php
// After docker compose pull, clear the cached local SHA
$updateStatusData = DockerUtil::loadJSON($dockerManPaths['update-status']);
if (isset($updateStatusData[$image])) {
    $updateStatusData[$image]['local'] = null;  // Force re-inspection
    DockerUtil::saveJSON($dockerManPaths['update-status'], $updateStatusData);
}
?>

Image Name Normalization

Docker Compose normalizes image names differently than Unraid’s Docker Manager. Compose adds docker.io/ prefix to Docker Hub images and may include @sha256: digests for pinned images. To match Unraid’s update-status keys, you need to strip these additions and use Unraid’s DockerUtil::ensureImageTag() function which adds library/ prefix to official images.

<?php
function normalizeImageForUpdateCheck($image) {
    // Strip docker.io/ prefix (docker compose adds this for Hub images)
    if (strpos($image, 'docker.io/') === 0) {
        $image = substr($image, 10);
    }
    // Strip @sha256: digest suffix (image pinning)
    if (($digestPos = strpos($image, '@sha256:')) !== false) {
        $image = substr($image, 0, $digestPos);
    }
    // Use Unraid's normalization for consistent format
    return DockerUtil::ensureImageTag($image);
}
?>

Before normalizing, check if the image is pinned using isImagePinned() (see Update Checking). Pinned images should display their pinned status rather than being checked for updates. ```

General Guidelines

  • Cache container lists when appropriate
  • Handle Docker service not running
  • Provide meaningful error messages
  • Don’t block UI on long operations

References