1765 words
9 minutes
Setting Up ArgoCD with Traefik and HTTPS (Let's Encrypt) on a Public Domain

In the previous article, we set up ArgoCD with NGINX Ingress Controller and accessed it via a custom endpoint (sub-path). That setup works great for experimentation and lab environments — ArgoCD is accessed through the node’s IP with a path like http://<IP>/argocd-lab, without a domain and without HTTPS.

But what if you have a domain?

Well, this article is the answer. We’ll configure ArgoCD to be accessible through a public domain (e.g., argocd.example.com) with HTTPS using certificates automatically generated by Let’s Encrypt — all handled by Traefik as the Ingress Controller, without needing to install cert-manager separately.

Intended for kubeadm bare-metal/on-prem clusters with a public domain whose DNS is already pointed to the node’s IP.


⚠️ Important Note: Not Production-Ready Yet#

WARNING

The configuration in this article is not suitable for production because several aspects are still insecure or suboptimal:

AspectIssueImpact
Traefik runs as root (runAsUser: 0)Container runs with full privilegesIf the container is exploited, the attacker gains root access on the node
hostNetwork: trueTraefik pod shares the host’s network namespacePort conflicts, no network isolation, an attacker who enters the pod gets direct host network access
allowPrivilegeEscalation: trueContainer can escalate privilegesSignificantly expands the attack surface
readOnlyRootFilesystem: falseContainer can write to any filesystem locationAttacker can modify binaries or configs inside the container
PersistentVolume hostPathCertificate data is stored directly on the node’s filesystemNo redundancy, data is lost if the node dies, not portable across nodes
Single replica TraefikOnly one ingress controller instanceSingle point of failure — if the pod dies, all traffic to the cluster stops

💡 Recommendations for Production#

Here are some recommendations if you plan to bring this setup to production:

1. Use Longhorn for Persistent Volumes

Instead of using hostPath which stores data directly on the node, use Longhorn — a distributed block storage solution for Kubernetes. Longhorn provides:

  • Data replication across nodes (data survives node failures)
  • Automatic snapshots & backups
  • Volume portability — pods can move between nodes without losing data
  • Built-in monitoring dashboard

2. Use MetalLB for Network Management

Instead of using hostNetwork: true (which bypasses network isolation), use MetalLB as a bare-metal LoadBalancer:

  • Provides proper external IPs for type: LoadBalancer Services
  • Eliminates the need for hostNetwork — Traefik can run with normal network isolation
  • Supports L2 (ARP) and BGP modes
  • Enables multiple Traefik replicas with the same IP (high availability)

3. Harden the Security Context

For production, improve Traefik’s security context:

  • Use runAsNonRoot: true with NET_BIND_SERVICE capability
  • Set readOnlyRootFilesystem: true
  • Set allowPrivilegeEscalation: false
  • Drop all capabilities except the ones actually needed

A production-grade setup with Longhorn + MetalLB + hardened Traefik will be covered in a separate article. For now, let’s focus on getting a functional setup first.


Prerequisites#

  • A running Kubernetes cluster (kubeadm)
  • A public domain pointed to the node’s IP (A record: argocd.example.com → <NODE-IP>)
  • Ports 80 and 443 on the node are open from the internet
  • kubectl and helm are configured

If Helm is not yet installed:

Terminal window
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
helm version

Expected output:

Terminal window
version.BuildInfo{Version:"v4.2.3", GitCommit:"43e8b7feece8beb0fcba47059ec9b522fd929a64", GitTreeState:"clean", GoVersion:"go1.26.5", KubeClientVersion:"v1.36"}

1. Deploy ArgoCD#

Download the official ArgoCD manifest.

Terminal window
wget https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

Edit the manifest to disable ArgoCD’s built-in TLS, since HTTPS will be handled entirely by Traefik in front of it.

Terminal window
nano install.yaml

Find the Deployment named argocd-server and change the args section to:

spec:
containers:
- args:
- /usr/local/bin/argocd-server
- --staticassets
- /shared/app
- --insecure

Why is --insecure safe here? This flag only makes ArgoCD serve HTTP from the pod side. Traefik sits in front and handles TLS termination — so external communication remains HTTPS-encrypted. Internal communication from Traefik to the ArgoCD pod goes over HTTP within the cluster, which is already isolated.

Create the namespace and deploy.

Terminal window
kubectl create namespace argocd
kubectl apply --server-side -f install.yaml -n argocd

Why use --server-side? Recent ArgoCD versions have very large CRDs, especially applicationsets.argoproj.io. By default, kubectl apply stores the entire manifest content as a kubectl.kubernetes.io/last-applied-configuration annotation on the CRD object itself. Kubernetes limits annotation size to a maximum of 256KB (262144 bytes) — and ArgoCD’s CRDs exceed this limit, causing regular kubectl apply to fail with:

The CustomResourceDefinition "applicationsets.argoproj.io" is invalid:
metadata.annotations: Too long: may not be more than 262144 bytes

With --server-side, the apply process is handled on the API server side (not the client), so the last-applied-configuration annotation is not stored and the 256KB limit doesn’t apply.

If a CRD was previously applied the regular way and a conflict occurs, add the --force-conflicts flag:

Terminal window
kubectl apply --server-side --force-conflicts -f install.yaml -n argocd

Wait until all pods are in Running status.

Terminal window
kubectl get pods -n argocd

2. Install Traefik via Helm#

Add the official Traefik Helm repository.

Terminal window
helm repo add traefik https://traefik.github.io/charts
helm repo update

Expected output:

Terminal window
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "traefik" chart repository
Update Complete. ⎈Happy Helming!⎈

First, create a directory on the host and a PersistentVolume to persist Let’s Encrypt certificates (acme.json).

Create the directory on the host node:

Terminal window
mkdir -p /data/traefik
chmod 700 /data/traefik

Create a pv.yaml file:

Terminal window
nano pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: traefik-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
hostPath:
path: /data/traefik
persistentVolumeReclaimPolicy: Retain
Terminal window
kubectl apply -f pv.yaml

Create a traefik-values.yaml file to configure Traefik for bare-metal and Let’s Encrypt.

Terminal window
nano traefik-values.yaml
# Use Deployment (not DaemonSet) because Traefik v3 does not support
# ACME/Let's Encrypt when running as a DaemonSet — each node would
# try to request certificates independently and conflict on acme.json
deployment:
kind: Deployment
replicas: 1
hostNetwork: true
service:
type: ClusterIP
ports:
web:
port: 80
hostPort: 80
expose:
default: true
websecure:
port: 443
hostPort: 443
expose:
default: true
additionalArguments:
- "--certificatesresolvers.letsencrypt.acme.email=email@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/data/acme.json"
- "--certificatesresolvers.letsencrypt.acme.tlsChallenge=true"
persistence:
enabled: true
storageClass: ""
size: 128Mi
ingressClass:
enabled: true
isDefaultClass: false
name: traefik
ingressRoute:
dashboard:
enabled: false
# ==============================================================================
# ROOT CONFIGURATION
# Forces Traefik to run as User 0 (Root) so it can freely use
# host ports 80/443 and manage the acme.json file
# ==============================================================================
podSecurityContext:
runAsUser: 0
runAsGroup: 0
runAsNonRoot: false
fsGroup: 0
securityContext:
readOnlyRootFilesystem: false
runAsNonRoot: false
runAsUser: 0
runAsGroup: 0
allowPrivilegeEscalation: true
capabilities:
drop: []

Replace email@example.com with your active email address for Let’s Encrypt notifications.

Breaking changes in Traefik v3 (Helm chart >= 30.x):

  • certResolvers at the values level is no longer recognized — ACME configuration is now passed directly via additionalArguments to the Traefik binary
  • tls inside ports.websecure is no longer allowed in the v3 schema
  • tlsStore.default.defaultCertResolver has been removed from the CRD — do not use it
  • Cert resolver is specified per-Ingress via annotation: traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt

Explanation of other configuration choices:

  • Deployment + hostNetwork: true — Traefik binds directly to host ports, the simplest solution for bare-metal without a LoadBalancer. Using Deployment (not DaemonSet) ensures only one replica manages ACME certificates
  • tlsChallenge=true — Traefik verifies the domain with Let’s Encrypt via the TLS-ALPN-01 challenge directly on port 443, no need for port 80 for the challenge (but port 80 is still open for HTTP → HTTPS redirect)
  • persistence.enabled: true — certificates are stored on a PersistentVolume so they don’t need to be re-requested from Let’s Encrypt every time the pod restarts (Let’s Encrypt has strict rate limits)

Install Traefik to the traefik namespace.

Terminal window
helm install traefik traefik/traefik \
--namespace traefik \
--create-namespace \
--values traefik-values.yaml

Expected output:

Terminal window
NAME: traefik
LAST DEPLOYED: Thu Jul 16 21:08:26 2026
NAMESPACE: traefik
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
TEST SUITE: None
NOTES:
traefik with docker.io/traefik:v3.7.6 has been deployed successfully on traefik namespace!

Verify that the Traefik pod is Running and the IngressClass is registered.

Terminal window
kubectl get pods -n traefik
kubectl get ingressclass

Expected output:

NAME CONTROLLER PARAMETERS AGE
traefik traefik.io/ingress-controller <none> 30s

3. Create an Ingress for ArgoCD#

Create an ingress.yaml file. Traefik will automatically request a certificate from Let’s Encrypt based on the traefik.ingress.kubernetes.io/router.tls.certresolver annotation.

Terminal window
nano ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argocd-server-ingress
namespace: argocd
annotations:
# Use the certresolver configured in Traefik values
traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
# Redirect all HTTP to HTTPS
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
traefik.ingress.kubernetes.io/router.middlewares: argocd-redirect-https@kubernetescrd
spec:
ingressClassName: traefik
tls:
- hosts:
- argocd.example.com
rules:
- host: argocd.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: argocd-server
port:
number: 80

Also create a Middleware for HTTP to HTTPS redirect.

Terminal window
nano middleware.yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-https
namespace: argocd
spec:
redirectScheme:
scheme: https
permanent: true

Apply both.

Terminal window
kubectl apply -f middleware.yaml
kubectl apply -f ingress.yaml

Verify the ingress is registered.

Terminal window
kubectl get ingress -n argocd

Expected output:

NAME CLASS HOSTS ADDRESS PORTS AGE
argocd-server-ingress traefik argocd.example.com 192.168.205.71 80, 443 1m

Monitor the certificate issuance process from the Traefik logs. This usually takes 1-2 minutes.

Terminal window
kubectl logs -n traefik -l app.kubernetes.io/name=traefik --follow

The certificate has been successfully issued if you see logs like:

2026-07-16T14:41:28Z INF Creating in-cluster Provider client providerName=kubernetescrd
2026-07-16T14:41:28Z INF Starting provider *acme.Provider
2026-07-16T14:41:28Z INF Testing certificate renew... acmeCA=https://acme-v02.api.letsencrypt.org/directory providerName=letsencrypt.acme
2026-07-16T14:45:02Z INF Register... providerName=letsencrypt.acme
2026-07-16T14:45:02Z INF Registering the account. email=email@example.com lib=lego
2026-07-16T14:45:02Z INF Obtaining bundled SAN certificate. domains=argocd.example.com lib=lego
2026-07-16T14:45:03Z INF Use solver. domain=argocd.example.com lib=lego type=tls-alpn-01
2026-07-16T14:45:03Z INF tlsalpn01: Trying to solve TLS-ALPN-01. domain=argocd.example.com lib=lego
2026-07-16T14:45:08Z INF The server validated our request. domain=argocd.example.com lib=lego
2026-07-16T14:45:08Z INF Validations succeeded; requesting certificates. domains=argocd.example.com lib=lego
2026-07-16T14:45:13Z INF Server responded with a certificate. domains=argocd.example.com lib=lego

4. Install the ArgoCD CLI#

Download and install the ArgoCD CLI binary.

Terminal window
curl -SL -o argocd-linux-amd64 https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd
rm argocd-linux-amd64
argocd version

5. Login and Password Setup#

5.1 Retrieve the Initial Admin Password#

Terminal window
argocd admin initial-password -n argocd

5.2 Login via CLI#

Since we’re already using HTTPS with a valid certificate from Let’s Encrypt, login is done directly without the --insecure or --plaintext flags.

Terminal window
argocd login argocd.example.com
Username: admin
Password: <password-from-the-previous-step>

If the certificate hasn’t finished being issued yet, you’ll get a TLS error. Wait a few minutes and try again.

5.3 Change the Admin Password#

Terminal window
argocd account update-password

6. Access ArgoCD via Browser#

https://argocd.example.com

The browser shows the HTTPS padlock because the certificate from Let’s Encrypt is valid. Accessing via http:// will automatically redirect to https://.

ArgoCD Dashboard — login page with HTTPS active


7. Tuning the Refresh Time Interval#

Terminal window
nano config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
labels:
app.kubernetes.io/name: argocd-cm
app.kubernetes.io/part-of: argocd
data:
timeout.reconciliation: 20s
Terminal window
kubectl apply -f config.yaml
kubectl describe configmap -n argocd argocd-cm
kubectl -n argocd rollout restart deploy argocd-repo-server

Troubleshooting#

Certificate is not being issued

Make sure port 443 is open from the internet. Check the Traefik logs for error details:

Terminal window
kubectl logs -n traefik -l app.kubernetes.io/name=traefik | grep -i "acme\|cert\|error"

argocd login fails with a TLS error

The certificate hasn’t finished being issued yet. Wait until the Obtained certificate log appears in Traefik, then try logging in again.

Traefik pod won’t start

Make sure no other process is already using port 80 or 443 on the node. Check with:

Terminal window
ss -tlnp | grep -E ':80|:443'

Certificate expired and not auto-renewing

Make sure the PersistentVolume for Traefik still exists and is accessible:

Terminal window
kubectl get pvc -n traefik

Comparison: Traefik vs NGINX Ingress + cert-manager#

AspectTraefik (this doc)NGINX + cert-manager
ComponentsTraefik onlyNGINX Ingress + cert-manager + ClusterIssuer
Let’s EncryptBuilt-in, configured in valuesSeparate via cert-manager CRDs
Configurationtraefik-values.yaml + annotationsingress.yaml + clusterissuer.yaml
Monitoring dashboardAvailable (optional)No built-in dashboard
Extra CRDsIngressRoute, MiddlewareCertificate, ClusterIssuer
ComplexitySimpler (fewer components)More components but more flexible
Best suited forLab, staging, simple productionProduction with many different certificates

References#


This article is a continuation of the bare-metal Kubernetes installation series. Adjust domains, IPs, and version numbers to match your own environment.

Setting Up ArgoCD with Traefik and HTTPS (Let's Encrypt) on a Public Domain
https://im-gatan.com/posts/argocd-traefik-https/
Author
Gatan
Published at
2026-07-16
License
CC BY-NC-SA 4.0