Skip to content

Instantly share code, notes, and snippets.

@brahimmachkouri
Created April 20, 2026 08:36
Show Gist options
  • Select an option

  • Save brahimmachkouri/ebf3c0ed17f8dd293c4ef6d2e4acf3ec to your computer and use it in GitHub Desktop.

Select an option

Save brahimmachkouri/ebf3c0ed17f8dd293c4ef6d2e4acf3ec to your computer and use it in GitHub Desktop.
Installation de Dockhand sous Ubuntu
#!/usr/bin/env bash
set -Eeuo pipefail
# ============================================================
# Install Dockhand on Ubuntu 24.04
# - Utilise Docker déjà installé
# - Peut appeler un script Docker existant si absent
# - Déploie Dockhand dans un répertoire configurable
# - Démarre Dockhand via docker compose
# ============================================================
readonly APP_NAME="dockhand"
readonly BASE_DIR="${BASE_DIR:-/opt/dockhand}"
readonly APP_DIR="${BASE_DIR}"
readonly DATA_DIR="${BASE_DIR}/data"
readonly COMPOSE_FILE="${BASE_DIR}/compose.yaml"
readonly LOG_FILE="/tmp/install-dockhand-$(date +%Y%m%d-%H%M%S).log"
# Optionnel :
# export DOCKER_INSTALL_SCRIPT="/chemin/vers/install-docker.sh"
DOCKER_INSTALL_SCRIPT="${DOCKER_INSTALL_SCRIPT:-}"
# Image Dockhand
DOCKHAND_IMAGE="${DOCKHAND_IMAGE:-fnsys/dockhand:latest}"
DOCKHAND_PORT="${DOCKHAND_PORT:-3000}"
# Fuseau horaire du conteneur
CONTAINER_TZ="${CONTAINER_TZ:-Europe/Paris}"
# ---------- Couleurs (désactivées si pas de terminal) ----------
if [[ -t 1 ]]; then
RED='\e[31m'; GREEN='\e[32m'; YELLOW='\e[33m'; BLUE='\e[34m'; CYAN='\e[36m'
BOLD='\e[1m'; RESET='\e[0m'
else
RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''
BOLD=''; RESET=''
fi
log() { echo -e "${BLUE}ℹ️ $*${RESET}"; }
ok() { echo -e "${GREEN}✅ $*${RESET}"; }
warn() { echo -e "${YELLOW}⚠️ $*${RESET}"; }
err() { echo -e "${RED}❌ $*${RESET}" >&2; }
die() {
local msg
for msg in "$@"; do
err "$msg"
done
exit 1
}
cleanup() {
[[ -n "${SUDO_LOOP_PID:-}" ]] && kill "${SUDO_LOOP_PID}" 2>/dev/null || true
}
trap cleanup EXIT
need_cmd() {
command -v "$1" &>/dev/null || die "'$1' introuvable — installe-le avant de relancer ce script."
}
need_cmd sudo
need_cmd id
need_cmd getent
need_cmd mkdir
need_cmd tee
need_cmd cp
need_cmd cmp
need_cmd mktemp
need_cmd awk
need_cmd grep
need_cmd tr
need_cmd cut
need_cmd hostname
sudo -v || die "Impossible d'obtenir les droits sudo."
# Keep-alive sudo
( while true; do sudo -n true 2>/dev/null; sleep 50; done ) &
SUDO_LOOP_PID=$!
# ---------- Utilisateur réel ----------
get_real_user() {
if [[ -n "${SUDO_USER:-}" && "${SUDO_USER}" != "root" ]]; then
echo "${SUDO_USER}"
return
fi
local lname
if lname="$(logname 2>/dev/null)" && [[ "${lname}" != "root" ]]; then
echo "${lname}"
return
fi
if [[ "${USER:-root}" == "root" ]]; then
die \
"Impossible de déterminer l'utilisateur non-root." \
"Lance ce script via sudo depuis un compte utilisateur normal," \
"ou définis SUDO_USER manuellement."
fi
echo "${USER}"
}
REAL_USER="$(get_real_user)"
GROUP_JUST_ADDED=false
log "Utilisateur cible : ${REAL_USER}"
# ---------- Docker ----------
ensure_docker() {
if command -v docker &>/dev/null; then
ok "Docker déjà installé : $(docker --version)"
else
warn "Docker n'est pas installé."
if [[ -n "${DOCKER_INSTALL_SCRIPT}" ]]; then
[[ -f "${DOCKER_INSTALL_SCRIPT}" ]] || die "Script Docker introuvable : ${DOCKER_INSTALL_SCRIPT}"
[[ -x "${DOCKER_INSTALL_SCRIPT}" ]] || chmod +x "${DOCKER_INSTALL_SCRIPT}"
log "Exécution du script Docker fourni : ${DOCKER_INSTALL_SCRIPT}"
"${DOCKER_INSTALL_SCRIPT}" 2>&1 | tee -a "${LOG_FILE}"
else
die "Docker absent. Définis DOCKER_INSTALL_SCRIPT ou installe Docker avant."
fi
fi
command -v docker &>/dev/null || die "Docker est toujours introuvable après tentative d'installation."
if docker compose version &>/dev/null; then
ok "Plugin Docker Compose disponible."
else
die "Le plugin 'docker compose' est introuvable."
fi
}
# ---------- Service Docker ----------
ensure_docker_running() {
if command -v systemctl &>/dev/null; then
if ! systemctl is-active --quiet docker; then
log "Le service Docker n'est pas actif, tentative de démarrage…"
sudo systemctl start docker
sudo systemctl enable docker
fi
systemctl is-active --quiet docker || die "Impossible de démarrer le service Docker."
ok "Service Docker actif."
else
if ! sudo docker info &>/dev/null 2>&1; then
die \
"Le démon Docker ne répond pas et systemctl n'est pas disponible." \
"Démarre Docker manuellement avant de relancer ce script."
fi
ok "Démon Docker accessible."
fi
}
# ---------- Groupe docker ----------
ensure_docker_group() {
if ! getent group docker &>/dev/null; then
log "Création du groupe docker"
sudo groupadd docker
fi
if ! getent group docker | cut -d: -f4 | tr ',' '\n' | grep -qx "${REAL_USER}"; then
log "Ajout de ${REAL_USER} au groupe docker"
sudo usermod -aG docker "${REAL_USER}"
ok "${REAL_USER} ajouté au groupe docker."
GROUP_JUST_ADDED=true
else
log "${REAL_USER} est déjà dans le groupe docker."
fi
}
# ---------- Répertoires ----------
prepare_dirs() {
log "Création de ${APP_DIR} et ${DATA_DIR}"
sudo mkdir -p "${DATA_DIR}"
sudo chown -R "${REAL_USER}:${REAL_USER}" "${APP_DIR}"
}
# ---------- Port ----------
check_port_available() {
if command -v ss &>/dev/null; then
if ss -ltn | awk '{print $4}' | grep -Eq "(^|:)${DOCKHAND_PORT}$"; then
# Port occupé — vérifier si c'est dockhand lui-même (relance idempotente)
if sudo docker ps \
--filter "name=^/dockhand$" \
--filter "status=running" \
--format '{{.Names}}' | grep -qx 'dockhand'; then
log "Le port ${DOCKHAND_PORT} est utilisé par le conteneur dockhand existant — OK."
return 0
fi
die "Le port ${DOCKHAND_PORT} est déjà utilisé par un autre processus." \
"Change DOCKHAND_PORT avant de relancer."
fi
else
warn "Commande 'ss' absente — contrôle du port ignoré."
fi
}
# ---------- Compose ----------
write_compose() {
local puid pgid docker_gid tmpfile
puid="$(id -u "${REAL_USER}")"
pgid="$(id -g "${REAL_USER}")"
docker_gid="$(getent group docker | cut -d: -f3)"
tmpfile="$(mktemp)"
cat > "${tmpfile}" <<EOF
services:
dockhand:
image: ${DOCKHAND_IMAGE}
container_name: dockhand
restart: unless-stopped
ports:
- "${DOCKHAND_PORT}:3000"
environment:
PUID: "${puid}"
PGID: "${pgid}"
TZ: "${CONTAINER_TZ}"
group_add:
- "${docker_gid}"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${DATA_DIR}:/app/data
EOF
if [[ -f "${COMPOSE_FILE}" ]]; then
if cmp -s "${tmpfile}" "${COMPOSE_FILE}"; then
log "${COMPOSE_FILE} est déjà à jour."
rm -f "${tmpfile}"
return
fi
local backup="${COMPOSE_FILE}.bak.$(date +%Y%m%d-%H%M%S)"
warn "Le fichier ${COMPOSE_FILE} existe déjà et va être mis à jour."
warn "Sauvegarde : ${backup}"
sudo cp "${COMPOSE_FILE}" "${backup}"
sudo chown "${REAL_USER}:${REAL_USER}" "${backup}"
fi
log "Écriture de ${COMPOSE_FILE}"
sudo cp "${tmpfile}" "${COMPOSE_FILE}"
sudo chown "${REAL_USER}:${REAL_USER}" "${COMPOSE_FILE}"
rm -f "${tmpfile}"
}
# ---------- Démarrage ----------
start_dockhand() {
log "Démarrage de Dockhand"
sudo docker compose -f "${COMPOSE_FILE}" up -d 2>&1 | tee -a "${LOG_FILE}"
}
# ---------- Vérification ----------
wait_running() {
local retries=15
local delay=2
local i
log "Attente du conteneur dockhand…"
for (( i=1; i<=retries; i++ )); do
if sudo docker ps \
--filter "name=^/dockhand$" \
--filter "status=running" \
--format '{{.Names}}' | grep -qx 'dockhand'; then
ok "Conteneur dockhand en cours d'exécution."
return 0
fi
sleep "${delay}"
done
warn "Le conteneur dockhand ne semble pas actif après $((retries * delay))s."
warn "Vérifie les logs : sudo docker logs -f dockhand"
}
# ---------- Résumé ----------
show_status() {
local host_ip
host_ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
host_ip="${host_ip:-<IP_DU_SERVEUR>}"
echo
ok "Dockhand installé."
echo -e "${CYAN}URL locale : ${BOLD}http://127.0.0.1:${DOCKHAND_PORT}${RESET}"
echo -e "${CYAN}URL réseau probable : ${BOLD}http://${host_ip}:${DOCKHAND_PORT}${RESET}"
echo -e "${CYAN}Compose : ${BOLD}${COMPOSE_FILE}${RESET}"
echo -e "${CYAN}Données : ${BOLD}${DATA_DIR}${RESET}"
echo -e "${CYAN}Fuseau horaire : ${BOLD}${CONTAINER_TZ}${RESET}"
echo -e "${CYAN}Logs : ${BOLD}${LOG_FILE}${RESET}"
echo
echo "Commandes utiles :"
echo " sudo docker ps"
echo " sudo docker logs -f dockhand"
echo " sudo docker compose -f ${COMPOSE_FILE} pull"
echo " sudo docker compose -f ${COMPOSE_FILE} up -d"
echo " sudo docker compose -f ${COMPOSE_FILE} down"
echo
if [[ "${GROUP_JUST_ADDED}" == true ]]; then
warn "Le groupe docker vient d'être ajouté à ${REAL_USER}."
echo -e "${CYAN} 👉 Pour utiliser docker sans sudo : ${BOLD}newgrp docker${RESET}"
echo -e "${CYAN} 👉 Ou déconnecte-toi et reconnecte-toi.${RESET}"
else
if docker info &>/dev/null 2>&1; then
ok "Docker est utilisable dans ce shell."
else
warn "Si Docker n'est pas encore utilisable sans sudo dans ce shell :"
echo -e "${CYAN} 👉 Lance : ${BOLD}newgrp docker${RESET}"
fi
fi
}
# ---------- Main ----------
main() {
log "Début de l'installation de ${APP_NAME}"
log "Journal : ${LOG_FILE}"
echo
ensure_docker
ensure_docker_running
ensure_docker_group
prepare_dirs
check_port_available
write_compose
start_dockhand
wait_running
show_status
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment