Comment servir les fichiers statiques avec Django en dev et en prod


Servir les fichiers CSS, javascript et les images avec Django a toujours été la plus grande source de confusion (heureusement ça s’est bien amélioré avec la 1.4, donc upgradez si vous pouvez). C’est d’autant plus déroutant que la manière de faire est différente selon la version de Django, et selon que l’on est en production ou en développement.

Donc, pour profiter de cet article:

  • Lisez les parties qui vous concernent uniquement.
  • Ne lisez pas en diagonale.
  • Si ça ne marche pas, partez de zéro (avec un projet tout neuf), et une fois que ça marche, essayez sur le votre.

C’est typiquement le cas où il faut comprendre ce qui se passe et pas juste copier/coller du code (ceci est une petite pique à Max qui est en train de décuver comme une loque sur le canap pendant que j’écris cette partie de l’article).

Préparation

Les fichiers statiques se gèrent à base de chemins absolus. Afin de faciliter celà, vous devriez donc toujours avoir ceci tout en haut de votre fichier settings.py:

import os
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))

Si votre fichier de settings n’est pas à la racine du projet (typiquement dans un projet Django 1.4), faites:

import os
SETTINGS_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_DIR = os.path.dirname(SETTINGS_DIR)

Le fichiers de settings étant un fichier Python, on peut mettre tout le code que l’on souhaite dedans, et ce code nous permet d’obtenir de manière dynamique le chemin absolu vers votre dossier de projet. Il servira de base pour tous les chemins vers les fichiers statiques.

__file__ contient le chemin vers le fichier en cours. abspath() permet d’obtenir le chemin absolu. dirname() permet d’obtenir le nom du dossier contenant le fichier.

Notez aussi l’emplacement des fichiers statiques de la Django admin dans un coin (dans la doc de votre site par exemple). Ces fichiers se trouvent dans django/contrib/admin/media, mais si vous galerez pour les trouver, vous pouvez toujours lancer la commande:

python -c "import django, os; print os.path.join(os.path.dirname(django.__file__), 'contrib/admin/media')"

Moi ça donne un truc comme ça:

/home/sam/.virtualenvs/project_env/local/lib/python2.7/site-packages/django/contrib/admin/media

En phase de développement, de la version 1.0 à la version 1.2

Vous êtes sur votre machine, et vous utilisez la commande runserver. C’est donc Django qui va servir vos fichier statiques, et nous allons voir comment, pour chaque version de Django, on peut lui demander de le faire.

C’est une vieille version, servir les fichiers statiques est assez relou, alors on va gruger pour vous faciliter la vie. Non, ce n’est pas la meilleur manière de faire, mais c’est la manière de faire qui vous évitera de retourner sur un forum pour demander pourquoi ça ne marche pas.

Créez un dossier nommé “static” à la racine de votre projet. Mettez tous vos fichiers statiques dedans: js, css, images… Organisez ça en sous-dossier si vous voulez, mais mettez tout dedans, y compris les fichiers statiques des autres apps Django que vous avez installé (et qu’il va falloir copier/coller ou alors faire un lien symbolique). La seule exception est l’admin.

Ce n’est pas particulièrement propre ni pratique, mais ça va marcher. Le surcoût en maintenant sera largement compensé par le fait que ce sera très simple à comprendre et à servir.

Dans votre fichier de settings, pointez MEDIA_ROOT sur ce dossier:

MEDIA_ROOT = os.path.join(PROJECT_DIR, 'static')

Et choissisez un chemin explicite pour MEDIA_URL:

MEDIA_URL = '/static/'

Ne touchez pas à ADMIN_MEDIA_PREFIX. Ne mettez pas MEDIA_URL égale à ADMIN_MEDIA_PREFIX.

Dans le fichier urls.py principal, c’est à dire ce lui est généralement à la racine de votre projet, faites:

# on importe ici tout les trucs nécessaires à urls.py
from django.conf.urls.defaults import *
# on va récupérer MEDIA_ROOT depuis le fichier de settngs
from django.conf import settings
 
# on active l'admin
from django.contrib import admin
admin.autodiscover()
 
# on créé un pattern vide. Cela va nous permettre d'insérer le service
# des fichiers statiques en premier
urlpatterns = patterns('')
 
# On active maintenant le service des fichiers statiques par Django
# mais uniquement en mode DEBUG (pour éviter de l'oublier en prod)
if settings.DEBUG:
    urlpatterns += patterns('',
        (r'%s/(?P<path>.*)$' % settings.MEDIA_URL.strip('/'),
         'django.views.static.serve',
         {'document_root': settings.MEDIA_ROOT}),
    )
 
# Ensuite on déclare nos patterns normalement
# ATTENTION, le signe doit-être "+=", pas "=", sinon vous allez tout écraser
urlpatterns += patterns('',
    # Example:
    (r'^', include('app.urls')),
 
    # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
    # to INSTALLED_APPS to enable admin documentation:
    (r'^admin/doc/', include('django.contrib.admindocs.urls')),
 
    # Uncomment the next line to enable the admin:
    (r'^admin/(.*)', admin.site.root),
)

Vous notez que contrairement à ce qu’on voit la doc officielle, on met l’url pour les fichiers statiques en premier. Cela donne une forme étrange à la déclation des urls car on a un urlpatterns vide au début, mais c’est très important: à un moment vous allez déclarer une URl attrape-tout qui court-circuitera tout ce qu’il y a après, et ce jour là vous passerez des heures à chercher pourquoi vos fichiers statiques ne sont pas déclarés.

Le strip() n’est pas obligatoire si vous mettez les slashes correctement partout. Mais ce n’est jamais le cas.

Maintenant, dans votre html, vous pouvez faire ceci:

<link href="/static/chemin/du/fichier/a/relatif/au/dossier/static.css"  rel="stylesheet" />

Et pareil pour le js, les images, etc.

Le chemin est bel et bien écrit en dur dans le templates HTML. Encore une fois, ce n’est pas la meilleure manière de faire, c’est juste celle qui vous garanti que ça marche. Si vous vous sentez à l’aise avec le processus, vous pouvez changer celà en utilisant un context_processor pour ajouter MEDIA_URL à chaque template et remplacer /static/ par {{ MEDIA_URL }}.

Télécharger le projet complet d’exemple

En phase de développement, pour la version 1.3 et 1.4

C’est globalement beaucoup plus facile. Il suffit de mettre tous vos fichiers statiques dans un dossier nommé “static” dans le dossier d’une de vos app pour qu’ils soient trouvés automatiquement quand vous lancerez manage.py runserver.

Voici à quoi ressemblera votre settings.py:

# plus rien à voir avec avant, MEDIA_ROOT et MEDIA_URL designent
# l'endroit où vous voulez que vos utilisateur upload et download des fichiers
# Ce n'est donc plus un paramètre primordial
MEDIA_ROOT = os.path.join(PROJECT_DIR, 'media')
MEDIA_URL = '/media/'
 
# Ce qui s'appelait avant MEDIA_ROOT s'appelle maintenant STATIC_ROOT
# mais il n'est utile pour la prod
# On le met quand même pour plus tard
STATIC_ROOT = os.path.join(PROJECT_DIR, 'static')
 
# Le settings le plus important: à quelle adresse servir les fichiers statiques
# Utile en dev afin de pouvoir bénéficier des templates tags
STATIC_URL = '/static/'
 
# Les medias de l'admin sont enfin une URL comme une autre qu'on peut
# mettre sous notre propre arborescence
ADMIN_MEDIA_PREFIX = '/static/admin/'
 
# Vous pouvez OPTIONNELLEMENT rajouter ici une liste de chemin vers
# des dossiers de fichiers statiques qui seront ainsi aussi servit automatiquement
# pendant le développement. Par exemple un dossier à la racine
STATICFILES_DIRS = (
    os.path.join(PROJECT_DIR, 'more_static'),
)
# N'utilisez PAS le même dossier que STATIC_ROOT

Il n’y a rien à faire de plus, vos fichiers seront servis automatiquement en dev, pas besoin de toucher aux urls.

Maintenant il faut juste les déclarer vos URLs dans le template.

Pour Django 1.3, vous pouvez ajouter un fichiers statiques ainsi pour éviter d’écrire l’URL en dur:

{% load static %}
{% get_static_prefix as STATIC_PREFIX %}
 
<link href="{{ STATIC_PREFIX }}app/css/style.css"  rel="stylesheet" />

Pour django 1.4, c’est encore plus simple:

{% load staticfiles %}
<img src="{% static "app/css/style.css" %}" />

Encore un détail, si vous n’utilisez pas le serveur de dev Django mais un autre serveur de dev (ex: werkzeug), vos fichiers statiques ne seront pas servis automatiquement, mais il est facile d’y remédier. Ajoutez à votre urls.py:

from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 
urlpatterns += staticfiles_urlpatterns()

Télécharger le projet complet d’exemple

Migration de 1.2 ou moins vers 1.3 ou 1.4

Bouger vos fichiers statiques du répertoire à la racine vers le répertoire nommé “static” d’une de vos apps (ou de plusieurs).

Ajoutez ceci dans votre fichier de config:

STATICFILES_DIRS = (
)
 
STATICFILES_FINDERS = (
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
)

Et ajoutez django.contrib.sites à vos INSTALLED_APPS.

Remplacez toutes les URls en dur dans le HTML par une version avec les templates tags (voir partie précédente).

Attention, ça ne couvre que la partie pour la gestion des fichiers statiques, il y a d’autres choses à migrer.

En production: toutes versions

Je ne vais pas expliquer ici comment mettre Django en production (c’est un article à lui tout seul, peut être même plusieurs). Ici je vais me concentrer sur comment servir les fichiers statiques, et présupposer DEBUG = False.

Si vous utilisez Djagno 1.2 ou moins, vous n’avez rien à faire côté Django. Si vous utilisez Django 1.3 ou 1.4, vous devez maintenant appeler la commande pyhon manage.py collectstatic. Celle-ci va prendre tous les fichiers statiques de tous les dossiers “static” des apps et ceux listés dans STATICFILES_DIRS et va les copier dans STATIC_ROOT.

Dans tous les cas, vous n’aurez qu’un seul dossier pour le js, le css et les images à servir avec Nginx ou Apache.

Nginx

Si vous avez votre propre serveur, je vous conseil Nginx plutôt qu’Apache pour mettre votre site en prod.

L’idée est de dire dans le fichier de configuration nginx que toutes les URLs qui concernent les fichiers statiques sont servies directement par Nginx, et les autres sont passées à Django:

server {
        listen      8080;
 
        ############################################################
        # la partie qui concerne les fichiers statiques commence ici
        ############################################################
 
        location /favicon.ico {
            # favicon.ico doit être à la racine du dossier img
            root  /chemin/absolu/vers/dossier/img;
        }
 
        # on sert nos fichiers statiques
        # l'url doit correspondre à MEDIA_URL
        location ~* ^/static/ {
            root  /chemin/absolu/vers/dossier/PARENT/du/dossier/statique;
            # on active gzip pour tous les petits fichiers qui contiennent du texte
            # si le navigateur le supporte
            gzip  on;
            gzip_http_version 1.0;
            gzip_vary on;
            gzip_comp_level 6;
            gzip_proxied any;
            gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
            # petite astuce gzip pour les gros fichier
            # voir http://blog.leetsoft.com/2007/7/25/nginx-gzip-ssl
            gzip_buffers 16 8k;
 
            # on desactive gzip pour les vieux navigateurs
            gzip_disable ~@~\MSIE [1-6].(?!.*SV1)~@~];
        }
 
       # on fait pareil mais pour l'admin Django
       # l'url doit corresponde à ADMIN_MEDIA_PREFIX
       location ~* ^/media/ {
            root  /chemin/absolu/du/dossier/PARENT/du/dossier/media/de/django;
            gzip  on;
            gzip_http_version 1.0;
            gzip_vary on;
            gzip_comp_level 6;
            gzip_proxied any;
            gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
            gzip_buffers 16 8k;
            gzip_disable ~@~\MSIE [1-6].(?!.*SV1)~@~];
        }
 
        ##############################################################
        # la partie qui concerne les fichiers statiques se termine ici
        ##############################################################
 
        location / {
            proxy_pass http://127.0.0.1:8000;
        }
}

Notez bien que la directive root suppose que vous mettiez le dossier parent du dossier que vous voulez servir, et que le dossier servi a le même nom que le chemin dans l’url.

Par exemple, si vos fichiers statiques sont dans /home/sam/project/static, alors l’url devra être ^/static/ et on passera à l’instruction root le chemin /sam/project.

Apache

Si vous êtes chez un hébergeur qui n’a qu’Apache, voici un manière de servir les fichiers statiques. Je ne connais pas Apache aussi bien que Nginx, donc je ne peux pas m’engager sur la gestion de Gzip.

# TOUS les chemins doivent être accessible pour le user apache (www-data le plus souvent)
# N'oubliez pas les slashes finaux, ils sont importants
 
WSGIPythonPath /chemin/absolu/vers/project/:/chemin/absolu/vers/dossier/parent/du/project/:/chemin/vers/site/package/du/virtualenv/
 
<VirtualHost *:80>
 
    WSGIScriptAlias / "/chemin/absolu/vers/fichier/project.wsgi"
 
    ########################################################
    # Début de la partie qui concerne les fichiers statiques
    ########################################################
 
    # on sert les fichiers statiques du site
    Alias /static/ "/chemin/absolu/vers/dossier/static/"
    Alias /favicon.ico "/chemin/absolu/vers/dossier/static/img/favicon.ico"
 
    # on donne l'autorisation à tous de les lire
    <Directory "/chemin/absolu/vers/dossier/static/">
        Order allow,deny
        Allow from all
    </Directory>
 
    # on sert les fichiers statiques de l'admin
    Alias /media/ "/chemin/absolu/vers/dossier/django/contrib/admin/media/"
    # on donne l'autorisation à tous de les lire
    <Directory "/chemin/absolu/vers/dossier/django/contrib/admin/media/">
        Order allow,deny
        Allow from all
    </Directory>
 
    ######################################################
    # Fin de la partie qui concerne les fichiers statiques
    ######################################################
 
</VirtualHost>

Si vous faites bien gaffe aux droits d’accès des dossiers (récursivement) et que vous avec pas oublié de “/” à la fin d’un chemin, tout ira bien.

Pour Apache, servir les fichiers statiques est généralement la partie facile, c’est faire marcher Django et mod_wsgi qui est difficile: problèmes de droits, mauvaise configuration, Python path qui foine… Mais je m’en branle, puisque c’est pas l’objet de l’article, donc démerdez-vous.

Notes de fin

Jusqu’à la version 1.2, MEDIA_ROOT et MEDIA_URL ne sont que des conventions qui ne servent pas à grand chose à part à être utilisées par des contexts managers et à vous éviter les chemins en durs dans les templates et urls.py.

A partir de Django 1.3, STATIC_ROOT et STATIC_URL sont vraiment utilisées: STATIC_ROOT est là où Django va mettre le resultat de la commande collecstatic et STATIC_URL et l’url que va utiliser la vue staticfiles_urlpatterns.

MEDIA_ROOT et MEDIA_URL existent toujours, mais sont utilisés pour designer le dossier et l’url qui pointent vers les contenus uploadés et downlodés par les utilisateurs. Si vous voulez les servir, il faut faire exactement la même chose à l’époque de Django 1.2 et avant: mettre la route à la main dans urls.py (mais cette fois, il y a un raccourcis pour ça), et rajouter une entrée supplémentaire dans le fichier de conf Nginx ou Apache.

Sinon, vous pouvez aussi vous éviter tout ce merdier en utilisant le middleware qui sert automatiquement tous les fichiers statiques de django_quicky.

Enfin, je n’ai pas abordé les storage backends, tout simplement parce que je n’en utilise pas, donc je ne peux pas vous donner de bons conseils sur la question.

13 thoughts on “Comment servir les fichiers statiques avec Django en dev et en prod

  • foxmask

    Quand je passe DEBUG à False et tape

    python manage.py runserver

    je n’ai plus accès à mon contenu static (que des 404) alors meme que :

    python manage.py findstatic css/monfichier.css

    est trouvé, une suggestion ?

    • Max

      Moi quand je passe la 3eme sur ma 404 j’ai les pignons qui craquent, une idée ?

  • Sam Post author

    @foxmask:

    staticfiles_urlpatterns() check en interne DEBUG et se désactive si on est sur False.

    Les dev de Django considère que c’est une feature, j’ai trouvé trouver ça agaçant.

    Deux solutions:

    Soit tu sers les fichiers à l’ancienne:

        urlpatterns += patterns('',
            (r'%s/(?P<path>.*)$' % settings.MEDIA_URL.strip('/'),
             'django.views.static.serve',
             {'document_root': settings.MEDIA_ROOT}),
        )

    Soit tu installe Django_quicky et tu utilise le middleware qui sert les static files. Il fait tout automatiquement, y a juste une ligne à rajouter dans le settings, et il sert aussi bien les medias, que les fichiers statiques des tes apps et celles de l’admin, dans tous les cas de figures.

    Bien entendu, dans le cas ou on active le service des fichiers statiques avec DEBUG = False, il faut faire très attention à la mise en production de bien servir les fichiers par le serveur front end et pas Django, car on le s’en rendra pas compte facilement. C’est facile de l’oublier.

  • foxmask

    @sam merci

    effectivement c’est horripilant j’ai tourné en rond, je ne voyais pas ce qui ne le satisfaisait pas. Bien entendu c’est pour avoir un premier aperçu de l’appli avant que le contenu static ne soit géré par le serveur HTTP.

    avec django_quicky le contenu static est géré par Django du coup ? ou les 2 (avec front HTTP et/ou django) ?

  • Samuel Martin

    Je serais tenté de dire que même en local il est préférable de déployer la même architecture qu’en production. Aussi chez moi même en local je sers les medias et les static via Nginx. Le comportement est proche de la prod et donc pas d’oubli.

  • Sam Post author

    Quand un projet est bien lancé et devient sérieux, ça a du sens. Mais quand on démarre un projet, quand il s’agit d’un petit projet ou quand la deadline est serrée, mirrorer la production c’est du boulot en plus.

    Et c’est aussi un frein pour les débutants.

    C’est la même problématique pour la base de données, le moteur de recherche, le gestionnaire de cache, le message broker, etc. Pour tous ces composants, on s’expose au risque d’avoir des différences de comportement en prod (qui n’a jamais eu des surprises de type ou de taille de nom de colonne en passant de sqlite à mysql). Mais en même temps, un setup complet ? C’est la journée de perdue.

    Peut être que le tout VM peut résoudre ça.

  • Sam Post author

    @foxmask: wooops, j’avais pas vu ton comment foxmask, sorry.

    Donc, django_quicky ne sert rien par défaut, il faut rajouter le middleware ‘django_quicky.middleware.StaticServe’. Dans ce cas oui, tous les fichiers statiques sont servis par Django. Idéalement on le met dans le fichier local_settings.py

    Moi j’utilise une troisième voie: je met ‘django_quicky.middleware.StaticServe’ dans tous les cas, et j’override en prod l’url des fichier statiques avec nginx. Mais faut vraiment faire gaffe à pas se planter dans ce cas, c’est pour ça que je ne recommande pas cette méthode.

  • salas

    Peut être que le tout VM peut résoudre ça.

    Bah maintenant, ca devient vraiment à la mode.
    Je sais pas ce que ca donne en python (je viens de rails) mais je pense qu’on peut de la meme facon utiliser Docker, ansible si gros besoin de deploiement et un jetkins pour l’integration continue (si besoin).

    Utilsant lxc, le docker peut etre utilisé en prod car y’a pas de reduction de perf ou très peu. Un petit dockerfile et hop on gènere des environnements clean et portable.
    Vive lxc !

    Je vais essayer de faire un article dessus sinon y’a ca !

    Sam, max, un article contributeur dessus ca vous interesserait ?

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.