← Back to dashboard

Portainer CE Integration

What is it?

The Portainer CE integration connects the dashboard to a single Portainer Community Edition instance managing your on-premises Docker hosts. It adds a Containers tab that lists running containers, starts/stops them, and deploys containers or compose stacks — from the same UI you use for cloud resources. (One Portainer instance can manage many Docker hosts; each appears as its own environment/endpoint.)

There are two ways to get one:

Either way the dashboard talks to Portainer over its REST API. No special network topology is required beyond the dashboard being able to reach the Portainer URL.

Kubernetes note. Portainer manages Docker hosts here. The dashboard's Kubernetes management plane is Rancher — see Kubernetes. Earlier versions could install a Portainer Agent into clusters and onto new VMs; that path was removed when the management plane moved to Rancher.


Use cases


Option A — deploy a managed Portainer server

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 panel is at the top.
  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 youportainer_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.

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 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.


Connect a Docker host (Edge agent)

A managed node cannot reach into your network. It runs unprivileged with no Docker socket of its own, and it sits on a public IP with no route to a LAN address. So it can manage a Docker host only if that host comes to it.

That is what an Edge agent does: the agent runs on the Docker host and polls outbound to the node's tunnel port 8000 — which the node's firewall already opens — so nothing inbound to your network is required, and no VPN.

  1. Open Containers → Portainer. Under Connect a Docker host, type an environment name and click Generate join command.
  2. Run the generated docker run on the Docker host you want managed.
  3. The environment appears within a few seconds; Refresh lists it.

The command sets EDGE_INSECURE_POLL=1, because the node serves a self-signed certificate. Without it the agent's first poll fails certificate verification and the environment simply never appears — with no error in a place you would think to look.

The Edge key is shown once and is tied to the node's URL. Portainer derives the key from the node URL, its tunnel host and the new environment's id — so whether a key survives a recreate depends on whether the address does. On GCP and AWS the node takes an ephemeral address, so recreating it changes the URL and every agent joined beforehand stops being able to check in — which shows up as environments quietly going offline, not as an error. On Azure the Standard public IP is static, so joined agents keep working. The Containers page warns when the stored URL no longer matches the running node; when it does, re-run Generate join command and re-join each host.


Import from another Portainer

Merges users, teams, team memberships and registries from another Portainer into the configured one. Existing names are matched rather than duplicated, so re-importing the same bundle is a no-op.

What a Portainer backup can and cannot do

Portainer's own Backup produces a tar.gz of its /data volume. Two limits matter:

So the archive is opened where it already lives, and the dashboard imports a small reviewable JSON bundle instead.

Step 1 — turn a backup into a bundle (on your own machine)

Skip to step 2 if the source Portainer is still running — point the exporter straight at it.

docker run -d --name portainer-scratch -p 9443:9443 portainer/portainer-ce:latest
curl -k -X POST https://localhost:9443/api/restore \
     -F "file=@portainer_backup.tar.gz" -F "password=<only if encrypted>"
python -m web_dashboard.scripts.portainer_migrate export \
     --url https://localhost:9443 --username admin --insecure --out bundle.json
docker rm -f portainer-scratch

The restore must be the first thing that scratch instance is asked to do — Portainer closes its first-run window a short time after the container starts.

The exporter is stdlib-only, so it needs no virtualenv. inspect reviews a bundle offline:

python -m web_dashboard.scripts.portainer_migrate inspect --bundle bundle.json

Step 2 — import it

Containers → Portainer → Import from another Portainer → choose the .json. This enqueues a portainer_import job; follow it at /jobs/{job_id}.

Optionally pick an environment under Deploy stacks onto. Be deliberate: Portainer has no "save a stack without running it" API, so this deploys the bundle's stacks and starts containers.

What does not come across

Not migrated Why
Environment connections They address a local Docker socket or a LAN host this node cannot route to. The bundle records them under reference so you can see what existed; re-establish them as Edge agents.
User passwords Portainer's API never returns them, and the bundle scrubs credential-shaped fields regardless. Imported users get fresh generated passwords, reported once in the job result.
Registry credentials Same reason. A registry is recreated with its name and URL but unauthenticated, and the job names each one that needs its password re-entered.
Administrator role An imported user is always created as standard, even if the source had it as an administrator — a bundle is a hand-editable file from another server. The job says which users were downgraded; promote them in Portainer deliberately.
The node's own admin Skipped outright. The dashboard holds that credential; colliding with it is how you lose access to the node.
Anything deployed on the environments Portainer's backup never covered this either.

Option B — connect your own Portainer server

Step 1 — Create a Personal Access Token

  1. Log in to Portainer → click your username (top right) → My account.
  2. Scroll to Access tokensAdd access token.
  3. Give it a name (e.g. vm-dashboard) and copy the token string.

Step 2 — Configure in the dashboard

Setup wizard (first run) — toggle Portainer on the wizard's Feature Flags step. That step carries toggles only; fill in the fields afterwards, in the same place as after first runSettings → Integrations → Portainer CE:

Field Example
Portainer URL http://portainer.local:9000
API Token (PAT) the token string, or a vault reference (below)
Verify SSL disable for self-signed certificates

The token is stored encrypted in the application database. To keep it in an external vault instead, enter a reference — bt_safe://Portainer_PAT, aws_sm://dashboard/portainer-pat, azure_kv://portainer-pat, or gcp_sm://portainer-pat — and the dashboard resolves it at runtime through the secrets backend configured on /secrets.

Settings changes apply immediately — no .env edit or restart required. (Legacy installs that kept the PAT in BeyondTrust Password Safe under the PORTAINER_PAT_SECRET_TITLE secret title continue to work as a fallback when no token is set here.)

Step 3 — Verify

A Containers entry appears in the navigation; open it and confirm the container list loads.


What it enables in the dashboard

Feature Description
Containers tab Lists containers from every environment on your Portainer instance
Start / Stop One-click container power toggle
Deploy Launch a container from an image, or a stack from a compose file
Managed server Deploy / tear down a dashboard-run Portainer CE node (Option A)
Edge agent join Register an Edge environment and get the docker run for a host the dashboard can't reach
Bundle import Merge users, teams, memberships and registries from another Portainer

Automating Portainer with Ansible

Config Management ships localhost playbooks that drive the Portainer API — see examples/playbooks/portainer/: list environments, create-or-update a compose stack, remove one, and prune a Docker host.

They need no per-run setup: whenever a Portainer server is configured (typed in below, or written by a managed-node deploy) the dashboard injects PORTAINER_URL, PORTAINER_PAT and PORTAINER_VERIFY_SSL into the Ansible runner as environment variables — the same channel that carries the PASSWORD_SAFE_* credentials. The token is added to the run's scrub set, so it can't leak into job output.

Because they're hosts: localhost plays that reach out over HTTPS, the target you pick on the run form is irrelevant — nothing is installed on it.

Configuration reference

Key Default Purpose
portainer_enabled true Containers router + /containers page + the container warmer
portainer_url "" Server URL; written by a managed deploy
portainer_pat "" API token (encrypted, or a vault reference); written by a managed deploy
portainer_verify_ssl true Verify the server's TLS certificate; a managed deploy turns this off
portainer_allowed_source_cidrs "" CSV of manual firewall sources; empty is fail-closed
portainer_dashboard_egress_cidr "" The dashboard's own egress CIDR; auto-detected on deploy
portainer_admin_password "" First-run admin password; blank auto-generates one
portainer_ready_timeout_s 300 How long the deploy waits for Portainer to serve
portainer_node_cloud gcp aws | azure | gcp — which cloud hosts the node. Picked on the deploy form and rewritten to where it actually landed, so teardown and bare redeploys stay put. Defaults to gcp because every node deployed before this key existed is a GCE VM
portainer_data_disk_enabled false Put /data on a separate volume that survives a teardown
portainer_ui_web_jump_enabled false Broker the UI via a PRA Web Jump (opt-in)
portainer_ui_verify_certificate false Web Jump TLS verification — off for the node's self-signed cert
portainer_ui_jump_group "" Jump Group for the Web Jump; blank = bt_jump_group_name
portainer_ui_jumpoint_name "" Gateway for the Web Jump; blank = bt_jumpoint_name
portainer_ui_vault_account_group_id "" Vault account group the admin credential is stored in; blank = bt_vault_account_group_id, else the password is shown
portainer_ui_jumpoint_cloud gcp Which managed Gateway host brokers the UI; its egress IP is auto-allowed
portainer_ui_jumpoint_egress_ip "" Captured egress IP of the SHARED Gateway (runtime-set; auto-added as a /32). Gateways you deploy yourself are read from the gateway registry instead, so every cluster node is allowed

Per-cloud node keys

One group per cloud, all optional — the defaults are usable. Only the group for the cloud the node runs on has any effect, which is why each cloud can keep its own.

Key Default Purpose
gcp_portainer_image / aws_portainer_image / azure_portainer_image portainer/portainer-ce:latest Server container image
gcp_portainer_machine_type e2-small GCE size — Portainer is light
aws_portainer_instance_type t3.small EC2 size
azure_portainer_vm_size Standard_B1s Azure size
gcp_portainer_name / aws_portainer_name / azure_portainer_name portainer-server VM (or instance) name, and the base name of its ingress rule (<name>-allow-mgmt) and data volume (<name>-data)
gcp_portainer_boot_disk_gb (20) / aws_portainer_boot_disk_gb (20) / azure_portainer_boot_disk_gb (30) Boot / root / OS disk. Holds /data when no data volume is enabled, and is deleted with the VM
gcp_portainer_data_disk_gb / aws_portainer_data_disk_gb / azure_portainer_data_disk_gb 10 Size of the durable data volume (a persistent disk, an EBS volume, a managed disk)
gcp_portainer_allow_open / aws_portainer_allow_open / azure_portainer_allow_open false Open 0.0.0.0/0 on that cloud when no CIDRs are set
gcp_portainer_network_tag portainer GCE network tag = the firewall rule's target. No analogue on AWS/Azure, where a dedicated security group / NSG is the scope
gcp_portainer_zone "" Blank auto-picks a zone in the region; overwritten on deploy with the actual one
aws_portainer_zone (runtime) Recorded, not chosen — the availability zone the node (and so its data volume) landed in. The subnet pins it
azure_portainer_zone (runtime) Recorded, not chosen — the location the node landed in, so a bare redeploy stays there

Troubleshooting

Containers tab is missing — verify Portainer is toggled on in Settings → Integrations → Portainer CE. The flag applies immediately; no restart needed.

"Portainer is not configured" card on the Containers page — the URL or API token is missing. Deploy a managed server, or fill both in under Settings → Integrations → Portainer CE.

Portainer shows "Your Portainer instance timed out for security purposes" — the node's admin-initialization window closed before an admin was created, so the whole API is fenced off. A redeploy can't fix it on any cloud (the running VM is reused, and whatever carries --admin-password is only read at boot): delete the node on the Containers page and deploy again. Nodes deployed by this version initialize their admin at startup and can't reach this state.

A Web Jump through a gateway you deployed can't reach the node — the node firewall allows a /32 per gateway, refreshed on every gateway deploy/teardown. Check Settings → Containers → Effective firewall sources: the gateway should be listed under Web-Jump Gateways. If it isn't, its egress IP was never recorded — redeploy the gateway, or add the IP to portainer_allowed_source_cidrs.

Deploy fails with "the Portainer node's firewall is closed" — no allowed source CIDRs, and the dashboard couldn't auto-detect its own egress IP. Set portainer_dashboard_egress_cidr or portainer_allowed_source_cidrs in Settings → Containers — or enable that cloud's *_portainer_allow_open — then redeploy.

Deploy fails: "cannot be placed in \<region>: … is not set for that region" — the chosen region has no configured subnet (and, on AWS, no VPC). That is deliberate: falling back would put the node in the default region's network while the form, the job and the row all said otherwise. Run that cloud's sandbox setup for the region, or add a per-region config under Settings → Multi-region.

Azure: the node is RUNNING but nothing answers on 9443 — an Azure VM with a Standard public IP and no NSG rule denies every inbound packet, which looks identical to a closed allow-list. Confirm the node's NSG (<node>-allow-mgmt) exists and carries an allow-mgmt inbound rule; a deploy whose ingress step failed leaves the VM up and unreachable.

AWS: the node came up but /data is empty on a durable redeploy — the data volume never attached, so user-data refused to start the container rather than letting Portainer write to the root volume. Check the instance's console output for node-data volume never appeared, and that the volume is in the same availability zone as the subnet.

Deploy finishes but reports "the node already had an admin user" — Portainer only allows first-run initialization while no admin exists, and closes that window shortly after the container starts. This happens when redeploying onto a reused VM. The node is running and usable; add an API token by hand in Settings → Containers.

Deploy times out waiting for Portainer to start — the VM is up but nothing answered. Usually the firewall doesn't admit the dashboard's egress IP; check the merged allow-list in Settings → Containers, then redeploy. Raise portainer_ready_timeout_s if the image pull is simply slow.

"Connection refused" or timeout — verify the Portainer URL is reachable from inside the container: docker compose exec app curl -Isk <portainer-url>/api/system/status.

"Unauthorized" error — the PAT may have expired or been deleted. Regenerate a token in Portainer and update it in Settings → Integrations → Portainer CE (or update the vault secret if you stored a reference).

An Edge environment never comes online — the agent polls outbound to the node's port 8000, so check that egress is allowed from the Docker host. If the node was recreated since the key was minted, the key is dead: Edge keys encode the node URL and the node's external IP is ephemeral. Generate a new join command and re-join the host.

An imported user can't log in — imported users get a freshly generated password, shown once in the portainer_import job result. If that job output is gone, reset the password in Portainer.

"That file is not a JSON migration bundle" — a Portainer .tar.gz backup was uploaded. It cannot be imported directly (it only restores into a pristine Portainer); open it with a throwaway Portainer and export a bundle first.

A deploy fails with "the Portainer data disk … already exists" — durable state is on and the volume holds an admin whose password isn't in Settings. Portainer ignores --admin-password on an initialized database, so the node would come up with a password nobody knows. Set portainer_admin_password to the one the volume was created with, or delete the volume to start clean. Same on all three clouds.

SSL certificate errors — for self-signed certificates (including a managed node's) turn off Verify SSL certificate in the Portainer panel. For production, add your CA cert to the container's trusted store via the Dockerfile.