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-modeparameter ensures regular users can runkubectldirectly without always usingsudo. -
--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.yamlin 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.yamland only encrypt thedataorstringDatasections. 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.yamlfiles: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.yamlfiles becomesecret.enc.yamland can be uploaded to GitHub. Delete the oldsecret.yamlfiles.
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 --preIf you see checkmarks, it succeeded.
-
Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic). Generate a new Token with the
repopermission. -
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 theappsdirectory, I created two files underclusters/k3s:infra.yamlandapps.yamlto 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 pressCTRL+Dto exit. -
Now Flux automatically scans your GitHub repository and deploys for you. Just
git pushafter 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=trueIt 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.