Why Portainer?
The Docker CLI is great. Until you're managing a dozen services across two servers and you need a teammate who doesn't know Docker to restart a container without SSHing in and running commands they half-understand.
Portainer is a self-hosted web UI for Docker (and Swarm, and Kubernetes). It doesn't replace the CLI — it sits on top of it. Every action you take in Portainer is just a Docker API call under the hood. But it adds a visual layer for container management, stack deployments, environment variable handling, log tailing, user access control, and image updates that makes running containerized infrastructure significantly less painful for everyone on your team.
This guide walks you from a fresh Linux server to a fully operational Portainer CE instance: installed via Docker Compose, served over HTTPS through Traefik, with stacks, users, and teams configured for real use.
Prerequisites
Before you start:
- A Linux server running Ubuntu 22.04 or 24.04 (1 vCPU / 1 GB RAM minimum, 2 GB recommended)
- Docker Engine v24+ installed — follow the official Docker install guide, not the Ubuntu snap package
- Docker Compose v2 (
docker compose versionshould return v2.x) - A domain or subdomain with a DNS A record pointing to your server's public IP
- Ports 80, 443, 8000, and 9443 open in your firewall
- sudo / root access on the server
# Verify prerequisites
docker --version # Docker Engine 24+
docker compose version # Docker Compose v2.x
# Check ports are free before deploying
sudo ss -tlnp | grep -E ':9443|:8000|:9000'
# Confirm Docker socket exists and is accessible
ls -la /var/run/docker.sock
New to Portainer? Our complete beginner's guide to Portainer covers the core concepts — environments, endpoints, stacks, and what each section of the UI actually does — before you dive into production setup.
Step 1: Install Portainer CE with Docker Compose
Portainer's own documentation recommends either docker run or Docker Compose. We'll use Compose — it's easier to version, update, and reproduce.
mkdir -p /opt/portainer && cd /opt/portainer
Create the docker-compose.yml:
# /opt/portainer/docker-compose.yml
services:
portainer:
image: portainer/portainer-ce:lts
container_name: portainer
restart: always
security_opt:
- no-new-privileges:true
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
ports:
- "9443:9443" # HTTPS UI
- "8000:8000" # Edge Agent tunnel (optional — remove if not using Edge)
labels:
# Traefik routing labels (see Step 3 for full Traefik setup)
- "traefik.enable=true"
- "traefik.http.routers.portainer.rule=Host(`portainer.yourdomain.com`)"
- "traefik.http.routers.portainer.entrypoints=websecure"
- "traefik.http.routers.portainer.tls=true"
- "traefik.http.routers.portainer.tls.certresolver=letsencrypt"
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
- "traefik.http.services.portainer.loadbalancer.server.scheme=http"
networks:
- traefik-public
volumes:
portainer_data:
name: portainer_data
networks:
traefik-public:
external: true
A few things to note:
- Docker socket mount —
/var/run/docker.sockgives Portainer control of your Docker daemon. This is intentional and required. Keep theno-new-privilegessecurity option in place. - Port 9000 vs 9443 — Portainer listens on 9000 (HTTP) and 9443 (HTTPS with a self-signed cert) internally. The Traefik label routes to port 9000 on the container, and Traefik handles the external HTTPS. This avoids cert chain confusion.
- Port 8000 — only needed if you plan to use Portainer Edge Agents for remote Docker hosts. Remove it if you don't need Edge.
# Create shared Traefik network if it doesn't exist
docker network create traefik-public 2>/dev/null || true
# Deploy Portainer
docker compose up -d
# Confirm it's running
docker ps | grep portainer
# Check logs
docker logs portainer
Navigate to https://portainer.yourdomain.com (once Traefik is configured in Step 3) or https://your-server-ip:9443 immediately to complete first-run setup. You have five minutes to set the admin password before Portainer times out the initialization window and requires a container restart.
Step 2: First-Run Setup and Connecting Your Docker Environment
Set the Admin Password
On first visit, Portainer shows the admin account creation form. Set a strong password and click Create user. This is the only superadmin account — treat it like a root password.
Connect to the Local Docker Environment
After login, Portainer asks which environment to manage. Choose Docker Standalone → Connect. Because Portainer already has the socket mounted, it detects the local Docker daemon automatically. Click Connect and you're in.
You'll land on the Home dashboard showing your environment, running containers, images, volumes, and networks at a glance.
Add a Remote Docker Host via Portainer Agent
To manage a second server, install the Portainer Agent there and register it as a new environment:
# Run this on the REMOTE server (the one you want to manage)
docker run -d \
-p 9001:9001 \
--name portainer_agent \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /var/lib/docker/volumes:/var/lib/docker/volumes \
portainer/agent:lts
Back in the Portainer UI: Settings → Environments → Add environment → Agent. Enter remote-server-ip:9001 as the environment URL. Portainer connects over TCP and you now manage both hosts from the same dashboard.
Step 3: Put Portainer Behind Traefik for Proper HTTPS
Portainer's self-signed certificate works but will throw browser warnings. If you're running Traefik already, routing Portainer through it takes two minutes.
The Traefik labels in the Compose file from Step 1 handle the routing automatically. All you need is a running Traefik instance on the same traefik-public network with a letsencrypt certificate resolver configured.
The key detail: Traefik must forward to Portainer's HTTP port 9000, not 9443. Traefik terminates TLS externally, so hitting 9443 (Portainer's own HTTPS with a self-signed cert) causes a TLS handshake mismatch. The label traefik.http.services.portainer.loadbalancer.server.scheme=http makes this explicit.
# Verify Portainer is reachable on port 9000 internally
curl -s http://localhost:9000/api/system/status | python3 -m json.tool
# Should return something like:
# {
# "Version": "2.x.x",
# "Edition": "CE"
# }
Once Traefik routes traffic to it, https://portainer.yourdomain.com loads with a valid Let's Encrypt certificate and no browser warnings.
Step 4: Deploy and Manage Stacks
Stacks are Portainer's name for Docker Compose deployments. You paste or upload a Compose file, set environment variables, and Portainer deploys it. Updates are one-click. Logs, container shell access, and resource stats are all one click away from the stack view.
Deploy a Stack from the UI
- In the sidebar, go to Stacks → Add stack
- Give it a name (lowercase, no spaces — this becomes the Docker Compose project name)
- Choose Web editor and paste your
docker-compose.ymlcontent - In the Environment variables section at the bottom, add any secrets (database passwords, API keys) — these are stored encrypted and never go into the Compose file
- Click Deploy the stack
Here's a minimal stack to test with:
# Paste this into the Portainer Web editor to test stack deployment
services:
whoami:
image: traefik/whoami
container_name: whoami-test
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`whoami.yourdomain.com`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
- "traefik.http.services.whoami.loadbalancer.server.port=80"
networks:
- traefik-public
networks:
traefik-public:
external: true
Deploy a Stack from a Git Repository
For anything you want in version control, connect Portainer to your Git repo instead of pasting Compose content:
- In Add stack, choose Git repository
- Enter the repo URL and the path to your
docker-compose.ymlwithin the repo - Enable Automatic updates — either polling (Portainer checks for new commits on a schedule) or via a webhook URL that you add to your CI/CD pipeline
With a webhook configured, every push to your main branch triggers a stack redeploy. This is lightweight GitOps without needing Flux or ArgoCD.
# Trigger a Portainer stack redeploy from CI/CD or a cron job
# (Get the webhook URL from: Stack → Edit → Automatic updates → Webhook URL)
STACK_WEBHOOK="https://portainer.yourdomain.com/api/stacks/webhooks/your-webhook-uuid"
curl -X POST "$STACK_WEBHOOK"
# Portainer pulls the latest commit and redeploys with --pull=always
Step 5: Users, Teams, and Access Control
Out of the box, Portainer CE has one admin account. For a real team you want multiple users with appropriate permissions — developers who can view logs and redeploy but can't delete volumes, for example.
Create Users
Go to Settings → Users → Add user. Set a username, password, and role:
- Administrator — full access to everything across all environments
- Standard user — access limited to environments and resources you explicitly grant
Create Teams and Assign Members
Teams let you manage permissions at group level instead of per-user. Go to Settings → Teams → Add team, create a team (e.g. developers, ops), and add users to it.
Control Resource Access with Labels
Portainer CE uses Docker labels to restrict which users or teams can see and manage specific resources. Add these labels to containers or stacks:
# In your docker-compose.yml — restrict this stack to specific teams
services:
myapp:
image: myapp:latest
labels:
# Only the 'developers' team can see and manage this container
- "io.portainer.accesscontrol.teams=developers"
# Or restrict to specific users:
# - "io.portainer.accesscontrol.users=alice,bob"
Standard users only see resources with their team or username in the access labels. Resources with no labels are only visible to administrators.
Need more granular RBAC? Role-based access control at the environment level — different teams scoped to different Docker hosts — is covered in our guide on Portainer advanced stack management, RBAC, and multi-environment deployments.
Step 6: Tips, Gotchas, and Troubleshooting
Here's what trips people up most often with a fresh Portainer setup.
Missed the 5-Minute Init Window
Symptom: Navigating to Portainer shows a timeout error instead of the setup form.
Fix: restart the container. The initialization window resets on every restart.
# Restart Portainer to reset the init window
docker restart portainer
# Then immediately navigate to the UI and complete setup
# https://portainer.yourdomain.com OR https://your-ip:9443
Portainer UI Loads but Shows No Containers
Symptom: You can log in but the Containers list is empty even though containers are running.
Almost always a Docker socket issue:
- Verify the socket mount:
docker inspect portainer | grep -A5 Mounts— confirm/var/run/docker.sockis mounted - Check socket permissions:
ls -la /var/run/docker.sock— the socket must be readable by the Portainer process. If needed, add the socket group:sudo chmod 666 /var/run/docker.sock(or better: add Portainer's user to thedockergroup) - Installed Docker via snap? Snap Docker uses a different socket path. Uninstall it and install via the official
aptmethod instead.
Connection Refused on Port 9443
Symptom: curl -k https://localhost:9443 returns connection refused.
- Check the container is actually running:
docker ps | grep portainer - Check for port conflicts:
sudo ss -tlnp | grep 9443— another process might own that port - Check Portainer logs for TLS startup errors:
docker logs portainer | grep -i "tls\|cert\|ssl\|error"
Stacks Show as "Limited" After Update
Symptom: A stack you deployed outside Portainer (directly via docker compose up on the CLI) shows as "Limited" in the Stacks view with no edit capability.
Portainer can only fully manage stacks it deployed itself. For stacks deployed externally, Portainer can see them but won't let you edit or redeploy through the UI. Solution: remove the stack from the CLI and re-deploy it through Portainer.
Environment Variables Not Resolving in Stacks
Portainer has two ways to pass env vars to stacks:
- .env file tab — works like a standard Docker Compose
.envfile; variables substitute${VAR}placeholders in the Compose YAML - Environment variables section — values are set in the Portainer UI and passed directly to the running containers as environment variables, but they do not substitute into the Compose YAML itself
Use the .env file for image tags, port numbers, and anything referenced with ${} in the Compose file. Use the Environment variables section for runtime secrets like API keys and passwords.
General Log Inspection
# Portainer application logs
docker logs portainer -f
# Filter for warnings and errors only
docker logs portainer 2>&1 | grep -iE "error|warn|fatal|failed"
# Check Portainer data volume for database issues
docker volume inspect portainer_data
Upgrading Portainer
When a new LTS version drops, upgrading is straightforward — Portainer persists all its state in the portainer_data volume, so the upgrade is just a pull-and-restart:
cd /opt/portainer
# Pull the latest image and redeploy
docker compose pull
docker compose up -d
# Confirm the new version is running
curl -sk https://localhost:9443/api/system/status | python3 -m json.tool | grep Version
What to Explore Next
Once you're comfortable with the basics, Portainer has a lot more to offer:
- App Templates — pre-built one-click deployments for common services (databases, monitoring tools, self-hosted apps). You can also add custom template registries.
- Registries — connect private Docker registries (Docker Hub, GHCR, ECR, Harbor) so you can pull private images directly from the stack editor
- Webhooks on stacks and services — trigger redeployments from GitHub Actions, GitLab CI, or any CI system with a single
curl - Container health and resource stats — per-container CPU, memory, and network graphs without needing a full Prometheus stack
- Edge Agents — manage Docker hosts that can't be directly reached (behind NAT, on-prem, air-gapped) by having the agent initiate the connection out to Portainer
Go further: Our guide on Portainer API automation, Edge deployments, security hardening, and container logging at scale covers everything you need for a hardened, automated production setup.
Need Help Running This at Scale?
Portainer CE covers a lot of ground for solo developers and small teams. But when you're coordinating container infrastructure across multiple environments, managing strict access control for large teams, or need audit logging and enterprise SSO, the operational complexity ramps up fast.
If you're building or scaling container infrastructure and want it designed right from the start, get in touch with Sysbrix. We help engineering teams set up production-grade Docker environments — from initial architecture to ongoing operations — so you spend less time fighting infrastructure and more time shipping.