Docker prod
This commit is contained in:
68
Caddyfile
Normal file
68
Caddyfile
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# ═══════════════════════════════════════════════════════
|
||||||
|
# Jool International — Caddyfile PRODUCTION
|
||||||
|
# Domaine : jool-international.com
|
||||||
|
# Caddy gère automatiquement HTTPS via Let's Encrypt
|
||||||
|
# ═══════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# Redirection www → non-www
|
||||||
|
www.jool-international.com {
|
||||||
|
redir https://jool-international.com{uri} permanent
|
||||||
|
}
|
||||||
|
|
||||||
|
jool-international.com {
|
||||||
|
|
||||||
|
# ── Fichiers statiques Django (WhiteNoise les sert aussi,
|
||||||
|
# mais Caddy est plus rapide pour les assets lourds) ──
|
||||||
|
handle_path /static/* {
|
||||||
|
root * /app/staticfiles
|
||||||
|
file_server
|
||||||
|
header Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── CVs uploadés : jamais accessibles publiquement ───
|
||||||
|
handle /media/careers/cvs/* {
|
||||||
|
respond 404
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Autres fichiers media ─────────────────────────────
|
||||||
|
handle_path /media/* {
|
||||||
|
root * /app/media
|
||||||
|
file_server
|
||||||
|
header Cache-Control "public, max-age=604800"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Application Django (Gunicorn) ─────────────────────
|
||||||
|
handle {
|
||||||
|
reverse_proxy web:8000 {
|
||||||
|
header_up X-Forwarded-Proto {scheme}
|
||||||
|
header_up X-Real-IP {remote_host}
|
||||||
|
transport http {
|
||||||
|
read_timeout 60s
|
||||||
|
write_timeout 60s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── En-têtes de sécurité ──────────────────────────────
|
||||||
|
header {
|
||||||
|
# HSTS — commencer à 1h, passer à 1 an après validation
|
||||||
|
Strict-Transport-Security "max-age=3600; includeSubDomains"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "DENY"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
Permissions-Policy "camera=(), microphone=(), geolocation=()"
|
||||||
|
# Masquer la signature du serveur
|
||||||
|
-Server
|
||||||
|
-X-Powered-By
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Logs ──────────────────────────────────────────────
|
||||||
|
log {
|
||||||
|
output stdout
|
||||||
|
format json
|
||||||
|
level WARN
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Encodage gzip/zstd automatique ───────────────────
|
||||||
|
encode zstd gzip
|
||||||
|
}
|
||||||
95
deploy.sh
Executable file
95
deploy.sh
Executable file
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ═══════════════════════════════════════════════════════
|
||||||
|
# deploy.sh — Jool International · déploiement production
|
||||||
|
# Usage : ./deploy.sh
|
||||||
|
# Prérequis : docker, docker compose, git, accès sudo
|
||||||
|
# ═══════════════════════════════════════════════════════
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Couleurs pour la lisibilité ────────────────────────
|
||||||
|
RESET="\033[0m"
|
||||||
|
BOLD="\033[1m"
|
||||||
|
GREEN="\033[32m"
|
||||||
|
YELLOW="\033[33m"
|
||||||
|
RED="\033[31m"
|
||||||
|
CYAN="\033[36m"
|
||||||
|
|
||||||
|
log() { echo -e "${CYAN}[DEPLOY]${RESET} $*"; }
|
||||||
|
ok() { echo -e "${GREEN}[OK]${RESET} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
|
||||||
|
error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; exit 1; }
|
||||||
|
title() { echo -e "\n${BOLD}━━━ $* ━━━${RESET}"; }
|
||||||
|
|
||||||
|
# ── Variables ──────────────────────────────────────────
|
||||||
|
COMPOSE_FILE="docker-compose.prod.yml"
|
||||||
|
ENV_FILE=".env.prod"
|
||||||
|
APP_SERVICE="web"
|
||||||
|
BRANCH="${DEPLOY_BRANCH:-main}"
|
||||||
|
|
||||||
|
# ── Vérifications préalables ───────────────────────────
|
||||||
|
title "Vérifications"
|
||||||
|
|
||||||
|
command -v docker >/dev/null 2>&1 || error "docker non trouvé"
|
||||||
|
command -v git >/dev/null 2>&1 || error "git non trouvé"
|
||||||
|
[[ -f "$ENV_FILE" ]] || error "Fichier $ENV_FILE introuvable — créer d'abord .env.prod"
|
||||||
|
[[ -f "$COMPOSE_FILE" ]] || error "Fichier $COMPOSE_FILE introuvable"
|
||||||
|
[[ -f "Caddyfile" ]] || error "Fichier Caddyfile introuvable"
|
||||||
|
ok "Docker, git, .env.prod et Caddyfile présents"
|
||||||
|
|
||||||
|
# ── Pull du code ───────────────────────────────────────
|
||||||
|
title "Récupération du code"
|
||||||
|
git fetch origin "$BRANCH"
|
||||||
|
LOCAL=$(git rev-parse HEAD)
|
||||||
|
REMOTE=$(git rev-parse "origin/$BRANCH")
|
||||||
|
|
||||||
|
if [[ "$LOCAL" == "$REMOTE" ]]; then
|
||||||
|
warn "Déjà à jour ($(git rev-parse --short HEAD)) — rebuild quand même"
|
||||||
|
else
|
||||||
|
log "Mise à jour : $(git rev-parse --short HEAD) → $(git rev-parse --short "origin/$BRANCH")"
|
||||||
|
git pull --ff-only origin "$BRANCH"
|
||||||
|
fi
|
||||||
|
ok "Code à jour : $(git rev-parse --short HEAD)"
|
||||||
|
|
||||||
|
# ── Build de l'image ───────────────────────────────────
|
||||||
|
title "Build Docker"
|
||||||
|
docker compose -f "$COMPOSE_FILE" build --no-cache "$APP_SERVICE"
|
||||||
|
ok "Image $APP_SERVICE construite"
|
||||||
|
|
||||||
|
# ── Vérification de la config Caddy ────────────────────
|
||||||
|
title "Validation Caddyfile"
|
||||||
|
docker run --rm -v "$(pwd)/Caddyfile:/etc/caddy/Caddyfile:ro" \
|
||||||
|
caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile \
|
||||||
|
&& ok "Caddyfile valide" \
|
||||||
|
|| error "Caddyfile invalide — déploiement annulé"
|
||||||
|
|
||||||
|
# ── Déploiement sans interruption ─────────────────────
|
||||||
|
title "Déploiement"
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d --remove-orphans
|
||||||
|
ok "Conteneurs démarrés"
|
||||||
|
|
||||||
|
# ── Attente de la santé du service web ─────────────────
|
||||||
|
title "Healthcheck"
|
||||||
|
MAX_WAIT=60
|
||||||
|
ELAPSED=0
|
||||||
|
log "En attente que '$APP_SERVICE' soit healthy (max ${MAX_WAIT}s)…"
|
||||||
|
until docker compose -f "$COMPOSE_FILE" ps "$APP_SERVICE" | grep -q "healthy"; do
|
||||||
|
if [[ $ELAPSED -ge $MAX_WAIT ]]; then
|
||||||
|
warn "Healthcheck timeout — vérifier : docker compose logs $APP_SERVICE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 3
|
||||||
|
ELAPSED=$((ELAPSED + 3))
|
||||||
|
done
|
||||||
|
ok "$APP_SERVICE opérationnel (${ELAPSED}s)"
|
||||||
|
|
||||||
|
# ── Nettoyage des images orphelines ────────────────────
|
||||||
|
title "Nettoyage"
|
||||||
|
docker image prune -f --filter "until=24h" >/dev/null 2>&1 || true
|
||||||
|
ok "Images inutilisées supprimées"
|
||||||
|
|
||||||
|
# ── Résumé ─────────────────────────────────────────────
|
||||||
|
title "Déploiement terminé"
|
||||||
|
echo -e " Commit : ${BOLD}$(git rev-parse --short HEAD)${RESET}"
|
||||||
|
echo -e " Site : ${BOLD}https://jool-international.com/${RESET}"
|
||||||
|
echo -e " Logs : docker compose logs -f"
|
||||||
|
echo -e " Rollback : git checkout <commit-précédent> && ./deploy.sh"
|
||||||
68
docker-compose.prod.yml
Normal file
68
docker-compose.prod.yml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# ═══════════════════════════════════════════════════════
|
||||||
|
# Jool International — Docker Compose PRODUCTION
|
||||||
|
# Stack : PostgreSQL 16 · Django/Gunicorn · Caddy 2
|
||||||
|
# Usage : docker compose -f docker-compose.prod.yml up -d --build
|
||||||
|
# ═══════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
# ── Base de données PostgreSQL ──────────────────────────
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
env_file: .env.prod
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# ── Application Django (Gunicorn) ───────────────────────
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
restart: always
|
||||||
|
env_file: .env.prod
|
||||||
|
volumes:
|
||||||
|
- static_volume:/app/staticfiles
|
||||||
|
- media_volume:/app/media
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
command: >
|
||||||
|
sh -c "python manage.py collectstatic --noinput &&
|
||||||
|
python manage.py migrate --noinput &&
|
||||||
|
python manage.py compilemessages --locale=en &&
|
||||||
|
gunicorn config.wsgi:application
|
||||||
|
--bind 0.0.0.0:8000
|
||||||
|
--workers 3
|
||||||
|
--timeout 60
|
||||||
|
--max-requests 1000
|
||||||
|
--max-requests-jitter 100
|
||||||
|
--access-logfile -
|
||||||
|
--error-logfile -"
|
||||||
|
|
||||||
|
# ── Caddy (reverse proxy + HTTPS automatique) ───────────
|
||||||
|
caddy:
|
||||||
|
image: caddy:2-alpine
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
- "443:443/udp" # HTTP/3 / QUIC
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- static_volume:/app/staticfiles:ro
|
||||||
|
- media_volume:/app/media:ro
|
||||||
|
- caddy_data:/data # certificats Let's Encrypt (persistants)
|
||||||
|
- caddy_config:/config # état interne Caddy
|
||||||
|
depends_on:
|
||||||
|
- web
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
static_volume:
|
||||||
|
media_volume:
|
||||||
|
caddy_data:
|
||||||
|
caddy_config:
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
services:
|
|
||||||
|
|
||||||
# ── Base de données PostgreSQL ──────────────────────────
|
|
||||||
db:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
env_file: .env.prod
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
# ── Application Django (Gunicorn) ───────────────────────
|
|
||||||
web:
|
|
||||||
build: .
|
|
||||||
restart: always
|
|
||||||
env_file: .env.prod
|
|
||||||
volumes:
|
|
||||||
- static_volume:/app/staticfiles
|
|
||||||
- media_volume:/app/media
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
command: >
|
|
||||||
sh -c "python manage.py migrate --noinput &&
|
|
||||||
gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3
|
|
||||||
--timeout 60 --access-logfile - --error-logfile -"
|
|
||||||
|
|
||||||
# ── Nginx (reverse proxy + static files) ───────────────
|
|
||||||
nginx:
|
|
||||||
image: nginx:alpine
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
- "443:443"
|
|
||||||
volumes:
|
|
||||||
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
|
||||||
- static_volume:/app/staticfiles:ro
|
|
||||||
- media_volume:/app/media:ro
|
|
||||||
- certbot_www:/var/www/certbot:ro
|
|
||||||
- certbot_certs:/etc/letsencrypt:ro
|
|
||||||
depends_on:
|
|
||||||
- web
|
|
||||||
|
|
||||||
# ── Certbot (SSL Let's Encrypt) ─────────────────────────
|
|
||||||
certbot:
|
|
||||||
image: certbot/certbot
|
|
||||||
volumes:
|
|
||||||
- certbot_www:/var/www/certbot
|
|
||||||
- certbot_certs:/etc/letsencrypt
|
|
||||||
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
static_volume:
|
|
||||||
media_volume:
|
|
||||||
certbot_www:
|
|
||||||
certbot_certs:
|
|
||||||
@@ -589,7 +589,7 @@
|
|||||||
<img src="{% static 'img/Kiriq AI.jpg' %}" alt="{% trans 'KIRiQ Ai, analyse parcellaire satellite' %}"
|
<img src="{% static 'img/Kiriq AI.jpg' %}" alt="{% trans 'KIRiQ Ai, analyse parcellaire satellite' %}"
|
||||||
style="width:100%;height:auto;display:block;border-radius:24px;" loading="lazy" decoding="async">
|
style="width:100%;height:auto;display:block;border-radius:24px;" loading="lazy" decoding="async">
|
||||||
<div class="chip-float cf-tl">
|
<div class="chip-float cf-tl">
|
||||||
<span class="material-icons-round">warning</span> {% trans "4 anomalies détectées" %}
|
<span class="material-icons-round">warning</span> {% trans "6 anomalies détectées" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="chip-float cf-br">
|
<div class="chip-float cf-br">
|
||||||
<span class="material-icons-round">trending_up</span> NDVI +12 pts
|
<span class="material-icons-round">trending_up</span> NDVI +12 pts
|
||||||
|
|||||||
Reference in New Issue
Block a user