Portainer Add-ons.
Add-ons are how Portainer installs, exposes, and governs companion products in the same cluster the Management Server runs on. The catalog defines them, a Helm-based lifecycle installs them, a reverse-proxy gateway serves their UIs, and a machine API lets them read and write their own settings. Portainer-Run installs this way; understanding the pattern makes every future add-on obvious.
What add-ons are
Add-ons extend Portainer with companion products deployed into the cluster the Portainer server is running on. An add-on is a first-class object with its own install/uninstall lifecycle, its own UI (exposed through Portainer's URL space), and its own settings store (accessed by the running add-on through a dedicated API).
The design purpose is not "list of features Portainer might one day include." It is the opposite: a mechanism for shipping additional products (like Portainer-Run, and future entries) without stapling them into the core server, without asking the customer to run a separate installer, and without duplicating auth, RBAC, or reverse-proxy plumbing they already trust.
One important constraint: add-ons are Kubernetes-only. Install, uninstall, and repair endpoints require the local environment (the environment the Portainer server itself runs on) to be a Kubernetes cluster. This is the operational reason we recommend Portainer on KubeSolo rather than Portainer on Docker Standalone for customer engagements; the Kubernetes substrate is what makes add-ons possible.
Four moving parts
Every add-on interaction goes through one of four subsystems. Understanding which is which makes triage tractable, because "the add-on is broken" is usually a specific subsystem being broken.
The catalog is where add-ons are defined: names, chart references, in-cluster upstream service URLs, health paths, proxy routes. The catalog is fetched from a URL and cached with a specific staleness policy.
The Helm lifecycle service is what installs, upgrades, uninstalls, and repairs add-ons. Every add-on is delivered as a Helm chart; the lifecycle service runs Helm operations as a server-owned goroutine and persists the resulting state.
The reverse-proxy gateway is what exposes the add-on's UI. Requests to /addons/<name>/* at the Portainer server's URL are proxied to the add-on's in-cluster upstream service. Authentication, per-add-on access control, and a dedicated CSP all apply at the gateway.
The machine API is how a running add-on reads and writes its own configuration. It is mounted at /api/addon-store/v1, and it uses per-add-on-instance tokens rather than user credentials. This is what lets an add-on persist state without a database of its own.
The catalog
The catalog is a JSON document Portainer fetches at runtime. By default it is addons/addons-catalog.json in the portainer/templates repository, ref v3. If the addons feature flag is on (Module 5, chapter 09), Portainer fetches addons-catalog-develop.json instead, which is the release catalog plus unreleased add-ons. An administrator can override the catalog URL entirely in Settings.
Caching behavior. A fresh catalog is served from memory for 10 minutes; a stale copy (last known good) or the failure itself is served for another 15 seconds before a re-attempt. The configured URL is re-read from settings at most every 5 seconds; fetches are bounded at 15 seconds wall time and 1 MB payload. An unreachable catalog yields no routes rather than preventing Portainer from starting; this is deliberately non-fatal because add-ons are optional, and the server should not fail to boot because a template repository is down.
Each catalog entry describes: the add-on's chart reference (an oci://host/path OCI reference with a TLSVerify flag), the in-cluster upstream service URL and health path, the proxy route, and metadata for the UI (name, description, icon).
An empty catalog page usually means the catalog fetch failed. Check the server log for the fetch error; the served-stale window is only 15 seconds, so a persistent outage produces "no add-ons" rather than "old add-ons." Alternatives: an admin has overridden the catalog URL to something wrong, or the add-on is only in the develop catalog and the addons feature flag is off.
Install, uninstall, repair
Installing an add-on runs as a server-owned goroutine. The flow: persist the add-on record in the installing state; execute the Helm upgrade against the local Kubernetes cluster; write the terminal status (deployed or failed) back. Because Helm upgrades can take minutes on real charts, the goroutine model is what keeps the API responsive while the install runs.
A failed atomic Helm upgrade is not the same as an install failure. Atomic upgrade means Helm rolls back to the previous release on failure; if the rollback restores a working release, Helm's status is still deployed, and the add-on shows as installed. That is correct behavior; the previous version is serving.
Panics during install are recovered and mark the add-on record failed with "internal error during install." The repair endpoint (POST /api/addons/{id}/repair) re-runs the install for a failed release; this is the usual first triage step when an add-on is stuck. Uninstall optionally prunes the add-on's stored configuration.
Chart sources: the catalog's OCI reference is pulled anonymously against the platform trust store, honoring the entry's TLSVerify flag. Alternatively, an administrator can install from a chart in a Portainer registry the administrator selected at install time, which uses that registry's credentials and TLS configuration. This is the path for air-gapped or private-chart environments.
Chart-source verification tries with TLS verification on first and only reports "unverified" if verification genuinely fails. This means a self-signed private registry with a legitimate certificate chain resolves cleanly; a real cert mismatch or expired cert reports the problem accurately. Do not panic on "unverified" without checking whether the certificate is actually broken.
The gateway
Add-on UIs are served at /addons/<name>/* (top level, NOT under /api). The gateway is a reverse proxy: it resolves the add-on's upstream service and route from the catalog on each request (so a catalog change applies without a Portainer restart), and it forwards the browser's HTTP or WebSocket traffic to the add-on's in-cluster service.
Authentication runs at the gateway. Unauthenticated requests get a 302 redirect to / (the Portainer login page). Once the user is logged in, per-add-on access control (see chapter 07) determines whether they can reach this specific add-on; failing access is a 403.
Two security details at the gateway. First, add-on routes get a dedicated Content Security Policy that is stricter than the main UI's: self-only scripts, no frames. This is why an add-on that tries to load scripts from a CDN or embed content in an iframe will silently fail to render fully; the CSP blocked it. Second, WebSocket upgrades check the Origin header against the request Host; when they differ (typical behind a corporate proxy), the mismatch is checked against the operator-configured --trusted-origins list. Set --trusted-origins if a customer's add-on WebSocket features fail behind their proxy.
The machine API
A running add-on stores its configuration through a dedicated API at /api/addon-store/v1. This is separate from the main Portainer API for two reasons: to give add-ons a persistence path without requiring a database, and to authenticate them differently from users.
Authentication is a per-add-on-instance token, sent in the Authorization header (not X-API-KEY, which the main bouncer would resolve against user API keys). Portainer generates one token per installed add-on at install time and provisions it into the cluster where the add-on can read it. The token is validated by the machine API's own middleware, distinct from the bouncer chain.
Two design consequences worth naming. First, there is no user row behind these tokens, so a machine token can never pass the bouncer's authenticated-user middleware on any normal user route; an add-on cannot impersonate a user. Second, no add-on ID appears in the machine-API paths; identity comes from the token, so one add-on cannot read or write another add-on's settings. This is the isolation property that lets you install several add-ons on one Portainer without them stepping on each other.
Machine-API activity gets logged under the AddonMachineAPI log context, distinct from AddonLifecycle (which is admin actions on the add-on). If a customer asks "did the operator do this or did the add-on do this," those two contexts are how you tell them apart.
Add-on token LastUsed is throttled to 60 seconds. A polling add-on that hits the config API every few seconds does not turn its reads into a stream of write-back updates to the token record. If you are diagnosing "why does this add-on hit the DB so often," the answer is: it does not; the write throttle prevents that.
Access control per add-on
Each installed add-on carries an access list: which users and which teams may open it. This is separate from Portainer's environment-level RBAC (Module 7, chapter 02), which does not know about add-ons. Access is checked at the gateway on every request; a user without access to a specific add-on gets a 403 when they try to reach its URL, even if they are authenticated to Portainer.
Manage the access list through PUT /api/addons/{id}/access (admin only). The typical pattern is: install the add-on with default access limited to admins, add the teams that should use it, add individual users only for exceptions.
Access control does not extend to what the add-on itself does. Once a user is inside an add-on's UI, the add-on's own authorization model (if any) takes over. Portainer's job at the gateway is to control who reaches the add-on; the add-on decides what they can do inside it.
Portainer-Run as an add-on
Portainer-Run, the developer-facing deployment surface Portainer ships for the "vibe-coded app" case, installs as an add-on. It is a canonical reference for the extension pattern; understanding how it fits makes every future add-on immediately understandable.
Portainer-Run's catalog entry references a chart in the portainer/charts organization (the canonical code example is portainer/charts/portainer-run). Install runs the standard Helm lifecycle; the upstream service is exposed at the gateway route the catalog entry specifies; access is granted to the developers who should be able to deploy through it.
Portainer-Run's own configuration (which Portainer environment to use for target deployments, which registries to accept, which templates to expose) is stored through the machine API. Its UI is served through the gateway. It authenticates end users through Portainer, so a developer using Portainer-Run does not need a separate login.
The point of showing this is not to teach Portainer-Run; the point is that the same shape applies to every future add-on. Portainer's roadmap includes several products (AiGrid for document ingestion and retrieval, Command for chat-based operator interfaces, others) that all install this way. If you learn the add-on pattern, you have learned the extension model for the whole product line.
If Portainer-Run is in your catalog, install it against your local Kubernetes environment. Watch the AddonLifecycle log context during install. When it lands, open it at /addons/portainer-run/. Confirm your Portainer login carries through and that the upstream service is reachable behind the gateway.
Chart sources and registries
Two paths for installing an add-on: the catalog's OCI reference (public), or a chart in a Portainer registry (private). Choose the second for air-gapped deployments and for customers whose security posture requires all container and chart artifacts to come from an approved registry.
Set up: register the private OCI registry in Portainer (Module 7, chapter 05); mirror the add-on chart into it (or publish your own chart there); at install time, select the private registry as the chart source rather than the catalog reference. The credential and TLS configuration on the registry apply automatically; the add-on's Helm chart pulls from your registry, not from the public repository.
This is the pattern for customer environments where "all traffic to public registries is blocked" is a hard security constraint. It works, but it means the customer takes on chart-mirroring as an operational task; every add-on version needs to land in their registry before it can be installed. Do not sell air-gapped add-ons as effortless; sell them as governed.
What is next
Module 10 is the last one. It is about operating Portainer itself: backups and restores, auto-patch on the LTS line, node counting and license management, disaster recovery posture, and the common failure modes with their triage paths. It is more reference than procedure; you will come back to it when your customers have real Portainer instances running production workloads.
Next: Module 10 · Operating Portainer