Skip to Content

Stop Using the CLI for Everything: The Definitive Portainer Docker Setup Guide

Install Portainer CE on your own server, connect it to Docker, deploy stacks through the UI, and lock down access with users and teams — all in under an hour.

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 version should 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.sock gives Portainer control of your Docker daemon. This is intentional and required. Keep the no-new-privileges security 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 StandaloneConnect. 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

  1. In the sidebar, go to Stacks → Add stack
  2. Give it a name (lowercase, no spaces — this becomes the Docker Compose project name)
  3. Choose Web editor and paste your docker-compose.yml content
  4. 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
  5. 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:

  1. In Add stack, choose Git repository
  2. Enter the repo URL and the path to your docker-compose.yml within the repo
  3. 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.sock is 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 the docker group)
  • Installed Docker via snap? Snap Docker uses a different socket path. Uninstall it and install via the official apt method 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 .env file; 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.

Self-Host Flowise in Minutes: The Complete Flowise Self-Host Guide for LLM App Builders
Deploy Flowise on your own server with Docker Compose, wire it up behind HTTPS, connect your first RAG chatflow, and lock it down for production — all without writing a single line of glue code.