diff --git a/blog/2026-02-10-dockhand-interface-docker-git.md b/blog/2026-02-10-dockhand-interface-docker-git.md new file mode 100644 index 0000000..45ec77f --- /dev/null +++ b/blog/2026-02-10-dockhand-interface-docker-git.md @@ -0,0 +1,480 @@ +--- +slug: dockhand-interface-docker-git +title: "Dockhand - Une interface Docker pour tout gérer via Git !" +authors: [tellserv] +tags: [docker, git, dockhand, homelab, infrastructure, secrets] +date: 2026-02-10 +--- + +Comment j'ai centralisé la gestion de toutes mes stacks Docker avec Dockhand, un outil moderne qui combine l'interface web intuitive, le versioning Git et la gestion sécurisée des secrets. + +

+ Dockhand Logo +

+ + + +## Le problème : mes stacks sans historique ni centralisation + +Quand on gère plusieurs dizaines de services Docker sur plusieurs machines, on se retrouve vite avec un problème : **comment garder une trace de tout ?** + +Avant Dockhand, ma situation ressemblait à ça : + +- Des fichiers `compose.yaml` éparpillés dans `/opt/stacks/` sur différentes VMs +- Aucun historique des modifications (qui a changé quoi ? quand ? pourquoi ?) +- Des secrets en clair dans les fichiers `.env` +- Impossible de déployer rapidement la même stack sur une autre machine +- Pas de centralisation : je devais SSH sur chaque machine pour modifier une config + +Bref, c'était le chaos. Et si je perdais une VM, je perdais aussi tout l'historique de configuration. + +## La solution : Git comme source of truth + Dockhand pour déployer + +La solution est simple et élégante : + +1. **Git devient la source de vérité** : tous mes `compose.yaml` sont versionnés dans un dépôt Git privé (Forgejo dans mon cas) +2. **Dockhand gère les déploiements** : une interface web moderne qui déploie depuis Git et gère les secrets de manière sécurisée +3. **Fini les secrets en clair** : Dockhand chiffre les secrets et les injecte au déploiement + +Cette approche me donne : + +- **Historique complet** : chaque modification est tracée dans Git +- **Centralisation** : un seul endroit pour gérer toutes mes stacks +- **Sécurité** : les secrets ne sont jamais committés en clair +- **Multi-environnements** : je peux gérer plusieurs VMs depuis une seule interface +- **Reproductibilité** : je peux redéployer n'importe quelle stack en quelques clics + +**Flux GitOps** : + +```mermaid +graph TB + A[Développeur] -->|1. Push code| B[Git Repository] + B -->|2. Webhook/Sync| C[Dockhand] + C -->|3. Clone repo| B + C -->|4. Inject secrets| D[Secrets chiffrés] + C -->|5. Deploy| E[Docker Compose] + E -->|6. Containers| F[Applications] + + style A fill:#e1f5ff + style B fill:#ffe1e1 + style C fill:#fff4e1 + style D fill:#e1ffe1 + style E fill:#f0e1ff + style F fill:#e1f5ff +``` + +## Qu'est-ce que Dockhand ? + +[Dockhand](https://github.com/Finsys/dockhand) est une interface de gestion Docker moderne développée par Finsys. C'est une alternative légère et élégante à Portainer. + +### Fonctionnalités principales + +- **Gestion des conteneurs** : démarrer, arrêter, redémarrer, surveiller en temps réel +- **Orchestration Compose** : éditeur visuel pour les déploiements Docker Compose +- **Intégration Git** : déploiement depuis des dépôts avec webhooks et synchronisation automatique +- **Support multi-environnements** : gestion des hôtes Docker locaux et distants +- **Terminal et logs** : accès shell interactif et streaming de logs en temps réel +- **Explorateur de fichiers** : navigation, upload et téléchargement depuis les conteneurs +- **Gestion des secrets** : chiffrement et injection sécurisée des variables sensibles + +### Stack technologique + +- **Frontend** : SvelteKit 2, Svelte 5, shadcn-svelte, TailwindCSS +- **Backend** : Runtime Bun avec routes API SvelteKit +- **Base** : SQLite ou PostgreSQL via Drizzle ORM +- **Infrastructure** : Communication directe avec l'API Docker + +### Licence + +Dockhand utilise la Business Source License 1.1 (BSL) : +- Gratuit pour : usage personnel, usage interne en entreprise, organisations à but non lucratif, éducation, évaluation +- La licence sera convertie en Apache 2.0 le 1er janvier 2029 + +## Mise en place de Dockhand + +### Étape 1 : Socket Proxy (sécurité) + +Avant de déployer Dockhand, j'utilise un **socket proxy** pour éviter d'exposer directement le socket Docker aux applications. C'est un principe de moindre privilège : chaque service ne peut accéder qu'aux endpoints Docker dont il a besoin. + +
+Configuration du Socket Proxy (cliquez pour déplier) + +```yaml +services: + socket-proxy: + image: wollomatic/socket-proxy:1.11.0 + container_name: socket-proxy + restart: unless-stopped + user: "65534:988" # nobody:docker + mem_limit: 64M + read_only: true + cap_drop: + - ALL + security_opt: + - no-new-privileges + command: + - '-loglevel=info' + - '-listenip=0.0.0.0' + - '-proxycontainername=socket-proxy' # Active les allowlists per-container + - '-watchdoginterval=3600' + - '-stoponwatchdog' + - '-shutdowngracetime=5' + environment: + - SP_ALLOWHEALTHCHECK=true + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - socket-proxy + healthcheck: + test: ["CMD", "./healthcheck"] + interval: 10s + timeout: 5s + retries: 2 + +networks: + socket-proxy: + name: socket-proxy + driver: bridge + internal: true +``` + +Je détaillerai le socket proxy dans un article dédié. Pour l'instant, retenez que c'est une couche de sécurité entre Docker et vos applications. + +
+ +**Architecture de sécurité** : + +```mermaid +graph LR + A[Utilisateur] -->|HTTPS| B[Traefik] + B -->|HTTP interne| C[Dockhand] + C -->|API limitée| D[Socket Proxy] + D -->|Socket Unix| E[Docker Engine] + + style A fill:#e1f5ff + style B fill:#ffe1e1 + style C fill:#fff4e1 + style D fill:#e1ffe1 + style E fill:#f0e1ff +``` + +Ce schéma montre comment chaque couche ajoute une protection supplémentaire entre l'utilisateur et le Docker Engine. + +### Étape 2 : Déploiement de Dockhand + +Voici mon fichier `compose.yaml` pour Dockhand : + +```yaml +services: + dockhand: + image: fnsys/dockhand:v1.0.14 + container_name: dockhand + restart: unless-stopped + ports: + - "192.168.100.160:3001:3000" + networks: + - traefik_private + - socket-proxy + volumes: + - /opt/stacks/dockhand:/opt/stacks/dockhand + environment: + - DATA_DIR=/opt/stacks/dockhand + - TZ=Europe/Paris + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik_private" + - "traefik.http.routers.dockhand-local.rule=Host(`dockhand.local.tellserv.fr`)" + - "traefik.http.routers.dockhand-local.entrypoints=local" + - "traefik.http.routers.dockhand-local.tls=true" + - "traefik.http.routers.dockhand-local.tls.certresolver=cloudflare-local" + - "traefik.http.services.dockhand.loadbalancer.server.port=3000" + - "socket-proxy.allow.get=.*" + - "socket-proxy.allow.post=.*" + - "socket-proxy.allow.delete=.*" + - "socket-proxy.allow.head=.*" + +networks: + socket-proxy: + external: true + traefik_private: + external: true +``` + +**Points importants** : + +- **Version de l'image** : `v1.0.14` - Vérifiez la dernière version stable sur [Docker Hub](https://hub.docker.com/r/fnsys/dockhand/tags) avant de déployer +- **Port bindé sur IP locale** : `192.168.100.160:3001` - Adaptez à votre configuration : + - Remplacez par votre IP statique locale si vous utilisez Traefik sur la même machine + - Utilisez `127.0.0.1:3001:3000` si vous n'utilisez qu'en local sans reverse proxy distant + - **Évitez** `0.0.0.0` qui expose le service sur toutes les interfaces (risque de sécurité) + - Note : Le port `3001` est exposé sur l'hôte, mais Traefik communique en interne sur le port `3000` du conteneur +- **Socket proxy** : connexion via le réseau `socket-proxy` au lieu d'exposer directement `/var/run/docker.sock` +- **Traefik** : reverse proxy pour l'accès HTTPS avec certificat automatique +- **Permissions socket-proxy** : les labels `socket-proxy.allow.*` définissent les points de terminaison (endpoints) autorisés de l'API Docker + +:::warning[Sécurité] +Dockhand donne un accès complet à votre infrastructure Docker. **Ne l'exposez jamais publiquement** et utilisez toujours une authentification forte. Gardez ce service en interne uniquement. +::: + +:::tip[Ordre de démarrage] +Le réseau `socket-proxy` est déclaré comme `external: true`, ce qui signifie qu'il doit déjà exister. Assurez-vous de **démarrer la stack Socket Proxy AVANT** celle de Dockhand, sinon vous obtiendrez une erreur indiquant que le réseau externe est introuvable. +::: + +**Déploiement** : + +```bash +docker compose up -d +``` + +L'interface est maintenant accessible sur `https://dockhand.local.tellserv.fr` (dans mon cas). + +### Étape 3 : Ajout des environnements Docker + +Une fois Dockhand déployé, je configure les **environnements** (mes différentes VMs Docker). + +**Settings → Environments → Add environment** + +![Configuration d'un environnement dans Dockhand](/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_settings_environment.png) + +Trois types de connexion sont possibles : + +1. **Unix socket** : si vous avez passé directement le socket Docker au conteneur Dockhand (déconseillé) +2. **Direct connection** : connexion HTTP/HTTPS à l'API Docker (via socket proxy dans mon cas) +3. **Hawser agent (edge)** : connexion passive où c'est l'environnement distant qui initie la connexion (parfait pour les machines derrière NAT) + +Dans mon cas, j'utilise **Direct connection** avec mon socket proxy : + +![Ajout d'un environnement avec connexion directe](/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_add_environment.png) + +- **Name** : `Tellprod` +- **Connection type** : `Direct connection` +- **Host** : `socket-proxy` (le nom du conteneur) +- **Port** : `2375` +- **Protocol** : `HTTP` (le socket proxy n'utilise pas TLS en interne) + +## Préparation du dépôt Git + +Maintenant que Dockhand est opérationnel, je vais migrer toutes mes stacks Docker vers Git. + +### Étape 1 : Créer le dépôt Git + +J'ai créé un dépôt privé sur mon instance Forgejo : `tellprod_compose.git` + +### Étape 2 : Organiser les stacks + +Structure de mon dépôt : + +``` +tellprod_compose/ +├── mobilizon/ +│ └── compose.yml +├── audiobookshelf/ +│ └── compose.yml +├── freshrss/ +│ └── compose.yml +├── zabbix/ +│ └── compose.yml +└── ... +``` + +Chaque service a son propre dossier avec un fichier `compose.yml`. + +### Étape 3 : Supprimer les secrets + +**CRITIQUE** : avant de commit, je dois supprimer **tous les secrets** de mes fichiers `compose.yml` et `.env`. + +Les secrets seront gérés par Dockhand et injectés au moment du déploiement. + +Par exemple, au lieu de : + +```yaml +environment: + - POSTGRES_PASSWORD=SuperSecretPassword123 + - MOBILIZON_INSTANCE_EMAIL=contact@tellserv.fr +``` + +Je mets des variables : + +```yaml +environment: + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - MOBILIZON_INSTANCE_EMAIL=${MOBILIZON_INSTANCE_EMAIL} +``` + +### Étape 4 : Commit et push + +```bash +git add . +git commit -m "Initial commit: All Compose stacks, with secrets placeholders" +git push origin main +``` + +![Dépôt Git avec toutes les stacks Compose](/img/blog/2026-02-10-dockhand-interface-docker-git/repo_tellprod_compose.png) + +Toutes mes stacks sont maintenant versionnées et centralisées. + +:::tip[Protection supplémentaire avec .gitignore] +Pour éviter tout risque de commit accidentel de secrets, ajoutez un fichier `.gitignore` à la racine de votre dépôt : + +```gitignore +# Fichiers de secrets locaux +.env +*.env +**/.env +**/*.env + +# Fichiers temporaires +*.tmp +*.swp +*~ +``` + +Ainsi, même si vous oubliez de remplacer un secret par une variable, Git refusera de le committer. +::: + +## Configuration du dépôt Git dans Dockhand + +### Étape 1 : Ajouter les credentials (si dépôt privé) + +**Settings → Git → Credentials → Add credential** + +- **Type** : `Password` (pour Forgejo) +- **Name** : `Forgejo (Password)` +- **Username** : mon nom d'utilisateur Forgejo +- **Password** : mot de passe ou token API + +### Étape 2 : Ajouter le dépôt + +**Settings → Git → Repositories → Add repository** + +![Configuration du dépôt Git](/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_git_settings.png) + +![Ajout d'un dépôt Git dans Dockhand](/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_add_git_repo.png) + +- **Name** : `tellprod_compose` +- **Repository URL** : `https://forgejo.tellserv.fr/Tellsanguis/tellprod_compose.git` +- **Branch** : `main` +- **Credential** : sélectionner `Forgejo (Password)` + +Cliquer sur **Test** pour vérifier la connexion, puis **Save changes**. + +## Déploiement depuis Git + +Maintenant vient la partie magique : déployer mes stacks directement depuis Git. + +### Étape 1 : Arrêter l'ancienne stack + +Avant de migrer vers Dockhand, je dois arrêter mes anciennes stacks déployées manuellement : + +```bash +cd /opt/stacks/mobilizon +docker compose down +``` + +![Arrêt de la stack mobilizon](/img/blog/2026-02-10-dockhand-interface-docker-git/compose_down_mobilizon.png) + +### Étape 2 : Déployer depuis Git + +**Stacks → From Git** (bouton en haut à droite) + +![Bouton "From Git" pour déployer depuis un dépôt](/img/blog/2026-02-10-dockhand-interface-docker-git/from_git.png) + +**Configuration du déploiement** : + +![Déploiement de la stack Mobilizon depuis Git](/img/blog/2026-02-10-dockhand-interface-docker-git/mobilizon_deploy_from_git.png) + +1. **Repository** : sélectionner `tellprod_compose` +2. **Stack name** : `mobilizon` +3. **Compose file path** : `mobilizon/compose.yml` (chemin relatif dans le repo) +4. **Environment variables** : + - Cliquer sur **Populate** en haut à droite + - Dockhand remplit automatiquement les variables avec les valeurs du `.env` du dépôt (si présent) + - Modifier les valeurs avec les vraies valeurs + - **Cliquer sur l'icône de clé** à droite des variables sensibles pour les marquer comme secrets chiffrés + +5. **Enable scheduled sync** : activer pour synchroniser automatiquement avec Git + - **Daily** à **03:00** (par exemple) + - Dockhand vérifiera quotidiennement s'il y a des changements dans le repo et redéploiera si nécessaire + +6. **Enable webhook** : OFF pour l'instant (on peut le configurer plus tard pour un déploiement immédiat à chaque push Git) + +7. **Deploy now** : ON + +Cliquer sur **Deploy** et c'est parti. + +Dockhand va : +1. Cloner le dépôt Git +2. Lire le fichier `compose.yml` dans `mobilizon/` +3. Injecter les secrets chiffrés +4. Déployer la stack via `docker compose up -d` + +En quelques secondes, la stack est déployée. Et toutes les variables sensibles sont stockées chiffrées dans la base de données de Dockhand. + +## Résultat : gestion centralisée et sécurisée + +Après quelques heures de migration, toutes mes stacks sont maintenant gérées par Dockhand : + +- **Historique complet** : chaque modification est versionnée dans Git +- **Centralisation** : une seule interface pour gérer toutes mes VMs +- **Sécurité** : les secrets sont chiffrés dans Dockhand, jamais en clair dans Git +- **Synchronisation automatique** : Dockhand redéploie automatiquement si le repo Git change +- **Reproductibilité** : je peux redéployer n'importe quelle stack en quelques clics +- **Multi-environnements** : je peux gérer plusieurs VMs depuis la même interface + +### Avantages concrets + +**Git comme source of truth** : +- Je peux revenir en arrière si une modification casse quelque chose +- Je peux voir qui a modifié quoi et quand +- Je peux collaborer avec d'autres (pull requests, code review) + +**Dockhand comme orchestrateur** : +- Interface moderne et intuitive (bien mieux que l'ancienne Portainer) +- Gestion native des secrets chiffrés +- Support multi-environnements (je peux gérer plusieurs VMs) +- Synchronisation automatique avec Git +- Logs en temps réel, accès shell, explorateur de fichiers + +**Sécurité** : +- Plus de secrets en clair dans les fichiers +- Socket proxy pour limiter l'accès à l'API Docker +- Service non exposé publiquement + +## Prochaines étapes : automatisation avec Renovate Bot + +Pour aller encore plus loin, je vais configurer **Renovate Bot** pour automatiser les mises à jour de mes images Docker. + +### Pourquoi Renovate plutôt que Watchtower ? + +Beaucoup utilisent **Watchtower** pour mettre à jour automatiquement leurs conteneurs. Mais Watchtower a un défaut majeur : il met à jour directement les conteneurs en production, sans validation. + +Avec l'approche **GitOps + Renovate** : +- Renovate crée des **Pull Requests** dans Git avec les nouvelles versions +- Je peux **tester et valider** avant de merger +- Git garde l'**historique** de toutes les mises à jour +- Si une mise à jour casse quelque chose, je peux facilement **revenir en arrière** +- Dockhand redéploie automatiquement après chaque merge + +C'est beaucoup plus sûr et professionnel que les mises à jour "magiques" de Watchtower. + +### Configuration de Renovate + +Renovate va : +1. Analyser tous mes `compose.yml` +2. Détecter les nouvelles versions d'images disponibles +3. Créer automatiquement des Pull Requests dans Forgejo +4. Me permettre de valider et merger les mises à jour en un clic + +Avec Dockhand qui synchronise automatiquement depuis Git, mes stacks seront mises à jour automatiquement après chaque merge. + +Je détaillerai cette configuration dans un prochain article. En attendant, vous pouvez consulter [la documentation officielle de Renovate](https://docs.renovatebot.com/). + +## Conclusion + +Dockhand est un outil formidable pour centraliser et moderniser la gestion de ses stacks Docker. + +L'approche **Git + Dockhand** combine le meilleur des deux mondes : +- Git pour l'historique et la collaboration +- Dockhand pour la gestion des secrets et le déploiement + +Si vous gérez plusieurs services Docker, je vous recommande vivement de tester Dockhand. C'est simple, élégant, et ça change vraiment la façon de travailler. diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 568f367..497c1b1 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -4,6 +4,7 @@ import type * as Preset from '@docusaurus/preset-classic'; const config: Config = { themes: [ + '@docusaurus/theme-mermaid', [ '@easyops-cn/docusaurus-search-local', { diff --git a/i18n/en/docusaurus-plugin-content-blog/2026-02-10-dockhand-interface-docker-git.md b/i18n/en/docusaurus-plugin-content-blog/2026-02-10-dockhand-interface-docker-git.md new file mode 100644 index 0000000..f661a72 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-blog/2026-02-10-dockhand-interface-docker-git.md @@ -0,0 +1,480 @@ +--- +slug: dockhand-interface-docker-git +title: "Dockhand - A Docker Interface to Manage Everything via Git!" +authors: [tellserv] +tags: [docker, git, dockhand, homelab, infrastructure, secrets] +date: 2026-02-10 +--- + +How I centralized the management of all my Docker stacks with Dockhand, a modern tool that combines an intuitive web interface, Git versioning, and secure secrets management. + +

+ Dockhand Logo +

+ + + +## The problem: stacks without history or centralization + +When managing dozens of Docker services across multiple machines, you quickly face a problem: **how do you keep track of everything?** + +Before Dockhand, my situation looked like this: + +- `compose.yaml` files scattered across `/opt/stacks/` on different VMs +- No modification history (who changed what? when? why?) +- Secrets in clear text in `.env` files +- Impossible to quickly deploy the same stack on another machine +- No centralization: I had to SSH into each machine to modify a config + +In short, it was chaos. And if I lost a VM, I also lost the entire configuration history. + +## The solution: Git as source of truth + Dockhand for deployment + +The solution is simple and elegant: + +1. **Git becomes the source of truth**: all my `compose.yaml` files are versioned in a private Git repository (Forgejo in my case) +2. **Dockhand manages deployments**: a modern web interface that deploys from Git and manages secrets securely +3. **No more clear text secrets**: Dockhand encrypts secrets and injects them at deployment time + +This approach gives me: + +- **Complete history**: every modification is tracked in Git +- **Centralization**: a single place to manage all my stacks +- **Security**: secrets are never committed in clear text +- **Multi-environment**: I can manage multiple VMs from a single interface +- **Reproducibility**: I can redeploy any stack with a few clicks + +**GitOps workflow**: + +```mermaid +graph TB + A[Developer] -->|1. Push code| B[Git Repository] + B -->|2. Webhook/Sync| C[Dockhand] + C -->|3. Clone repo| B + C -->|4. Inject secrets| D[Encrypted secrets] + C -->|5. Deploy| E[Docker Compose] + E -->|6. Containers| F[Applications] + + style A fill:#e1f5ff + style B fill:#ffe1e1 + style C fill:#fff4e1 + style D fill:#e1ffe1 + style E fill:#f0e1ff + style F fill:#e1f5ff +``` + +## What is Dockhand? + +[Dockhand](https://github.com/Finsys/dockhand) is a modern Docker management interface developed by Finsys. It's a lightweight and elegant alternative to Portainer. + +### Main features + +- **Container management**: start, stop, restart, monitor in real-time +- **Compose orchestration**: visual editor for Docker Compose deployments +- **Git integration**: deployment from repositories with webhooks and automatic synchronization +- **Multi-environment support**: management of local and remote Docker hosts +- **Terminal and logs**: interactive shell access and real-time log streaming +- **File explorer**: navigation, upload and download from containers +- **Secrets management**: encryption and secure injection of sensitive variables + +### Technology stack + +- **Frontend**: SvelteKit 2, Svelte 5, shadcn-svelte, TailwindCSS +- **Backend**: Bun runtime with SvelteKit API routes +- **Database**: SQLite or PostgreSQL via Drizzle ORM +- **Infrastructure**: Direct communication with Docker API + +### License + +Dockhand uses the Business Source License 1.1 (BSL): +- Free for: personal use, internal business use, non-profits, education, evaluation +- The license will be converted to Apache 2.0 on January 1st, 2029 + +## Setting up Dockhand + +### Step 1: Socket Proxy (security) + +Before deploying Dockhand, I use a **socket proxy** to avoid directly exposing the Docker socket to applications. It's a least privilege principle: each service can only access the Docker endpoints it needs. + +
+Socket Proxy configuration (click to expand) + +```yaml +services: + socket-proxy: + image: wollomatic/socket-proxy:1.11.0 + container_name: socket-proxy + restart: unless-stopped + user: "65534:988" # nobody:docker + mem_limit: 64M + read_only: true + cap_drop: + - ALL + security_opt: + - no-new-privileges + command: + - '-loglevel=info' + - '-listenip=0.0.0.0' + - '-proxycontainername=socket-proxy' # Enables per-container allowlists + - '-watchdoginterval=3600' + - '-stoponwatchdog' + - '-shutdowngracetime=5' + environment: + - SP_ALLOWHEALTHCHECK=true + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - socket-proxy + healthcheck: + test: ["CMD", "./healthcheck"] + interval: 10s + timeout: 5s + retries: 2 + +networks: + socket-proxy: + name: socket-proxy + driver: bridge + internal: true +``` + +I'll detail the socket proxy in a dedicated article. For now, remember that it's a security layer between Docker and your applications. + +
+ +**Security architecture**: + +```mermaid +graph LR + A[User] -->|HTTPS| B[Traefik] + B -->|Internal HTTP| C[Dockhand] + C -->|Limited API| D[Socket Proxy] + D -->|Unix Socket| E[Docker Engine] + + style A fill:#e1f5ff + style B fill:#ffe1e1 + style C fill:#fff4e1 + style D fill:#e1ffe1 + style E fill:#f0e1ff +``` + +This diagram shows how each layer adds additional protection between the user and the Docker Engine. + +### Step 2: Dockhand deployment + +Here's my `compose.yaml` file for Dockhand: + +```yaml +services: + dockhand: + image: fnsys/dockhand:v1.0.14 + container_name: dockhand + restart: unless-stopped + ports: + - "192.168.100.160:3001:3000" + networks: + - traefik_private + - socket-proxy + volumes: + - /opt/stacks/dockhand:/opt/stacks/dockhand + environment: + - DATA_DIR=/opt/stacks/dockhand + - TZ=Europe/Paris + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik_private" + - "traefik.http.routers.dockhand-local.rule=Host(`dockhand.local.tellserv.fr`)" + - "traefik.http.routers.dockhand-local.entrypoints=local" + - "traefik.http.routers.dockhand-local.tls=true" + - "traefik.http.routers.dockhand-local.tls.certresolver=cloudflare-local" + - "traefik.http.services.dockhand.loadbalancer.server.port=3000" + - "socket-proxy.allow.get=.*" + - "socket-proxy.allow.post=.*" + - "socket-proxy.allow.delete=.*" + - "socket-proxy.allow.head=.*" + +networks: + socket-proxy: + external: true + traefik_private: + external: true +``` + +**Important points**: + +- **Image version**: `v1.0.14` - Check the latest stable version on [Docker Hub](https://hub.docker.com/r/fnsys/dockhand/tags) before deploying +- **Port bound to local IP**: `192.168.100.160:3001` - Adapt to your configuration: + - Replace with your local static IP if using Traefik on the same machine + - Use `127.0.0.1:3001:3000` if only accessing locally without a remote reverse proxy + - **Avoid** `0.0.0.0` which exposes the service on all interfaces (security risk) + - Note: Port `3001` is exposed on the host, but Traefik communicates internally on the container's port `3000` +- **Socket proxy**: connection via the `socket-proxy` network instead of directly exposing `/var/run/docker.sock` +- **Traefik**: reverse proxy for HTTPS access with automatic certificate +- **Socket-proxy permissions**: the `socket-proxy.allow.*` labels define the allowed Docker API endpoints + +:::warning[Security] +Dockhand provides full access to your Docker infrastructure. **Never expose it publicly** and always use strong authentication. Keep this service internal only. +::: + +:::tip[Startup order] +The `socket-proxy` network is declared as `external: true`, meaning it must already exist. Make sure to **start the Socket Proxy stack BEFORE** the Dockhand one, otherwise you'll get an error indicating that the external network cannot be found. +::: + +**Deployment**: + +```bash +docker compose up -d +``` + +The interface is now accessible at `https://dockhand.local.tellserv.fr` (in my case). + +### Step 3: Adding Docker environments + +Once Dockhand is deployed, I configure the **environments** (my different Docker VMs). + +**Settings → Environments → Add environment** + +![Environment configuration in Dockhand](/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_settings_environment.png) + +Three connection types are possible: + +1. **Unix socket**: if you passed the Docker socket directly to the Dockhand container (discouraged) +2. **Direct connection**: HTTP/HTTPS connection to the Docker API (via socket proxy in my case) +3. **Hawser agent (edge)**: passive connection where the remote environment initiates the connection (perfect for machines behind NAT) + +In my case, I use **Direct connection** with my socket proxy: + +![Adding an environment with direct connection](/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_add_environment.png) + +- **Name**: `Tellprod` +- **Connection type**: `Direct connection` +- **Host**: `socket-proxy` (the container name) +- **Port**: `2375` +- **Protocol**: `HTTP` (the socket proxy doesn't use TLS internally) + +## Preparing the Git repository + +Now that Dockhand is operational, I'm going to migrate all my Docker stacks to Git. + +### Step 1: Create the Git repository + +I created a private repository on my Forgejo instance: `tellprod_compose.git` + +### Step 2: Organize the stacks + +Structure of my repository: + +``` +tellprod_compose/ +├── mobilizon/ +│ └── compose.yml +├── audiobookshelf/ +│ └── compose.yml +├── freshrss/ +│ └── compose.yml +├── zabbix/ +│ └── compose.yml +└── ... +``` + +Each service has its own folder with a `compose.yml` file. + +### Step 3: Remove secrets + +**CRITICAL**: before committing, I must remove **all secrets** from my `compose.yml` and `.env` files. + +Secrets will be managed by Dockhand and injected at deployment time. + +For example, instead of: + +```yaml +environment: + - POSTGRES_PASSWORD=SuperSecretPassword123 + - MOBILIZON_INSTANCE_EMAIL=contact@tellserv.fr +``` + +I put variables: + +```yaml +environment: + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - MOBILIZON_INSTANCE_EMAIL=${MOBILIZON_INSTANCE_EMAIL} +``` + +### Step 4: Commit and push + +```bash +git add . +git commit -m "Initial commit: All Compose stacks, with secrets placeholders" +git push origin main +``` + +![Git repository with all Compose stacks](/img/blog/2026-02-10-dockhand-interface-docker-git/repo_tellprod_compose.png) + +All my stacks are now versioned and centralized. + +:::tip[Additional protection with .gitignore] +To prevent any risk of accidentally committing secrets, add a `.gitignore` file at the root of your repository: + +```gitignore +# Local secret files +.env +*.env +**/.env +**/*.env + +# Temporary files +*.tmp +*.swp +*~ +``` + +This way, even if you forget to replace a secret with a variable, Git will refuse to commit it. +::: + +## Configuring the Git repository in Dockhand + +### Step 1: Add credentials (if private repository) + +**Settings → Git → Credentials → Add credential** + +- **Type**: `Password` (for Forgejo) +- **Name**: `Forgejo (Password)` +- **Username**: my Forgejo username +- **Password**: password or API token + +### Step 2: Add the repository + +**Settings → Git → Repositories → Add repository** + +![Git repository configuration](/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_git_settings.png) + +![Adding a Git repository in Dockhand](/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_add_git_repo.png) + +- **Name**: `tellprod_compose` +- **Repository URL**: `https://forgejo.tellserv.fr/Tellsanguis/tellprod_compose.git` +- **Branch**: `main` +- **Credential**: select `Forgejo (Password)` + +Click **Test** to verify the connection, then **Save changes**. + +## Deploying from Git + +Now comes the magic part: deploying my stacks directly from Git. + +### Step 1: Stop the old stack + +Before migrating to Dockhand, I must stop my old manually deployed stacks: + +```bash +cd /opt/stacks/mobilizon +docker compose down +``` + +![Stopping the mobilizon stack](/img/blog/2026-02-10-dockhand-interface-docker-git/compose_down_mobilizon.png) + +### Step 2: Deploy from Git + +**Stacks → From Git** (button in the top right) + +!["From Git" button to deploy from a repository](/img/blog/2026-02-10-dockhand-interface-docker-git/from_git.png) + +**Deployment configuration**: + +![Deploying the Mobilizon stack from Git](/img/blog/2026-02-10-dockhand-interface-docker-git/mobilizon_deploy_from_git.png) + +1. **Repository**: select `tellprod_compose` +2. **Stack name**: `mobilizon` +3. **Compose file path**: `mobilizon/compose.yml` (relative path in the repo) +4. **Environment variables**: + - Click **Populate** in the top right + - Dockhand automatically fills the variables with values from the repository's `.env` (if present) + - Modify the values with the real values + - **Click the key icon** to the right of sensitive variables to mark them as encrypted secrets + +5. **Enable scheduled sync**: enable to automatically synchronize with Git + - **Daily** at **03:00** (for example) + - Dockhand will check daily if there are changes in the repo and redeploy if necessary + +6. **Enable webhook**: OFF for now (can be configured later for immediate deployment on each Git push) + +7. **Deploy now**: ON + +Click **Deploy** and here we go. + +Dockhand will: +1. Clone the Git repository +2. Read the `compose.yml` file in `mobilizon/` +3. Inject the encrypted secrets +4. Deploy the stack via `docker compose up -d` + +In a few seconds, the stack is deployed. And all sensitive variables are stored encrypted in Dockhand's database. + +## Result: centralized and secure management + +After a few hours of migration, all my stacks are now managed by Dockhand: + +- **Complete history**: every modification is versioned in Git +- **Centralization**: a single interface to manage all my VMs +- **Security**: secrets are encrypted in Dockhand, never in clear text in Git +- **Automatic synchronization**: Dockhand automatically redeploys if the Git repo changes +- **Reproducibility**: I can redeploy any stack with a few clicks +- **Multi-environment**: I can manage multiple VMs from the same interface + +### Concrete advantages + +**Git as source of truth**: +- I can go back if a modification breaks something +- I can see who modified what and when +- I can collaborate with others (pull requests, code review) + +**Dockhand as orchestrator**: +- Modern and intuitive interface (much better than old Portainer) +- Native encrypted secrets management +- Multi-environment support (I can manage multiple VMs) +- Automatic synchronization with Git +- Real-time logs, shell access, file explorer + +**Security**: +- No more clear text secrets in files +- Socket proxy to limit access to the Docker API +- Service not exposed publicly + +## Next steps: automation with Renovate Bot + +To go even further, I'm going to configure **Renovate Bot** to automate updates of my Docker images. + +### Why Renovate instead of Watchtower? + +Many people use **Watchtower** to automatically update their containers. But Watchtower has a major flaw: it updates containers directly in production, without validation. + +With the **GitOps + Renovate** approach: +- Renovate creates **Pull Requests** in Git with new versions +- I can **test and validate** before merging +- Git keeps the **history** of all updates +- If an update breaks something, I can easily **roll back** +- Dockhand automatically redeploys after each merge + +It's much safer and more professional than Watchtower's "magic" updates. + +### Renovate configuration + +Renovate will: +1. Analyze all my `compose.yml` files +2. Detect new available image versions +3. Automatically create Pull Requests in Forgejo +4. Allow me to validate and merge updates with one click + +With Dockhand automatically synchronizing from Git, my stacks will be automatically updated after each merge. + +I'll detail this configuration in a future article. In the meantime, you can check [Renovate's official documentation](https://docs.renovatebot.com/). + +## Conclusion + +Dockhand is a great tool for centralizing and modernizing Docker stack management. + +The **Git + Dockhand** approach combines the best of both worlds: +- Git for history and collaboration +- Dockhand for secrets management and deployment + +If you manage multiple Docker services, I highly recommend testing Dockhand. It's simple, elegant, and really changes the way you work. diff --git a/static/img/blog/2026-02-10-dockhand-interface-docker-git/compose_down_mobilizon.png b/static/img/blog/2026-02-10-dockhand-interface-docker-git/compose_down_mobilizon.png new file mode 100644 index 0000000..926338b Binary files /dev/null and b/static/img/blog/2026-02-10-dockhand-interface-docker-git/compose_down_mobilizon.png differ diff --git a/static/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_add_environment.png b/static/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_add_environment.png new file mode 100644 index 0000000..3c6194e Binary files /dev/null and b/static/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_add_environment.png differ diff --git a/static/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_add_git_repo.png b/static/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_add_git_repo.png new file mode 100644 index 0000000..2054c45 Binary files /dev/null and b/static/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_add_git_repo.png differ diff --git a/static/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_git_settings.png b/static/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_git_settings.png new file mode 100644 index 0000000..432de1a Binary files /dev/null and b/static/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_git_settings.png differ diff --git a/static/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_logo.png b/static/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_logo.png new file mode 100644 index 0000000..0c48136 Binary files /dev/null and b/static/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_logo.png differ diff --git a/static/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_settings_environment.png b/static/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_settings_environment.png new file mode 100644 index 0000000..5e0037a Binary files /dev/null and b/static/img/blog/2026-02-10-dockhand-interface-docker-git/dockhand_settings_environment.png differ diff --git a/static/img/blog/2026-02-10-dockhand-interface-docker-git/from_git.png b/static/img/blog/2026-02-10-dockhand-interface-docker-git/from_git.png new file mode 100644 index 0000000..1bff495 Binary files /dev/null and b/static/img/blog/2026-02-10-dockhand-interface-docker-git/from_git.png differ diff --git a/static/img/blog/2026-02-10-dockhand-interface-docker-git/mobilizon_deploy_from_git.png b/static/img/blog/2026-02-10-dockhand-interface-docker-git/mobilizon_deploy_from_git.png new file mode 100644 index 0000000..c0f1b54 Binary files /dev/null and b/static/img/blog/2026-02-10-dockhand-interface-docker-git/mobilizon_deploy_from_git.png differ diff --git a/static/img/blog/2026-02-10-dockhand-interface-docker-git/repo_tellprod_compose.png b/static/img/blog/2026-02-10-dockhand-interface-docker-git/repo_tellprod_compose.png new file mode 100644 index 0000000..94a170c Binary files /dev/null and b/static/img/blog/2026-02-10-dockhand-interface-docker-git/repo_tellprod_compose.png differ diff --git a/zen_586INu5evn.png b/zen_586INu5evn.png deleted file mode 100644 index 76dc9da..0000000 Binary files a/zen_586INu5evn.png and /dev/null differ