For a friend, I did a Telegram bot and containerized the app with Docker. In reality, that was the beginning of a long struggle to keep only Docker and configure a reverse proxy, because I’m really bad at that.

Instead, I installed k3s, which allowed me to have single-node Kubernetes, meaning all running on a single VM. Kubernetes makes it possible to orchestrate containers, thus solving many of my issues.

"k3s" banner generated by Flux with Perplexity

How to do it

This tutorial is nothing amazing, but I made it because k3s doesn’t come with all the features I needed. Here’s how to proceed.

  1. Have a Linux VM. At least 2 CPUs and 2 GB of RAM are sufficient
  2. Make sure ports 80 and 443 are open inbound (NSG, firewall, iptables, ufw, depending on what you have)
  3. Install Docker (unless containerD is enough for you)
  4. Install k3s: curl -sfL https://get.k3s.io | sh -
  5. Enable k3s on next reboot with sudo systemctl enable k3s
  6. Add the line export KUBECONFIG=/etc/rancher/k3s/k3s.yaml to your rc file (.bashrc, .zshrc, etc.)
    • Note: if you encounter problems, you can relax the permissions of the k3s file with sudo chmod 644 /etc/rancher/k3s/k3s.yaml (not recommended)
  7. Install ingress-nginx for your Ingress resources (if using a cloud provider) with kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.11.3/deploy/static/provider/cloud/deploy.yaml
  8. Install cert-manager to enable HTTPS later with kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.1/cert-manager.yaml
  9. Install MetalLB to create LoadBalancers/Ingresses with kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml
For all the .yaml files below, you can apply them with the command:
kubectl apply -f myfile.yaml

Configuring MetalLB

MetalLB needs to be able to create LoadBalancers using a pool of available IPs. To determine which range to use:

  • Run hostname -I.
  • Pick a range of addresses that doesn’t overlap with the listed IPs.

Create and apply the file metallb.yaml:

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: first-pool
  namespace: metallb-system
spec:
  addresses:
  - 10.0.0.240-10.0.0.250 # adjust according to your network

---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: l2-advert
  namespace: metallb-system
spec:
  ipAddressPools:
  - first-pool

Configuring Let’s Encrypt

Let’s Encrypt must confirm that you own your domain names. Create a file clusterissuer-letsencrypt.yaml:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: timothe@chauvet.cloud # replace this
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
        ingress:
          class: traefik # replace this if you want nginx

If you need a certificate, you can create one with a file like certificate-yourls.yaml:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: yourls-tls
  namespace: default
spec:
  secretName: yourls-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
  - tmth.fr

Hosting My Apps

My Telegram Bot

For my Telegram bot, it was quite simple because it doesn’t need to receive HTTP requests. A simple Deployment was enough:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: telebot
spec:
  replicas: 1
  selector:
    matchLabels:
      app: telebot
  template:
    metadata:
      labels:
        app: telebot
    spec:
      restartPolicy: Always
      containers:
      - name: telebot
        image: ghcr.io/timothechauvet/telebot:latest
        env:
        - name: TELEGRAM_BOT_TOKEN
          valueFrom:
            secretKeyRef:
              name: telebot-secrets
              key: TELEGRAM_BOT_TOKEN
      imagePullSecrets:
      - name: ghcr-secret

An HTTP App

I wanted an URL shortener. You may have already seen it by clicking on tmth.fr/k3s to come here! I used Yourls for that. It requires a MySQL database. That was the trickiest part to set up.

For Yourls, I needed:

  • A ConfigMap with config.php
  • A Certificate for my domain tmth.fr
  • A Traefik Ingress (I prefer that because Nginx is for smart people 😭)
  • The Service to map to the Ingress
  • A PersistentVolume and its claim for the MySQL database
  • The Deployment, of course.

I think the most important one is the Ingress which creates rules for when a request to tmth.fr is made

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: yourls-ingress
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    traefik.ingress.kubernetes.io/router.entrypoints: "web,websecure"
spec:
  ingressClassName: traefik
  tls:
  - hosts:
    - tmth.fr
    secretName: yourls-tls
  rules:
  - host: tmth.fr
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: yourls
            port: 
              number: 80

I’m obviously not going to publish ALL my configurations here.

Alas!!

Alas!!

I published my configurations, that you can find on my GitHub. Feel free to use them as examples!


If you have any questions or suggestions, don’t hesitate to contact me by email, on LinkedIn, or by sending an issue on GitHub.