Déployer une application Django sur PythonAnywhere

Dans cet article je vous montre pas-à-pas comment déployer une application Django sur le service PythonAnywhere.

Publié le par Thibault Houdon (mis à jour le )
paceTemps de lecture estimé : 52 minutes

Dans cet article, je vous explique pas-à-pas comment mettre en ligne un projet Django sur PythonAnywhere.

PythonAnywhere est un service qui, comme Heroku ou Digital Ocean, nous donne accès à un serveur sur lequel nous pouvons héberger notre application Django. Ils disposent d'une offre gratuite qui permet de créer une application web avec très peu de restrictions.

Bien que j'explique toutes les étapes dans le détail, il est préférable d'être un minimum à l'aise avec les commandes Bash et les commandes de Git.

Créer un compte sur PythonAnywhere

La première étape est de créer un compte sur PythonAnywhere en vous rendant sur la page d'inscription. Pour des besoins basiques vous pouvez créer un compte gratuit qui vous permettra de créer une application web.

Le compte gratuit dispose de certaines limitations :

  • Vous ne pouvez créer qu'une seule application.
  • Vous devez cliquer sur un bouton tous les 3 mois dans l'interface du site pour maintenir le site en ligne.
  • Vous ne pouvez pas utiliser de base de données PostgreSQL.

Créer le projet sur PythonAnywhere

Une fois votre compte créé, vous pouvez créer une application web en vous rendant dans l'onglet Web.

Cliquez sur "Add a new web app" pour créer une application. Cliquez sur "Next" et choisissez la configuration manuelle :

Choisissez ensuite la version de Python que vous souhaitez utiliser pour votre projet.

Votre application est maintenant créée. Vous pouvez accéder à votre site web temporaire à l'adresse indiquée (1). Vous remarquez également le fameux bouton (2) sur lequel vous devrez cliquer au moins une fois tous les 3 mois avec un compte gratuit pour que votre site reste actif (vous serez averti par email une semaine avant la désactivation de votre site) :

Configurer les variables d'environnement

Pour extraire les données sensibles de notre fichier settings.py, nous allons utiliser une bibliothèque pour lire un fichier d'environnement qui ne sera pas ajouté à notre dépôt Git.

Nous allons donc installer la bibliothèque python-environ :

pip install python-environ

Nous allons ensuite créer un fichier .env qui va contenir nos variables d'environnements. Ces variables d'environnements pourront ainsi être différentes pour notre environnement de développement local et notre environnement de production.

Il ne faut pas confondre le dossier .env (1) qui correspond à mon environnement virtuel et le fichier .env (2) qui va contenir mes variables d'environnement.

Vous pouvez utiliser des noms différents si vous souhaitez ne pas risquer de vous mélanger entre les deux.

À l'intérieur de ce fichier .env, je vais mettre trois valeurs :

  • La clé secrète de mon application
  • La variable de debug
  • La variable allowed_hosts
SECRET_KEY='wec0ngwsdfisubf$l3ddt_n!-_j00+ye_a(gha*_jmcujm5i_-o)#ct'
DEBUG=True
ALLOWED_HOSTS='127.0.0.1'

En local, nous mettons le DEBUG à True et nous autorisons dans ALLOWED_HOSTS l'adresse IP locale de notre ordinateur (127.0.0.1). Ces valeurs seront modifiées pour notre environnement en production.

Nous allons maintenant charger ces valeurs dans notre fichier settings.py :

# blog/settings.py
from pathlib import Path
import environ

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

env = environ.Env()
environ.Env.read_env(env_file=str(BASE_DIR / "blog" / ".env"))

Nous importons déjà le module (import environ) puis nous créons un objet env à partir de environ.Env().

Ensuite, nous allons lire les variables contenues dans le fichier .env.

Pour ça, j'utilise la concaténation avec pathlib et je convertis le chemin en chaîne de caractères avec la fonction str car le paramètre env_file n'aime pas les objets de type Path de pathlib 😉

Dans mon cas, j'ai mis le fichier .env à l'intérieur du dossier blog (str(BASE_DIR / "blog" / ".env")) il faudra donc modifier le chemin pour votre projet afin qu'il pointe vers le bon fichier.

Il ne nous reste plus qu'à aller lire les valeurs contenues dans notre variable env :

SECRET_KEY = env("SECRET_KEY")
DEBUG = env.bool("DEBUG")
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')

Pour convertir automatiquement la valeur de DEBUG en booléen, on utilise env.bool.

Pareil pour les 'hosts' avec env.list (on pourrait avoir plusieurs valeurs dans ALLOWED_HOSTS).

Et voilà, on a réussi à séparer les données sensibles de notre application et à les stocker dans un fichier qui ne sera pas intégré à notre système de gestion de versions (Git).

Préparer les fichiers du projetIl nous reste encore quelques éléments à configurer dans le fichier de settings et dans notre projet.

Tout d'abord, nous allons ajouter 3 variables à la fin du fichier settings.py (la variable STATIC_URL est normalement déjà définie) :

STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'

MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'mediafiles'

Nous rajoutons STATIC_ROOT pour indiquer dans quel dossier collecter les fichiers statiques. Et comme pour cette application, nous avons également des fichiers téléversés par l'utilisateur (les images des articles du blog), nous spécifions également les variables MEDIA_URL et MEDIA_ROOT.

Nous en avons maintenant terminé avec le fichier settings.py !

Créer le fichier requirements.txt

Afin de pouvoir installer les mêmes bibliothèques que nous utilisons dans notre projet local sur notre environnement de développement, nous allons créer un fichier requirements.txt avec pip.

Pour ça, assurez-vous d'activer votre environnement virtuel, et à la racine du projet, utilisez la commande :

pip freeze > requirements.txt

Le nom du fichier, requirements.txt, est une convention (vous pourriez l'appeler autrement, patrick.txt, par exemple).

Votre fichier devrait ressembler à ceci :

asgiref==3.3.1
Django==3.1.7
Pillow==8.1.2
python-environ==0.4.54
pytz==2021.1
sqlparse==0.4.1

Créer un dépôt Git

Nous allons maintenant mettre notre projet en ligne sur Github. Vous pouvez bien entendu utiliser un autre service comme Bitbucket ou Gitlab. Assurez-vous également d'avoir téléchargé git sur votre ordinateur.

La première chose à faire est donc de créer un nouveau dépôt (repository en anglais) sur Github :

Une fois le dépôt créé, Github (gentil comme il est) nous indique toutes les démarches à suivre pour initialiser un dépôt et le pousser (push) sur Github :

echo "# django-blog" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/DocstringFr/django-blog.git
git push -u origin main

Nous allons d'abord exécuter les trois premières commandes :

~/django_blog
$ echo "# django-blog" >> README.md

~/django_blog
$ git init
Initialized empty Git repository in /Users/thibh/django_blog/.git/

~/django_blog
$ git add README.md

À ce stade, nous allons créer un fichier .gitignore qui va contenir les fichiers et dossiers que nous ne souhaitons pas inclure dans notre dépôt Git.

~/django_blog
$ touch .gitignore

À l'intérieur de ce fichier, vous pouvez mettre les lignes suivantes :

.idea/
db.sqlite3
__pycache__/
.env/
.env

.idea est un dossier créé par PyCharm pour sauvegarder les préférences de notre projet.

db.sqlite3 est notre base de données sqlite. Nous ne voulons pas inclure la base de donnée local dans notre dépôt, nous en créerons une nouvelle directement sur PythonAnywhere.

Les dossiers __pycache__ sont des dossiers de cache que nous n'avons pas besoin d'inclure.

La ligne .env/ cible le dossier de notre environnement virtuel. Nous allons recréer un environnement virtuel sur notre serveur PythonAnywhere, nous ne souhaitons donc pas inclure l'environnement virtuel local dans notre dépôt.

Et enfin, la dernière ligne .env concerne le fichier de configuration qui contient les données sensibles de notre application. Nous ne souhaitons donc pas non plus l'inclure dans le dépôt.

Si vous effectuez maintenant un git status pour voir l'état de votre dépôt, vous devriez avoir ceci :

~/django_blog
$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached ..." to unstage)
    new file:   README.md

Untracked files:
  (use "git add ..." to include in what will be committed)
    .gitignore
    requirements.txt
    src/

On peut maintenant ajouter les fichiers et dossiers restants avec la commande git add :

~/django_blog
$ git add .gitignore requirements.txt src/

Et nous pouvons continuer avec la suite de commandes indiquées par Github :

git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/DocstringFr/django-blog.git
git push -u origin main

De retour sur Github, notre dépôt contient maintenant tous les fichiers nécessaires pour le déploiement de notre site sur PythonAnywhere 🥳

Cloner le dépôt

Nous allons maintenant cloner le dépôt Github sur notre serveur PythonAnywhere.

Rendez-vous dans l'onglet Consoles et cliquez sur Bash pour ouvrir un terminal sur votre serveur :

De retour sur Github, copiez l'URL de votre dépôt dans le presse-papier :

Puis, dans votre console bash, utilisez la commande git clone suivie de l'URL du dépôt Github :

~ $ git clone https://github.com/DocstringFr/django-blog.git                                
Cloning into 'django-blog'...                                                                     
Username for 'https://github.com': thibh                                                          
Password for 'https://thibh@github.com':                                                          
remote: Enumerating objects: 36, done.
remote: Counting objects: 100% (36/36), done.
remote: Compressing objects: 100% (28/28), done.
remote: Total 36 (delta 2), reused 36 (delta 2), pack-reused 0
Unpacking objects: 100% (36/36), done.                                                            
Checking connectivity... done. 

Votre dossier est maintenant cloné sur le serveur PythonAnywhere !

Vous pourrez ainsi à tout moment apporter des modifications localement dans votre projet, pousser ces changements (avec git add, git commit et git push) et les récupérer (avec git pull) sur votre serveur PythonAnywhere.

Créer l'environnement virtuelToujours à l'intérieur de la console PythonAnywhere, nous allons maintenant créer l'environnement virtuel pour notre projet.

Déplacez-vous à l'intérieur du dossier django avec la commande cd et créez un environnement virtuel avec l'exécutable de python3.8 et le module venv :

~ $ cd django-blog/                                                                         
~/django-blog (main) $ /usr/bin/python3.8 -m venv .env

Une fois l'environnement virtuel créé, vous pouvez l'activer avec la commande source et installer les bibliothèques à partir du fichier requirements.txt:

~/django-blog (main) $ source .env/bin/activate                                              
(.env) ~/django-blog (main) $ pip install -r requirements.txt 

De retour dans votre tableau de bord PythonAnywhere, dans l'onglet Web, vous devez renseigner le chemin vers l'environnement virtuel dans la partie virtualenv. Indiquez le chemin complet vers le dossier .env qui contient votre environnement virtuel :

Et voilà, notre environnement virtuel est 100% opérationnel !

Créer le fichier .env

Dans les parties précédentes, nous avons cloné le dépôt Github. Mais ce dépôt ne contient pas notre fichier d'environnement (et heureusement). Nous allons donc devoir le créer à la main et modifier les valeurs qu'il contient.

Déplacez-vous dans le dossier src/blog et créez le fichier avec la commande touch. Puis éditez-le avec vim :

(.env) ~/django-blog (main) $ cd src/blog/
(.env) ~/django-blog/src/blog (main) $ touch .env                                            
(.env) ~/django-blog/src/blog (main) $ vim .env

Une fois à l'intérieur de vim, appuyez sur la touche i (pour insert) pour éditer le fichier.

Vous pouvez écrire les valeurs suivantes en les adaptant bien sûr à votre projet :

SECRET_KEY='sdggheedsubf$l3ddt_n!-_j00+ye_a(gha*_jmcujm5i_-o)#cs'
DEBUG=False
ALLOWED_HOSTS='Docstring.pythonanywhere.com'

En production, nous passons la valeur de DEBUG à False.

Pour la variable ALLOWED_HOSTS, il faut indiquer le nom de domaine de votre site.

Vous pouvez trouver cette URL dans votre tableau de bord PythonAnywhere dans l'onglet Web :

Pour sortir de vim, appuyez sur la touche Échap (pour sortir du mode d'édition) puis inscrivez :wq dans la commande pour sauvegarder le fichier et sortir de l'éditeur (w: write, q: quit) :

Modifier le fichier wsgi

On arrive au bout, ne vous inquiétez pas !

Nous devons maintenant modifier le fichier de configuration de notre application sur PythonAnywhere.

Dans l'onglet Web de votre tableau de bord, cliquez sur le chemin du fichier associé à WSGI configuration file (normalement, /var/www/docstring_pythonanywhere_com_wsgi.py) :

PythonAnywhere va ouvrir un éditeur de code qui contient différentes configurations que vous pouvez utiliser pour différentes framework (Flask, Django, ...) :

Supprimez tout ce qui est contenu dans le fichier de configuration et collez ces lignes :

import os
import sys

# assuming your django settings file is at '/home/Docstring/mysite/mysite/settings.py'
# and your manage.py is is at '/home/Docstring/mysite/manage.py'
path = '/home/Docstring/django_blog/src'  # Le dossier qui contient manage.py
if path not in sys.path:
    sys.path.append(path)

os.environ['DJANGO_SETTINGS_MODULE'] = 'blog.settings'  # Indiquez le dossier qui contient le fichier settings.py

# then:
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

Attention, veillez bien à modifier le chemin de la variable path pour indiquer le dossier qui contient le fichier manage.py :

path = '/home/Docstring/django_blog/src'

Il faut également modifier la variable d'environnement DJANGO_SETTINGS_MODULE. Dans notre cas, on met 'blog.settings', car le fichier settings.py se trouve à l'intérieur du dossier blog :

os.environ['DJANGO_SETTINGS_MODULE'] = 'blog.settings'

Pensez bien à sauvegarder le fichier :

Et voilà ! On peut maintenant retourner sur l'onglet Web de PythonAnywhere pour actualiser notre application en cliquant sur le bouton Reload Docstring.pythonanywhere.com :

Si vous avez tout fait correctement, vous pouvez maintenant vous rendre à l'adresse du blog (https://docstring.pythonanywhere.com/blog/) et votre site devrait s'afficher :

Pour l'instant, nous n'avons aucun article affiché et les feuilles de style ne sont pas chargées. C'est normal ! Nous n'avons pas encore créé la base de données ni indiqué à PythonAnywhere où se trouvent nos fichiers statiques.

Créer la base de donnéesDe retour dans la console, assurez-vous de sourcer votre environnement virtuel, et à l'intérieur du dossier src (le dossier qui contient manage.py), utilisez la commande python manage.py migrate :

(.env) ~/django-blog/src (main) $ python manage.py migrate                                   
Operations to perform:                                                                            
  Apply all migrations: admin, auth, contenttypes, posts, sessions                                
Running migrations:                                                                               
  Applying contenttypes.0001_initial... OK                                                        
  Applying auth.0001_initial... OK                                                                
  Applying admin.0001_initial... OK                                                               
  Applying admin.0002_logentry_remove_auto_add... OK                                              
  Applying admin.0003_logentry_add_action_flag_choices... OK                                      
  Applying contenttypes.0002_remove_content_type_name... OK                                       
  Applying auth.0002_alter_permission_name_max_length... OK                                       
  Applying auth.0003_alter_user_email_max_length... OK                                            
  Applying auth.0004_alter_user_username_opts... OK                                               
  Applying auth.0005_alter_user_last_login_null... OK                                             
  Applying auth.0006_require_contenttypes_0002... OK                                              
  Applying auth.0007_alter_validators_add_error_messages... OK                                    
  Applying auth.0008_alter_user_username_max_length... OK                                         
  Applying auth.0009_alter_user_last_name_max_length... OK                                        
  Applying auth.0010_alter_group_name_max_length... OK                                            
  Applying auth.0011_update_proxy_permissions... OK                                               
  Applying auth.0012_alter_user_first_name_max_length... OK                                       
  Applying posts.0001_initial... OK                                                               
  Applying posts.0002_blogpost_thumbnail... OK                                                    
  Applying sessions.0001_initial... OK 

Votre base de donnée est maintenant créé (vous devriez voir le fichier db.sqlite3 à l'intérieur du dossier src).

On va en profiter pour tout de suite créer un super utilisateur avec la commande createsuperuser afin de pouvoir accéder à l'interface d'administration et écrire des articles :

(.env) ~/django-blog/src (main) $ python manage.py createsuperuser                           
Nom dutilisateur (leave blank to use 'docstring'): docstring                                              
Adresse électronique: hello@docstring.fr                                                          
Password:                                                                                         
Password (again):                                                                                 
Superuser created successfully.

Collecter les fichiers statiques

On arrive à la fin de cet article 🥵

La dernière chose qu'il nous reste à faire, c'est d'indiquer dans notre tableau de bord l'URL et les dossiers qui contiennent nos fichiers statiques et nos fichiers de médias (souvenez-vous, les fameuses variables STATIC_URL, STATIC_ROOT, MEDIA_URL et MEDIA_ROOT).

Dans l'onglet Web, entrez les valeurs correspondantes à celles que vous avez entrées dans le fichier settings.py pour ces 4 variables (respectivement l'URL et le dossier pour les fichiers statiques et l'URL et le dossier pour les fichiers de médias) :

Il ne nous reste plus qu'à collecter tous les fichiers statiques de notre application dans le dossier correspondant à STATIC_ROOT avec la commande python manage.py collectstatic.

De retour dans notre console, dans le dossier src :

(.env) ~/django-blog/src (main) $ python manage.py collectstatic                             

134 static files copied to '/home/Docstring/django-blog/src/staticfiles'.

Actualisez votre application en cliquant sur le gros bouton vert :

Vous pouvez maintenant retourner sur votre blog. Les fichiers CSS sont maintenant bien chargés par notre application et la mise en forme est correctement appliquée :

Conclusion

C'était compliqué vous ne trouvez pas 🥵 ? Le déploiement d'un site web n'est en effet jamais chose aisée.

Mais si vous refaites toutes ces étapes une deuxième fois, vous verrez que pour la plupart, il s'agit d'étapes que l'on ferait pour n'importe quel projet Python (création d'un dépôt Git, création d'un environnement virtuel, installation des bibliothèques avec pip).

Personnellement, je trouve que PythonAnywhere réussi le pari de proposer un bon entre deux entre Heroku et Digital Ocean.

On a accès à notre serveur via une console et on se retrouve donc dans un environnement très similaire à notre environnement local. Et plusieurs étapes assez pénibles à mettre en place comme la gestion des fichiers statiques sont tout de même gérées de façon efficace pour nous.

Dans cet article, nous n'avons pas vu comment utiliser un nom de domaine personnalisé ou mettre en place une base de donnée PostgreSQL. Pour ça, il vous faudra investir dans un compte payant et ces étapes feront l'objet d'un article dédié.