Portainer architecture.
The Management Server and three flavors of Agent are all Portainer has as building blocks. This module walks each one, then the reverse-tunnel machinery, then the datastore, then the constraints those choices impose on high availability and disaster recovery. When you finish, you can pick the right agent mode per environment and defend the choice to a security team.
The Management Server
Portainer's Management Server is one process, one binary, one container. It runs on Kubernetes (recommended, typically as a KubeSolo single-VM install for small deployments), or on Docker Standalone or Docker Swarm for customers with an existing Docker operating model; it holds all the state; and it is the thing users, agents, and integrations talk to.
Internally, the server is a Go monorepo. The commercial (Business) build imports Portainer CE as a Go module and layers EE-specific handlers, RBAC, licensing, edge async, add-ons, policies, and the observability integration on top. That composition is invisible to you as an operator, but it explains a couple of behaviors you will hit later: some core constants (GitOps Source and Workflow types, some policy type IDs) are defined in the CE module and shared with EE, which is why the same objects appear in both codebases with the same names.
At boot the server does the following, in order: it parses its CLI flags (via kingpin); if --health-check is set it probes the running instance and exits; it applies logging configuration; it builds the whole dependency graph (platform detection, auto-patch service, embedded Prometheus, embedded Alertmanager, Omni recovery, recommendations, source scheduler); and it starts serving HTTP. The first line in its logs is starting Portainer with version, build number, image tag, and Go version. That log line is your authoritative confirmation of the exact build a customer is running; grab it before you try to triage anything.
The server is a single process. There is no separate "control plane" and "data plane"; there is Portainer, and there are Agents. Everything else in the architecture is either a service the server calls internally (embedded Prometheus, embedded Alertmanager, the auto-patch scheduler) or an external system it integrates with (Git providers, OCI registries, LDAP/AD/OAuth, OneUptime, SIEM syslog endpoints).
The Portainer Agent (standard mode)
The Portainer Agent is a lightweight process that speaks the Docker API, the Kubernetes API, or the Podman API on behalf of the Management Server. In standard mode (not edge), the server dials the agent directly; the agent listens on a port (default 9001), the server has network line-of-sight to it, and the two talk over authenticated HTTPS.
The agent's job is to be a signed, well-behaved proxy. It authenticates requests using a shared secret (AGENT_SECRET) and signature verification, and it exposes the target environment's API in a way the server can reason about consistently across environment types. On Docker Swarm the agent participates in a serf-based cluster membership so the server can target specific nodes by header (X-PortainerAgent-Target); on Kubernetes the agent uses the in-cluster service account by default and runs as a Deployment with one replica plus a headless service.
Standard-mode agents are the right pick when the Management Server can dial the environment directly: same VPC, same VPN, same routable network. Data-center Docker hosts, Swarms behind the same firewall as the server, and Kubernetes clusters the server can reach on their API service, all fit here. You do not need standard-mode agents if you are running Docker locally on the same host as the Management Server; the server can just use the local socket.
There is a security shutdown to know about. An agent that is never associated with a server and is not secured with an explicit AGENT_SECRET will terminate itself after 72 hours (the AGENT_SECRET_TIMEOUT default), which is a deliberate feature to prevent stray agents sitting on the network waiting to be adopted. If a customer reports "the agent randomly stopped after three days," this is nearly always why.
The Edge Agent (standard tunnel mode)
The Edge Agent is the same binary as the standard Agent, run in edge mode (EDGE=1). Standard edge mode is the flavor that supports interactive management: the agent polls the server on a configurable interval (5 seconds by default), and when the server needs to reach the agent's environment interactively it marks the tunnel REQUIRED; on the next poll the agent opens a reverse tunnel back to the server, and traffic flows through that tunnel until it goes idle.
The reverse tunnel is chisel over websocket. The agent connects out to the server's tunnel port (default 8000, configurable with --tunnel-port) as a client; the server allocates a random loopback port on its side (in the 49152-65535 range, bound to 127.0.0.1) and routes requests to it through the agent's chisel connection. That is why an edge deployment never requires an inbound firewall rule at the agent site; the agent dials out, and everything flows over that outbound connection.
The agent's poll response tells it: what interval to poll at next, whether the tunnel is required, the encrypted chisel credentials if so, the edge jobs schedule, the current edge stacks (with version numbers), the alert rules (structured and as pre-compiled Prometheus YAML for local evaluation), the policy state, and the edge configurations. The response format shifts slightly by agent version, because Portainer supports mixed-version fleets deliberately; a 2.42 agent will get legacy per-chart policy summaries, a 2.43+ agent will get the newer per-policy state format, from the same server, in the same fleet.
The single most common cause of "agent never appears" is an edge key generated with the wrong external URL or the wrong tunnel address. The agent polls the URL inside the key, not whatever the server currently thinks its URL is; if you moved the server behind a new load balancer, generate new keys. Where the reachable tunnel host differs from the UI host (typical when the UI is behind a corporate proxy but the tunnel port is exposed separately), set --edge-tunnel-server-address and re-issue keys.
The Edge Agent (async mode)
Async mode (EDGE_ASYNC=1) removes tunnels entirely. In async mode the agent periodically POSTs to the server carrying a snapshot of its environment and a set of status reports, and receives back a queue of commands to execute. There is no reverse tunnel; there is no live interactive management; everything is command-and-response.
The protocol runs three independent tickers on the agent side: a ping ticker, a snapshot ticker, and a command ticker. Each has its own interval, dictated by the server per-endpoint (or globally through settings). The agent posts a full or differential snapshot on the snapshot ticker; it fetches queued commands on the command ticker; it lets the server know it is alive on the ping ticker. The server stores the last snapshot and applies incoming JSON patches; if a patch cannot apply cleanly (hash mismatch, restart, EDGE_ID collision between two devices sharing one endpoint), the server flips needFullSnapshot=true in its next response, and the agent goes back to sending full snapshots for a while.
Async mode is the right pick where the environment is not always connected: mobile fleet, satellite site with intermittent uplink, air-gapped site with periodic sync window, industrial gateway on a metered link. It is also the right pick where you deliberately do not want interactive management for security reasons; policy enforcement, stack deployment, and configuration all still work, but nobody can open an exec session or a live log stream on an async-mode environment because the tunnel plumbing simply is not there.
One consequence to remember: server-side actions on an async environment (deploy a stack, apply a config, distribute a policy) get enqueued as commands rather than executed live. If someone clicks "deploy" and nothing seems to happen, the command is almost certainly queued and waiting for the agent's next command tick. This is not a bug; it is the model.
Policy distribution to async environments only happens when the async-policies feature flag is turned on. Without the flag, policies attached to a group containing async environments will silently not reach those environments, and the policy status will show them as unaddressed rather than failed. Turn the flag on if you have async fleet and want policies enforced there.
Choosing your agent per environment
You get to pick per environment, not per fleet. A customer with a data-center cluster, three regional Docker Swarms, and forty industrial edge sites will use standard-mode agents for the data center, edge-standard agents for the regional Swarms (if connectivity is stable enough to support tunnels), and edge-async agents for the industrial sites (if links are intermittent). One Portainer server, three agent modes, one operating model on top.
| Mode | Use when | Do not use when |
|---|---|---|
| Standard Agent | Server has direct network line-of-sight to the environment. Data center, same VPC, same VPN. | The environment is behind NAT, firewall, or corporate proxy the server cannot traverse. |
| Edge Agent (standard/tunnel) | Agent has outbound internet or reverse-proxied HTTPS to the server. Regional clusters, branch sites, remote environments with stable connectivity. | Connectivity is intermittent or metered. Websocket upgrades are blocked. Interactive management would create a security exposure you cannot accept. |
| Edge Agent (async) | Environment is intermittently connected, air-gapped-with-sync-windows, or you explicitly do not want interactive management. Industrial sites, ship-based fleets, mobile fleets, sensitive OT. | You need live logs, exec into containers, or interactive troubleshooting through Portainer. Async gives you configuration and policy, not a live session. |
You can change your mind. An environment that starts as edge-standard can be reconfigured to edge-async by changing the agent's environment (EDGE_ASYNC=1) and re-associating; the endpoint's AsyncMode flag on the server side has to match. What you cannot do is have one environment behave both ways simultaneously; each endpoint is one mode at a time.
The chisel tunnel, in more depth
Because the tunnel is the single most-triaged component in an edge deployment, it is worth pinning down how it actually works.
When the server marks a tunnel REQUIRED, it allocates a random port in the 49152-65535 range on its loopback (127.0.0.1 on the server's bind), generates random chisel credentials keyed to the environment's EdgeID, and encrypts those credentials in the poll response the agent picks up next. The agent decrypts, opens a websocket to the server's tunnel port (--tunnel-port, default 8000), and authenticates as a chisel client. From then on, requests to 127.0.0.1:<allocated-port> on the server go through the chisel tunnel to the agent's local API.
The server tracks tunnel health by ping (8-second timeout) on a 10-second sweep, and refreshes a LastActivity timestamp. Tunnels idle for longer than 4 minutes 30 seconds get torn down; the agent independently closes its end after 5 minutes of inactivity by default (EDGE_INACTIVITY_TIMEOUT). Keep-alive on the websocket runs at 25 seconds, which is what stops middleboxes (corporate proxies, load balancers) from terminating an idle connection.
If tunnel establishment does not happen within twice the effective check-in interval, the caller sees "unable to open the tunnel." That is the error users hit when they click into an edge environment that is supposed to be online but the tunnel plumbing is broken. Verify in this order: is the tunnel address in the edge key reachable from the agent site (dig the FQDN from the device, telnet the port, one of them will fail); is --tunnel-port actually exposed on the server (many load balancers terminate everything but 443); is a corporate proxy blocking the websocket upgrade (set HTTP_PROXY/HTTPS_PROXY on the agent); is EDGE_TUNNEL disabled on the agent (there is a setting to run poll-only, useful for pure config-management edge sites where you never want interactive management).
The Server's HA constraints
Portainer's data store is a single-writer BoltDB (bbolt) file. That is the load-bearing architectural fact for high availability, and it needs to be understood explicitly, because it drives every HA and DR conversation you will have with a customer.
Because bbolt is single-writer, Portainer does not run active-active. There is one writer at a time. The HA model is: single-replica Management Server with durable persistent storage underneath, platform-native rescheduling on top (Kubernetes will restart the Portainer pod on another node if the current one fails; Swarm will move the Portainer service; a systemd-managed container will restart), and resilient agent reconnection logic so that when the Management Server comes back up all the agents pick their conversations back up.
Control plane failure is deliberately non-disruptive to workloads. Managed Kubernetes clusters and Docker environments continue running whether Portainer is up or down; Portainer is not in the request path of any workload. If Portainer is unreachable for an hour, applications keep running; management access is unavailable during that hour, and edge agents queue up their next poll but do not crash. As soon as Portainer comes back, agents reconnect, edge state resynchronizes, and management resumes.
This is not the same as an active-active database. If a customer asks whether Portainer can run two writers simultaneously with a replicated datastore, the answer is no; the architecture is single-writer by design, and the DR strategy is durable storage plus backup, not clustering. Anyone who claims to need active-active management typically actually needs "the workloads stay up when the management server goes down," and that is what Portainer already delivers.
The datastore (BoltDB)
The datastore is a single BoltDB file in the server's --data directory. Access goes through a repository-per-entity dataservices layer, with explicit transactions (never nested). Everything Portainer needs to remember lives here: users and teams, endpoints and endpoint groups, roles, policies and policy status, licenses and enforcement state, edge async command queues, edge configurations and state, edge stack logs, cloud credentials, git credentials, backup targets and schedulers, alert rules, auto-update history, installed add-on records with per-instance machine tokens, and the GitOps Source and Workflow stores.
The datastore can be encrypted at rest, with the encryption key held in a file referenced by --secret-key-name. If that key file is missing or changed between boots, the store will not open, which will manifest as a boot loop with clear error text; this is not a migration problem, it is a key mismatch.
Migrations run on boot when the schema version is older than the binary. The flow is: check for an interrupted prior migration (an IsUpdating flag left set means a previous attempt crashed mid-flight; boot fails with "database is updating" and you either restore the automatic backup or use --force-rollback); take a pre-migration backup of the DB file; run the versioned migration chain inside a fail-safe wrapper that clears IsUpdating on success and automatically restores the pre-migration backup on failure. This is why a failed upgrade leaves the customer running the old version cleanly; the fail-safe wrapper reverted them, and the migration error in the log is the thing to escalate.
Two migrations in the 2.44 and 2.45 line are worth knowing about, because they change customer-visible behavior. The 2.44 migration moved the GitOps polling interval from per-stack fields to the Source, and where multiple stacks shared one repo with different intervals, the shortest interval wins after migration; some customers see intervals converge. The 2.45 migration cleans the workflow store, pruning artifacts whose backing stack no longer exists; workflows left with no artifacts are deleted entirely.
Ports, protocols, and network paths
Portainer's network model is small, which is one of the things security teams appreciate about it.
The Management Server listens on two ports by default: 9000 for HTTP (--bind) and 9443 for HTTPS (--bind-https); most production deployments turn HTTP off (--http-disabled) and put the server behind a reverse proxy or ingress that terminates TLS with a real certificate. The tunnel server binds on --tunnel-port (default 8000); this port only matters if you are running edge-standard agents that need to open reverse tunnels.
Standard-mode Agents listen on 9001 by default. The server dials that port over authenticated HTTPS with signature verification. Nothing else is exposed on the agent side.
Edge Agents in standard mode dial out only. The agent polls the server URL from its edge key over HTTPS (typically 443 or whatever the server is exposed on), and when the server marks the tunnel REQUIRED the agent opens a websocket to the server's tunnel port. There is no inbound listener on the agent side that the server ever touches.
Edge Agents in async mode also dial out only, over HTTPS to the server URL. There is no tunnel, so there is no second port; everything is HTTPS request-response.
For auth and identity, LDAP/AD is a normal LDAP connection (389 or 636) from the server. OIDC/OAuth flows over HTTPS to your identity provider. For observability integration, OneUptime is HTTPS to the OneUptime endpoint. For SIEM, syslog runs on 514 (UDP) or a configured port (TCP or TCP+TLS). For registry pulls, whatever your registry uses.
What Portainer does not deploy
The architecture picture is not complete without saying what Portainer does not deploy: the environments themselves. Portainer manages Docker Standalone, Docker Swarm, and Kubernetes environments; it does not, on its own, install any of them. You bring the runtime, or you have Portainer provision Talos and Kubernetes through Sidero Omni (Module 6 covers this); either way, the substrate exists before Portainer starts governing it.
This is a deliberate scope decision, aligned with the deliberately-less principle from Module 1. If Portainer tried to be a Kubernetes distribution too, it would inherit every operating burden a distribution owner carries: kernel and CNI compatibility matrices, CSI drivers, etcd tuning, load balancer integration, ingress controller opinions, and so on. Portainer's answer is to integrate cleanly with the distribution you have (or the distribution Talos gives you), and to concentrate on the layers above the distribution.
The one exception is KubeSolo, Portainer's own lightweight single-node Kubernetes for cases where full cluster semantics do not fit: constrained hardware, edge appliances, environments where clustering provides no benefit. KubeSolo is a Portainer product; it is managed centrally through Portainer; it is Kubernetes-compatible without the Kubernetes overhead. Module 6 shows you how to provision one.
What is next
Module 4 is where the lab starts. You are going to deploy a Portainer Management Server in your lab environment, get through the first-boot admin flow, add your license, and get to a working baseline you can build on. Modules 5 through 10 assume you have a running Portainer to work with.
Have your lab VMs ready before you start Module 4; the welcome page tells you the minimum (four VMs) and the ideal (seven, plus a KaaS cluster if you want to work through Module 6's managed-Kubernetes onboarding path).
Next: Module 4 · Deploy the server