Self-hosting grants absolute sovereignty over data, eliminates subscription lock-in, and allows for an infrastructure tailored to individual needs. Yet, this digital independence comes with an unspoken tax: the self-hoster is not just the user, but the entire operations team. When a service hangs, there is no on-call engineer to page. The responsibility begins and ends with the administrator.

The Context-Switching Tax

For those running Docker environments, Portainer Community Edition (CE) serves as a reliable control plane. But even with an elegant GUI, the daily operational routine remains fragmented. Diagnosing a minor issue typically triggers a familiar, high-friction loop:

  • Realizing a self-hosted service is unresponsive.
  • Opening a browser tab to access the Portainer dashboard and retrieving credentials from a password manager.
  • Navigating through endpoints to locate the target environment.
  • Drilling down into the container list, filtering for the broken service, and opening the log viewer.
  • Alternatively, abandoning the GUI entirely to open a terminal, SSH into the host machine, and manually run docker ps and docker logs --tail 50.

This context-switching is more than a minor annoyance; it interrupts primary workflows and slows down incident response.

The Natural Language Solution

Bypassing the dashboard and the terminal for routine maintenance is now possible. By bringing natural language to infrastructure management, the paradigm shifts. Instead of navigating menus, administrators simply converse. Asking the assistant to check on a service, fetch its latest errors, or initiate a restart transforms complex multi-step procedures into a fluid dialogue.


Bridging the Gap: Open Assistant Meets Portainer CE

Open Assistant introduces a dedicated Portainer plugin. This integration is designed specifically for self-hosting enthusiasts, homelab administrators, and small DevOps teams running Portainer CE. By connecting the cognitive capabilities of the AI system with Portainer's robust API, the assistant becomes an active participant in infrastructure management.

With this plugin active, Open Assistant functions as a unified, conversational control plane. It translates ambiguous human requests — such as "Why is my Nextcloud instance slow?" or "Restart the database container" — into precise, structured API payloads. The assistant handles the orchestration: querying the environment, identifying the target container ID, inspecting its current state, and executing the requested action.

Security and Authentication

Operating infrastructure via an AI system demands strict boundaries. The Portainer plugin respects these requirements by utilizing Portainer's native API key authentication.

  • Local Control: Portainer API keys are stored securely within the local instance configuration.
  • No Shared Credentials: The assistant never requests or stores master Portainer passwords.
  • Granular Access: The plugin operates strictly within the permissions granted to the provisioned API key, ensuring the assistant can only see and modify authorized resources.

This integration is built with the core tenets of the self-hosting community in mind: privacy, control, and efficiency. It doesn't rely on third-party cloud relays or proprietary middleware. It establishes a direct, secure line of communication between the assistant instance and home labs or private VPS.


Zero-Overhead Architecture: No MCP Engineering Tax

In the previous architectural deep-dive, The Zero-Overhead Integration Standard: How Open Assistant Eliminates the MCP Engineering Tax, we detailed how modern AI integrations have become unnecessarily complex.

Many systems champion the Model Context Protocol (MCP) as the default standard for tool use. While MCP has its place, it introduces a significant engineering tax for self-hosters. It requires deploying, securing, and maintaining separate daemon servers just to translate API calls between the AI and target applications.

For a self-hoster, adding another background service to manage is counterproductive. It represents another container that can crash, another set of dependencies to patch, and another port to expose.

Open Assistant eliminates this overhead. The Portainer plugin does not require an MCP server. It requires no middleware, no external node deployments, and no custom Go or Python daemons running on the host.

Instead, the plugin relies entirely on a single, declarative JSON schema. The assistant reads this schema, understands the available endpoints, constructs the HTTP requests dynamically, and parses the JSON responses natively.

  • Zero Additional Attack Surface: No new ports are opened on the host.
  • Zero Resource Footprint: There is no background process consuming CPU or RAM.
  • Instant Portability: The entire integration is defined in a single text block that can be backed up, modified, or restored instantly.

This declarative design makes customization accessible to everyone. Expanding the plugin to support Portainer Stacks or custom network configurations does not require rewriting an integration backend; it simply requires updating the JSON schema. To learn how to build and customize zero-overhead integrations, review the Open Assistant Plugin Schema Documentation.


Capabilities: What the Portainer Plugin Can Do

The Portainer plugin exposes a comprehensive suite of endpoints that allow the assistant to inspect, diagnose, and manage Docker infrastructure. Next to that, you can easily extend the tools declared below to whatever you might need.

Environment and Host Discovery

Before the assistant can manage containers, it must map out the infrastructure.

  • list_environments: Queries the Portainer instance to discover all configured environments (endpoints). Whether managing a single local Docker socket (typically Endpoint ID 1) or a multi-node cluster, the assistant dynamically maps these IDs to target subsequent commands accurately.
  • portainer_status: Verifies connectivity, checking the running Portainer version and instance health to ensure the control plane is fully operational.

Container Lifecycle Management

The plugin provides complete control over container states through natural language.

  • list_containers: Retrieves a comprehensive list of all running and stopped containers, complete with names, active ports, image tags, and operational states.
  • inspect_container: Fetches low-level configuration details, including environment variables, network mounts, restart policies, and current resource states.
  • start_container / stop_container / restart_container: Executes state changes gracefully. If the assistant is told, "The Plex container is acting up, please restart it," the system resolves the name to its container ID and triggers a clean restart cycle.

Real-Time Diagnostics

When a service fails, diagnostics are critical. The plugin brings logs directly into the chat window.

  • container_logs: Fetches stdout and stderr outputs. The assistant can apply filters, limit the output using the tail parameter, or retrieve logs since a specific time window (e.g., "Show me the last 50 log lines for the Nginx container"). This allows administrators to spot database connection failures or syntax errors in seconds.

System Health Monitoring

Monitor host-level resource allocation without leaving the conversation.

  • docker_system_info: Pulls high-level hardware and daemon statistics from the underlying host, including total CPU cores, memory limits, active container counts, and storage driver statuses.

The Self-Hosting Angle: Empowering the One-Person Ops Team

When managing personal infrastructure, speed of resolution is paramount. If a service goes offline, waiting until a physical desk is reached to troubleshoot is highly inefficient.

With the Portainer plugin active, Open Assistant acts as a co-pilot. Diagnostics can be run on the go from a mobile device via the assistant's interface:

User: "The media server is unresponsive. Can you check if the container is running?"

Assistant: [Invokes list_containers with a filter for 'jellyfin'] "The jellyfin container is currently running, but it has been in a restarting loop for the last 10 minutes."

User: "Show me the last 10 lines of its logs."

Assistant: [Invokes container_logs with tail=10] "Here is the recent output. It looks like a database lock error: [2026-03-30T04:12:01Z] [ERR] SQLite Error 5: database is locked Would you like me to stop the container so you can clear the lock file?"*

By consolidating monitoring, log retrieval, and container control into a single chat window, routine operations are accelerated. There is no longer a need to keep multiple browser tabs open or memorize obscure CLI flags. Infrastructure is managed on the administrator's own terms, quickly and securely.


The Plugin Schema: Copy-and-Paste Configuration

To deploy this plugin to an Open Assistant instance, no packages need to be installed and no compilation scripts need to be run. Simply copy the JSON schema below, navigate to the Open Assistant settings, create a new custom plugin, and paste the payload directly into the schema editor.

Once pasted, two configuration fields will appear:

  1. Portainer Host: The domain or IP address and port of the Portainer instance (e.g., portainer.local:9443 or 192.168.1.50:9000).
  2. X-API-Key: The Portainer access token, which can be generated in the Portainer user settings under My Account > Access Tokens.

The Complete JSON Schema

{
  "id": "portainer",
  "display_name": "Portainer",
  "description": "Manage Docker containers (start, stop, restart), fetch logs, and inspect system info via Portainer CE.",
  "icon": "🐳",
  "base_url": "https://{portainer_host}",
  "auth": {
    "type": "header",
    "header_name": "X-API-Key"
  },
  "config_fields": [
    {
      "key": "portainer_host",
      "display_name": "Portainer Host",
      "description": "Hostname and optional port of your Portainer instance, without protocol. E.g. portainer.example.com:9443 or 192.168.1.10:9000",
      "required": true,
      "placeholder": "portainer.example.com:9443"
    }
  ],
  "endpoints": [
    {
      "name": "list_environments",
      "display_name": "List Environments",
      "description": "List all Docker environments (endpoints) configured in Portainer. Call this first to discover available endpoint IDs before performing container operations. Most single-host setups have only endpoint ID 1.",
      "method": "GET",
      "path": "/api/endpoints",
      "parameters": [
        {
          "name": "search",
          "in": "query",
          "type": "string",
          "description": "Optional search string to filter environments by name.",
          "required": false
        }
      ]
    },
    {
      "name": "list_containers",
      "display_name": "List Containers",
      "description": "List containers in a Portainer environment. Returns container IDs, names, images, ports, and status. Set all=true to include stopped containers. Container names or IDs from this list are needed for start, stop, restart, inspect, and logs operations.",
      "method": "GET",
      "path": "/api/endpoints/{endpoint_id}/docker/containers/json",
      "parameters": [
        {
          "name": "endpoint_id",
          "in": "path",
          "type": "integer",
          "description": "Portainer environment/endpoint ID. Use list_environments to discover IDs. Defaults to 1 for single-host setups.",
          "required": true,
          "default": 1
        },
        {
          "name": "all",
          "in": "query",
          "type": "boolean",
          "description": "Include stopped and exited containers in the response. Defaults to true.",
          "required": false,
          "default": true
        },
        {
          "name": "filters",
          "in": "query",
          "type": "string",
          "description": "JSON-encoded filter map to narrow results. Examples: {\"name\":[\"my-app\"]} to find a container by name, {\"status\":[\"exited\"]} for stopped ones, {\"label\":[\"app=myapp\"]} by label.",
          "required": false
        }
      ]
    },
    {
      "name": "inspect_container",
      "display_name": "Inspect Container",
      "description": "Get detailed information about a specific container: image, environment variables, mounts, network settings, restart policy, and current state (running/stopped/restarting/paused). Use this before acting on a container to confirm its current state. container_id can be the full ID, short ID, or container name.",
      "method": "GET",
      "path": "/api/endpoints/{endpoint_id}/docker/containers/{container_id}/json",
      "parameters": [
        {
          "name": "endpoint_id",
          "in": "path",
          "type": "integer",
          "description": "Portainer environment/endpoint ID. Defaults to 1.",
          "required": true,
          "default": 1
        },
        {
          "name": "container_id",
          "in": "path",
          "type": "string",
          "description": "Container ID (full or short) or container name (without the leading slash).",
          "required": true
        }
      ]
    },
    {
      "name": "start_container",
      "display_name": "Start Container",
      "description": "Start a stopped Docker container. Returns HTTP 204 on success, 304 if the container is already running, or 404 if not found. container_id can be the full ID, short ID, or name.",
      "method": "POST",
      "path": "/api/endpoints/{endpoint_id}/docker/containers/{container_id}/start",
      "parameters": [
        {
          "name": "endpoint_id",
          "in": "path",
          "type": "integer",
          "description": "Portainer environment/endpoint ID. Defaults to 1.",
          "required": true,
          "default": 1
        },
        {
          "name": "container_id",
          "in": "path",
          "type": "string",
          "description": "Container ID (full or short) or container name.",
          "required": true
        }
      ]
    },
    {
      "name": "stop_container",
      "display_name": "Stop Container",
      "description": "Gracefully stop a running Docker container. Sends SIGTERM first, then SIGKILL after the timeout expires. Returns HTTP 204 on success, 304 if already stopped. container_id can be the full ID, short ID, or name.",
      "method": "POST",
      "path": "/api/endpoints/{endpoint_id}/docker/containers/{container_id}/stop",
      "parameters": [
        {
          "name": "endpoint_id",
          "in": "path",
          "type": "integer",
          "description": "Portainer environment/endpoint ID. Defaults to 1.",
          "required": true,
          "default": 1
        },
        {
          "name": "container_id",
          "in": "path",
          "type": "string",
          "description": "Container ID (full or short) or container name.",
          "required": true
        },
        {
          "name": "t",
          "in": "query",
          "type": "integer",
          "description": "Seconds to wait for the container to stop gracefully before force-killing it. Defaults to 10.",
          "required": false,
          "default": 10
        }
      ]
    },
    {
      "name": "restart_container",
      "display_name": "Restart Container",
      "description": "Restart a Docker container (graceful stop then start). Works on both running and already-stopped containers. Useful after config changes or when a container is misbehaving. container_id can be the full ID, short ID, or name.",
      "method": "POST",
      "path": "/api/endpoints/{endpoint_id}/docker/containers/{container_id}/restart",
      "parameters": [
        {
          "name": "endpoint_id",
          "in": "path",
          "type": "integer",
          "description": "Portainer environment/endpoint ID. Defaults to 1.",
          "required": true,
          "default": 1
        },
        {
          "name": "container_id",
          "in": "path",
          "type": "string",
          "description": "Container ID (full or short) or container name.",
          "required": true
        },
        {
          "name": "t",
          "in": "query",
          "type": "integer",
          "description": "Seconds to wait for the container to stop gracefully during the restart. Defaults to 10.",
          "required": false,
          "default": 10
        }
      ]
    },
    {
      "name": "container_logs",
      "display_name": "Container Logs",
      "description": "Fetch stdout and/or stderr log output from a container. Use tail to limit the number of lines returned. Use since to restrict to recent log entries. Useful for debugging crashes, checking startup output, or monitoring recent activity.",
      "method": "GET",
      "path": "/api/endpoints/{endpoint_id}/docker/containers/{container_id}/logs",
      "parameters": [
        {
          "name": "endpoint_id",
          "in": "path",
          "type": "integer",
          "description": "Portainer environment/endpoint ID. Defaults to 1.",
          "required": true,
          "default": 1
        },
        {
          "name": "container_id",
          "in": "path",
          "type": "string",
          "description": "Container ID (full or short) or container name.",
          "required": true
        },
        {
          "name": "stdout",
          "in": "query",
          "type": "boolean",
          "description": "Include stdout output in the response. Defaults to true.",
          "required": false,
          "default": true
        },
        {
          "name": "stderr",
          "in": "query",
          "type": "boolean",
          "description": "Include stderr output in the response. Defaults to true.",
          "required": false,
          "default": true
        },
        {
          "name": "tail",
          "in": "query",
          "type": "string",
          "description": "Number of log lines to return from the end of the log, or 'all' for the complete log. Defaults to '100'.",
          "required": false,
          "default": "100"
        },
        {
          "name": "timestamps",
          "in": "query",
          "type": "boolean",
          "description": "Prefix each log line with an RFC3339Nano timestamp. Defaults to true.",
          "required": false,
          "default": true
        },
        {
          "name": "since",
          "in": "query",
          "type": "string",
          "description": "Return logs only since this point. Accepts a Unix timestamp (e.g. 1609459200) or a relative duration string (e.g. 1h, 30m, 15s).",
          "required": false
        }
      ]
    },
    {
      "name": "docker_system_info",
      "display_name": "Docker System Info",
      "description": "Retrieve low-level system information from the Docker host behind a Portainer environment: running/stopped/paused container counts, total image count, CPU count, total memory, Docker version, kernel version, and storage driver. Useful for a quick health overview of the host.",
      "method": "GET",
      "path": "/api/endpoints/{endpoint_id}/docker/info",
      "parameters": [
        {
          "name": "endpoint_id",
          "in": "path",
          "type": "integer",
          "description": "Portainer environment/endpoint ID. Defaults to 1.",
          "required": true,
          "default": 1
        }
      ]
    },
    {
      "name": "portainer_status",
      "display_name": "Portainer Status",
      "description": "Get Portainer server status: version number, instance ID, and edition. Use this to verify connectivity to the Portainer instance and confirm the running version.",
      "method": "GET",
      "path": "/api/system/status",
      "parameters": []
    }
  ]
}

Be careful, there are some caveats when you're running Open Assistant on the same server as portainer. Be sure to use the internal IP of portainer with 'http' over port 9000.


Get Started

Ready to simplify self-hosted infrastructure management? Deploy an instance of the assistant and activate the Portainer plugin in minutes.