Put Your App on the Internet
Now that your k3s cluster is running, it's time to expose your application to the world. In this guide, you'll install cert-manager for automatic SSL certificates, then create an Ingress resource that routes traffic to your application with HTTPS enabled. By the end, your app will be accessible via a secure HTTPS URL with a valid Let's Encrypt certificate.
Understanding the Architecture
Before we dive in, let's understand how traffic flows to your application:
- DNS: Your domain points to your VM's IP address
- Traefik: k3s's built-in ingress controller receives incoming HTTP/HTTPS requests
- Ingress Resource: Defines routing rules (which domain goes to which service)
- cert-manager: Automatically obtains and renews SSL certificates from Let's Encrypt
- Your Application: Receives the routed traffic
Prerequisites
Before starting, ensure you have:
- A working k3s cluster (completed in Step 4)
- kubectl configured to access your cluster
- Your application deployed in Kubernetes (Deployment + Service)
- Your VM connected via SSH
For this workshop, you'll use: dj-viscon-workshop-X.vsos.ethz.ch (replace X with your participant number)
Verify your cluster is ready:
kubectl get nodesYou should see your node in Ready status.
Part 1: Install cert-manager
cert-manager is a powerful Kubernetes add-on that automates the management and issuance of TLS certificates. It will handle requesting, renewing, and managing certificates from Let's Encrypt automatically.
Step 1: Install cert-manager
cert-manager provides official Kubernetes manifests for easy installation. Apply the latest version:
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.2/cert-manager.yaml
This single command creates:
- A new
cert-managernamespace - Custom Resource Definitions (CRDs) for certificates and issuers
- The cert-manager controller and webhook components
Step 2: Verify cert-manager Installation
cert-manager creates three main deployments. Wait for all pods to be ready:
kubectl get pods -n cert-managerYou should see three pods running:
cert-manager-xxxxxcert-manager-cainjector-xxxxxcert-manager-webhook-xxxxx
All should show 1/1 in the READY column and Running status. This may take 1-2 minutes.
Verify the installation is healthy:
kubectl get deployments -n cert-managerAll deployments should show READY 1/1 and AVAILABLE 1.
Step 3: Create a Let's Encrypt Issuer
cert-manager needs an "Issuer" to know where to request certificates from. We'll create a ClusterIssuer for Let's Encrypt, which provides free SSL certificates trusted by all browsers.
Important: Replace your-email@example.com with your actual email address—Let's Encrypt uses this for certificate expiration notifications.
First, create a file for your ClusterIssuer configuration:
vim letsencrypt-issuer.yamlAdd the following configuration:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: your-email@example.com # CHANGE THIS to your actual email
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: traefikUnderstanding the ClusterIssuer:
server: Let's Encrypt's ACME server endpoint (production)email: Your email for certificate expiration notificationsprivateKeySecretRef: Where cert-manager stores its private keysolvers: How cert-manager proves you control the domain (HTTP-01 challenge using Traefik)
Save the file, then apply it to your cluster:
kubectl apply -f letsencrypt-issuer.yamlYou should see:
clusterissuer.cert-manager.io/letsencrypt-prod createdVerify your issuer is ready:
kubectl get clusterissuerThe issuer should show True under the READY column.
Understanding cert-manager
cert-manager automates the entire certificate lifecycle using the HTTP-01 challenge:
- Certificate Request: When you create an Ingress with cert-manager annotations, it detects the request
- HTTP Challenge: cert-manager creates a temporary route to serve a validation file at
http://your-domain/.well-known/acme-challenge/ - Domain Validation: Let's Encrypt verifies you control the domain by fetching this file
- Certificate Issuance: Once validated, Let's Encrypt issues a valid SSL certificate
- Secret Storage: The certificate is stored as a Kubernetes Secret
- Automatic Renewal: Certificates are automatically renewed before expiration
Part 2: Deploy with Ingress
Now that cert-manager is installed and configured, let's expose your application to the internet.
Step 4: Verify Your Application is Running
First, make sure your application is deployed and accessible within the cluster.
Check your deployments:
kubectl get deploymentsYou should see both your frontend and backend deployments running. Check your services:
kubectl get servicesYou should see:
furnet-frontendservice of typeClusterIPon port8080furnet-backendservice of typeClusterIPon port8000
Step 5: Understand the Ingress Resource
An Ingress is a Kubernetes resource that manages external access to services in your cluster. Let's break down what we need:
Key Components:
- Metadata: Name, labels, and annotations for configuration
- IngressClassName: Tells Kubernetes which ingress controller to use (Traefik)
- TLS Configuration: Defines which domains need HTTPS and where to store certificates
- Rules: Maps domains and paths to backend services
Step 6: Create the Ingress Manifest
Create a new file for your Ingress configuration using your favorite editor (e.g., vim):
vim ingress.yamlWhy a separate file? Keeping your Ingress in its own file makes it easy to modify routing without touching your application deployment.
Step 7: Add Metadata and Annotations
Start by defining the basic structure and metadata:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: furnet-ingress
labels:
app: furnet
annotations:
# Traefik annotations for k3s
traefik.ingress.kubernetes.io/redirect-entry-point: websecure
# cert-manager annotations for automatic TLS certificate provisioning
cert-manager.io/cluster-issuer: "letsencrypt-prod"Understanding the annotations:
-
traefik.ingress.kubernetes.io/redirect-entry-point: websecure- Automatically redirects HTTP (port 80) traffic to HTTPS (port 443)
- Ensures all traffic uses secure connections
- Users visiting
http://your-domain.comwill be redirected tohttps://your-domain.com
-
cert-manager.io/cluster-issuer: "letsencrypt-prod"- Tells cert-manager to automatically create a certificate
- Uses the
letsencrypt-prodClusterIssuer we created earlier - cert-manager watches for Ingresses with this annotation and handles certificate issuance
Step 8: Specify the Ingress Class
Add the ingress class specification:
spec:
# Note: k3s uses traefik as the default ingress class
# You can omit ingressClassName or explicitly set it to "traefik"
ingressClassName: traefikWhat is ingressClassName?
- Kubernetes supports multiple ingress controllers in one cluster
- This field tells Kubernetes which controller should handle this Ingress
- k3s comes with Traefik pre-installed, so we use
traefik
Fun fact: You could run NGINX and Traefik side-by-side and route different applications through different controllers!
Step 9: Configure TLS/SSL
Add the TLS configuration:
tls:
- hosts:
- dj-viscon-workshop-0.vsos.ethz.ch # CHANGE THIS to your participant number
secretName: furnet-tls-certUnderstanding TLS configuration:
-
hosts: List of domains that need SSL certificates- Replace
0with your participant number (1, 2, 3, etc.) - This must match your actual domain
- Replace
-
secretName: furnet-tls-cert- Where cert-manager will store your certificate
- This is a Kubernetes Secret that will be automatically created
- Traefik reads this secret to serve HTTPS traffic
What happens here?
When you create this Ingress:
- cert-manager sees the annotation and TLS config
- It creates a Certificate resource automatically
- Let's Encrypt validates you own the domain (HTTP-01 challenge)
- The certificate is stored in the
furnet-tls-certsecret - Traefik uses this certificate for HTTPS connections
Step 10: Define Routing Rules
Add the routing rules to direct traffic to both your frontend and backend:
rules:
- host: dj-viscon-workshop-0.vsos.ethz.ch # CHANGE THIS to your participant number
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: furnet-backend
port:
number: 8000
- path: /
pathType: Prefix
backend:
service:
name: furnet-frontend
port:
number: 8080Breaking down the routing:
-
host: dj-viscon-workshop-0.vsos.ethz.ch- Only requests to this domain will be routed by this rule
- Replace with your actual workshop domain
-
http.paths: List of path-based routing rules- Order matters! More specific paths should come first
Backend API Path (/api):
-
path: /api- Matches all paths starting with
/api - Examples:
/api/posts,/api/users
- Matches all paths starting with
-
pathType: Prefix- Match based on URL path prefix
-
backend.service.name: furnet-backend- Routes API requests to the backend service
- The backend service should be running on port
8000
Frontend Path (/):
-
path: /- Matches everything else (the catch-all route)
- Must come after the
/apipath
-
backend.service.name: furnet-frontend- Routes all other requests to the frontend service
- The frontend service should be running on port
8080
Why order matters:
Traefik evaluates paths in order. If we put / first, it would match everything (including /api requests) and the backend would never receive traffic. By putting /api first, we ensure API requests go to the backend, while everything else goes to the frontend.
Step 11: Complete Ingress Manifest
Here's your complete Ingress configuration with both frontend and backend routing:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: furnet-ingress
labels:
app: furnet
annotations:
# Traefik annotations for k3s
traefik.ingress.kubernetes.io/redirect-entry-point: websecure
# cert-manager annotations for automatic TLS certificate provisioning
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
# Note: k3s uses traefik as the default ingress class
# You can omit ingressClassName or explicitly set it to "traefik"
ingressClassName: traefik
tls:
- hosts:
- dj-viscon-workshop-0.vsos.ethz.ch # CHANGE THIS to your actual domain
secretName: furnet-tls-cert
rules:
- host: dj-viscon-workshop-0.vsos.ethz.ch # CHANGE THIS to your actual domain
http:
paths:
# Backend API - must come first (more specific path)
- path: /api
pathType: Prefix
backend:
service:
name: furnet-backend
port:
number: 8000
# Frontend - catch-all path (less specific)
- path: /
pathType: Prefix
backend:
service:
name: furnet-frontend
port:
number: 8080Important: Replace dj-viscon-workshop-0.vsos.ethz.ch with your assigned domain in both places (tls.hosts and rules.host).
What this configuration does:
- All requests to
https://your-domain.com/api/*→ Backend service on port 8000 - All other requests to
https://your-domain.com/*→ Frontend service on port 8080 - Both routes are served over HTTPS with the same certificate
Step 12: Apply the Ingress
Save the file and apply it to your cluster:
kubectl apply -f ingress.yamlYou should see:
ingress.networking.k8s.io/furnet-ingress createdStep 13: Verify the Ingress
Check that your Ingress was created:
kubectl get ingressYou should see something like:
NAME CLASS HOSTS ADDRESS PORTS AGE
furnet-ingress traefik dj-viscon-workshop-0.vsos.ethz.ch 10.43.123.45 80, 443 10sGet detailed information:
kubectl describe ingress furnet-ingressLook for:
- The TLS configuration
- The routing rules
- Events showing cert-manager activity
Step 14: Watch Certificate Issuance
cert-manager detected your Ingress and started requesting a certificate. Watch the process:
kubectl get certificateYou should see a certificate named based on your Ingress:
NAME READY SECRET AGE
furnet-tls-cert False furnet-tls-cert 20sCheck the certificate details:
kubectl describe certificate furnet-tls-certWhat to look for:
- Events showing "Created new CertificateRequest"
- "Waiting for CertificateRequest to complete"
- Eventually: "Certificate issued successfully"
This takes 1-3 minutes. Let's Encrypt needs to:
- Create an HTTP challenge
- Verify your domain ownership
- Issue the certificate
- Store it in the secret
Step 15: Verify the Certificate Secret
Once the certificate shows READY True, check the secret:
kubectl get secret furnet-tls-certView the certificate details:
kubectl describe secret furnet-tls-certYou should see tls.crt and tls.key entries. These are your SSL certificate and private key!
Step 16: Test Your Application
Now let's test both the frontend and backend through the Ingress!
Test the Frontend:
Open your browser and navigate to your domain:
https://dj-viscon-workshop-0.vsos.ethz.ch
Expected results:
- Your frontend application loads over HTTPS
- The browser shows a lock icon (secure connection)
- No certificate warnings
- The certificate is issued by "Let's Encrypt"
Test the Backend Health Endpoint:
Test the backend health endpoint by visiting it in your browser or using curl:
curl https://dj-viscon-workshop-0.vsos.ethz.ch/health
Or open in your browser:
https://dj-viscon-workshop-0.vsos.ethz.ch/health
You should see a response from your backend health endpoint. Note that health endpoints are typically exposed at the root level for monitoring purposes.
Test the certificate:
Click the lock icon in your browser and view the certificate details. You should see:
- Issued to: your domain
- Issued by: Let's Encrypt
- Valid for: 90 days
What's happening:
- Requests to
/→ Frontend (React app) - Requests to
/api/*→ Backend (FastAPI) - Both use the same HTTPS certificate
- Both are routed through a single Ingress
Understanding HTTP to HTTPS Redirect
Thanks to the redirect-entry-point: websecure annotation we configured, Traefik automatically redirects all HTTP traffic to HTTPS. Try visiting:
http://dj-viscon-workshop-0.vsos.ethz.ch
You should be automatically redirected to HTTPS. This ensures users always access your application securely, even if they type http:// in their browser.
Troubleshooting
Problem: Certificate stays in "False" state
Check the certificate request:
kubectl get certificaterequest
kubectl describe certificaterequestCommon causes:
- Domain doesn't point to your VM IP
- Firewall blocking port 80 (needed for HTTP-01 challenge)
- Invalid email in ClusterIssuer
Check cert-manager logs:
kubectl logs -n cert-manager -l app=cert-managerProblem: "503 Service Unavailable"
Your Ingress is working, but one of the backend services isn't reachable.
Check which service is failing:
- If it's the homepage: check the frontend service
- If it's the API: check the backend service
Check your services:
kubectl get service furnet-frontend
kubectl describe service furnet-frontend
kubectl get service furnet-backend
kubectl describe service furnet-backendCheck your pods:
kubectl get pods
kubectl logs <frontend-pod-name>
kubectl logs <backend-pod-name>
Ensure:
- Service selector matches your pod labels
- Service port matches container port
- Pods are in
Runningstate - For backend: API endpoints are correctly exposed on port 8000
- For frontend: Static files are served on port 8080
Problem: "404 Not Found"
Traefik is working, but can't find a route.
Check your Ingress:
kubectl describe ingress furnet-ingressEnsure:
- Host matches your domain exactly
- Path is correct
- Backend service name is correct
Problem: Certificate warnings in browser
Check if cert-manager created the certificate:
kubectl get certificate
kubectl describe certificate furnet-tls-certIf the certificate exists but shows warnings:
- Verify the domain in the certificate matches your URL
- Check if you're using the correct ClusterIssuer (prod, not staging)
Problem: Can't access application at all
Check Traefik is running:
kubectl get pods -n kube-system | grep traefikNext Steps
Congratulations! Your application is now live on the internet with HTTPS.
Continue to Step 7: Achievements and Future Steps to review what you've accomplished and explore ideas for taking your DevOps skills even further.