Post

Deploy Portainer for Docker management behind Traefik

Manage Docker containers from a web UI behind Traefik, then add built-in Portainer OAuth with Authentik.

Deploy Portainer for Docker management behind Traefik

Manage Docker containers from a web UI behind Traefik, then add built-in Portainer OAuth with Authentik while understanding the risk of Docker socket access.

This is a standalone service guide. It explains the service in isolation so you can deploy it without copying unrelated parts of another stack.

All domains, emails, usernames, IP addresses, passwords, and tokens in this post are placeholders. Replace them for your own server, but do not publish real secrets or private infrastructure details.


What this service does

Route or access pattern:

1
portainer.example.com

Main components:

1
Portainer EE/CE container, persistent data directory, Docker socket access.

Network and port model:

1
Traefik routes HTTPS to Portainer port 9000; Docker socket is mounted read-only but still powerful.

In a Docker homelab, the safest pattern is to keep the application private on Docker networks and let the reverse proxy handle HTTPS traffic.


Folder layout

Use a dedicated folder:

1
2
3
4
5
/home/ubuntu/portainer/
├── docker-compose.yml
├── .env
├── data/              # or service-specific persistent data
└── backups/           # optional local backup destination

Keep .env private. Public documentation should show only placeholders.


Environment file

Example .env values:

PORTAINER_DOMAIN=portainer.example.com

Rules:

  • generate long random passwords and tokens;
  • keep .env out of Git;
  • do not paste production values into public tutorials;
  • rotate exposed credentials immediately if they ever leak.

Compose pattern

A minimal Traefik-aware Compose pattern looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
services:
  portainer:
    image: <service-image>:<version>
    container_name: portainer
    restart: unless-stopped
    env_file:
      - .env
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.portainer.entrypoints=https"
      - "traefik.http.routers.portainer.rule=Host(`portainer.example.com`)"
      - "traefik.http.routers.portainer.tls=true"
      - "traefik.http.routers.portainer.tls.certresolver=cloudflare"
      - "traefik.http.services.portainer.loadbalancer.server.port=<internal-port>"

networks:
  proxy:
    external: true

Adapt image names, volumes, internal ports, and service-specific environment variables for the actual application.


Deployment steps

Create the directory:

1
2
mkdir -p /home/ubuntu/portainer
cd /home/ubuntu/portainer

Create the .env file with placeholder values replaced by your own secrets:

1
2
nano .env
chmod 600 .env

Create or edit docker-compose.yml, then start the service:

1
docker compose up -d

Check status:

1
2
docker compose ps
docker compose logs --tail=100

Verification checklist

Verify the container is running:

1
docker ps --filter name=portainer

Verify Traefik can see the route:

1
docker logs traefik --tail=100

Verify the public route, if the service has one:

1
curl -I https://service.example.com

Expected results vary by service:

  • 200 means the app is reachable;
  • 302 can be correct when authentication redirects to an identity provider;
  • 401 can be correct for APIs that require authentication;
  • 404 usually means the Traefik router rule did not match.

Built-in OAuth with Authentik

Portainer supports its own OAuth/OIDC login. This is different from protecting Portainer with a Traefik forward-auth middleware: Portainer itself receives the OAuth callback, exchanges the authorization code, reads the user profile, and maps the OAuth identity to a Portainer user.

Use this pattern when you want Portainer’s Login with OAuth button instead of only placing an SSO wall in front of the UI.

1. Create the Authentik OAuth2/OpenID provider

In Authentik, create an application and provider pair:

1
2
3
4
5
6
7
Application name: Portainer
Provider type: OAuth2/OpenID Connect
Provider name: Portainer Built-in OAuth
Provider slug: portainer-oauth
Redirect URI mode: Strict
Strict redirect URI: https://portainer.example.com/
Signing key: any valid Authentik signing key

Keep the generated client secret private. Do not paste it into documentation, Git, screenshots, or chat logs.

Useful Authentik endpoints for Portainer:

1
2
3
4
5
Authorization URL: https://auth.example.com/application/o/authorize/
Access Token URL:  https://auth.example.com/application/o/token/
Resource URL:      https://auth.example.com/application/o/userinfo/
Logout URL:        https://auth.example.com/application/o/portainer-oauth/end-session/
OIDC metadata:     https://auth.example.com/application/o/portainer-oauth/.well-known/openid-configuration

2. Configure Portainer OAuth

In Portainer, open:

1
Settings → Authentication → OAuth → Custom

Use these values:

1
2
3
4
5
6
7
8
9
10
Client ID:         <authentik-provider-client-id>
Client secret:     <authentik-provider-client-secret>
Authorization URL: https://auth.example.com/application/o/authorize/
Access token URL:  https://auth.example.com/application/o/token/
Resource URL:      https://auth.example.com/application/o/userinfo/
Redirect URL:      https://portainer.example.com/
Logout URL:        https://auth.example.com/application/o/portainer-oauth/end-session/
User identifier:   preferred_username
Scopes:            email openid profile
Auth style:        Auto Detect

Portainer’s scopes field may display items visually like separate tokens. Enter them as a space-separated string, not comma-separated:

1
email openid profile

3. Match OAuth users to Portainer users

Portainer maps the OAuth user by the configured User identifier claim. If User identifier is preferred_username, then Authentik must send a preferred_username value that matches the Portainer username.

Examples:

1
2
preferred_username=xripaire → Portainer looks for user xripaire
preferred_username=admin    → Portainer looks for user admin

If you want OAuth login to land on an existing local Portainer admin account named admin, create a Portainer-specific Authentik scope mapping for the profile scope that returns:

1
2
3
4
5
6
return {
    "name": "admin",
    "given_name": request.user.name or request.user.username,
    "preferred_username": "admin",
    "nickname": "admin",
}

Attach this mapping only to the Portainer OAuth provider and remove the default profile mapping from that provider. Do not change the global default profile mapping unless you want every OAuth application to receive the same username.

A safer alternative is to create a normal Portainer user whose username matches the Authentik username, then grant that user the required Portainer role.

4. Decide whether to allow automatic users

Portainer has two related options:

1
2
Automatic user provisioning
Hide internal authentication prompt
  • Enable Automatic user provisioning if you want Portainer to create new standard users on first OAuth login.
  • Disable it if OAuth identities must match users that already exist in Portainer.
  • Enable Hide internal authentication prompt only after confirming OAuth can log you in as an admin. Otherwise you can lock yourself out of the local username/password login.

5. Container DNS check

Portainer performs the token exchange from inside the container. If the browser reaches Authentik but Portainer logs token exchange failures, check DNS from the Portainer container/network.

A Compose-level resolver override can help on hosts where Docker’s embedded DNS misbehaves:

1
2
3
4
5
services:
  portainer:
    dns:
      - 1.1.1.1
      - 8.8.8.8

Avoid hard-coding auth.example.com to a Docker gateway IP unless HTTPS is actually reachable there. A wrong extra_hosts entry can make DNS look fixed while breaking the token request.

6. Troubleshooting built-in OAuth

Common symptoms:

1
2
3
Unable to login via OAuth
Invalid OAuth state, try again.
Auto OAuth team membership failed: user not created beforehand in Portainer and automatic user provisioning not enabled

What to check:

  • Authentik provider redirect URI exactly matches Portainer’s Redirect URL, including the trailing slash.
  • Portainer can reach the Authentik token and userinfo endpoints from inside Docker.
  • The client secret in Portainer matches the Authentik provider secret.
  • User identifier points to a claim Authentik actually returns, usually preferred_username or email.
  • The returned identifier matches an existing Portainer user, unless automatic user provisioning is enabled.
  • If hiding internal auth, keep a tested admin OAuth path before saving.

Useful verification commands:

1
2
3
curl -sS https://portainer.example.com/api/status
curl -sS https://portainer.example.com/api/settings/public
curl -sS https://auth.example.com/application/o/portainer-oauth/.well-known/openid-configuration

For deeper debugging, temporarily start Portainer with debug logs:

1
2
3
4
services:
  portainer:
    command:
      - --log-level=DEBUG

Then inspect:

1
docker logs --since=10m portainer

Remove debug logging again after troubleshooting if you prefer quieter logs.


Backup checklist

Back up at least:

1
2
3
/home/ubuntu/portainer/docker-compose.yml
/home/ubuntu/portainer/.env        # private backup only
/home/ubuntu/portainer/data/       # or named Docker volumes

For database-backed services, prefer application-aware dumps in addition to copying files:

1
docker compose exec <database> <dump-command> > backup.sql

Never publish backup archives. They often contain tokens, password hashes, uploads, user data, or private keys.


Common problems

The domain returns 404

Check:

  • the container has traefik.enable=true;
  • the service is attached to the proxy network;
  • the router Host(...) rule matches the exact domain;
  • traefik.docker.network=proxy is set when the container has multiple networks.

The domain returns bad gateway

Traefik found the route but cannot reach the internal app port.

Check the application logs and confirm the internal port used by:

1
traefik.http.services.<service>.loadbalancer.server.port

The service starts but data disappears after restart

The persistent directory or Docker volume is missing. Add a named volume or a bind mount for the service data before using it seriously.

Authentication loops or redirects fail

Check the public URL, trusted proxy headers, cookie domain, and whether the app knows it is behind HTTPS.


Security notes

  • Keep admin dashboards behind authentication or private networks.
  • Do not expose databases, caches, or admin APIs directly to the internet.
  • Use least-privilege API tokens.
  • Prefer internal Docker networks for dependencies.
  • Keep public docs generic: use example.com, <token>, <password>, and documentation IP ranges.

Service documentation map

This post is part of the standalone homelab service documentation series. Use these guides together when building the full stack:

This post is licensed under CC BY 4.0 by the author.