When migrating around 20 internal microservices from Docker Swarm to Fedora Server running Podman, my biggest hurdle was configuring Nginx. Every time a new container was spun up, the same tedious routine repeated: open nginx.conf, write an upstream block, reload the service, and run Certbot to fetch SSL. A single misplaced semicolon could bring down the entire proxy cluster. Traefik completely eliminates this friction: it listens directly to events from Podman’s socket, routes traffic via labels, and automatically obtains Let’s Encrypt certificates.
Fedora is an ideal environment for Podman thanks to Red Hat’s early kernel updates and deep cgroups v2 integration. Combining Traefik v3 with Podman on Fedora 39/40 yields a lightweight, rock-solid proxy cluster that requires minimal maintenance.
Quick Start (5-Minute Deployment)
Have a domain ready with its A/AAAA records pointed to your Fedora server’s IP address, and open ports 80 and 443 in the firewall:
sudo firewall-cmd --permanent --add-port={80,443}/tcp
sudo firewall-cmd --reload
1. Enable the Podman System Socket
Traefik communicates with Podman through a Docker API-compatible socket. Enable the systemd socket:
sudo systemctl enable --now podman.socket
# Verify that the socket is active
curl -s --unix-socket /run/podman/podman.sock http://d/v1.41/version | jq .Version
2. Create Directories and Configuration Files for Traefik
Create a dedicated directory at /opt/traefik to store configuration files and certificates:
sudo mkdir -p /opt/traefik
cd /opt/traefik
sudo touch acme.json
sudo chmod 600 acme.json
Create the static configuration file traefik.yml:
api:
dashboard: true
insecure: false
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
certificatesResolvers:
letsencrypt:
acme:
email: "[email protected]"
storage: "/acme.json"
httpChallenge:
entryPoint: web
Note: Replace [email protected] with your actual email address to receive expiration notifications.
3. Launch Traefik with Podman Compose
Create the file /opt/traefik/compose.yml:
version: "3.8"
services:
traefik:
image: docker.io/library/traefik:v3.0
container_name: traefik
restart: always
ports:
- "80:80"
- "443:443"
security_opt:
- label=disable
volumes:
- /run/podman/podman.sock:/var/run/docker.sock:ro
- /opt/traefik/traefik.yml:/traefik.yml:ro,z
- /opt/traefik/acme.json:/acme.json:z
networks:
- proxy-net
networks:
proxy-net:
name: proxy-net
Run the Traefik container:
podman compose up -d
4. Deploy a Test Application with SSL
Deploy a simple Nginx web server to verify automatic certificate issuance:
podman run -d --name demo-web \
--network proxy-net \
--label "traefik.enable=true" \
--label "traefik.http.routers.demo.rule=Host(\`app.example.com\`)" \
--label "traefik.http.routers.demo.entrypoints=websecure" \
--label "traefik.http.routers.demo.entrypoints=websecure" \
--label "traefik.http.routers.demo.tls.certresolver=letsencrypt" \
--label "traefik.http.services.demo.loadbalancer.server.port=80" \
docker.io/library/nginx:alpine
Open https://app.example.com in your browser. You should see a valid HTTPS connection in about 3 to 5 seconds, without configuring a single line of Nginx on the host.
Core Technical Concepts to Understand
The Provider Mechanism and Docker API Compatibility
Traefik does not poll containers periodically. Instead, it listens directly to the event stream from podman.socket.
Whenever you run podman run or stop a container, Traefik catches the signal instantly. Its in-memory routing table updates in under 10 milliseconds. The entire process occurs behind the scenes without reloading processes or dropping existing TCP connections.
Because Podman provides a standard Docker API over a Unix socket, Traefik can directly leverage its built-in docker provider without third-party plugins.
Handling SELinux Obstacles on Fedora Server
Fedora runs SELinux in Enforcing mode by default. If files or sockets are mounted without the appropriate security context, the container will instantly throw a Permission Denied error.
Key points to remember:
- The
:zvolume flag assigns a shared SELinux label (shared contextcontainer_file_t) to configuration files liketraefik.ymlandacme.json. - Never use the
:zflag on the system socket file/run/podman/podman.sock. Relabeling system sockets can break systemd permissions. Instead, disable SELinux label confinement specifically for the Traefik container usingsecurity_opt: ["label=disable"].
The Let’s Encrypt SSL Issuance Lifecycle
When the first request hits app.example.com on port 443:
- Traefik checks
acme.json. - If no certificate exists, Traefik triggers its ACME client to request a new one from Let’s Encrypt.
- Let’s Encrypt issues an
HTTP-01challenge back to port 80. Traefik intercepts this specific request and returns the valid verification token. - Upon successful validation, Let’s Encrypt issues the certificate pair along with the private key.
- Traefik writes the details to
acme.jsonand loads them directly into memory. When the certificate has fewer than 30 days remaining, Traefik automatically renews it.
Advanced Configuration
1. Secure the Traefik Dashboard with Basic Auth
The dashboard has public access disabled by default (insecure: false). To expose it via a dedicated subdomain (such as traefik.example.com), attach a router to the internal api@internal service alongside an authentication middleware:
# Generate password hash (install: sudo dnf install -y httpd-tools)
htpasswd -nb admin StrongPassword_2026@Secure
Add the following labels to the Traefik service in compose.yml:
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
- "traefik.http.routers.dashboard.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$xyz...hash_string_above"
Note: In compose files, every $ character in the password hash must be escaped as $$ to prevent compose from interpreting it as an environment variable.
2. Issue Wildcard Certificates via DNS-01 Challenge (Cloudflare)
The HTTP-01 method has limitations: it cannot issue wildcard certificates like *.example.com and requires port 80 to be publicly accessible from the internet. For internal services, the DNS challenge is the ideal alternative:
# Append to traefik.yml
certificatesResolvers:
dns-cloudflare:
acme:
email: "[email protected]"
storage: "/acme.json"
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
Add the Cloudflare API token as an environment variable in Traefik’s compose file:
environment:
- CF_DNS_API_TOKEN=vj98aX_example_cloudflare_token
Production Best Practices and Field Experience
A few crucial tips to help you avoid production pitfalls:
- File permissions for
acme.json: Traefik will refuse to start if this file has permissions other than600. If you accidentallychmodit to644or copy it from another machine, Traefik will throw the errorpermissions 0644 for /acme.json are too open. - Watch out for Let’s Encrypt rate limits: When testing your configuration, temporarily point to the staging server by adding
caServer: "https://acme-staging-v02.api.letsencrypt.org/directory". Let’s Encrypt enforces a strict limit of 5 failed validation attempts per account/hostname per hour. Exceeding this will temporarily block your IP from issuing certificates. - Enable JSON Access Logs for easier troubleshooting: Add
accessLog: { format: json }to your configuration file. JSON formatting makes it straightforward to filter and ship logs via Fluent Bit or Promtail into Grafana Loki, giving full visibility into upstream response times and container HTTP statuses. - 404 errors caused by network mismatches: In 90% of cases, seeing a 404 error after configuring labels means the application container hasn’t joined the
proxy-netnetwork alongside Traefik. Podman’s default bridge network does not provide automatic container-to-container DNS resolution.

