{"id":717,"date":"2025-11-28T17:24:34","date_gmt":"2025-11-28T17:24:34","guid":{"rendered":"https:\/\/wqrld.net\/blog\/?p=717"},"modified":"2026-02-07T17:12:59","modified_gmt":"2026-02-07T17:12:59","slug":"setting-up-opendesk-on-kubernetes","status":"publish","type":"post","link":"https:\/\/wqrld.net\/blog\/setting-up-opendesk-on-kubernetes\/","title":{"rendered":"Setting up Opendesk on Kubernetes"},"content":{"rendered":"\n<p>Since i had little prior experience with K8S, this document serves as a sort of self-documentation of setting up <a href=\"https:\/\/www.opendesk.eu\/en\" data-type=\"link\" data-id=\"https:\/\/www.opendesk.eu\/en\">opendesk<\/a>, a sovereign office suite. While it might be nice to use ArgoCD for such a setup in production, I have no experience with that *yet*.<\/p>\n\n\n\n<p><strong>This is not a full tutorial. Just a reference for your own install and it may have some things out of order!<\/strong><\/p>\n\n\n\n<p>Ensure you have enough CPU cores and RAM or opendesk will fail to install<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">First of all, you want to set up DNS: <\/h2>\n\n\n\n<p>I am just going to link to the opendesk guys here:<br><a href=\"https:\/\/gitlab.opencode.de\/bmi\/opendesk\/deployment\/opendesk\/-\/blob\/develop\/docs\/getting-started.md#dns\">https:\/\/gitlab.opencode.de\/bmi\/opendesk\/deployment\/opendesk\/-\/blob\/develop\/docs\/getting-started.md#dns<\/a><\/p>\n\n\n\n<p>When setting up opendesk on a second level domain, I ran into issues with ruby limits and K8S&#8217;s ndots settings: <a href=\"https:\/\/gitlab.opencode.de\/bmi\/opendesk\/deployment\/opendesk\/-\/issues\/252\">https:\/\/gitlab.opencode.de\/bmi\/opendesk\/deployment\/opendesk\/-\/issues\/252<\/a><br>If you still run into this issue you may want to use <a href=\"https:\/\/github.com\/maxlaverse\/ndots-admission-controller\">https:\/\/github.com\/maxlaverse\/ndots-admission-controller<\/a>. This is more performant anyways.<\/p>\n\n\n\n<p>Optional, but it might be a good idea to set up a <a href=\"https:\/\/goharbor.io\/docs\/2.14.0\/install-config\/download-installer\/\">Harbor <\/a>image proxy. DockerHub&#8217;s ratelimit are quite strict and will give you headaches if you need to do any debugging during the setup. See <a href=\"https:\/\/wqrld.net\/blog\/setting-up-a-harbor-proxy-to-help-with-docker-rate-limits\/\" data-type=\"post\" data-id=\"764\">Setting up a Harbor proxy to help with docker rate limits<\/a><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Kubernetes using KubeAdm<\/h2>\n\n\n\n<div class=\"wp-block-media-text is-stacked-on-mobile\" style=\"grid-template-columns:25% auto\"><figure class=\"wp-block-media-text__media\"><img loading=\"lazy\" decoding=\"async\" width=\"225\" height=\"225\" src=\"https:\/\/wqrld.net\/blog\/wp-content\/uploads\/2025\/11\/images.png\" alt=\"\" class=\"wp-image-719 size-full\" srcset=\"https:\/\/wqrld.net\/blog\/wp-content\/uploads\/2025\/11\/images.png 225w, https:\/\/wqrld.net\/blog\/wp-content\/uploads\/2025\/11\/images-150x150.png 150w\" sizes=\"auto, (max-width: 225px) 85vw, 225px\" \/><\/figure><div class=\"wp-block-media-text__content\">\n<p>I chose kubeadm as it seems to be a bit more production-oriented than minikube. Interestingly, i found the <a href=\"https:\/\/cri-o.io\/\" data-type=\"link\" data-id=\"https:\/\/cri-o.io\/\">cri-o<\/a> site to be the best source for information regarding K8S installation. Cri-o is a container runtime for Kubernetes similar to containerd. I will be using <strong>ubuntu 24.04<\/strong> as the base.<\/p>\n<\/div><\/div>\n\n\n\n<pre class=\"wp-block-code\"><code>KUBERNETES_VERSION=v1.34\nCRIO_VERSION=v1.34\n\napt-get update\napt-get install -y software-properties-common curl unzip\n\ncurl -fsSL https:\/\/pkgs.k8s.io\/core:\/stable:\/$KUBERNETES_VERSION\/deb\/Release.key |\n    gpg --dearmor -o \/etc\/apt\/keyrings\/kubernetes-apt-keyring.gpg\n\necho \"deb &#91;signed-by=\/etc\/apt\/keyrings\/kubernetes-apt-keyring.gpg] https:\/\/pkgs.k8s.io\/core:\/stable:\/$KUBERNETES_VERSION\/deb\/ \/\" |\n    tee \/etc\/apt\/sources.list.d\/kubernetes.list\n\ncurl -fsSL https:\/\/download.opensuse.org\/repositories\/isv:\/cri-o:\/stable:\/$CRIO_VERSION\/deb\/Release.key |\n    gpg --dearmor -o \/etc\/apt\/keyrings\/cri-o-apt-keyring.gpg\n\necho \"deb &#91;signed-by=\/etc\/apt\/keyrings\/cri-o-apt-keyring.gpg] https:\/\/download.opensuse.org\/repositories\/isv:\/cri-o:\/stable:\/$CRIO_VERSION\/deb\/ \/\" |\n    tee \/etc\/apt\/sources.list.d\/cri-o.list\n\napt-get update\napt-get install -y cri-o kubelet kubeadm kubectl\n\nsystemctl start crio.service\n\nswapoff -a\nmodprobe br_netfilter\necho 'br_netfilter' &gt; \/etc\/modules-load.d\/br_netfilter.conf\nsysctl -w net.ipv4.ip_forward=1\n\nkubeadm init --pod-network-cidr=10.244.0.0\/16\nkubectl get pods --all-namespaces<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># Setup kubectl\nmkdir -p $HOME\/.kube\ncp -i \/etc\/kubernetes\/admin.conf $HOME\/.kube\/config\nchown $(id -u):$(id -g) $HOME\/.kube\/config<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo sysctl -w fs.inotify.max_user_watches=2099999999\nsudo sysctl -w fs.inotify.max_user_instances=2099999999\nsudo sysctl -w fs.inotify.max_queued_events=2099999999<\/code><\/pre>\n\n\n\n<p>You might get very close to the pod limit which gives very cryptic errors (110), so up it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#nano \/var\/lib\/kubelet\/config.yaml\nmaxPods: 1024<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl restart kubelet<\/code><\/pre>\n\n\n\n<p>Since i&#8217;m running a single node cluster for now, I need to tell k8s that it can use the current machine:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>kubectl taint nodes --all node-role.kubernetes.io\/control-plane-\nkubectl label nodes --all node.kubernetes.io\/exclude-from-external-load-balancers-<\/code><\/pre>\n\n\n\n<p>Cri-o since kube 1.34 does some stupid name deduplication that breaks longhorn. Disable it by creating a \/etc\/crio\/crio.conf.d\/20-shortname.conf<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#\/etc\/crio\/crio.conf.d\/20-shortname.conf\n&#91;crio.image]\nshort_name_mode = \"disabled\"<\/code><\/pre>\n\n\n\n<p><code>service crio restart<\/code><\/p>\n\n\n\n<p>Since the normal kubectl CLI get quite limiting, I like to use the k9s TUI for easier cluster management:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>wget https:\/\/github.com\/derailed\/k9s\/releases\/download\/v0.50.16\/k9s_linux_amd64.deb\ndpkg -i k9s_linux_amd64.deb<\/code><\/pre>\n\n\n\n<p>Kubernetes needs a Container Network Interface (CNI) for networking. I chose to use <a href=\"https:\/\/github.com\/flannel-io\/flannel\">Flannel <\/a>just because it is a popular choice:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>kubectl apply -f https:\/\/github.com\/flannel-io\/flannel\/releases\/latest\/download\/kube-flannel.yml<\/code><\/pre>\n\n\n\n<p>You also want to install Helm, Helmdiff, Helmfile to manage opendesk:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl https:\/\/raw.githubusercontent.com\/helm\/helm\/main\/scripts\/get-helm-3 | bash\nhelm plugin install https:\/\/github.com\/databus23\/helm-diff\nwget https:\/\/github.com\/helmfile\/helmfile\/releases\/download\/v1.1.3\/helmfile_1.1.3_linux_amd64.tar.gz\ntar -zxvf helmfile_1.1.3_linux_amd64.tar.gz\nmv helmfile \/usr\/local\/bin<\/code><\/pre>\n\n\n\n<p>We install ingress-nginx, which i believe is going out of support but is the only currently supported ingress controller for opendesk<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># This may be outdated. Take care!\nhelm repo add ingress-nginx https:\/\/kubernetes.github.io\/ingress-nginx\nhelm repo update\nhelm install quickstart ingress-nginx\/ingress-nginx --set controller.config.annotations-risk-level=Critical   --set controller.config.strict-validate-path-type=false --set controller.allowSnippetAnnotations=true\n --set controller.admissionWebhooks.allowSnippetAnnotations=true \n # -f values.yaml<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># You may or may not need this values.yaml:\ncontroller:\n#  service:\n#    type: \"NodePort\"\n  hostPort:\n    enabled: true\n  service:\n    type: \"ClusterIP\"\n  config:\n    annotations-risk-level: \"Critical\"\n    strict-validate-path-type: \"false\"\n  allowSnippetAnnotations: true\n  admissionWebhooks:\n    allowSnippetAnnotations: true<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Storage<\/h3>\n\n\n\n<p>The local path provisioner does not work for opendesk as it needs the sticky bit. Use Longhorn!<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>helm repo add longhorn https:\/\/charts.longhorn.io\nhelm repo update\nhelm install longhorn longhorn\/longhorn --namespace longhorn-system --create-namespace --version 1.10.0\nUSER=luc; PASSWORD=&lt;snip&gt;; echo \"${USER}:$(openssl passwd -stdin -apr1 &lt;&lt;&lt; ${PASSWORD})\" &gt;&gt; auth\nkubectl -n longhorn-system create secret generic basic-auth --from-file=auth\nnano longhorn-ingress.yml # See below\nkubectl -n longhorn-system apply -f longhorn-ingress.yml\nkubectl -n longhorn-system get ingress<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># longhorn-ingress.yml\napiVersion: networking.k8s.io\/v1\nkind: Ingress\nmetadata:\n  name: longhorn-ingress\n  namespace: longhorn-system\n  annotations:\n    # type of authentication\n    nginx.ingress.kubernetes.io\/auth-type: basic\n    # prevent the controller from redirecting (308) to HTTPS\n    nginx.ingress.kubernetes.io\/ssl-redirect: 'false'\n    # name of the secret that contains the user\/password definitions\n    nginx.ingress.kubernetes.io\/auth-secret: basic-auth\n    # message to display with an appropriate context why the authentication is required\n    nginx.ingress.kubernetes.io\/auth-realm: 'Authentication Required '\n    # custom max body size for file uploading like backing image uploading\n    nginx.ingress.kubernetes.io\/proxy-body-size: 10000m\nspec:\n  ingressClassName: nginx\n  rules:\n  - http:\n      paths:\n      - pathType: Prefix\n        path: \"\/\"\n        backend:\n          service:\n            name: longhorn-frontend\n            port:\n              number: 80<\/code><\/pre>\n\n\n\n<p>Now go to the longhorn web UI to check! Under settings, set the default replica count to 1 and minimum number of backingimage copies to 1 aswell if running single-node. You might have to do this after the opendesk install in the UI aswell since it doesnt seem to follow the defaults.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Certificates<\/h3>\n\n\n\n<p>Use cert-manager with Nginx<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>kubectl apply -f https:\/\/github.com\/cert-manager\/cert-manager\/releases\/download\/v1.19.1\/cert-manager.yaml\nkubectl create --edit -f https:\/\/raw.githubusercontent.com\/cert-manager\/website\/master\/content\/docs\/tutorials\/acme\/example\/production-issuer.yaml # Change Issuer to ClusterIssuer and remove the namespace<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Loadbalancer<\/h3>\n\n\n\n<p>I chose metalLB, though this has caused some issues. Can&#8217;t flannel help here?<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>kubectl apply -f https:\/\/raw.githubusercontent.com\/metallb\/metallb\/v0.15.2\/config\/manifests\/metallb-native.yaml\nkubectl apply -f pool.yml # you need the file below\nkubectl apply -f adv.yml # See below<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># pool.yml\napiVersion: metallb.io\/v1beta1\nkind: IPAddressPool\nmetadata:\n  name: first-pool\n  namespace: metallb-system\nspec:\n  addresses:\n  - 45.136.141.166-45.136.141.168\n  - 45.136.141.154-45.136.141.156 # I don't think you need this many IPs<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>#adv.yml\napiVersion: metallb.io\/v1beta1\nkind: L2Advertisement\nmetadata:\n  name: example\n  namespace: metallb-system<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Install Opendesk<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code># Update with the newest version\nwget https:\/\/gitlab.opencode.de\/bmi\/opendesk\/deployment\/opendesk\/-\/archive\/v1.10.0\/opendesk-v1.10.0.zip\nunzip opendesk-v1.10.0.zip\nkubectl create namespace opendesk\nkubectl config set-context --current --namespace opendesk<\/code><\/pre>\n\n\n\n<p>Now, configure and start!<\/p>\n\n\n\n<p>First of all, have you setup all required DNS settings?<\/p>\n\n\n\n<p>Then, configure the domain:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>root@Kubernetes:~\/opendesk-v1.10.0# cat helmfile\/environments\/dev\/global.yaml.gotmpl\nglobal:\n  domain: \"rabevcqhguoovcu.xyz\"<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>export MASTER_PASSWORD=\"&lt;snip&gt;\"<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>root@Kubernetes:~\/opendesk-v1.10.0# helmfile apply -e dev -n opendesk<\/code><\/pre>\n\n\n\n<p><a href=\"https:\/\/gitlab.opencode.de\/bmi\/opendesk\/deployment\/opendesk\/-\/blob\/develop\/docs\/getting-started.md\">https:\/\/gitlab.opencode.de\/bmi\/opendesk\/deployment\/opendesk\/-\/blob\/develop\/docs\/getting-started.md<\/a><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Bash history<\/h2>\n\n\n\n<p>Since it may help in debugging, my bash history can be found here: <a href=\"https:\/\/gist.github.com\/Wqrld\/8b9a1b6569f215d2807764f1e717aa53\">https:\/\/gist.github.com\/Wqrld\/8b9a1b6569f215d2807764f1e717aa53<\/a><\/p>\n\n\n\n<p>Warning: It is a mess and i had no clue what i was doing. Don&#8217;t read too much into it.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Since i had little prior experience with K8S, this document serves as a sort of self-documentation of setting up opendesk, a sovereign office suite. While it might be nice to use ArgoCD for such a setup in production, I have no experience with that *yet*. This is not a full tutorial. Just a reference for &hellip; <a href=\"https:\/\/wqrld.net\/blog\/setting-up-opendesk-on-kubernetes\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Setting up Opendesk on Kubernetes&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-717","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/wqrld.net\/blog\/wp-json\/wp\/v2\/posts\/717","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wqrld.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wqrld.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/wqrld.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/wqrld.net\/blog\/wp-json\/wp\/v2\/comments?post=717"}],"version-history":[{"count":19,"href":"https:\/\/wqrld.net\/blog\/wp-json\/wp\/v2\/posts\/717\/revisions"}],"predecessor-version":[{"id":766,"href":"https:\/\/wqrld.net\/blog\/wp-json\/wp\/v2\/posts\/717\/revisions\/766"}],"wp:attachment":[{"href":"https:\/\/wqrld.net\/blog\/wp-json\/wp\/v2\/media?parent=717"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wqrld.net\/blog\/wp-json\/wp\/v2\/categories?post=717"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wqrld.net\/blog\/wp-json\/wp\/v2\/tags?post=717"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}