<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anatole HAGBE</title>
    <description>The latest articles on DEV Community by Anatole HAGBE (@hanatole).</description>
    <link>https://dev.to/hanatole</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3844689%2F340605b9-b40e-4e9f-9c70-6a844de7b3b1.jpeg</url>
      <title>DEV Community: Anatole HAGBE</title>
      <link>https://dev.to/hanatole</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hanatole"/>
    <language>en</language>
    <item>
      <title>Installer mon premier cluster K3s pour BMI : choix, automatisation et premières difficultés</title>
      <dc:creator>Anatole HAGBE</dc:creator>
      <pubDate>Sun, 11 Oct 2026 21:18:35 +0000</pubDate>
      <link>https://dev.to/hanatole/installer-mon-premier-cluster-k3s-pour-bmi-choix-automatisation-et-premieres-difficultes-1ibk</link>
      <guid>https://dev.to/hanatole/installer-mon-premier-cluster-k3s-pour-bmi-choix-automatisation-et-premieres-difficultes-1ibk</guid>
      <description>&lt;p&gt;Dans le &lt;a href="https://dev.to/hanatole/de-mon-application-go-a-un-service-exploite-sur-kubernetes-point-de-depart-et-objectifs-de-mon-2g51"&gt;premier article&lt;/a&gt;, j’ai présenté BMI et le plan de mon mentorat avec &lt;a href="https://www.linkedin.com/in/edouardhinvi/" rel="noopener noreferrer"&gt;Edouard HINVI&lt;/a&gt; qui a généreusement mis à ma disposition un VPS qui tourne sous Ubuntu 26.04 LTS, avec environ 8 Go de RAM et 190 Go de disque.&lt;/p&gt;

&lt;p&gt;Dans cet article, je fais le point de comment j’ai installé un cluster &lt;strong&gt;K3s&lt;/strong&gt; avec &lt;strong&gt;Ansible&lt;/strong&gt;, et y déployé BMI à l'aide de mon pipeline GitLab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi K3s
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.k3s.io/" rel="noopener noreferrer"&gt;K3s&lt;/a&gt; est une distribution Kubernetes certifiée, légère, livrée en un seul binaire avec les composants de base déjà inclus. Je l’ai choisi, avec ses réglages par défaut, parce que mon workload est simple et léger, et que je voulais un cluster que mon VPS puisse porter sans difficulté. K3s simplifie l’installation. Plus tard, j’aimerais monter chaque composant (réseau, stockage, control plane, certificats, etc.) pour mieux comprendre le fonctionnement d’un cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi Ansible plutôt que Terraform
&lt;/h2&gt;

&lt;p&gt;Le backlog prévoyait de créer le cluster avec &lt;strong&gt;Terraform&lt;/strong&gt; et &lt;strong&gt;Cloud-init&lt;/strong&gt;. Cependant, j'ai retenu &lt;strong&gt;Ansible&lt;/strong&gt; pour l'installation. En effet le VPS existait déjà : il n’y avait rien à provisionner, seulement K3s à installer. De plus le projet &lt;a href="https://docs.k3s.io/" rel="noopener noreferrer"&gt;K3s&lt;/a&gt; maintient une collection, &lt;a href="https://github.com/k3s-io/k3s-ansible" rel="noopener noreferrer"&gt;k3s-ansible&lt;/a&gt;, qui automatise la création du cluster. Je n’ai qu’à décrire mon serveur et à lancer le playbook. Si j’avais dû créer le serveur, &lt;strong&gt;Terraform&lt;/strong&gt; avec &lt;strong&gt;cloud-init&lt;/strong&gt; aurait été le bon choix. Ce choix m'a poussé à modifier légèrement mon schéma d'architecture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbdrfc5dh6uxeqqy4ojg5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbdrfc5dh6uxeqqy4ojg5.png" alt="Nouvelle architecture" width="799" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;J'ai créé un nouveau &lt;a href="https://gitlab.com/hanatole/bmi-cluster" rel="noopener noreferrer"&gt;projet Ansible&lt;/a&gt; en dehors de &lt;strong&gt;BMI&lt;/strong&gt; pour installer le cluster et conserver les templates des manifests de déploiement. Il est géré avec &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt;, un gestionnaire de paquets et d’environnements Python rapide. Il isole les dépendances, facilite la gestion des versions et évite les conflits avec le système.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="s"&gt;name = "bmi-cluster"&lt;/span&gt;
&lt;span class="s"&gt;version = "0.1.0"&lt;/span&gt;
&lt;span class="s"&gt;description = "Create BMI cluster and manage deployments"&lt;/span&gt;
&lt;span class="s"&gt;requires-python = "&amp;gt;=3.14"&lt;/span&gt;
&lt;span class="s"&gt;dependencies = [&lt;/span&gt;
    &lt;span class="s"&gt;"ansible&amp;gt;=14.5.0",&lt;/span&gt;
    &lt;span class="s"&gt;"ansible-lint&amp;gt;=26.9.0",&lt;/span&gt;
    &lt;span class="s"&gt;"netaddr&amp;gt;=1.3.0",&lt;/span&gt;
&lt;span class="err"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ensuite, j'ai créé un fichier &lt;a href="https://gitlab.com/hanatole/bmi-cluster/-/blob/399bbb36068b1bcfb868f76edd2848f11d0681e9/requirements.yml" rel="noopener noreferrer"&gt;requirements.yml&lt;/a&gt; qui référence la collection k3s-ansible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;collections&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/k3s-io/k3s-ansible.git&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;git&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.2.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Son installation est très simple :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run ansible-galaxy &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le fichier &lt;strong&gt;inventory.yml&lt;/strong&gt; définit les paramètres de connexion SSH au VPS, qui sont lus dans les variables d’environnement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;k3s_cluster&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;vars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ansible_host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup('env',&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'CLUSTER_HOST')&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;ansible_port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup('env',&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'CLUSTER_PORT')&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;ansible_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup('env',&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'CLUSTER_USER')&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;api_endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup('env',&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'CLUSTER_HOST')&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;k3s_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1.37.0+k3s1&lt;/span&gt;
  &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;staging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La création du cluster tient en une commande :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run ansible-playbook &lt;span class="nt"&gt;-i&lt;/span&gt; inventory.yml k3s.orchestration.site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Les captures suivantes montrent le début et la fin de l'installation. Le warning sur la deuxième capture s'explique par le fait que j'ai un cluster mono-noeud : juste le server, pas d'agent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmifmra2jq546l4dx9bmp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmifmra2jq546l4dx9bmp.png" alt="Début de l'installation" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvo8jq6rokb2m1o8lnjnq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvo8jq6rokb2m1o8lnjnq.png" alt="Fin de l'installation" width="799" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;À la fin de l'installation, j'ai vérifié le status du service systemd &lt;strong&gt;k3s&lt;/strong&gt;, du noeud et des pods.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flxkb4n55c98kxlfrl48k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flxkb4n55c98kxlfrl48k.png" alt="Service k3s status" width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw3nt8cfewecdqh683m7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw3nt8cfewecdqh683m7n.png" alt="Node + Pods" width="800" height="273"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Le playbook &lt;a href="https://github.com/k3s-io/k3s-ansible" rel="noopener noreferrer"&gt;k3s-ansible&lt;/a&gt; crée aussi sur le &lt;strong&gt;control node&lt;/strong&gt;, un fichier &lt;strong&gt;~/.kube/config.new&lt;/strong&gt; qui donne un accès admin au cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Déployer BMI depuis le pipeline
&lt;/h2&gt;

&lt;p&gt;Côté &lt;a href="https://gitlab.com/hanatole/bmi" rel="noopener noreferrer"&gt;BMI&lt;/a&gt;, j’ai modifié le pipeline pour déployer sur le cluster. Il récupère le fichier &lt;a href="https://gitlab.com/hanatole/bmi-cluster/-/blob/108bc8f306bd4d6800bef8debea6e30b3dff4ad2/manifests/bmi.tpl.yml" rel="noopener noreferrer"&gt;bmi.tpl.yml&lt;/a&gt; qui contient des templates de &lt;strong&gt;namespace&lt;/strong&gt;, de &lt;strong&gt;deployment&lt;/strong&gt;, de &lt;strong&gt;service&lt;/strong&gt; et d' &lt;strong&gt;ingress&lt;/strong&gt;. Il y injecte des paramètres pour générer un manifest Kubernetes, l’applique, vérifie le déploiement et sauvegarde le manifest dans les artifacts. Voici un extrait des templates :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Namespace&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Namespace&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${APP_NAMESPACE}&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="c1"&gt;# Service&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bmi&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${APP_NAMESPACE}&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bmi&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le manifest complet est dans le &lt;a href="https://gitlab.com/hanatole/bmi-cluster/-/blob/20072693e69c6e66aeb73f8fbf7c3f4549dcbc84/manifests/bmi.tpl.yml" rel="noopener noreferrer"&gt;dépôt&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Le déploiement se fait automatiquement dans l'environnement &lt;strong&gt;staging&lt;/strong&gt; quand je pousse sur la branche &lt;strong&gt;main&lt;/strong&gt;. Pour la &lt;strong&gt;production&lt;/strong&gt;, il faut un &lt;strong&gt;tag&lt;/strong&gt; et un &lt;strong&gt;clic manuel&lt;/strong&gt;. Voici un extrait du &lt;a href="https://gitlab.com/hanatole/bmi/-/blob/231710d0d05b0c2dcd76f7234289552bd28aaf1a/cicd/deploy.yml" rel="noopener noreferrer"&gt;pipeline de déploiement&lt;/a&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;deploy-staging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.deploy&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;staging&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_BRANCH&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$CI_DEFAULT_BRANCH'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;never&lt;/span&gt;

&lt;span class="na"&gt;deploy-production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.deploy&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_TAG'&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manual&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;never&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le &lt;strong&gt;kubeconfig&lt;/strong&gt; est stocké dans une variable GitLab secrète et masquée (&lt;strong&gt;KUBECONFIG_B64&lt;/strong&gt;), encodée en base64. Il n’apparaît donc jamais dans les logs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy9qtxs090uk1a4b7w2th.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy9qtxs090uk1a4b7w2th.png" alt="Extrait du pipeline" width="752" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Difficultés
&lt;/h2&gt;

&lt;p&gt;Ma principale difficulté a été de choisir les bonnes valeurs pour les ressources (&lt;strong&gt;cpu&lt;/strong&gt; et &lt;strong&gt;memory&lt;/strong&gt;) du Deployment, ainsi que les paramètres des probes (&lt;strong&gt;readinessProbe&lt;/strong&gt; et &lt;strong&gt;livenessProbe&lt;/strong&gt;). Pour l’instant, mes valeurs sont des estimations. Je vais les ajuster en observant la consommation réelle de BMI avec le monitoring, et en testant les probes sur le temps de démarrage de l’application.&lt;/p&gt;

&lt;p&gt;Une autre est liée aux limites des runners du compte gratuit sur GitLab : mon pipeline reste parfois plus de 30 minutes en attente avant de démarrer. Je compte donc déployer mon propre runner dans le cluster Kubernetes et l'utiliser les prochaines fois.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prochaines étapes
&lt;/h2&gt;

&lt;p&gt;Rien n’a cassé, mais un cluster qui répond ne fait pas un service exploité. La semaine prochaine, je travaillerai sur le monitoring du cluster et de l’application, puis sur la centralisation des logs.&lt;/p&gt;

&lt;p&gt;Je suis conscient que le &lt;strong&gt;kubeconfig&lt;/strong&gt; actuellement donne un accès admin au cluster, ce qui est trop large pour un simple déploiement. Je vais créer un ServiceAccount limité au namespace de BMI, un par environnement.  Et vous, comment sécurisez-vous l’accès de vos pipelines à votre cluster ?&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>ansible</category>
      <category>cicd</category>
      <category>french</category>
    </item>
    <item>
      <title>De mon application Go à un service exploité sur Kubernetes : Point de départ et objectifs de mon mentorat</title>
      <dc:creator>Anatole HAGBE</dc:creator>
      <pubDate>Sun, 04 Oct 2026 17:00:46 +0000</pubDate>
      <link>https://dev.to/hanatole/de-mon-application-go-a-un-service-exploite-sur-kubernetes-point-de-depart-et-objectifs-de-mon-2g51</link>
      <guid>https://dev.to/hanatole/de-mon-application-go-a-un-service-exploite-sur-kubernetes-point-de-depart-et-objectifs-de-mon-2g51</guid>
      <description>&lt;p&gt;J'ai créé BMI, une application qui calcule l'indice de masse corporelle. Le sujet concerne beaucoup de monde, mais surtout, c'est un support que je maîtrise de bout en bout pour travailler ce qui m'intéresse vraiment : l'exploitation d'un service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5v42yt1v0y3ucn7t9asi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5v42yt1v0y3ucn7t9asi.png" alt="BMI Form" width="548" height="663"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcwfb6e2kdnolswpvla6x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcwfb6e2kdnolswpvla6x.png" alt="BMI Response" width="548" height="663"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Les captures d'écran montrent l'état actuel de BMI. Un formulaire demande le nom, la taille et le poids. Une fois validé, un modal affiche l'IMC, la catégorie correspondante (normal, surpoids, etc.) et un court message, par exemple une invitation à revoir son alimentation. Aucune donnée n'est stockée, il n'y a pas d'authentification et aucun secret à gérer. Le code est en Go, avec la seule bibliothèque standard pour l'instant. Plus tard, j'imagine un compte utilisateur, un historique pour suivre son évolution et des notifications.&lt;/p&gt;

&lt;p&gt;Côté DevOps, il y a déjà une base. Le code est sur &lt;a href="https://gitlab.com/hanatole/bmi" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;, et un pipeline exécute les tests unitaires, construit l'image Docker, la scanne, puis la pousse sur Docker Hub. Un &lt;a href="https://bmi-staging.bitnoises.com" rel="noopener noreferrer"&gt;environnement de test&lt;/a&gt; tourne sur AWS et l'application expose un healthcheck. En revanche, il n'y a pas encore de supervision. C'est là que se situe la différence entre une application qui fonctionne et un service exploitable.&lt;/p&gt;

&lt;p&gt;BMI n'a jamais été soumise à de vraies conditions de production. Je ne sais pas encore comment elle réagit quand un déploiement échoue, quand un pod redémarre ou quand il faut comprendre une panne à partir de ce qu'on observe plutôt que de ce qu'on suppose. Un healthcheck dit si le service répond, pas pourquoi il ne répond plus.&lt;/p&gt;

&lt;p&gt;Pour travailler ces questions, je serai accompagné pendant quatre semaines (deux séances de deux heures par semaine) par &lt;a href="https://www.linkedin.com/in/edouardhinvi/" rel="noopener noreferrer"&gt;Edouard HINVI&lt;/a&gt;, Senior Ingénieur DevOps et mentor chez &lt;a href="https://helpallskills.com/" rel="noopener noreferrer"&gt;Help All Skills&lt;/a&gt; qui organise cet accompagnement. Nous avons posé un backlog dès la première séance. Je joue à la fois le rôle de développeur et de DevOps, mais le mentorat cible uniquement le second.&lt;/p&gt;

&lt;p&gt;Cette semaine, j’ai mis à jour &lt;a href="https://gitlab.com/hanatole/bmi/-/blob/81c167ba17a6f2f43baa9c9d7714455b8dae3839/README.md" rel="noopener noreferrer"&gt;la documentation&lt;/a&gt; de BMI en y ajoutant, entre autres, le schéma de l’architecture cloud. J’ai opté pour un cluster K3s mono-nœud, compte tenu de mon budget limité. J’ai enfin mis en place le pipeline qui construit l’image, la scanne, puis la pousse dans le registre Docker Hub.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F04l2n0vd9m12vref1y4s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F04l2n0vd9m12vref1y4s.png" alt="Cluster architecture" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;La semaine prochaine, je créerai le cluster avec Terraform, y déploierai BMI et mettrai l’application en ligne en HTTPS.&lt;/p&gt;

&lt;p&gt;Je passe mes journées à découvrir de nouveaux outils, à lire et à pratiquer, notamment en m’appuyant sur le blog de &lt;a href="https://www.linkedin.com/in/stephanerobert1/" rel="noopener noreferrer"&gt;Stéphane ROBERT&lt;/a&gt;. Mais manipuler Kubernetes ne signifie pas encore savoir réellement l’exploiter. C’est précisément cet écart que je veux combler. C’est ambitieux pour quatre semaines, et je ne sais pas encore si tout tiendra dans le délai imparti. Les inconnues seront listées dès la première étape. Je publierai un article par semaine sur ce qui avance, mais aussi sur ce qui résiste.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>cloud</category>
      <category>go</category>
    </item>
    <item>
      <title>Running Sonatype Nexus 3 on a 1 Gi RAM VPS — A Practical Guide</title>
      <dc:creator>Anatole HAGBE</dc:creator>
      <pubDate>Sun, 05 Apr 2026 12:59:40 +0000</pubDate>
      <link>https://dev.to/hanatole/running-sonatype-nexus-3-on-a-1-gi-ram-vps-a-practical-guide-3bec</link>
      <guid>https://dev.to/hanatole/running-sonatype-nexus-3-on-a-1-gi-ram-vps-a-practical-guide-3bec</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — I self-host a private Docker registry and artifact store using Sonatype Nexus 3 on a VPS with only 1 Gi of RAM and 25 Gi of SSD. This post covers every decision made, every pitfall hit, and every config line tuned to make it work reliably in production.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why Self-Host a Registry?
&lt;/h2&gt;

&lt;p&gt;Every time I push an image to Docker Hub on a free plan, I think about rate limits, retention policies, and the slow creep of vendor lock-in. For personal and small team projects, a self-hosted registry gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No rate limits&lt;/strong&gt; on pulls (critical in CI/CD pipelines)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private images&lt;/strong&gt; without paying for a cloud registry&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A single artifact store&lt;/strong&gt; for Docker images, npm packages, Maven artifacts, and more — all under one roof&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full control&lt;/strong&gt; over retention and access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The catch: &lt;code&gt;Sonatype Nexus 3&lt;/code&gt; is a Java application. It was built for enterprise servers, not budget VPS instances. The official documentation recommends a minimum of 8 Gi of RAM. Running it on 1 Gi is a constraint-driven engineering problem — and those are my favourite kind.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Infrastructure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| Component     | Details                                                           |
| ------------- | ------------------------------------------------------------------|
| VPS           | 1 vCPU, 1 Gi RAM, 25 Gi SSD                                       |
| OS            | Ubuntu 22.04 LTS                                                  |
| Nexus version | 3.90.2-alpine                                                     |
| Database      | PostgreSQL (external, on the same host)                           |
| Reverse proxy | Traefik v3 with automatic TLS                                     |
| DNS           | `repository.bitnoises.com` (UI), `registry.bitnoises.com` (Docker)|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbipepshxju8e41m6fdqq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbipepshxju8e41m6fdqq.png" alt="Architecture" width="800" height="730"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Traefik handles all TLS termination. Nexus never sees raw HTTPS traffic — it only speaks plain HTTP internally, which simplifies configuration considerably.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Memory Problem
&lt;/h2&gt;

&lt;p&gt;Here is the honest picture of my RAM budget before Nexus even starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total RAM:              957 Mi
OS + kernel:           ~150 Mi
Docker daemon:          ~50 Mi
Traefik:                ~30 Mi
PostgreSQL:             ~80 Mi
─────────────────────────────
Available for Nexus:   ~647 Mi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nexus is a JVM application. Its memory footprint has two main components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Heap&lt;/strong&gt; (&lt;code&gt;-Xmx&lt;/code&gt;) — object allocations, managed by the garbage collector&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct memory&lt;/strong&gt; (&lt;code&gt;-XX:MaxDirectMemorySize&lt;/code&gt;) — off-heap buffers, used heavily for I/O&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The default Sonatype recommendation is &lt;code&gt;-Xmx2703m&lt;/code&gt;. On my VPS, that would require 4× the available RAM. The JVM would immediately start swapping and the container would be OOM-killed within minutes.&lt;/p&gt;

&lt;p&gt;The solution is aggressive but careful downsizing.&lt;/p&gt;

&lt;h3&gt;
  
  
  JVM Tuning
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-Xms128m                      # Start heap small, grow as needed
-Xmx384m                      # Hard heap ceiling
-XX:MaxDirectMemorySize=192m  # Off-heap buffer ceiling
-XX:+UseG1GC                  # Better GC under memory pressure
-XX:MaxGCPauseMillis=300      # GC pause target
-XX:G1HeapRegionSize=4m       # Smaller regions = less wasted space
-XX:+UseStringDeduplication   # G1 deduplicates identical strings (~5-10% heap savings)
-XX:SoftRefLRUPolicyMSPerMB=0 # Aggressively clear soft references under pressure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight is separating &lt;code&gt;-Xms&lt;/code&gt; from &lt;code&gt;-Xmx&lt;/code&gt;. Starting at 128m means the JVM consumes minimal RAM on boot and only grows the heap as Nexus actually needs it. On a quiet personal registry, it rarely needs to grow much past 250–300m in practice.&lt;/p&gt;

&lt;p&gt;Total JVM footprint: ~680m. Docker &lt;code&gt;mem_limit&lt;/code&gt; is set to 700m, leaving a 20m buffer for JVM overhead variability.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Swap Imperative
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Never run a JVM application on a machine with no swap.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Swap on SSD is not a performance strategy — it is a safety net. Without it, the Linux OOM killer will terminate your container the instant it crosses the memory limit, with no warning and no graceful shutdown. Nexus does not handle abrupt kills well; you risk database corruption or a blob store in an inconsistent state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fallocate &lt;span class="nt"&gt;-l&lt;/span&gt; 2G /swapfile
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /swapfile
mkswap /swapfile
swapon /swapfile
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'/swapfile none swap sw 0 0'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/fstab

&lt;span class="c"&gt;# Only use swap as a last resort&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'vm.swappiness=10'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/sysctl.conf
sysctl &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;vm.swappiness=10&lt;/code&gt;, Linux strongly prefers to keep data in RAM and only swills to the swapfile under real pressure. The SSD takes a minor hit, but your service survives spikes.&lt;/p&gt;

&lt;p&gt;After adding swap, my memory picture looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;free &lt;span class="nt"&gt;-h&lt;/span&gt;
               total   used    free    buff/cache  available
Mem:           957Mi   194Mi   91Mi    671Mi       594Mi
Swap:          2.0Gi   125Mi   1.9Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;buff/cache&lt;/code&gt; column (671Mi) looks alarming but is not — Linux uses free RAM as disk cache, and that cache is immediately evictable the moment Nexus needs it. The &lt;code&gt;available&lt;/code&gt; column (594Mi) is the number that matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  The compose.yml
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nexus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sonatype/nexus3:3.90.2-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nexus&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;200:200"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;INSTALL4J_ADD_VM_PARAMS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
        &lt;span class="s"&gt;-Xms128m&lt;/span&gt;
        &lt;span class="s"&gt;-Xmx384m&lt;/span&gt;
        &lt;span class="s"&gt;-XX:MaxDirectMemorySize=192m&lt;/span&gt;
        &lt;span class="s"&gt;-XX:+UseG1GC&lt;/span&gt;
        &lt;span class="s"&gt;-XX:MaxGCPauseMillis=300&lt;/span&gt;
        &lt;span class="s"&gt;-XX:G1HeapRegionSize=4m&lt;/span&gt;
        &lt;span class="s"&gt;-XX:+UseStringDeduplication&lt;/span&gt;
        &lt;span class="s"&gt;-XX:SoftRefLRUPolicyMSPerMB=0&lt;/span&gt;
        &lt;span class="s"&gt;-Djava.util.prefs.userRoot=/nexus-data/javaprefs&lt;/span&gt;
        &lt;span class="s"&gt;-Dnexus.datastore.enabled=true&lt;/span&gt;
        &lt;span class="s"&gt;-Dnexus-ssl-proxy=true&lt;/span&gt;
      &lt;span class="na"&gt;NEXUS_DATASTORE_NEXUS_JDBCURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}&lt;/span&gt;
      &lt;span class="na"&gt;NEXUS_DATASTORE_NEXUS_USERNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_USER}&lt;/span&gt;
      &lt;span class="na"&gt;NEXUS_DATASTORE_NEXUS_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./data:/nexus-data"&lt;/span&gt;
    &lt;span class="na"&gt;mem_limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;700m&lt;/span&gt;
    &lt;span class="na"&gt;memswap_limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1400m&lt;/span&gt;      &lt;span class="c1"&gt;# 700m RAM + 700m swap headroom&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-sf&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://localhost:8081/service/rest/v1/status&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;180s&lt;/span&gt;      &lt;span class="c1"&gt;# Nexus is slow to boot on constrained hardware&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefiknetwork&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;infra&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.docker.network=traefiknetwork"&lt;/span&gt;
      &lt;span class="c1"&gt;# UI&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nexus-ui.rule=Host(`${NEXUS_HOST}`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nexus-ui.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nexus-ui.tls=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nexus-ui.tls.certresolver=letsencrypt"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nexus-ui.service=nexus-ui"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.nexus-ui.loadbalancer.server.port=8081"&lt;/span&gt;
      &lt;span class="c1"&gt;# Docker registry&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nexus-docker.rule=Host(`${REGISTRY_HOST}`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nexus-docker.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nexus-docker.tls=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nexus-docker.tls.certresolver=letsencrypt"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nexus-docker.service=nexus-docker"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.nexus-docker.loadbalancer.server.port=5000"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.docker-headers.headers.customrequestheaders.Docker-Distribution-Api-Version=registry/2.0"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.nexus-docker-buffering.buffering.maxRequestBodyBytes=0"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nexus-docker.middlewares=docker-headers,nexus-docker-buffering"&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefiknetwork&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;infra&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Things worth calling out
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;user: "200:200"&lt;/code&gt;&lt;/strong&gt; — The Nexus image internally runs as UID 200. Explicitly setting this prevents accidental root execution. The &lt;code&gt;./data&lt;/code&gt; directory must be pre-owned: &lt;code&gt;sudo chown -R 200:200 ./data&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;start_period: 180s&lt;/code&gt;&lt;/strong&gt; — Without this, Docker marks the container unhealthy before it has finished booting, which can trigger restart loops. On constrained hardware, Nexus takes 2–3 minutes to start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;maxRequestBodyBytes=0&lt;/code&gt;&lt;/strong&gt; — The single most important Traefik label for a Docker registry. Without it, pushing any image layer larger than Traefik's default body size limit (2m) will fail with a cryptic 413 error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;-Dnexus-ssl-proxy=true&lt;/code&gt;&lt;/strong&gt; — Tells Nexus it is behind a TLS-terminating proxy. Without this, the UI generates incorrect &lt;code&gt;http://&lt;/code&gt; URLs in some contexts.&lt;/p&gt;




&lt;h2&gt;
  
  
  PostgreSQL vs Embedded Database
&lt;/h2&gt;

&lt;p&gt;Nexus 3 supports two database backends: an embedded H2 database and external PostgreSQL. I chose PostgreSQL for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reliability&lt;/strong&gt; — H2 is fine for development, but I've seen it corrupt under abrupt JVM kills. PostgreSQL handles dirty shutdowns gracefully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt; — I can query the database directly to inspect state, run backups, and monitor connection counts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt; — PostgreSQL is already running on my infrastructure for other services. One less moving part.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff: there is no &lt;code&gt;admin.password&lt;/code&gt; file on a PostgreSQL-backed install. The default credentials are simply &lt;code&gt;admin&lt;/code&gt; / &lt;code&gt;admin123&lt;/code&gt;, and Nexus forces a password change on first login.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuring the Docker Registry
&lt;/h2&gt;

&lt;p&gt;After the first login:&lt;/p&gt;

&lt;h3&gt;
  
  
  Enable the Bearer Token Realm
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Administration&lt;/code&gt; → &lt;code&gt;Security&lt;/code&gt; → &lt;code&gt;Realms&lt;/code&gt; → move &lt;strong&gt;Docker Bearer Token Realm&lt;/strong&gt; to Active.&lt;/p&gt;

&lt;p&gt;This is the single most common cause of &lt;code&gt;401 Unauthorized&lt;/code&gt; errors when using &lt;code&gt;docker login&lt;/code&gt;. It must be enabled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the Repository
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Administration&lt;/code&gt; → &lt;code&gt;Repository&lt;/code&gt; → &lt;code&gt;Repositories&lt;/code&gt; → &lt;strong&gt;Create repository&lt;/strong&gt; → &lt;strong&gt;docker (hosted)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set the HTTP port to &lt;code&gt;5000&lt;/code&gt; (this is what Traefik will route to), leave HTTPS unchecked (Traefik handles that), and set the deployment policy to your preference.&lt;/p&gt;




&lt;h2&gt;
  
  
  CI/CD Integration
&lt;/h2&gt;

&lt;p&gt;I created a dedicated &lt;code&gt;ci&lt;/code&gt; user with a minimal role, following the principle of least privilege. The role has only the privileges needed to push and pull from the Docker repository — no admin access, no access to other repositories.&lt;/p&gt;

&lt;p&gt;In GitLab CI/CD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;push&lt;/span&gt;

&lt;span class="na"&gt;push-alpine-to-nexus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;push&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker:29.2.1&lt;/span&gt;
  &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker:29.2.1-dind&lt;/span&gt;

  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DOCKER_TLS_CERTDIR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;

  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Log in to the private registry&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "$NEXUS_PASSWORD" | docker login registry.bitnoises.com \&lt;/span&gt;
        &lt;span class="s"&gt;--username "$NEXUS_USER" \&lt;/span&gt;
        &lt;span class="s"&gt;--password-stdin&lt;/span&gt;

    &lt;span class="c1"&gt;# Pull Alpine image from Docker Hub&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker pull alpine:latest&lt;/span&gt;

    &lt;span class="c1"&gt;# Tag the image for the private registry&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker tag alpine:latest registry.bitnoises.com/alpine:latest&lt;/span&gt;

    &lt;span class="c1"&gt;# Push the image to Nexus&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker push registry.bitnoises.com/alpine:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the commit SHA as the image tag instead of &lt;code&gt;latest&lt;/code&gt; gives you full traceability — every deployment can be traced back to an exact commit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Disk Management
&lt;/h2&gt;

&lt;p&gt;The SSD is the other constraint. 25 Gi sounds like a lot until you start storing Docker images — a typical Node.js app image is 200–400 Mi, and you accumulate many versions fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleanup Policies
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;Administration&lt;/code&gt; → &lt;code&gt;Repository&lt;/code&gt; → &lt;code&gt;Cleanup Policies&lt;/code&gt;, I created a policy that removes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Components older than &lt;strong&gt;30 days&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Components not downloaded in &lt;strong&gt;14 days&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Attached to the &lt;code&gt;docker-hosted&lt;/code&gt; repository and scheduled as a weekly task, this keeps the blob store from growing unbounded.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitoring
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# How much is Nexus actually using?&lt;/span&gt;
&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; ./data/blobs/

&lt;span class="c"&gt;# Docker layer cache on the host&lt;/span&gt;
docker system &lt;span class="nb"&gt;df&lt;/span&gt;

&lt;span class="c"&gt;# Full picture&lt;/span&gt;
&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;JVM memory is not just heap.&lt;/strong&gt; Many guides say "set &lt;code&gt;-Xmx&lt;/code&gt; to half your RAM" without mentioning direct memory, metaspace, code cache, or JVM thread stacks. The real footprint is heap + direct + ~100m of JVM internals. Budget for all of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Swap before anything else.&lt;/strong&gt; I almost deployed without swap because "SSD swap is slow." SSD swap at 10% swappiness is effectively never used during normal operation, but it has saved me from OOM kills more than once during Nexus startup when memory pressure is highest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traefik label order matters.&lt;/strong&gt; The &lt;code&gt;middlewares&lt;/code&gt; label must reference middleware names defined in other labels in the same service. If you define &lt;code&gt;docker-headers&lt;/code&gt; and &lt;code&gt;nexus-docker-buffering&lt;/code&gt; but only reference one in the router label, the other silently does nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;start_period&lt;/code&gt; is not optional for slow services.&lt;/strong&gt; Docker's healthcheck &lt;code&gt;start_period&lt;/code&gt; is the grace period before failed checks count against &lt;code&gt;retries&lt;/code&gt;. For a service that takes 2–3 minutes to boot, setting this to &lt;code&gt;30s&lt;/code&gt; means Docker will restart the container before it has even finished starting — creating an infinite restart loop that looks like a memory issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL default credentials are not in the logs.&lt;/strong&gt; Coming from H2-backed Nexus where an &lt;code&gt;admin.password&lt;/code&gt; file is generated, this caught me off guard. The PostgreSQL-backed install simply uses &lt;code&gt;admin&lt;/code&gt; / &lt;code&gt;admin123&lt;/code&gt; with no file or log entry indicating this.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;A fully functional private Docker registry and artifact store, running reliably on hardware that costs a few euros a month. Memory usage in steady state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker stats nexus &lt;span class="nt"&gt;--no-stream&lt;/span&gt;
CONTAINER   CPU %   MEM USAGE / LIMIT   MEM %
nexus       0.3%    412MiB / 700MiB     58.9%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;58% memory utilisation at idle, with 42% headroom before the hard limit and 700m of swap available beyond that. For a personal infrastructure project, that is a comfortable margin.&lt;/p&gt;

&lt;p&gt;The full configuration, scripts, and documentation are available on GitHub: &lt;a href="https://gitlab.com/hanatole/nexus" rel="noopener noreferrer"&gt;gitlab.com/hanatole/nexus&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have questions or improvements? Open an issue on the repository.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>cicd</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
