🚀🛠️ Déploiement automatisé de mon portfolio avec GitLab et Docker

Dans cet article, je vais présenter comment j'ai conçu et automatisé le déploiement de mon portfolio avec GitLab CI/CD, une image Docker personnalisée, Watchtower.

🧠 Création et déploiement automatisé de mon portfolio avec GitLab, Docker et Traefik

🎯 Introduction

Dans le cadre de mon portfolio professionnel, j’ai souhaité mettre en ligne un site statique pour présenter mes projets et mes compétences. Ce site devait être simple à maintenir, facilement modifiable et intégré dans une logique DevOps.

🔗 Dépôt GitLab du projet : https://gitlab.ggcorp.ovh/GGauzins/portfolio


🧠 Pourquoi GitLab ?

J’ai choisi d’utiliser GitLab comme plateforme principale pour héberger et déployer mon site pour plusieurs raisons :

  • Self-hosting possible : GitLab peut être hébergé sur mes propres serveurs, ce qui me donne un contrôle total sur mon infrastructure, la sécurité et les données.

  • Simplicité du déploiement : je n’ai qu’à cloner le dépôt, modifier, puis git push pour publier.

  • Automatisation via CI/CD : GitLab construit l’image Docker à chaque commit.

  • Développement structuré : je peux développer ou tester mon site sur des branches secondaires sans impacter la production. Le déploiement ne se déclenche que sur la branche principale (main).


💡 Idée initiale : GitLab Pages

Mon idée de départ était d’héberger mon site via GitLab Pages directement sur mon instance GitLab auto-hébergée.
Cette solution avait plusieurs avantages :

  • Tout était intégré à GitLab (CI + Pages).
  • Le déploiement statique Hugo était très simple.

Mais j’ai vite rencontré une limitation : l’utilisation d’un domaine personnalisé (www.ggauzins.fr) n’est pas possible sans une deuxième adresse IP publique sur le serveur GitLab. Cette contrainte réseau ne s’adaptait pas à mon infrastructure personnelle.


🔁 Nouvelle stratégie : Docker + Watchtower

Pour pallier cette contrainte, j’ai opté pour une solution Dockerisée :

  • Le site est généré avec Hugo via GitLab CI.
  • Une image Docker est construite et poussée dans le GitLab Container Registry.
  • Une VM personnelle sous Docker exécute un conteneur basé sur cette image.
  • Watchtower surveille ce conteneur et tire automatiquement la dernière version publiée.

Résultat : je garde la simplicité d’un git push pour publier, tout en gardant le contrôle total de l’hébergement et de mon domaine personnalisé.


🛠️ Technologies utilisées

  • Hugo — Générateur de site statique
  • Docker — Conteneurisation du site
  • GitLab CI/CD — Build et déploiement automatisé
  • GitLab Container Registry — Stockage des images
  • Watchtower — Déploiement automatisé des mises à jour Docker
  • Traefik — Reverse proxy + gestion SSL avec Let’s Encrypt
  • DNS OVH + (DNS challenge et wildcard)

⚙️ CI/CD avec GitLab

Le pipeline GitLab CI (.gitlab-ci.yml) est structuré en deux étapes :

build_image

  • Construction de l’image Docker avec Hugo.
  • Push dans le Container Registry GitLab.

pages

  • Build statique Hugo (non utilisé ici en production, mais disponible).

Seule la branche main déclenche le build et le push de l’image.


🧾 Fichier .gitlab-ci.yml

Voici la configuration complète utilisée pour automatiser la génération du site et la création de l’image Docker :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
variables:
  DART_SASS_VERSION: "1.85.0"
  GIT_DEPTH: 0
  GIT_STRATEGY: clone
  GIT_SUBMODULE_STRATEGY: recursive
  HUGO_VERSION: "0.144.2"
  NODE_VERSION: "23.x"
  TZ: "Europe/Paris"

stages:
  - build_image
  - pages

build_image:
  stage: build_image
  image: docker:latest

  services:
    - name: docker:dind
      alias: docker

  variables:
    DOCKER_HOST: "tcp://docker:2375"
    DOCKER_TLS_CERTDIR: ""

  before_script:
    - docker login -u gitlab-ci-token -p "$CI_JOB_TOKEN" "$CI_REGISTRY"
  script:
    - docker info
    - |
      docker build \
        --build-arg HUGO_VERSION=$HUGO_VERSION \
        -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
    - |
      if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
        docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" "$CI_REGISTRY_IMAGE:latest"
        docker push "$CI_REGISTRY_IMAGE:latest"
      fi

  allow_failure: true
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      when: on_success
    - when: never

pages:
  stage: pages
  image:
    name: golang:1.23.4-bookworm
  script:
    - apt-get update
    - apt-get install -y brotli
    - curl -LJO https://github.com/sass/dart-sass/releases/download/${DART_SASS_VERSION}/dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz
    - tar -xf dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz
    - cp -r dart-sass/ /usr/local/bin
    - rm -rf dart-sass*
    - export PATH=/usr/local/bin/dart-sass:$PATH
    - curl -LJO https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb
    - apt-get install -y ./hugo_extended_${HUGO_VERSION}_linux-amd64.deb
    - rm hugo_extended_${HUGO_VERSION}_linux-amd64.deb
    - curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION} | bash -
    - apt-get install -y nodejs
    - '[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true'
    - hugo --gc --minify --baseURL ${CI_PAGES_URL}
    - find public -type f -regex '.*\.(css|html|js|txt|xml)$' -exec gzip -f -k {} \;
    - find public -type f -regex '.*\.(css|html|js|txt|xml)$' -exec brotli -f -k {} \;
  artifacts:
    paths:
      - public
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      when: always
    - when: never

🐋 Dockerfile

Le fichier Dockerfile utilisé pour builder l’image Docker à partir du site Hugo :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# --- ÉTAPE 1 : builder avec Go et Hugo Extended ---
  FROM golang:1.23.4-bookworm AS builder

  # variables
  ARG HUGO_VERSION=0.144.2
  
  # installer git et ca-certificates pour cloner Hugo et gérer HTTPS
  RUN apt-get update \
   && apt-get install -y --no-install-recommends git ca-certificates \
   && rm -rf /var/lib/apt/lists/*
  
  # récupérer le code source de Hugo
  WORKDIR /go/src/hugo
  RUN git clone --branch v${HUGO_VERSION} https://github.com/gohugoio/hugo.git . 
  
  # compiler Hugo avec le tag 'extended'
  RUN go build --tags extended -ldflags="-s -w" -o /usr/local/bin/hugo
  
  # copier votre site et générer
  WORKDIR /src
  COPY . .
  RUN /usr/local/bin/hugo --gc --minify
  
  # --- ÉTAPE 2 : runtime léger avec Nginx ---
  FROM nginx:1.25-alpine AS runtime
  
  # on ne conserve que le dossier public généré
  COPY --from=builder /src/public /usr/share/nginx/html
  
  EXPOSE 80
  CMD ["nginx", "-g", "daemon off;"]

🌍 Reverse Proxy avec Traefik

🌐 Site principal – www.ggauzins.fr

Voici la configuration utilisée pour exposer mon conteneur hugo-portfolio :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
http:
  routers:
    hugo-portfolio:
      entryPoints:
        - websecure
      rule: "Host(`www.ggauzins.fr`)"
      tls:
        certResolver: lets-encr
        domains:
          - main: "www.ggauzins.fr"
      service: hugo-portfolio

    redirect-naked:
      entryPoints:
        - websecure
      rule: "Host(`ggauzins.fr`)"
      middlewares:
        - redirect-to-www
      service: noop@internal
      tls:
        certResolver: lets-encr
        domains:
          - main: "ggauzins.fr"

  middlewares:
    redirect-to-www:
      redirectRegex:
        regex: "^https?://ggauzins\.fr/(.*)"
        replacement: "https://www.ggauzins.fr/"
        permanent: true

  services:
    hugo-portfolio:
      loadBalancer:
        servers:
          - url: "http://172.16.20.7:8086/"

Cela permet :

  • De sécuriser les deux domaines (www. et nu) avec HTTPS via Let’s Encrypt.
  • De rediriger automatiquement ggauzins.fr vers www.ggauzins.fr.

📁 GitLab Pages – *.pages.ggcorp.ovh

Même si je n’utilise pas GitLab Pages pour le site final, j’ai quand même configuré Traefik pour qu’ils soient accessibles si besoin :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
http:
  routers:
    gitlab-pages:
      entryPoints:
        - "websecure"
      rule: "HostRegexp(`^.+\.pages\.ggcorp\.ovh$`)"
      tls:
        certResolver: lets-encr
        domains:
          - main: "pages.ggcorp.ovh"
            sans:
              - "*.pages.ggcorp.ovh"
      service: gitlab-pages

  services:
    gitlab-pages:
      loadBalancer:
        servers:
          - url: "http://172.16.20.8:8090"

⚠️ Cette configuration nécessite un certificat wildcard via DNS challenge.
J’ai donc configuré Traefik avec le DNS challenge OVH pour obtenir ce certificat automatiquement via Let’s Encrypt.


🐳 Docker Compose utilisé

Voici le fichier docker-compose.yml que j’utilise sur ma VM pour exécuter le site et Watchtower :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
services:
  portfolio:
    image: registry.gitlab.ggcorp.ovh/ggauzins/portfolio:latest
    container_name: portfolio
    restart: unless-stopped
    ports:
      - "8086:80"

  watchtower:
    image: containrrr/watchtower
    container_name: watchtower
    environment:
      - WATCHTOWER_POLL_INTERVAL=300     # toutes les 10 minutes
      - WATCHTOWER_CLEANUP=false          # supprime les anciennes images
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: portfolio

Ce fichier permet :

  • D’exécuter automatiquement l’image Docker générée par GitLab CI
  • De mettre à jour automatiquement le conteneur dès qu’une nouvelle image est disponible dans le registre

✅ Résultat final

  • Site en ligne avec HTTPS à l’adresse : https://www.ggauzins.fr
  • Déploiement 100 % automatisé : un simple git push suffit
  • Image Docker toujours à jour sur ma VM grâce à Watchtower
  • Domaine personnalisé et infrastructure maîtrisée

🔚 Conclusion

Ce projet m’a permis de mettre en pratique des compétences DevOps complètes :
CI/CD, conteneurisation, reverse proxy, gestion de certificat SSL, automatisation du déploiement.

Généré avec Hugo
Thème Stack conçu par Jimmy