Created on 22 Jul 2018 ; Modified on 21 Dec 2019
Sono un sostenitore di Python e Django. Per questo motivo sto per affermare un concetto che non mi piace: "non è facile installare in produzione una applicazione sviluppata con Django".
D'altro canto, non è neanche una "mission impossible", se si sa dove e perché mettere le mani.
In questo articolo illustro la procedura che seguo quando installo in produzione una applicazione Django. Come vedremo, non è banale, ma, con un po' di pazienza, funziona.
Cosa serve?
Prerequisito è la disponibilità di un server con sistema operativo CentOS 7, con openssh, e servizio web server NGINX.
Il DB utilizzato è sqlite3, incluso in Python.
L'installazione dell'applicazione avviene via remote copy, copiando i file applicativi e di configurazione. Quindi modificando manualmente i file di configurazione. Ed infine inizializzando l'ambiente (DB e file statici).
In coda all'articolo indicheremo anche qualche altra operazione, che solitamente è utile.
E' necessaria un minimo di conoscenza dei comandi Linux da console. Così come saper utilizzare un editor di testo da console. Un classico è vi, ma qualunque editor presente nel server va bene.
Ipotizziamo di avere un progetto che si chiama base e che ha la seguente struttura sui computer di sviluppo (struttura familiare per chi lavora con Django):
base/ |-- .git/ # non in produzione |-- deployment/ # non in produzione |-- docs/ # non in produzione |-- venv/ # non in produzione | |-- sito/ # in produzione | | | |-- app1/ # in produzione | | |-- templates/ | | | +-- app1/ | | +-- urls.py, views.py, models.py, ... | | | |-- sito/ | | |-- settings.py, # in produzione MA MODIFICATO | | +-- urls.py, views.py, wsgi.py | | | |-- templates/ | | +-- base.html, home.html, login.html | | | +-- static/ | |-- css/ | |-- images/ | +-- js/ | +-- .gitignore, requirements.txt # in produzione solo requirements.txt
Il nostro progetto risponderà al dominio www.sito.org.
Lato web server utilizzeremo Nginx che passa la richiesta tramite socket a Gunicorn. Questo a sua volta incamera l'applicazione Django base/sito, e la esegue passando la richiesta proveniente da Nginx.
Gunicorn, Django, base/sito, sono applicazioni Python 3.6 ospitate in un ambiente virtuale leggero configurato tramite virtualenv.
Da Python versione 3.3 e successive, virtualenv è incluso nell'ambiente di base dell'interprete, nel modulo venv.
L'applicazione base verrà installata nella directory /usr/share/nginx/html/base.
Vediamo le diverse fasi dell'installazione.
Per installare Nginx possiamo usare la seguente [1]:
# > utenza root su server yum install epel-release # aggiungere il repository epel yum install nginx # installare nginx systemctl start nginx # avviare nginx # aprire le porte http(s) su firewall firewall-cmd --permanent --zone=public --add-service=http firewall-cmd --permanent --zone=public --add-service=https firewall-cmd --reload systemctl enable nginx # nginx starts on system boot curl localhost.localdomain # to check mkdir /etc/nginx/sites-available mkdir /etc/nginx/sites-enabled yum -y install vim # un minimo di comfort rispetto vi # disk usage: 1.2G
Questa procedura [2] installa Python 3.6 di fianco a Python 2.7, che è la normale dotazione di CentOS 7, e che NON può essere sostituito.
# utenza root su server # installare utilities varie yum install deltarpm #yum update ## ATTENZIONE: può dare problemi alla # configurazione di php, se presente nel sistema # e utilizzato dal server http yum install yum-utils yum groupinstall development # IUS (Inline with Upstream Stable) repository e installazione python 3.6 (di fianco al 2.7) yum install https://centos7.iuscommunity.org/ius-release.rpm yum install python36u yum install python36u-pip yum install python36u-devel
cd /usr/share/nginx/html mkdir base cd base python3.6 -m venv venv mkdir run mkdir log # copiare dal sistema di sviluppo, via remote copy, l'applicazione in /usr/share/nginx/html # la dir.da cui iniziare la copia è .../base/sito, inoltre copiare il file # .../base/requirements.txt. NON copiare sul server il venv del pc di sviluppo # ad esempio, da client di sviluppo con windows, utilizzando il programma pscp.exe: # pscp -r C:\...\Sviluppi\base\sito root@server_address:/usr/share/nginx/html/base/sito # pscp C:\...\Sviluppi\base\requirements.txt root@server_address:/usr/share/nginx/html/base source venv/bin/activate pip install -r requirements.txt pip install --upgrade pip pip install gunicorn
La configurazione dell'applicazione consiste in:
In pratica i seguenti passi.
cd sito/sito # modificare /usr/share/nginx/html/base/sito/sito/settings.py # in questo file porre: # ... # DEBUG = False # ALLOWED_HOSTS = ['www.sito.org', ] # ...
Per settare la SECRET_KEY ad un valore non conosciuto a chi sviluppa, è possibile generare un file secretkey.txt utilizzato poi da settings.py.
Questa operazione non è del tutto immediata. Perché dobbiamo generare una chiave segreta in modo randomico. Per farlo è bene utilizzare Django. Ma per utilizzare Django abbiamo bisogno di un file di configurazione funzionante, ovvero con la SECRET_KEY già impostata.
Un bell'esempio di cane che si morde la coda.
Per aggirare il problema: utilizziamo una chiave conosciuta, generiamo quella segreta, e poi mettiamo al lavoro quest'ultima. Questo si traduce in:
Quindi riconfigurare la seguente sezione di settings.py:
# ... # SECURITY WARNING: keep the secret key used in production secret! # SECRET_KEY = '71t4+5nfq^#$i*ltas_%ssc$#!t^^rap2%i#3i2&ye)e)c=d@0' with open(BASE_DIR + '/secretkey.txt') as f: SECRET_KEY = f.read().strip() # ...
in questo modo:
# ... # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '71t4+5nfq^#$i*ltas_%ssc$#!t^^rap2%i#3i2&ye)e)c=d@0' #with open(BASE_DIR + '/secretkey.txt') as f: # SECRET_KEY = f.read().strip() # ...
Quindi eseguire:
python mksecret.py >secretkey.txt
Infine riportare il settings.py nella configurazione iniziale.
Il file mksecret.py [3] è come segue:
1 # filename: mksecret.py 2 import os, sys 3 4 # CHANGE THIS - this the project's manage.py directory 5 proj_path = "/usr/share/nginx/html/base_fs_rst/rstsite" 6 # CHANGE THIS - This is so Django knows where to find stuff. 7 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rstsite.settings") 8 sys.path.append(proj_path) 9 10 # This is so my local_settings.py gets loaded. 11 os.chdir(proj_path) 12 13 # This is so models get loaded. 14 from django.core.wsgi import get_wsgi_application 15 application = get_wsgi_application() 16 17 ################## FROM HERE - your script 18 19 from django.core.management import utils 20 print(utils.get_random_secret_key())
cd .. # cd .../base/sito # rm db.sqlite3 # se esiste il file db.sqlite3 dal pc di sviluppo # crea il DB e il suo amministratore python3.6 manage.py makemigrations python3.6 manage.py migrate python3.6 manage.py createsuperuser
python manage.py collectstatic --noinput # contenuti statici per il server http
Questa attività va fatta se l'applicazione da installare può gestire più di un linguaggio (non è banale: il multilingua é una problematica molto vasta).
python manage.py compilemessages -l it # compila il dizionario dei messaggi soggetti a traduzione
Gunicorn è il passa-dati tra il server web e l'applicazione Django [4].
Dopo averlo installato (ricordate pip install gunicorn?), per metterlo al lavoro dobbiamo fare due cose.
Primo. Dobbiamo fare in modo che Gunicorn sappia come avviare la nostra applicazione. Per questo creiamo o copiamo il file gunicorn_start nella directory /usr/share/nginx/html/base/venv/bin/
Il contenuto del file può essere il seguente (o simile):
#!/bin/bash NAME="sito" # Name of the application DJANGODIR=/usr/share/nginx/html/base/sito # Django project directory SOCKFILE=/usr/share/nginx/html/base/run/gunicorn.sock # we will communicte using this unix socket #USER=nginx # the user to run as #GROUP=webdata # the group to run as USER=root # the user to run as GROUP=root # the group to run as NUM_WORKERS=1 # how many worker processes should Gunicorn spawn DJANGO_SETTINGS_MODULE=sito.settings # which settings file should Django use DJANGO_WSGI_MODULE=sito.wsgi # WSGI module name echo "Starting $NAME as `whoami`" # Activate the virtual environment cd $DJANGODIR source /usr/share/nginx/html/base/venv/bin/activate export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE export PYTHONPATH=$DJANGODIR:$PYTHONPATH # Create the run directory if it doesn't exist RUNDIR=$(dirname $SOCKFILE) test -d $RUNDIR || mkdir -p $RUNDIR # Start your Django Unicorn # Programs meant to be run under supervisor should not daemonize themselves (do not use --daemon) exec /usr/share/nginx/html/base/venv/bin/gunicorn ${DJANGO_WSGI_MODULE}:application \ --name $NAME \ --workers $NUM_WORKERS \ --user=$USER --group=$GROUP \ --bind=unix:$SOCKFILE \ --log-level=debug \ --log-file=-:
Attenzione. Prendetevi il tempo necessario per capire che cosa fa lo script precedente, perché deve essere personalizzato per l'installazione in corso.
I parametri da variare sono parecchi, ed è inutile dire che basta sbagliare un nonnulla per impedire la comunicazione tra Nginx e l'applicazione.
Un'altra osservazione. Notate le variabili USER e GROUP, originariamente [5] poste a nginx e webdata. Noi le abbiamo poste a root. In generale questo modo di fare non è raccomandabile: può generare criticità di sicurezza nel sistema. Quindi, se avete tempo, provate a mantenere e far funzionare il tutto con i valori originali. Personalmente vado sempre di corsa, e finora ho continuato a lavorare con root [6].
Proseguiamo. La seconda cosa da fare è fare partire Gunicorn come servizio. Per questo dobbiamo creare o copiare il seguente script gunicorn_sito.service nella directory /etc/systemd/system/.
Lo script, più semplice del precedente, è:
[Unit] Description=sito gunicorn daemon [Service] Type=simple User=root ExecStart=/usr/share/nginx/html/base/venv/bin/gunicorn_start [Install] WantedBy=multi-user.target
Quasi ci siamo. Dobbiamo abilitare il servizio e avviarlo. Come segue:
systemctl enable gunicorn_sito
systemctl start gunicorn_sito
systemctl status -l gunicorn_sito # per controllare se il servizio è partito; credeteci: ne vale la pena!
Troubleshooting. Aspettatevi segnalazioni di errore al lancio di Gunicorn. Un errore frequente è il 203. Nella mia esperienza vi sono almeno due possibili cause, entrambe relative allo script /usr/share/nginx/htm/base/venv/bin/gunicorn_start:
Per approfondire si può consultare: Setting up Gunicorn for Django Project - 203/EXEC.
La configurazione di Nginx è un altro punto chiave.
Un esempio di configurazione di base si ottiene creando o copiando nel file /etc/nginx/nginx.conf il seguente:
1 # sito@centos7: this file @ /etc/nginx/nginx.conf 2 # For more information on configuration, see: 3 # * Official English Documentation: http://nginx.org/en/docs/ 4 # * Official Russian Documentation: http://nginx.org/ru/docs/ 5 6 user nginx; 7 worker_processes auto; 8 error_log /var/log/nginx/error.log; 9 pid /run/nginx.pid; 10 11 # Load dynamic modules. See /usr/share/nginx/README.dynamic. 12 include /usr/share/nginx/modules/*.conf; 13 14 events { 15 worker_connections 1024; 16 } 17 18 http { 19 log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 20 '$status $body_bytes_sent "$http_referer" ' 21 '"$http_user_agent" "$http_x_forwarded_for"'; 22 23 access_log /var/log/nginx/access.log main; 24 25 sendfile on; 26 tcp_nopush on; 27 tcp_nodelay on; 28 keepalive_timeout 65; 29 types_hash_max_size 2048; 30 31 include /etc/nginx/mime.types; 32 default_type application/octet-stream; 33 34 # Load modular configuration files from the /etc/nginx/conf.d directory. 35 # See http://nginx.org/en/docs/ngx_core_module.html#include 36 # for more information. 37 include /etc/nginx/conf.d/*.conf; 38 39 server { 40 listen 80 default_server; 41 listen [::]:80 default_server; 42 43 #server_name localhost; 44 server_name sito.org; 45 46 root /usr/share/nginx/html; 47 index index.html index.htm; 48 49 # Add headers to serve security related headers 50 # Before enabling Strict-Transport-Security headers please read into this 51 # topic first. 52 #add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"; 53 add_header X-Content-Type-Options nosniff; 54 add_header X-XSS-Protection "1; mode=block"; 55 add_header X-Robots-Tag none; 56 add_header X-Download-Options noopen; 57 add_header X-Permitted-Cross-Domain-Policies none; 58 59 # Load configuration files for the default server block. 60 include /etc/nginx/default.d/*.conf; 61 # include /etc/nginx/sites-enabled/*.conf; # DON'T do this: it must be 62 # at the end of this configuration file 63 # thanks to Massimiliano Trocar 64 65 location / { 66 try_files $uri $uri/ =404; 67 } 68 69 error_page 404 /404.html; 70 location = /40x.html { 71 } 72 73 error_page 500 502 503 504 /50x.html; 74 location = /50x.html { 75 } 76 77 location ~ (\.php$) { 78 return 403; 79 } 80 81 listen [::]:443 ssl ipv6only=on; # managed by Certbot 82 listen 443 ssl; # managed by Certbot 83 ssl_certificate /etc/letsencrypt/live/defalcoalfano.org/fullchain.pem; # managed by Certbot 84 ssl_certificate_key /etc/letsencrypt/live/defalcoalfano.org/privkey.pem; # managed by Certbot 85 include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 86 ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot 87 } 88 89 # add other enabled servers 90 include /etc/nginx/sites-enabled/*; 91 }
Alcune osservazioni.
Alcune linee hanno il commento # managed by Certbot, queste sono state introdotte dal software per il rilascio del certificato ssl del sito. Ne riparliamo tra poco. In questa fase NON saranno presenti.
Vi sono riferimenti alla directory /etc/nginx/sites-enabled. Questa è un tecnica che facilita la gestione dei siti da mettere in (o togliere dalla) linea. In pratica:
Ad esempio creiamo o copiamo nel file /etc/nginx/sites-available/www.sito.org.conf il seguente:
1 upstream gunicorn_handler { 2 server unix:/usr/share/nginx/html/base/run/gunicorn.sock fail_timeout=10s; 3 } 4 5 server { 6 listen 80; 7 server_name www.sito.org; 8 9 root /usr/share/nginx/html/base; 10 index index.html index.htm; 11 12 client_max_body_size 4G; 13 14 access_log /usr/share/nginx/html/base/logs/access.log; 15 error_log /usr/share/nginx/html/base/logs/error.log warn; 16 17 location /static/ { 18 autoindex on; 19 alias /usr/share/nginx/html/base/sito/static_root/; 20 } 21 22 location /media/ { 23 autoindex on; 24 alias /usr/share/nginx/html/base/sito/media/; 25 } 26 27 location / { 28 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 29 proxy_set_header Host $http_host; 30 proxy_redirect off; 31 32 if (!-f $request_filename) { 33 proxy_pass http://gunicorn_handler; 34 break; 35 } 36 } 37 38 location ~ (\.php$) { 39 return 403; 40 } 41 42 # managed by Certbot 43 44 listen 443 ssl; # managed by Certbot 45 ssl_certificate /etc/letsencrypt/live/luciano.defalcoalfano.it/fullchain.pem; # managed by Certbot 46 ssl_certificate_key /etc/letsencrypt/live/luciano.defalcoalfano.it/privkey.pem; # managed by Certbot 47 include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 48 #ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot 49 ssl_dhparam /etc/ssl/certs/dhparam.pem; # better Diffie-Hellman params 50 } 51 52 server { 53 if ($host = luciano.defalcoalfano.it) { 54 return 301 https://$host$request_uri; 55 } # managed by Certbot 56 57 58 server_name www.sito.org; 59 listen 80; 60 return 404; # managed by Certbot 61 }
Di nuovo, ipotizziamo che le linee commentate come # managed by Certbot in questa fase non esistano.
Le linee da 1 a 3 della configurazione, istruiscono Nginx ad usare il socket gunicorn.sock per comunicare con l'applicazione Django quando si richiede il dominio www.sito.org.
Le linee da 17 a 25 istruiscono Nginx a servire direttamente i contenuti statici dell'applicazione, senza impegnare Django.
Quindi, per abilitare il sito dobbiamo linkarlo da enabled e riavviare Nginx:
ln -s /etc/nginx/sites-available/www.sito.org.conf /etc/nginx/sites-enabled/
nginx -t # controlla la sintassi dei file di config.di Nginx
systemctl restart nginx # riavvia il web server
Passiamo ad alcune operazioni finali, di solito molto utili.
Questo sarebbe meglio non farlo, e spendere il tempo necessario per configurare opportunamente i profili delle aree dati impattate da Nginx e dall'applicazione Django. Ma, andando di fretta:
sestatus # per controllare
setenforce Permissive
vim /etc/sysconfig/selinux # modificare a: SELINUX=disabled
Questo è fondamentale, altrimenti Nginx non sarà in grado di gestire i flussi di dati:
cd /usr/share/nginx/http
chown nginx:nginx base # qui NON -R perché coinvolgerebbe venv
cd base
chown -R nginx:nginx sito
chown -R nginx:nginx log
chown -R nginx:nginx run
Per sicurezza, verificare che in .../base/venv/bin/gunicorn_start valgano le seguenti:
... USER=root # the user to run as GROUP=root # the group to run as ...
A questo punto, se da WEB browser richiediamo il dominio http://www.sito.org dovrebbe rispondere la nostra applicazione.
Il condizionale è d'obbligo :-) debug, debug, troubleshoot, troubleshoot, ... [8]
Quando avremo verificato che l'applicazione risponde all'indirizzo Web previsto utilizzando il protocollo http, usualmente avremo necessità di abilitare anche, o esclusivamente, il protocollo https.
A questo fine dovremo ottenere ed installare un certificato ssl relativo al sito in configurazione.
Qui entra in campo Let's Encrypt, un servizio che rilascia certificati ssl/tls senza necessità di acquisto, ed agisce come certification authority (CA). Quindi i certificati rilasciati da Let's Encrypt sono validi a tutti gli effetti.
Come ottenere e installare questi certificati in un server CentOS 7 è documentato in: How To Secure Nginx with Let's Encrypt on CentOS 7
Qui si seguito sintetizziamo utilizzando la strada più semplice, che nel nostro caso consiste nell'usare l'applicazione certbot per nginx:
# yum install epel-release # già installato yum install certbot-nginx # installa certbot x nginx # genera il certificato e modifica la configurazione di nginx certbot --nginx -d sito.org -d www.sito.org # rispondere alle domande # genera un certificato con parametri Diffie-Hellman migliori openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
Attenzione a modificare la configurazione di www.sito.org.conf aggiungendo la linea 49.
Vi sono rari casi in cui si ha la necessità di generare certificati autofirmati. Ad esempio se si lavora in TLD di tipo .local.
In questi casi si può usare questa procedura:
# directory per le chiavi private (cert x i certificati pubblici deve già esistere) mkdir /etc/ssl/private chmod 700 /etc/ssl/private # generare una chiave e relativo certificato pubblico openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/nginx-selfsigned.key -out /etc/ssl/certs/nginx-selfsigned.crt # generare un Diffie-Hellmann group openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048 # controllare la configurazione di /etc/nginx/sites-available/www.sito.org.conf
Quando si effettua debugging, è comune la necessità di controllare gli orari riportati nei file di log per capire quando è successo qualcosa.
E qui sorge il problema del confronto con il proprio orario. Già, perché solitamente un server utilizza il fuso orario UTC, che spesso non é quello utilizzato dal client. Ad esempio lavorando su Roma, è normale essere un'ora in anticipo rispetto UTC.
Quindi può essere comodo regolare l'orario del server su quello del client, come segue:
timedatectl set-timezone "Europe/Rome"
timedatectl set-time 15:58:30
timedatectl set-local-rtc 1
Al termine delle attività di debugging, ricordarsi di riportare il sever su UTC:
timedatectl set-timezone UTC # prima del rilascio mettersi in UTC
La procedura è stata costruita consultando (creativamente :-) le indicazioni dei riferimenti riportati qui di seguito. Cui si rimanda per i dovuti approfondimenti.
21 dic 2019 - Un rigraziamento a Massimiliano Trocar, che ha scoperto un errore di configurazione di Nginx nel file nginx.conf.
Questo articolo è aggiornato di conseguenza.
[1] | Ripresa da installare nginx su centos 7 |
[2] | Ripresa da How-to install Python 3.6.1 on CentOS 7 |
[3] | Grazie a Standalone Django scripts: The definitive guide |
[4] | Più in generale, Gunicorn mette a disposizione di una qualunque applicazione Python un modo di comunicare dati detto wsgi (alias: Web Server Gateway Interface), inventato dai pythonisti per comunicare tra applicazione Pyhton e un ipotetico WEB server. |
[5] | Ovvero da Deploying nginx + django + python 3 |
[6] | Questo è outing accompagnato da cenere cosparsa sul capo, e la intima convinzione che capiterà di nuovo :-) |
[7] | In questo caso: chmod +x /usr/share/nginx/htm/base/venv/bin/gunicorn_start |
[8] | Riflettendo, probabilmente il troubleshooting di una configurazione di produzione vale un articolo a parte. Ci penseremo e, se avremo tempo e capacità, vedremo di scrivere qualcosa. |