BeyondTrust/POV

← Back to dashboard

Portainer: a managed server

Audience: operator · Profile: demo · Read this when: you want the dashboard to deploy Portainer CE for you on AWS, Azure or GCP, and want to know what it creates, how to reach it, and how it is torn down.

Part of Portainer.

What gets created

A single portainer/portainer-ce container on one VM with a public, source-restricted IP — the same shape as the Rancher node:

Aspect Detail
VM tag / label purpose=portainer (how the dashboard finds it again)
Ports 9443 HTTPS UI/API, 8000 Edge agent tunnel
Privileges Unprivileged, no Docker socket — the server administers remote Docker hosts over the API, so it needs neither
State /data on the VM's own disk, or on a separate volume — see Durable state
TLS self-signed certificate on 9443

The node, per cloud

The container is identical everywhere. What differs is the machinery around it.

AWS Azure GCP
Host EC2, ECS-optimized AL2023 AMI (Docker preinstalled) Ubuntu 22.04 VM Container-Optimized OS VM
Container started by docker run from EC2 user-data docker run from cloud-init gce-container-declaration (konlet)
Default size t3.small Standard_B1s e2-small
Ingress gate a dedicated security group a dedicated NSG on the NIC a firewall rule targeting a network tag
Durable /data a gp3 EBS volume (DeleteOnTermination=false) a managed disk (delete_option=Detach) a persistent disk (auto_delete=false)
Public IP ephemeral static — survives a recreate ephemeral

By default the node is ephemeral. Its disk is deleted with the VM, so tearing the node down — or recreating it — wipes /data: users, environments and settings are all lost. On GCP and AWS the external IP changes too, which additionally invalidates every Edge key; on Azure the address is static and survives. Turn on durable state to keep the data either way.

Durable state

Tick Keep Portainer's data on a persistent disk in Settings → Containers (portainer_data_disk_enabled) and /data moves to a separate volume named <node-name>-data, created so it outlives the VM. A teardown then keeps the users, environments and settings, and the next deploy reattaches them.

The container must not start before that volume is mounted. If it does, Portainer writes its database to the VM's own disk and loses it on the next recreate — silently, with nothing in any log to say so. Each cloud reaches that guarantee differently:

Cloud How the ordering is guaranteed
GCP a konlet gcePersistentDisk volume: konlet formats a blank disk (mkfs.ext4), fscks a used one, mounts it, and only then starts the container
Azure the disk is attached at VM create time and its device path is deterministic, so cloud-init mounts it before the docker run
AWS an existing EBS volume cannot be attached by run_instances, so the attach lands after boot — user-data therefore waits for the device (up to 5 minutes, resolved via /dev/disk/by-id because Nitro renames it), mounts it, and refuses to start the container at all if it never arrives

On every cloud the format step is conditional on the volume having no filesystem, so a redeploy can never reformat the disk holding your only copy of the node's state.

Four things follow from durable state, and they are the whole reason it is opt-in:

Prerequisites

Requirement Notes
One configured cloud AWS, Azure or GCP credentials under Settings. You pick which hosts the node on the deploy form
Permissions for it GCP roles/compute.admin (from setup-gcp.sh); AWS EC2 + security-group + EBS volume actions (all in setup-aws.sh's dashboard-app-policy); Azure Contributor on the resource group (from setup-azure.sh)
A region with a configured subnet The node needs a public subnet, and a subnet is regional on every cloud — only configured regions are offered
Allowed source CIDRs Who may reach 9443/8000. Fail-closed: see Firewall

Deploy

  1. Open Containers → Portainer. The Managed Portainer server card is at the top; click its header to expand it. The setup cards (this one, Just-in-time access (Entitle), Connect a Docker host and Import from another Portainer) start collapsed once Portainer is configured, so the environment list sits near the top. Each header shows a configured / not configured badge, and this card opens by itself while Portainer is not configured.
  2. Pick a Cloud, and optionally a Region (and a Zone, on GCP only). Blank region keeps the node's current region; blank zone auto-picks the region's first available one, falling back to a sibling if that is capacity-exhausted. The Zone field is hidden on AWS and Azure because an EC2 subnet already pins its availability zone and Azure has no zone in this shape.
  3. Click Deploy Portainer server. You land on the job page.

The job runs on the durable worker (VM boot plus bootstrap outlasts a web timeout):

Step What happens
Configuring firewall Auto-detects the dashboard's public egress IP, merges it with your CIDRs, applies the ingress rule. Fails fast if the merged set is empty
Launching the node VM Creates (or reuses/starts) the VM; relocates it if you picked a different region or cloud
Waiting for Portainer Polls GET /api/system/status until it serves
Signing in as the admin user The VM was launched with --admin-password, so the admin already exists
Minting an API token Logs in and creates a personal access token

On success the job writes the connection settings for you — portainer_url, portainer_pat, and portainer_verify_ssl (off, because of the self-signed cert) — so the Containers tab starts working with no Settings round-trip.

Minting a token later

The deploy is not the only way to get one. Mint a new API token, under the node table on the Containers page, does the same two calls on demand: the dashboard signs in as the Portainer admin (using portainer_admin_password) and mints a token through POST /api/users/{id}/tokens. Use it when the stored token was revoked, when a node was redeployed without durable state so the DB that issued the old one is gone, or when a bootstrap reported that it could not mint one.

Each mint gets a distinct description (vm-dashboard-<unix>) because Portainer refuses two tokens with the same description for one user. It adds a token rather than replacing one — revoke the old ones in Portainer if you want them gone.

On the managed node, minting manages the node's ingress as well. A deploy writes the allow-list from one egress detection and never revisits it, so by the time you mint, the dashboard's own outbound address may have moved — a worker rescheduled behind a different SNAT address, a proxy pool picking a different one — and the node's rule still names the old one. The mint then fails with a ConnectTimeout (dropped packets, not a closed port). Rather than report it, the dashboard re-detects its egress address, re-applies the ingress rule and mints again; if the rule had been deleted outright, re-applying puts it back. Only for a node this dashboard deployed — a Portainer you merely point it at has a firewall that is yours, and it says so instead of touching anything.

Minting also re-stages the token to the portainer_access adapter, if one is deployed (see Portainer through Entitle). Re-send the token to the adapter does only that half, for when the dashboard's own token is fine and only the function's copy is stale.

If portainer_pat holds a vault reference rather than a literal, minting is refused: storing a token over the reference would leave the vault holding a stale value that nothing reads. Rotate the token in Portainer, update the vault secret, then re-send it to the adapter.

A short-lived Portainer JWT is the means here, never the product. Portainer's session token expires in hours and nothing can refresh one on an integration's behalf, so a JWT stored as the credential would work this afternoon and start answering 401 tomorrow. An API token does not expire, and is revocable from Portainer's own UI.

If you left Admin password blank the dashboard generates a 24-character one and shows it once, on the Containers page (Log in as admin / …). Change it in Portainer after first login.

The password is settled before the VM is created and passed to the container as a bcrypt hash (--admin-password), so Portainer initializes its admin at startup. That is deliberate: Portainer only accepts POST /api/users/admin/init for a short window after the container starts, and once that window closes it answers every request with administrator initialization timeout — a node with no admin that nobody can log into until the container restarts. Initializing at boot means there is no window to lose.

This holds on every cloud, and so does the way out of it: a node already in that state can't be repaired by redeploying, because the launcher reuses a running VM and the thing that carries --admin-password (the container declaration on GCP, user-data on AWS, cloud-init on Azure) is only read at boot. Delete the node and deploy again; the job says so instead of reporting a misleading "already had an admin user".

The hash travels in instance metadata / user-data, which is readable from inside the VM. It is a bcrypt hash rather than the password, and it is the same exposure the GCE path has always had — but it is why the node is given no other secret.

PRA Web Jump (optional)

By default the node's UI is reachable only from the source CIDRs you allow, and an auto-generated admin password has to be shown on the Containers page so you can use it. Ticking Broker the Portainer UI via a PRA Web Jump on the deploy form fixes both:

Provisioning runs after first-run bootstrap (the password has to exist to be vaulted) and is best-effort — a PRA hiccup logs a warning and leaves the node deployed and usable over its public IP. Jump Group, Gateway and Vault group default to the bt_* settings when not chosen on the form.

Requires PRA to be configured (bt_api_host, bt_client_id, bt_jumpoint_name); the fieldset stays hidden otherwise.

Teardown

Stop on the node row first retires the Entitle adapter — deregistering the integration, destroying the function and retiring its staged API token — because an adapter that outlives its Portainer is a billable function that can only fail and a grantable integration pointed at nothing. It is best-effort: an unreachable Entitle tenant leaves a warning in the job result rather than blocking the teardown. Remove adapter on the card does the same thing on its own.

Stop then removes the PRA Web Jump (when one exists), deletes the VM and its ingress rule, then clears portainer_url and the node's other runtime config. On AWS it also reclaims the node's security group once the terminating instance releases it; on Azure it removes the NIC, the public IP and the NSG the VM owned.

The volume can outlive the sandbox. Because it is meant to survive a teardown, it can end up the only thing left in a region. The sandbox rollback scripts therefore refuse to run while a managed node or an orphaned node volume is present, rather than cascading over your only copy of the node's state.

Firewall

The node's ingress opens tcp 9443 and 8000 to a merged source set:

It is fail-closed on every cloud — an empty merged set leaves the node unreachable — but the mechanism differs, because the three clouds do not offer the same primitive:

Cloud Open Closed
GCP a firewall rule targeting the node's network tag the rule is deleted
AWS a dedicated security group on the node's ENI every ingress permission is revoked — a security group in use by a running instance cannot be deleted
Azure one allow rule in a dedicated NSG on the node's NIC the rule is deleted; the NSG stays (it is attached to a live NIC, and a Standard public IP denies all inbound without a rule anyway)

Set that cloud's *_portainer_allow_open to open 0.0.0.0/0 when no CIDRs are set — a deliberate opt-in, per cloud. Settings → Containers shows the live merged allow-list.

Re-applying it

The deploy configures the firewall from one egress detection and never revisits it, so the rule ages out from under a node that is running perfectly well: the worker is rescheduled behind a different SNAT address, a proxy pool picks another one, a gateway comes back with a new IP, or someone deletes the rule in the cloud console. Every caller then reports the same unhelpful thing — unreachable — and the only cure used to be a redeploy.

Re-apply the node firewall (POST /api/containers/portainer/node/firewall) is that step on its own: re-detect the dashboard's egress address, recompute the merged set, re-apply the rule. It reports what it added and removed, and says so plainly when the result is closed — an empty merged set is a real outcome here, not an error. Safe to click on a healthy node: the per-cloud apply is idempotent, and a rule that had been deleted is simply put back.

It is in two places, because the two halves of this are in two places: under the node table on the Containers page, and next to the allow-list readout in Settings → Containers — where the button is labelled Re-apply and the breakdown re-renders from the result, so what you are reading afterwards is the rule that now exists.

Minting a token does this for itself on a dropped connect, so reach for the button when what is failing is something else — listing environments, an Edge registration, a Web Jump.