Created
April 20, 2026 08:36
-
-
Save brahimmachkouri/ebf3c0ed17f8dd293c4ef6d2e4acf3ec to your computer and use it in GitHub Desktop.
Installation de Dockhand sous Ubuntu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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