TO DO: vitejs, git et les hook, template, le processe cote prod, SSL
L'objectif de cette feuille de route est de créer un projet web via Django et de permettre un déploiement des mises à jours automatisé via les hooks de git.
J'en profite également pour détailler l'intégration de BootstrapCSS via ViteJS.
Cela me permettra par la suite, de me servir de cette page comme référence afin d'implémenter des scripts afin d'automatiser ces différentes étapes.
Ce qui j'entend par Workflow Ci/Cd pour un projet web, c'est de pouvoir facilement build des assets et mettre à jour le contenu du projet via une simple commande.
Requirement: avoir un serveur web (NGinx), une connexion ssh, un nom de domaine (sujets non traités ici).
Sur le poste de DEV, on va créer un répertoire de travail, ajouter git, l'environnement virtuel, Django et créer notre page "Hello World".
Creation et activation de l'environnement virtuel:
py -m venv venv
#linux
source venv/bin/activate
# Windows
./venv/scripts/activate
On install django, on créer les fichiers de bases et la db (sqlite):
pip install Django
# créer le projet
django-admin startproject nom_du_projet
cd ./nom_du_projet
# créer le repertoire des fichiers static
mkdir static
# DB
py manage.py makemigrations
py manage.py migrate
On va régler Django pour fonctionner par défaut en production et ajouter la configuration de développement dans un fichier séparé. On utilisera une variable d'environnement "activer" le "mode" DEV.
# éditer settings.py
# vérifier/ajouter les éléments suivants
import os
DEBUG = False
ALLOWED_HOSTS = ['domain.tld','localhost']
LANGUAGE_CODE = 'fr-fr'
TIME_ZONE = 'Europe/Paris'
USE_I18N = True
USE_TZ = True
USE_I18N = True # Active la traduction des textes
USE_L10N = True # Active la localisation des formats (dates, nombres)
USE_THOUSAND_SEPARATOR = True # Active le séparateur des milliers
STATIC_URL = 'static/'
STATICFILES_DIRS = [BASE_DIR / 'static']
# adapter suivant l'architecture de production
STATIC_ROOT = os.path.join('/var/www/nom_du_projet/public')
### My spec here
# DEV MOD
# win : set DJANGO_DEVELOPMENT=true
# : $env:DJANGO_DEVELOPMENT="true"
# Linux : export DJANGO_DEVELOPMENT=true
# inspired by: https://stackoverflow.com/a/34891731/21281469
if os.getenv('DJANGO_DEVELOPMENT') == 'true':
from .dev_settings import *
else:
pass
On créer le fichier pour les settings en mode DEV: touch dev_settings.py
# dev_settings.py
DEBUG = True
ALLOWED_HOSTS = ['*']
Enfin on vérifie si le serveur DEV fonctionne:
# on active 'le mode' DEV - ici sous Windows
$env:DJANGO_DEVELOPMENT="true"
On lance le serveur: http://127.0.0.1:8000
py manage.py runserver
Si tout s'est bien déroulé la page affiche "L’installation s’est déroulée avec succès. Félicitations"
On ajoute une app Django: pages:
py manage.py startapp pages
On l'ajoute dans settings.py:
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'pages',
]
On créer notre vue dans /nom_du_projet/pages/templates/pages/index.html
# création des répertoires
mkdir -p ./pages/templates/pages
# création de la vue
touch ./pages/templates/pages/index.html
On complète notre vue:
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test - Hello World</title>
</head>
<body>
<p>Hello World</p>
</body>
</html>
Il maintenant implémenter le Controller et configurer l'url au niveau du routing.
On va éditer le fichier /nom_du_projet/pages/views.py
# editer le fichier /nom_du_projet/pages/views.py
def index(request):
return render(request,'pages/index.html')
On définit également notre route "principale" au niveau du routing:
# editer le fichier /nom_du_projet/nom_du_projet/urls.py
from pages import views
...
urlpatterns = [
path('', views.index),
]
Le site doit maintenant afficher "Hello World".
On va maintenant créer notre repo de Dev local à ce niveau /répertoire_de_travail/nom_du_projet, un peut coup de: git init
On en profite pour ajouter le fichier .gitignore
# création du fichier .gitignore
touch .gitignore
Dans lequel on ajoute différentes répertoires:
# python env
venv/
__pycache__/
# django dev files
dev-requirements.txt
upon/dev_settings.py
# vitejs
static/.vite
static/vitejs/*
Avant d'aller plus loin dans la mise en place du projet Web. On va, configurer la partie Serveur.
Via Git et les hooks, on va déployer l'application. Puis on mettra en place les processus pour la Prod. Enfin, on configura Nginx pour servir les fichiers "static".
Une fois connecté en ssh au serveur, choisissez un répertoire où créer vos repos (ex: /var/repo), puis initialiser un repo pour notre site: git init --bare
# se rendre dans le repertoire des repos puis...
sudo mkdir nom_du_projet
sudo chown user_name:www-data nom_du_projet
cd nom_du_projet
git init --bare
La branche par défaut doit être main et non master, penser à configurer git si besoin: git: git config --global init.defaultBranch main
On va ensuite créer un hook "post-receive" (adapter les chemins suivant vos besoins):
# nano ./hooks/post-receive
#!/bin/bash
while read oldrev newrev ref
do
if [[ $ref =~ .*/main$ ]];
then
echo "Master ref received. Deploying master branch to production..."
git --work-tree=/var/www/nom_du_projet --git-dir=/var/repo/nom_du_projet checkout -f
# Collect static files
#sudo /var/www/nom_du_projet/venv/bin/python /var/www/nom_du_projet/manage.py collectstatic --noinput
# Restart Gunicorn
#sudo systemctl restart nom_du_projet_gunicorn
else
echo "Ref $ref successfully received. Doing nothing: only the master branch may be deployed on this server."
fi
done
On rend le script executable: sudo chmod +x ./hooks/post-receive
Remarque: les commandes sudo sont commentées pour le moment (normal).
On créer ensuite le répertoire qui va "accueillir" notre application, adapter les chemins suivant vos besoins:
# creation du répertoire
mkdir /var/www/nom_du_projet
chown username:www-data /var/www/nom_du_projet
Si ce n'est pas déjà fait on créer notre fichier "requirements.txt" (/repertoire_de_travail/nom_du_projet).
Votre environnement virtuel doit-être activé: pip freeze > requirements.txt
Afin de déployer une premier fois l'application, on va ajouter notre repo Prod au remote et pusher:
# sur le repo Dev local
git remote add origin "ssh://username@domain.tld:port/path"
# pour éditer en cas d'erreur
git remote set-url origin "..."
# et on push
git status
git add .
git commit -m 'initial commit'
git push prod main
Dans un premier temps on va déployer Django via le serveur de Test afin de vérifier son bon fonctionnement.
On va créer l'environnement virtuel et installer les dépendances du projet:
# au niveau du repertoire du projet ex: /var/www/nom_du_projet
# au besoin créer le répertoire 'static'
mkdir static
# création de l'environnement virtuel
python3 -m venv venv
# activation
source venv/bin/activate
# installation des dépendances du projets
pip install -r requirements.txt
# installation de Gunicorn
pip install gunicorn
# démarre le serveur test
python3 manage.py runserver 0.0.0.0:8000
Tester si cela fonctionne dans le navigateur (adapter suivant la situation): http://domain.tld ou http://sub.domain.tld:8000
On s'occuppe des fichiers "static" puis on test de nouveau via Gunicorn:
# traite les fichiers statics
python3 manage.py collectstatic --noinput
# on lance gunicorn
gunicorn --bind 0.0.0.0:8000 myproject.wsgi
Tester si cela fonctionne dans le navigateur (adapter suivant la situation): http://domain.tld ou http://sub.domain.tld:8000
On désactive l'environnement virtuel, nous allons créer le processus:
# désactivation de l'environnement virtuel
deactivate
Création du socket:
# sudo nano /etc/systemd/system/project_name_gunicorn.socket
[Unit]
Description=gunicorn socket
[Socket]
ListenStream=/run/project_name_gunicorn.sock
[Install]
WantedBy=sockets.target
On créer ensuite le service:
# sudo nano /etc/systemd/system/project_name_gunicorn.service
[Unit]
Description=gunicorn daemon for project: project_name
Requires=project_name_gunicorn.socket
After=network.target
[Service]
User=username
Group=www-data
WorkingDirectory=/path/to/your/project
ExecStart=/path/to/your/project/venv/bin/gunicorn \
--access-logfile - \
--workers 3 \
--bind unix:/run/project_name_gunicorn.sock \
project_name.wsgi:application
[Install]
WantedBy=multi-user.target
On active ensuite le socket:
#activation du socket
sudo systemctl start project_name_gunicorn.socket
sudo systemctl enable project_name_gunicorn.socket
>> Created symlink /etc/systemd/system/sockets.target.wants/project_name_gunicorn.socket → /etc/systemd/system/project_name_gunicorn.socket.
# vérifie le socket
sudo systemctl status project_name_gunicorn.socket
>> Loaded: loaded (/etc/systemd/system/project_name_gunicorn.socket; enabled; preset: enabled)
Active: active (listening) since Thu 2025-03-27 07:30:05 UTC; 2min 26s ago
# vérifie le sock file
file /run/project_name_gunicorn.sock
>> /run/project_name_gunicorn.sock: socket
On va maintenant configurer Nginx en tant que reverse proxy et pour servir les fichiers static:
# sudo nano /etc/nginx/sites-available/nom_du_projet
server {
listen 80;
server_name domain.tld;
location = /favicon.ico { access_log off; log_not_found off; }
location /static/ {
alias /var/www/project_name/public/;
}
location / {
include proxy_params;
proxy_pass http://unix:/run/project_name_gunicorn.sock;
}
}
On créer le lien symbolique, on test er redémarre Nginx:
# création du lien symbolique
sudo ln -s /etc/nginx/sites-available/domain.tld /etc/nginx/sites-enabled
# on test la configuration Nginx
sudo nginx -t
>> nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
# on redémarre le service
sudo systemctl restart nginx
Tester si cela fonctionne dans le navigateur: http://domain.tld
On va maintenant installer un certificat SSL via certbot de Let's Encrypt afin d'activer https.
On passe par Snaps (snapcraft.io - installing-snapd):
sudo apt install snapd
# pour tester que c'est OK
sudo snap install hello-world
On install certbot et on génère les certifs. :
# retire d'éventuelle ancienne installations
sudo apt-get remove certbot
# install certbot
sudo snap install --classic certbot
# on prépare Certbot afin qu'il puisse executer des commandes
sudo ln -s /snap/bin/certbot /usr/bin/certbot
# on demande à certbot de générer un certif. automatiquement
sudo certbot --nginx
> Successfully received certificate. ...
Congratulations! You have successfully enabled HTTPS>
# ou on souhaite le faire manuellement, dans ce cas on demande juste le certificat
sudo certbot certonly --nginx
# enfin on test la procédure de renouvellement des certifs
sudo certbot renew --dry-run
Le site doit maintenant être accéssible en https.
Si ce n'est déjà fait décommenter les lignes sudo dans le hook: nano ./hooks/post-receive
On peut tester le workflow: réaliser un commit et en pushant.
Il ne reste plus qu'a intégrer BootstrapCSS via viteJS et inclure notre premier template de "base".
On va intégrer ViteJS au projet afin de personaliser BootstrapCSS. On implémentera un helper qui va aller lire le fichier manifest.json afin d'intégrer les derniers assets buildés.
Je reprends ici, ce que je fais avec php et ViteJS depuis des années: vitejs.fr - backend-integration.
C'est un choix personel, j'ai pour habitude de créer les ressources nécessaires au front-end dans un répertoire nommé rsc où l'on y trouve BootstrapCSS "personalisé".
ViteJS est configuré pour délivrer les assets dans le répertoire /project_name/static/builder
Ensuite le helper vite_utils va lire le manifest afin que le Controller puisse transmettre les chemins des assets à la vue.
Dans un premier temps on va intégrer le package helper. Pour le moment, je me contente de copier/coller. J'imagine par la suite surement un autre moyen plus automatique.
# /répertoire de travail/project_name/helpers/vite_utils.py
import json
from pathlib import Path
from django.conf import settings
def get_vite_assets():
static_dir = settings.STATICFILES_DIRS[0]
manifest_path = Path(static_dir) / 'builder/.vite/manifest.json'
with open(manifest_path, 'r') as file:
manifest = json.load(file)
asset_paths = {}
for key, value in manifest.items():
if value.get('isEntry'):
asset_paths['js'] = value.get('file')
asset_paths['css'] = value.get('css', [])
return asset_paths
On oublie pas d'ajouter le fichier: __init__.py
Au niveau du controller, on ajoutera les variables au context:
# views.py
# exemple d'implémentation
from django.shortcuts import render
from helpers.vite_utils import get_vite_assets
LIVE_MODE=False
def index(request):
asset_paths = get_vite_assets()
context={
'live_mode': LIVE_MODE,
'css_styles': 'builder/' + asset_paths['css'][0],
'main_js': 'builder/' + asset_paths['js'],
'current_page': 'index',
'title': 'Project Name',
}
return render(request,'pages/index.html', context)
On va donc créer un répertoire /repertoire_de_travail/project_name/templates. Dans lequel, on va mettre nos fichiers composant le template: hreader.html, base.html, [menu.html], [header.html].
Penser à régler dans settings.py, le chemin pour ce template:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
ibre d'adapter suivant le projet.
# header.html
{% load static %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{% if live_mode %}
<script type="module" src="http://localhost:5173/@vite/client"></script>
<script type="module" src="http://localhost:5173/rsc/tpl_1/main.js"></script>
{% else %}
<link rel="stylesheet" href="{% static css_styles %}" />
<script type="module" src="{% static main_js %}"></script>
{% endif %}
<title>{% if title %} {{title}} {% else %} Up.oN {% endif %}</title>
</head>
<body>
# base.html
{% include "header.html" %}
{% include "menu.html" %}
{% block content %}
{% endblock %}
</body>
</html>
Au niveau de la vue:
{% extends 'base.html' %}
{% block content %}
{% endblock %}
Voici mon organisation. Vous devais avoir NodeJs sur votre machine de Dev. Aussi, je vous invite les docs de Bootstrap, ViteJS et éventuellement NodeJS pour adapter la configuration à vos bessoins.
Je créer le repertoire:/repertoire_de_travail/vitejs:
# création du répertoire
mkdir vitejs
cd vitejs
# on initialise un projet NodeJs
npm init -y
# installation de vitejs, Bootstrap, Bootstrap Icons et Sass
npm i -D vite && npm i --save bootstrap @popperjs/core && npm i bootstrap-icons && npm i --save-dev sass
Pour le confort et par habitude, j'ajoute dans le fichier package.json les commandes dev et build:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "vite",
"build": "vite build"
},
Ainsi pour dev le front-end, lancer le server live: npm run dev et pour builder les assets: npm run build.
Ensuite, j'ajoute mon fichier de configuration vite.config.mjs
# création du fichier
touch vite.config.mjs
# contenu du fichier
import { defineConfig } from "vite"
import path from "path"
export default defineConfig({
base: './',
server:{
port: '5173',
origin: 'http://localhost:5173',
},
build: {
copyPublicDir:false,
outDir: '../project_name/static/builder',
//assetsDir:'assets',
emptyOutDir: true,
manifest: true,
rollupOptions: {
input: 'rsc/tpl_1/main.js',
},
},
})
Ensuite, je place mes templates dans le repertoire rsc et j'adapte en conséquence. Par exemple, ici pour le template tpl_1, je vais créer un fichier /vitejs/rsc/tpl_1/main.js
# /vitejs/rsc/tpl_1/main.js
// pour le fonts => https://fontsource.org/
import '@fontsource/nom_de_la_font';
// Import configured BS's CSS
import './bs_config.scss'
// Import all of Bootstrap's JS
import * as bootstrap from 'bootstrap'
//import './bs_color_mode_toggler'
import "./main.css"
Pour personaliser bootstrap, cela passe par un fichier "principale", qu'on adapte suivant les besoins. Ici, pleins de cas sont possible. Je mets un exemple bs_config.scss, juste pour la forme.
// Custom.scss
// Option A: Include all of Bootstrap
// Include any default variable overrides here (though functions won't be available)
//@import "bootstrap/scss/bootstrap";
// Option B: Include parts of Bootstrap
// 1. Include functions first (so you can manipulate colors, SVGs, calc, etc)
// Customize some defaults
$primary: #3572A0;
$secondary: #6c8599;
// Configuration
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bs_custom_variables";
@import "bootstrap/scss/variables-dark";
@import "bs_custom_colors";
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bs_custom_it";
@import "bootstrap/scss/utilities";
//@import "bs_custom_utilities";
// Layout & components
@import "bootstrap/scss/root";
@import "bootstrap/scss/reboot";
@import "bootstrap/scss/type";
@import "bootstrap/scss/images";
@import "bootstrap/scss/containers";
@import "bootstrap/scss/grid";
@import "bootstrap/scss/tables";
@import "bootstrap/scss/forms";
@import "bootstrap/scss/buttons";
@import "bootstrap/scss/transitions";
@import "bootstrap/scss/dropdown";
@import "bootstrap/scss/button-group";
@import "bootstrap/scss/nav";
@import "bootstrap/scss/navbar";
@import "bootstrap/scss/card";
@import "bootstrap/scss/accordion";
@import "bootstrap/scss/breadcrumb";
@import "bootstrap/scss/pagination";
@import "bootstrap/scss/badge";
@import "bootstrap/scss/alert";
@import "bootstrap/scss/progress";
@import "bootstrap/scss/list-group";
@import "bootstrap/scss/close";
@import "bootstrap/scss/toasts";
@import "bootstrap/scss/modal";
@import "bootstrap/scss/tooltip";
@import "bootstrap/scss/popover";
@import "bootstrap/scss/carousel";
@import "bootstrap/scss/spinners";
@import "bootstrap/scss/offcanvas";
@import "bootstrap/scss/placeholders";
// Helpers
@import "bootstrap/scss/helpers";
// Utilities
@import "bootstrap/scss/utilities/api";
// Icons BS
@import "bootstrap-icons/font/bootstrap-icons.css";
Et voilà! Maintenant pour pusher les modifications du projet un simple git push prod main suffit.
Je pense automatiser le processus, que ce soit coté dev ou prod. Car, pour mettre en place ce Workflow beaucoup d'étapes sont nécessaires et une erreur dans un fichier de config. peut faire perdre des heures de recherches. C'est pour cela que j'ai condencé le tout ici afin d'avoir une ligne rouge à suivre lors de la création de scripts d'automatisation.