Installing k3s

You can install k3s with the following command:

curl -sfL https://get.k3s.io | sh -s - \
  --write-kubeconfig-mode 644 \
  --node-ip <your_ip> \
  --flannel-iface <your_network_interface_name>
  • I currently use a regular user. The --write-kubeconfig-mode parameter ensures regular users can run kubectl directly without always using sudo.

  • --node-ip: I now have a static IP, and this parameter forces k3s to use it.

  • --flannel-iface: Force using a network interface. I’m currently using Wi-Fi.

    • After switching to Ethernet, update the network interface in /etc/systemd/system/k3s.service.

    • Restart:

      sudo systemctl daemon-reload
      sudo systemctl restart k3s
      

Managing Secrets

The biggest benefit of k3s is implementing GitOps—storing all deployment files and service configurations on GitHub. This makes system migration simple: just use Git to backup. However, all secret.yaml files cannot be uploaded to GitHub, even in private repositories. To solve this, we use Age + SOPS.

Age

Age is a tool for generating keys.

  • First, install Age on Debian:

    sudo apt update
    sudo apt install age
    
  • Generate a key:

    age-keygen -o keys.txt
    

SOPS

Once you have the key, you need a tool to encrypt secret.yaml files using that key. Here we use SOPS.

  • Install on Debian:

    curl -LO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64
    chmod +x sops-v3.9.4.linux.amd64
    sudo mv sops-v3.9.4.linux.amd64 /usr/local/bin/sops
    
  • Create .sops.yaml in your project:

    creation_rules:
      - path_regex: .*/secret(\.enc)?\.yaml$
        age: "age123..."
        encrypted_regex: '^(data|stringData)$'
    

    This means SOPS will search the project for files named secret.yaml and only encrypt the data or stringData sections. SOPS automatically looks for the key at ~/.config/sops/age/keys.txt. If that doesn’t exist or you prefer not to leave files on disk, use an environment variable:

    export SOPS_AGE_KEY="AGE-SECRET-KEY-YOUR-PRIVATE-KEY"
    
  • Run the command to encrypt all secret.yaml files:

    find . -name "secret.yaml" -exec sh -c 'sops --encrypt "$1" > "${1%.yaml}.enc.yaml"' _ {} \;
    
  • To encrypt a single file:

    export SOPS_AGE_KEY="AGE-SECRET-KEY-YOUR-PRIVATE-KEY"
    sops --encrypt secret.yaml > secret.enc.yaml
    
  • To decrypt:

    export SOPS_AGE_KEY="AGE-SECRET-KEY-YOUR-PRIVATE-KEY"
    sops -d secret.enc.yaml > secret.yaml
    
  • After encryption, all secret.yaml files become secret.enc.yaml and can be uploaded to GitHub. Delete the old secret.yaml files.

FluxCD

Now that you have encrypted secret.yaml, how do you use it in k3s? That’s where FluxCD comes in. It includes its own SOPS for decryption only, storing the decrypted secret.enc.yaml contents in memory for k3s to use.

  • Install on Debian. While you could use Helm Charts, the official team strongly recommends using the Flux CLI (Bootstrap mode):

    curl -s https://fluxcd.io/install.sh | sudo bash
    
  • FluxCD looks for the k8s API (running on port 8080), but k3s runs on port 6443. You need to modify a file:

    sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
    
  • Check:

    flux check --pre
    

    If you see checkmarks, it succeeded.

  • Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic). Generate a new Token with the repo permission.

  • Run locally on Debian:

    export GITHUB_TOKEN=Your_GitHub_Token
    export GITHUB_USER=Your_GitHub_Username
    
    flux bootstrap github \
      --owner=$GITHUB_USER \
      --repository=k3s \
      --branch=main \
      --path=./clusters/k3s \
      --personal
    
  • Since I kept system-level apps like databases and Cloudflared in the infra/ directory and other services in the apps directory, I created two files under clusters/k3s: infra.yaml and apps.yaml to manage these two Stacks:

    # infra.yaml
    apiVersion: kustomize.toolkit.fluxcd.io/v1
    kind: Kustomization
    metadata:
      name: infra
      namespace: flux-system
    spec:
      interval: 1h
      path: "./infra"
      prune: true
      sourceRef:
        kind: GitRepository
        name: flux-system
      decryption:
        provider: sops
        secretRef:
          name: sops-age     # Corresponds to the secret created in the cluster
    
    # apps.yaml
    apiVersion: kustomize.toolkit.fluxcd.io/v1
    kind: Kustomization
    metadata:
      name: apps
      namespace: flux-system
    spec:
      interval: 10m
      path: "./apps"
      prune: true
      wait: true
      dependsOn:
        - name: infra
      sourceRef:
        kind: GitRepository
        name: flux-system
      decryption:
        provider: sops
        secretRef:
          name: sops-age
    
  • Reference these two files in clusters/k3s/flux-system/kustomization.yaml:

    # clusters/k3s/flux-system/kustomization.yaml
    apiVersion: kustomize.config.k8s.io/v1beta1
    kind: Kustomization
    resources:
      - gotk-components.yaml
      - gotk-sync.yaml
      - ../apps.yaml
      - ../infra.yaml
    
  • Run:

    kubectl create secret generic sops-age \
      --namespace=flux-system \
      --from-file=sops.agekey=keys.txt \
      --from-file=keys.txt=keys.txt
    
  • Paste the AGE-SECRET-KEY from keys.txt. It will pass sensitive information directly to Flux, then press CTRL+D to exit.

  • Now Flux automatically scans your GitHub repository and deploys for you. Just git push after making changes. If you don’t want to wait for automatic pulls, run:

    flux reconcile kustomization apps --with-source
    flux reconcile kustomization infra --with-source
    
  • Check deployment status:

    flux get kustomization
    

Auto-updating Images with FluxCD

  • FluxCD has features like Watchtower and Keel, automatically updating images.

  • Install these two components:

    flux bootstrap github \
      --owner=Kyxie \
      --repository=k3s \
      --branch=main \
      --path=./clusters/k3s/flux-system \
      --components-extra=image-reflector-controller,image-automation-controller \
      --personal \
      --token-auth=true
    

    It will ask for your GitHub PAT. If you forgot it, regenerate one.

  • Create k3s/clusters/k3s/flux-system/images.yaml. You need to configure each image to update, including the source and update conditions. Here’s an example with strava and homepage:

    # Strava
    ---
    apiVersion: image.toolkit.fluxcd.io/v1beta2
    kind: ImageRepository
    metadata:
      name: strava-repo
      namespace: flux-system
    spec:
      image: index.docker.io/robiningelbrecht/strava-statistics
      interval: 5m
    ---
    apiVersion: image.toolkit.fluxcd.io/v1beta2
    kind: ImagePolicy
    metadata:
      name: strava-policy
      namespace: flux-system
    spec:
      imageRepositoryRef:
        name: strava-repo
      policy:
        semver:
          range: '>=4.0.0'
    ---
    
    # Homepage
    apiVersion: image.toolkit.fluxcd.io/v1beta2
    kind: ImageRepository
    metadata:
      name: homepage-repo
      namespace: flux-system
    spec:
      image: ghcr.io/gethomepage/homepage
      interval: 5m
    ---
    apiVersion: image.toolkit.fluxcd.io/v1beta2
    kind: ImagePolicy
    metadata:
      name: homepage-policy
      namespace: flux-system
    spec:
      imageRepositoryRef:
        name: homepage-repo
      policy:
        semver:
          range: '>=1.0.0 <2.0.0'
    ---
    
    # Automation
    apiVersion: image.toolkit.fluxcd.io/v1beta1
    kind: ImageUpdateAutomation
    metadata:
      name: k3s-all-images-automation
      namespace: flux-system
    spec:
      interval: 1m
      sourceRef:
        kind: GitRepository
        name: flux-system
      git:
        checkout:
          ref:
            branch: main
        commit:
          author:
            email: [email protected]
            name: fluxcdbot
          messageTemplate: 'chore: Automated update image {{range .UpdatedImages}}{{ . }}{{end}}'
        push:
          branch: main
      update:
        path: ./clusters/k3s
        strategy: Setters
    
  • Add comments to the corresponding deployment.yaml:

    image: robiningelbrecht/strava-statistics:v4.3.4 # {"$imagepolicy": "flux-system:strava-policy"}
    
  • Add the file to k3s/clusters/k3s/flux-system/kustomization.yaml:

    apiVersion: kustomize.config.k8s.io/v1beta1
    kind: Kustomization
    resources:
    - gotk-components.yaml
    - gotk-sync.yaml
    - ../apps.yaml
    - ../infra.yaml
    - images.yaml
    
  • Finally, push to GitHub.