Skip to content
Geek is the Way!
Menu
  • Forums
  • Sobre o blog
  • Contato
  • English
Menu

Exposing Your First Public Kubernetes App: “Hello World” on mydomain.com

Posted on June 24, 2026June 24, 2026 by Thiago Crepaldi

Last Updated on June 24, 2026 by Thiago Crepaldi

Today, we are taking our private, enterprise-grade Kubernetes cluster and safely piercing the veil to the public internet. Building on our previous guides, we will deploy a standard Nginx-based “Hello World” application and configure our infrastructure so it is reachable at hello.mydomain.com with valid, public-trusted SSL certificates—all while keeping your cluster security intact.

By the end of this tutorial, you will have successfully routed traffic from the global internet, through Cloudflare, through your pfSense firewall, into your bare-metal K3s LoadBalancer (MetalLB), and finally to a containerized pod.

1. The Traffic Flow Architecture

To expose your cluster securely, we rely on a three-tier handshake:

  1. Cloudflare (DNS Layer): Acts as your authoritative DNS. It will map the subdomain hello.mydomain.com to your home WAN IP.
  2. pfSense (Perimeter Firewall): Acts as your gatekeeper. It performs Network Address Translation (DNAT/Port Forwarding) to push inbound HTTPS traffic from your public IP to your internal K3s LoadBalancer IP.
  3. K3s & NGINX Ingress (Orchestration Layer): MetalLB catches the traffic on the internal Virtual IP (192.168.1.201), hands it to NGINX, which decrypts the TLS (using the certificates we provisioned via Cert-Manager), and routes the request to your specific “Hello World” pod.

2. Configure pfSense (The Gatekeeper)

By default, pfSense drops all unsolicited inbound traffic. We need to explicitly tell it to take traffic hitting port 443 (HTTPS) on your public IP and forward it to your MetalLB IP.

In pfSense, this is a two-step process: Translation (NAT) and Allowance (Firewall Rule). Fortunately, pfSense can do both at once if configured correctly.

Step 2.1: Create the Port Forward (NAT)

  1. Log into your pfSense WebGUI.
  2. Navigate to Firewall -> NAT -> Port Forward.
  3. Click Add (to add a rule to the top or bottom of the list).
  4. Configure the rule exactly as follows:
    • Interface: WAN
    • Address Family: IPv4
    • Protocol: TCP
    • Destination: WAN Address
    • Destination Port Range: From HTTPS (443) to HTTPS (443)
    • Redirect Target IP: 192.168.1.201 (Replace with your specific MetalLB Ingress IP)
    • Redirect Target Port: HTTPS (443)
    • Description: K3s NGINX Ingress - HTTPS
    • Filter rule association: Select Add associated filter rule (This is crucial!).
  5. Click Save and then Apply Changes.

Step 2.2: Verify the Firewall Rule

Because pfSense evaluates NAT before Firewall rules, the destination IP in the firewall rule must be the internal, post-translated IP. Let’s verify pfSense generated this correctly.

  1. Navigate to Firewall -> Rules -> WAN.
  2. Look for the rule created by the NAT process (it will usually have a linked icon next to it).
  3. Ensure the rule looks exactly like this:
    • Action: Pass (Green checkmark)
    • Protocol: IPv4 TCP
    • Source: * (Any)
    • Port: *
    • Destination: 192.168.1.201 (Your MetalLB IP)
    • Port: 443 (HTTPS)
    • Gateway: *
  4. If this rule is missing, click Add and create it manually using the parameters above. Click Apply Changes.

3. Configure Cloudflare DNS

Now that the gate is open, we need to point the domain to your house.

  1. Log into your Cloudflare Dashboard and select your mydomain.com domain.
  2. Go to DNS -> Records.
  3. Click Add Record:
    • Type: A
    • Name: hello
    • IPv4 Address: Your Home WAN Public IP (You can find this by googling “What is my IP” from your home network).
    • Proxy Status:DNS Only (Grey Cloud).
      • Geek Note: We are using Cert-Manager with DNS-01 challenges, so the proxy status technically doesn’t impact certificate generation. However, setting it to “DNS Only” initially removes Cloudflare’s proxy from the equation, making it vastly easier to troubleshoot your pfSense routing. You can turn the proxy on (Orange Cloud) after everything is working!
  4. Click Save.

4. The Kubernetes Manifest

We will define three Kubernetes resources in a single file: a Deployment (the actual app), a Service (internal cluster networking), and an Ingress (the external routing and SSL request).

On your management machine, create a file named hello-world.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-world-deployment
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello-world
  template:
    metadata:
      labels:
        app: hello-world
    spec:
      containers:
      - name: hello-world
        image: nginxdemos/hello:plain-text
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: hello-world-service
  namespace: default
spec:
  selector:
    app: hello-world
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hello-world-ingress
  namespace: default
  annotations:
    # This annotation tells Cert-Manager to automatically provision an SSL cert!
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    kubernetes.io/ingress.class: "nginx"
spec:
  tls:
  - hosts:
    - hello.mydomain.com
    secretName: hello-world-tls
  rules:
  - host: hello.mydomain.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: hello-world-service
            port:
              number: 80

5. Deploy and Validate

Apply the manifest to your cluster:

kubectl apply -f hello-world.yaml

Verification Steps

  1. Check Certificate Issuance: Let’s Encrypt can take anywhere from 30 seconds to 3 minutes to verify the DNS challenge via Cloudflare. Watch the status:kubectl get certificate hello-world-tls -w Wait until the READY column changes to True. Press Ctrl+C to exit.
  2. Check Ingress Status:kubectl get ingress hello-world-ingress Ensure the ADDRESS field populates with your MetalLB IP (192.168.1.201).
  3. The Public Test: Turn off Wi-Fi on your smartphone (to ensure you are testing from an external network and bypassing potential local NAT reflection issues) and navigate to https://hello.mydomain.com.You should see the Nginx “Hello World” plain-text response, showing your server address and connection details, fully secured by a verified Let’s Encrypt padlock icon.

6. The Lab Medic: Troubleshooting

If things aren’t working, here is how to isolate the problem:

  • Connection Timed Out? The traffic isn’t reaching your cluster. From an external network (like a mobile hotspot), run telnet <Your-Public-WAN-IP> 443. If it times out, your pfSense Port Forward/Firewall rule is incorrect, or your ISP blocks inbound port 443 (common on some residential plans).
  • “Site Not Secure” Warning? Your Ingress is reachable, but the SSL certificate failed to generate. NGINX is falling back to its default “Fake Certificate”. Check the Cert-Manager logs to see why the Cloudflare API failed: kubectl logs -n cert-manager -l app.kubernetes.io/name=cert-manager
  • 502 Bad Gateway? Traffic reached NGINX, but NGINX can’t find your pod. Check if your deployment is running: kubectl get pods. Ensure the selector labels in your Service match the labels in your Deployment.

7. Advanced: Exposing Non-Standard Ports (e.g., Port 3012)

You might find yourself needing to expose a port other than 80 or 443. A common example is Vaultwarden, which historically used port 3012 for WebSocket notifications, or perhaps you are hosting a game server (like Minecraft on 25565).

Ingress resources are strictly for Layer 7 (HTTP/HTTPS) traffic. To expose raw TCP or UDP ports, you bypass the NGINX Ingress entirely and use a Layer 4 LoadBalancer service. MetalLB will assign a dedicated IP address directly to that service.

Step 7.1: Create a LoadBalancer Service

Instead of defining an Ingress, define your service as type: LoadBalancer and specify the port you need:

apiVersion: v1
kind: Service
metadata:
  name: vaultwarden-websocket-service
  namespace: default
  annotations:
    # Optional: Force MetalLB to use a specific IP from your pool
    metallb.universe.tf/loadBalancerIPs: 192.168.1.202
spec:
  type: LoadBalancer
  selector:
    app: vaultwarden # Must match your app's deployment labels
  ports:
  - name: websocket
    protocol: TCP
    port: 3012
    targetPort: 3012

When you apply this, MetalLB will assign an IP (e.g., 192.168.1.202). You can check this by running kubectl get svc vaultwarden-websocket-service.

Step 7.2: Update pfSense

Just like we did for port 443 in Phase 1, you must tell your firewall to forward inbound traffic for this new port.

Navigate to Firewall -> NAT -> Port Forward and create a new rule:

  • Protocol: TCP (or UDP, depending on your app)
  • Destination Port Range: 3012 to 3012
  • Redirect Target IP: 192.168.1.202 (The IP MetalLB assigned to this specific service)
  • Redirect Target Port: 3012
  • Filter rule association: Add associated filter rule.

Important Note on SSL for Non-HTTP Ports

Because you are bypassing the NGINX Ingress Controller, Cert-Manager cannot automatically terminate SSL for this connection. Traffic on this port will hit your container exactly as it left the client. If the connection needs to be encrypted (like wss:// for secure WebSockets), the application itself (e.g., Vaultwarden) must be configured to handle the SSL certificates natively.

Congratulations! You are now hosting real, secure traffic directly from your own hardware to the public internet. Now that the plumbing is working, you can easily swap out the “Hello World” image for Nextcloud, WordPress, or your own custom applications.

Conclusion

At this point, if you visit https://hello.mydomain.com, you should be welcomed by your brand new website, including Let’s Encrypt SSL certificates automatically generated for you.

Share this:

  • Tweet

Related

Leave a ReplyCancel reply

LIKED? SUPPORT IT :)

Buy Me a Coffee


Search


Categories

  • Cooking (1)
  • Homelab (87)
    • APC UPS (6)
    • Kubernetes (3)
    • pfSense (43)
    • Plex (1)
    • Prometheus & Grafana (1)
    • Proxmox (23)
    • Shopping (1)
    • Supermicro (2)
    • Synology NAS (8)
    • Ubiquiti (6)
    • UDM-Pro (4)
  • Random (3)
  • Wordpress (1)

Tags

Agentless monitoring (3) AP9631 (3) Apache2 (3) APC UPS (6) apt-get software (2) Bind9 (3) certificates (5) CloudFlare (2) DDNS (5) debian (3) DNS (7) DSM (6) Dynamic DNS (4) Firewall (9) gmail (3) GPU (3) kubernetes (3) Let's Encrypt Certificates (7) monitoring (19) networking (21) PBS (3) pfsense (44) port forwarding (3) privacy (2) proxmox (18) proxmox backup server (3) proxmox virtual environment (17) pve (5) rev202207eng (76) security (29) SNMP (4) SNMPv1 (3) ssh (4) SSL (6) Synology (7) udm-pro (5) UDR (2) unifi (6) unifi controller (3) UPS (5) VLAN (4) vpn (9) wifi (4) Zabbix (18) Zabbix Agent2 (11)

See also

Privacy policy

Sitemap

©2026 Geek is the Way! | Design by Superb