Tutoriel 2 — Environnement “Geek” pour Django en ligne
Recommandés
Le setup qui sent le terminal, les dotfiles bien rangés et la vélocité clavier.
Pitch
Objectif simple : ouvrir un IDE cloud et obtenir un poste de dev Django qui claque — shell nerveux, prompts qui parlent, linters qui mordent, hot-reload propre, logs lisibles, raccourcis partout. Zéro bricolage manuel. Tout se déclare via conteneur + dotfiles + scripts idempotents.
Un environnement Django en ligne peut être austère ou plaisant. Version “geek”, il devient rapide, verbeux quand il faut, silencieux quand il faut, et surtout prédictible. Copiez-collez ces fichiers, poussez dans votre repo, ouvrez le workspace : votre équipe a désormais un cockpit clavier-first pour shipper sans yak-shaving.
1) Base image & devcontainer : que tout le monde voie la même machine
Dockerfile (dev)
FROM mcr.microsoft.com/devcontainers/python:3.12
# Outils CLI "geek" (rapides & lisibles)
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
git ripgrep fd-find fzf bat eza direnv tmux \
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
&& ln -s /usr/bin/batcat /usr/local/bin/bat \
&& rm -rf /var/lib/apt/lists/*
# Python tooling (ultra-rapide) : uv + ruff + mypy
RUN pipx install uv && pipx ensurepath
ENV PATH="/root/.local/bin:${PATH}"
# Node minimal (pour Pyright ou front léger)
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs
# Starship prompt
RUN curl -fsSL https://starship.rs/install.sh | sh -s -- -y

.devcontainer/devcontainer.json
{
"name": "django-geek-dev",
"build": { "dockerfile": "Dockerfile" },
"features": {},
"settings": {
"terminal.integrated.defaultProfile.linux": "bash",
"editor.rulers": [100],
"python.analysis.typeCheckingMode": "basic",
"files.trimTrailingWhitespace": true
},
"postCreateCommand": "just bootstrap",
"forwardPorts": [8000],
"remoteEnv": {
"DJANGO_SETTINGS_MODULE": "config.settings.dev"
}
}

2) Dotfiles & prompt qui parle
~/.bashrc (extrait)
eval "$(direnv hook bash)"
export EDITOR="nvim"
export PIP_DISABLE_PIP_VERSION_CHECK=1
export PYTHONDONTWRITEBYTECODE=1
eval "$(starship init bash)"
alias ll='eza -lh --git --icons --group-directories-first'
alias cat='bat --paging=never'
alias g='git'
~/.config/starship.toml (prompt minimal lisible)
add_newline = false
[directory] truncation_length = 3
[python] format = "via [ $virtualenv]($style) "
[git_branch] symbol = " "
[cmd_duration] min_time = 500
~/.tmux.conf (multiplexage sobre)
setw -g mode-keys vi
set -g mouse on
bind r source-file ~/.tmux.conf \; display "reloaded"
3) Arborescence Django “opinionated”
project/
├─ .devcontainer/
├─ .env.example
├─ justfile
├─ pyproject.toml
├─ manage.py
├─ src/
│ ├─ config/
│ │ ├─ asgi.py
│ │ ├─ wsgi.py
│ │ └─ settings/{base.py,dev.py,prod.py}
│ ├─ apps/core/
│ ├─ templates/
│ └─ static/
└─ tests/

4) Dépendances & qualité : uv + ruff + pytest
pyproject.toml (extrait)
[project]
name = "django-geek"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"django>=5.0",
"dj-database-url>=2.2",
"python-dotenv>=1.0",
"django-extensions>=3.2",
][project.optional-dependencies]
dev = [« pytest », « pytest-django », « ruff », « black », « mypy », « ipython »]
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = « config.settings.dev » pythonpath = [« src »]
Installation express
uv pip install -e ".[dev]"
5) Settings 12-Factor : sobres mais affûtés
src/config/settings/base.py
from pathlib import Path
import os, dj_database_url
BASE_DIR = Path(__file__).resolve().parents[2]
ENV = os.getenv("ENV", "dev")
def env(key, default=None):
v = os.getenv(key, default)
return None if v is None else v
SECRET_KEY = env("DJANGO_SECRET_KEY", "change-me")
DEBUG = env("DJANGO_DEBUG", "0") == "1"
ALLOWED_HOSTS = env("DJANGO_ALLOWED_HOSTS", "").split(",") if env("DJANGO_ALLOWED_HOSTS") else []
CSRF_TRUSTED_ORIGINS = env("DJANGO_CSRF_TRUSTED_ORIGINS","").split(",") if env("DJANGO_CSRF_TRUSTED_ORIGINS") else []
LANGUAGE_CODE = env("DJANGO_LANGUAGE_CODE", "fr-fr")
TIME_ZONE = env("DJANGO_TIME_ZONE", "Europe/Paris")
USE_I18N = True; USE_TZ = True
INSTALLED_APPS = [
"django.contrib.admin","django.contrib.auth","django.contrib.contenttypes",
"django.contrib.sessions","django.contrib.messages","django.contrib.staticfiles",
"django_extensions","apps.core",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "config.urls"
WSGI_APPLICATION = "config.wsgi.application"
ASGI_APPLICATION = "config.asgi.application"
DATABASES = {"default": dj_database_url.parse(env("DATABASE_URL","sqlite:///db.sqlite3"))}
STATIC_URL = "/static/"; STATIC_ROOT = BASE_DIR / "staticfiles"
MEDIA_URL = "/media/"; MEDIA_ROOT = BASE_DIR / "media"
LOGGING = {
"version": 1, "disable_existing_loggers": False,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"root": {"handlers": ["console"], "level": env("DJANGO_LOG_LEVEL","INFO")},
}

dev.py et prod.py
# dev.py
from .base import *
DEBUG = True
INTERNAL_IPS = ["127.0.0.1"]
# prod.py
from .base import *
SECURE_SSL_REDIRECT = os.getenv("DJANGO_SSL_REDIRECT","1") == "1"
SESSION_COOKIE_SECURE = True; CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = int(os.getenv("DJANGO_HSTS_SECONDS","0"))
6) Variables d’environnement : contrat d’infra
.env.example
DJANGO_SECRET_KEY=changeme
DJANGO_DEBUG=1
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:8000
DJANGO_LOG_LEVEL=INFO
DJANGO_LANGUAGE_CODE=fr-fr
DJANGO_TIME_ZONE=Europe/Paris
DATABASE_URL=sqlite:///db.sqlite3
DJANGO_SSL_REDIRECT=0
DJANGO_HSTS_SECONDS=0
direnv pour l’auto-load.envrc
dotenv
export DJANGO_SETTINGS_MODULE=config.settings.dev
Puis :
direnv allow
7) Task runner sans friction : just au lieu de retenir 12 commandes
justfile
set shell := ["bash", "-cu"]
bootstrap:
uv pip install -e ".[dev]"
pre-commit install || true
run:
python manage.py migrate
python manage.py runserver 0.0.0.0:8000
shell:
python manage.py shell_plus
lint:
ruff check .
black --check .
mypy src
fix:
ruff check . --fix
black .
test:
pytest -q
collect:
python manage.py collectstatic --noinput
Exécution :
just bootstrap
just run
8) Expérience dev “clavier only”
• Recherche ultra-rapide : rg "pattern" -n src/
• Fuzzy-open un fichier : fzf puis enter
• Git lisible :
git config --global core.pager "delta"
git log --graph --oneline --decorate --all
• Django shell++ : just shell (via django-extensions)
• Hot-reload fiable : runserver 0.0.0.0:8000 + volumes montés (devcontainer)
• Multiplexage : tmux new -s dj && tmux split-window -h && tmux split-window -v
Panneau gauche : serveur. Haut droit : tests. Bas droit : logs.
9) Observabilité locale propre
Astuce logging de dev : niveaux lisibles, timestamps courts.
# dans base.py, remplacez "handlers" par:
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "simple"
}},
"formatters": {
"simple": { "format": "[%(levelname).1s %(asctime)s] %(name)s: %(message)s",
"datefmt": "%H:%M:%S" }
}
Résultat : [I 14:22:10] django.server: "GET /admin/..." 200
10) DB & services “à la carte” en dev
docker-compose.yml (optionnel)
services:
db:
image: postgres:16
environment:
POSTGRES_USER: django
POSTGRES_PASSWORD: django
POSTGRES_DB: django
ports: ["5432:5432"]
redis:
image: redis:7
ports: ["6379:6379"]
Env : DATABASE_URL=postgres://django:django@localhost:5432/django
Bonus Celery/Beat en split-process via Procfile si besoin.
11) Qualité automatique
pre-commit (extrait .pre-commit-config.yaml)
repos:
- repo: https://github.com/psf/black
rev: 24.8.0
hooks: [{id: black}]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
hooks: [{id: ruff}]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: end-of-file-fixer
- id: detect-private-key
Une fois installé : chaque commit est “peigné” automatiquement.
12) Routine quotidienne (ultra-courte)
Ouvrir le workspace → just run → coder → just test → commit/push.
Aucun copier-coller de commandes ésotériques, aucune divergence locale. L’IDE cloud, le conteneur et les dotfiles font le gros du travail.
13) Débogage “geek”
• python manage.py runserver_plus (Werkzeug debugger — via django-extensions).
• ipdb/breakpoint() pour pause granulaire.
• pytest -k nom -q exécute un seul test, plus vite que tout.
• Profilage express :
from django_extensions.management.commands.runserver_plus import Command
# ou utilisez --print-sql / django-silk en local
De l’IDE cloud à la préprod/prod (Geek edition)
But. Transformer votre setup Django “en ligne” en un pipeline CI/CD prêt pour la préprod et la prod : build Docker multi-stage, statiques via WhiteNoise/CDN, médias sur stockage objet, migrations automatiques, workers Celery, healthchecks et monitoring.
1) Architecture d’exécution (vue rapide)
- web :
gunicorn config.wsgi:application - worker :
celery -A config worker -l info - scheduler :
celery -A config beat -l info - services : Postgres, Redis, stockage objet (S3-compatible)
- statiques : WhiteNoise (et/ou CDN)
- médias : bucket S3, jamais sur le conteneur
- logs : stdout/stderr (niveau piloté par env)
2) Dockerfile prod (multi-stage, slim & rapide)
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS build
WORKDIR /app
RUN apt-get update && apt-get install -y build-essential libpq-dev && rm -rf /var/lib/apt/lists/*
COPY pyproject.toml ./
RUN pip install --upgrade pip && pip install --no-cache-dir "pip-tools"
# Si vous utilisez requirements.txt: COPY requirements.txt . && pip install -r requirements.txt
FROM python:3.12-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
RUN useradd -m django && apt-get update && apt-get install -y libpq5 && rm -rf /var/lib/apt/lists/*
COPY --from=build /usr/local /usr/local
COPY ./src ./src
COPY manage.py gunicorn.conf.py ./
ENV DJANGO_SETTINGS_MODULE=config.settings.prod
USER django
CMD ["gunicorn","-c","gunicorn.conf.py","config.wsgi:application"]
gunicorn.conf.py (prod)
bind = "0.0.0.0:8000"
workers = 3
threads = 2
timeout = 60
graceful_timeout = 30
accesslog = "-" # stdout
errorlog = "-" # stderr
3) Statiques et WhiteNoise
settings/prod.py (extraits)
from .base import *
INSTALLED_APPS = ["whitenoise.runserver_nostatic", *INSTALLED_APPS]
MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware")
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
SECURE_SSL_REDIRECT = os.getenv("DJANGO_SSL_REDIRECT","1") == "1"
SESSION_COOKIE_SECURE = True; CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = int(os.getenv("DJANGO_HSTS_SECONDS","31536000"))
Build/collectstatic (CI avant image finale)
python manage.py collectstatic --noinput
4) Médias sur S3 (django-storages)
# settings/prod.py (suite)
INSTALLED_APPS += ["storages"]
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY")
AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_STORAGE_BUCKET_NAME")
AWS_S3_REGION_NAME = os.getenv("AWS_S3_REGION_NAME", "eu-west-1")
AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=31536000, public"}
Env requis
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_STORAGE_BUCKET_NAME, AWS_S3_REGION_NAME
5) Healthchecks & readiness
Une vue “prête” pour l’orchestrateur :
# src/apps/core/views.py
from django.http import JsonResponse
def health(request): return JsonResponse({"status":"ok"})
def ready(request): return JsonResponse({"db":"ok","cache":"ok"})
# src/config/urls.py
from django.urls import path
from apps.core.views import health, ready
urlpatterns = [path("health/", health), path("ready/", ready)]
6) Celery & Redis
src/config/celery.py
import os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE","config.settings.prod")
app = Celery("config")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()
Env (ex.)
CELERY_BROKER_URL=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/1
7) GitHub Actions — pipeline CI/CD minimal
.github/workflows/ci.yml
name: ci
on:
push: { branches: [main] }
pull_request: {}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.12' }
- name: Install deps
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint & Tests
run: |
ruff check .
black --check .
pytest -q
.github/workflows/cd.yml (build & push image + déploiement via SSH)
name: cd
on:
push: { branches: [main] }
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- name: Login GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & Push
uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/${{ github.repository }}:latest
- name: Deploy (SSH)
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
script: |
docker pull ghcr.io/${{ github.repository }}:latest
docker compose -f /srv/app/compose.prod.yml up -d --force-recreate
docker compose -f /srv/app/compose.prod.yml exec -T web python manage.py migrate
8) compose.prod.yml (web + worker + beat)
services:
web:
image: ghcr.io/owner/repo:latest
env_file: /srv/app/.env
ports: ["80:8000"]
depends_on: [db, redis]
restart: always
worker:
image: ghcr.io/owner/repo:latest
command: celery -A config worker -l info
env_file: /srv/app/.env
depends_on: [redis]
restart: always
beat:
image: ghcr.io/owner/repo:latest
command: celery -A config beat -l info
env_file: /srv/app/.env
depends_on: [redis]
restart: always
db:
image: postgres:16
volumes: ["pgdata:/var/lib/postgresql/data"]
environment:
POSTGRES_DB: django
POSTGRES_USER: django
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
redis:
image: redis:7
volumes:
pgdata:
9) Secrets & variables (checklist)
- Django :
DJANGO_SECRET_KEY,DJANGO_ALLOWED_HOSTS,DJANGO_CSRF_TRUSTED_ORIGINS,DJANGO_LOG_LEVEL - DB/Cache :
DATABASE_URL,CELERY_BROKER_URL,CELERY_RESULT_BACKEND - Sécurité :
DJANGO_SSL_REDIRECT=1,DJANGO_HSTS_SECONDS=31536000 - I18N/TZ :
DJANGO_LANGUAGE_CODE,DJANGO_TIME_ZONE - Stockage : clés S3 si médias externes
10) Monitoring minimal (Sentry)
# settings/prod.py
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
SENTRY_DSN = os.getenv("SENTRY_DSN")
if SENTRY_DSN:
sentry_sdk.init(dsn=SENTRY_DSN, integrations=[DjangoIntegration()], traces_sample_rate=0.2)
11) Runbook de déploiement (récap)
- CI : lint + tests → OK
- Build image → push registry
- CD : pull sur serveur,
compose up -d - Migrations :
manage.py migrate(étape dédiée) - Health :
/health/et/ready/→ 200 - Roll-back : repasser au tag précédent, relancer
compose
12) Panneaux rouges (troubleshooting)
- 403 CSRF : URL prod manquante dans
CSRF_TRUSTED_ORIGINS - DisallowedHost : host non listé dans
ALLOWED_HOSTS - Statiques 404 :
collectstaticoublié ou mauvaisSTATICFILES_STORAGE - Uploads perdus : pas de bucket S3 → activer
DEFAULT_FILE_STORAGE - Celery muet :
CELERY_BROKER_URLincorrect / Redis non joignable - Timeout : ajuster workers/threads/
timeoutde Gunicorn ou index DB





