Time To Go Live

Le istruzioni riportate in questo articolo sono basilari e rappresentano il minimo indispensabile per mettere on-line un’applicazione web minimale. Per applicazioni “reali” è doveroso consultare la documentazione del framework prescelto e apportare le dovute integrazioni.

Sviluppare un’applicazione web non è soltanto una questione di codifica in un linguaggio di programmazione, tecniche algoritmiche, design, adozione di un framework e bugfix. C’è anche la parte del deployment in produzione, ovvero la necessità di rendere l’applicazione accessibile e fruibile agli utenti per i quali è stata pensata.

Ai tempi d’oro del PHP, questa fase di deployment era piuttosto semplice: caricavi il codice su un server, tipicamente un hosting condiviso, e avevi finito; la parte più complicata, al massimo, poteva essere quella di configurare il database.

Oggi che le tecnologie, come anche le metodologie di sviluppo, sono cambiate, non è più sufficiente copiare e incollare il codice su un server, ma bisogna considerare tutta una serie di fattori.

La difficoltà del processo per mettere in produzione un’applicazione, quindi, dal mio punto di vista fa parte dei criteri di selezione del linguaggio di programmazione e di un eventuale framework. Vediamo qualche esempio.

Ruby on Rails

Supponiamo di aver sviluppato un’applicazione web con Rails partendo da zero, con il classico comando

rails new --css=sass test-bootstrapCode language: Bash (bash)

Dotiamo questa applicazione di un banalissimo controller con due views per mostrare due pagine statiche:

rails generate controller Pages home aboutCode language: Bash (bash)

Non ci preoccupiamo del setup del database e decidiamo di utilizzare il semplice SQLite, pertanto applichiamo le migrazioni con

rails db:migrateCode language: Bash (bash)

e la nostra applicazione è pronta. I più sofisticati magari decideranno anche di aggiungere un framework CSS come Bootstrap.

Adesso è giunto il momento di andare live! Che si fa?

Upload dell’applicazione

Per prima cosa bisogna assicurarsi che il server sia dotato del software necessario ad eseguire l’applicazione; si tratta di un prerequisito che vale per qualsiasi soluzione scelta. Per Ruby ci si può affidare al tool “mise“.

Successivamente bisogna mettere il codice sul server; lo si può trasferire direttamente (tramite ssh/scp o sftp e simili) oppure possiamo usare un repository Git dedicato; l’importante che a un certo punto l’applicazione sia sul server. Adesso viene il bello!

Configurazione di Puma

Per eseguire l’applicazione Rails useremo NGINX come reverse proxy e Puma come application server. Affinché Puma si avvii correttamente dobbiamo modificare il suo file di configurazione e predisporre alcune sottodirectory all’interno della directory principale dell’applicazione (che nel mio caso è /srv/rails/rails-bootstrap).

threads_count = ENV.fetch("RAILS_MAX_THREADS", 3)
threads threads_count, threads_count

# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
port ENV.fetch("PORT", 3000)

# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart

# Run the Solid Queue supervisor inside of Puma for single-server deployments
plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]

# Specify the PID file. Defaults to tmp/pids/server.pid in development.
# In other environments, only set the PID file if requested.
pidfile ENV["PIDFILE"] if ENV["PIDFILE"]


app_dir = "/srv/rails/rails-bootstrap"
shared_dir = "#{app_dir}/shared"

bind "unix://#{shared_dir}/tmp/sockets/puma.sock"
stdout_redirect "#{shared_dir}/log/puma.stdout.log", "#{shared_dir}/log/puma.stderr.log", true
pidfile "#{shared_dir}/tmp/pids/puma.pid"
state_path "#{shared_dir}/tmp/pids/puma.state"
activate_control_appCode language: Ruby (ruby)

La directory da creare è shared, insieme ad alcune sottodirectory:

shared/
├── log
└── tmp
    ├── pids
    └── sockets

Ora è il momento di configurare anche la service unit systemd che si occupa di gestire il servizio Puma; creiamo il file /etc/systemd/system/puma.service con questo contenuto:

[Unit]
Description=Puma HTTP Server
After=network.target

[Service]
Environment="RAILS_ENV=production"
User=gianluca
WorkingDirectory=/srv/rails/rails-bootstrap
ExecStart=/home/gianluca/.local/share/mise/installs/ruby/3.4.5/bin/puma -C config/puma.rb
Restart=always

[Install]
WantedBy=multi-user.targetCode language: JavaScript (javascript)

Occhio alla voce User, che deve essere un utente esistente con diritti di lettura e scrittura all’interno della directory dell’applicazione.

Occupiamoci ora del resto del software necessario.

Installazione del software applicativo

Dalla directory dell’applicazione, eseguiamo il comando bundle install per installare Rails e tutte le sue dipendenze. Eseguiamo anche gem install puma, che potrebbe non essere elencato nel Gemfile ma che è ovviamente necessario.

NGINX

Ora è il momento della configurazione di NGINX, che si suppone sia già installato:

upstream rails_bootstrap {
  server unix:/srv/rails/rails-bootstrap/shared/tmp/sockets/puma.sock fail_timeout=0;
}
server {
    server_name rails.verolinux.com;

    location / {
    proxy_pass http://rails_bootstrap;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }

  location /assets/ {
    alias /srv/rails/rails-bootstrap/public/assets/;
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }


    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/rails.verolinux.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/rails.verolinux.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}


server {
    if ($host = rails.verolinux.com) {
        return 301 https://$host$request_uri;
    }

    listen 80;
    server_name rails.verolinux.com;
    return 404;
}Code language: Nginx (nginx)

Questa configurazione include anche la parte dei certificati SSL, ottenuti tramite Certbot e Let’s Encrypt; do per scontato che si sappia come fare per ottenere un certificato SSL valido.

Master key e compilazione degli asset

Manca ancora qualcosa, ovvero la preparazione/compilazione degli asset, ma prima di procedere, se state usando Git per gestire l’applicazione sul server, è necessario importare la master key del progetto, che di default non viene inclusa nel repository. Bisogna quindi copiare il file config/master.key dal vostro ambiente di sviluppo al server. Per maggiori informazioni sulla master key e sulla gestione di credenziali e segreti in Rails, potete consultare questo articolo o questo ancora.

Successivamente si può procedere alla compilazione degli asset:

RAILS_ENV=production bundle exec rails assets:precompile

A questo punto dovrebbe essere possibile attivare la service unit di Puma con:

sudo systemctl daemon-reload
sudo systemctl enable --now pumaCode language: Bash (bash)

riavviare NGINX (se non lo avete già fatto dopo la configurazione del virtual server) e il sito dovrebbe essere raggiungibile e funzionante:

Screenshot dell’applicazione rails di test

La view mostrata è stata realizzata prendendo il codice dell’esempio di blog che potete scaricare dal sito ufficiale di Bootstrap.

Django

Mettere in produzione un’applicazione Django non è molto differente da quanto fatto per Rails; anche in questo caso ci si può avvalere di un repository Git oppure caricare a mano il codice sul server, l’importante è che ci sia il file requirements.txt necessario ad installare tutte le dipendenze software. Questo file è per Python qualcosa di simile al Gemfile di un progetto Ruby.

Il server deve essere ovviamente equipaggiato con la giusta versione di Python; a seconda dei casi, possiamo usare l’interprete Python fornito dalla distribuzione oppure usare un software manager come Mise (come per Ruby) oppure uv.

In funzione del metodo di gestione del software adottato, andiamo quindi ad installare le dipendenze, ad esempio con un virtual environment e pip: pip install -r requirements.txt.

Impostazioni del progetto

Quando si mette in produzione un’applicazione Django, è necessario apportare alcuni adattamenti al file delle impostazioni, ovvero al file settings.py che si trova nella subdirectory del progetto. Le opzioni più importanti da modificare sono la modalità debug, la lista degli hostname consentiti e la secret key:

DEBUG = False
ALLOWED_HOSTS = [ 'django.verolinux.com` ]
SECRET_KEY = '...'Code language: Python (python)

Per sapere come generare una secret key casuale, potete consultare questo articolo.

Migrazione db e compilazione assets

Applichiamo le migrazioni del database per sincronizzarne la struttura con quella prevista in fase di sviluppo con ./manage.py migrate; successivamente, con il comando ./manage.py collectstatic andiamo a “staticizzare” gli asset in modo da servirli direttamente tramite server web senza passare per Python. Questo comando produrrà una directory con dentro gli asset previsti. Il nome della directory e la corrispondente URL sono definite in settings.py con le seguenti opzioni:

STATIC_ROOT = BASE_DIR / 'staticfiles'
STATIC_URL = 'static/'Code language: Python (python)

Configurazione Gunicorn

Se non già previsto nei requirements, installiamo il package gunicorn con pip install gunicorn e andiamo a configurare la corrispondente service unit systemd; nel mio caso, dato che il progetto si chiama “test” ed è contenuto nella directory /srv/django/django-test, il file sarà fatto come segue:

[Unit]
Description=gunicorn daemon
After=network.target

[Service]
User=gianluca
Group=nginx
WorkingDirectory=/srv/django/django-test
ExecStart=/srv/django/django-test/.venv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/srv/django/django-test/gunicorn.sock test.wsgi:application

[Install]
WantedBy=multi-user.target
Code language: JavaScript (javascript)

Supponendo di aver chiamato questo file /etc/systemd/system/gunicorn.service, attiviamo e lanciamo il servizio con:

sudo systemctl daemon-reload
sudo systemctl enable --now gunicorn

NGINX

La configurazione di NGINX come reverse proxy assomiglia molto al caso Rails:

server {
    server_name django.verolinux.com;

    location / {
        proxy_pass http://unix:/srv/django/django-test/gunicorn.sock;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /static/ {
        alias /srv/django/django-test/staticfiles/;
    }

    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/django.verolinux.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/django.verolinux.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

}

server {
    if ($host = django.verolinux.com) {
        return 301 https://$host$request_uri;
    }

    listen 80;
    server_name django.verolinux.com;
    return 404;
}
Code language: PHP (php)

Poiché per questo test non ho adottato nessun framework CSS e non ho perso tempo a importare un layout di esempio, il risultato è una semplice home page con un titolo e un paragrafo:

Home page dell’applicazione Django di test

Flask

Anche per Flask mi avvalgo di un repository Git contenente il progetto; lo potete trovare, disponibile pubblicamente, su GitLab.

Dato che gestisco il progetto e le sue dipendenze tramite uv, dopo aver clonato il repository sul server eseguo il comando uv sync per installare i pacchetti necessari. Se vi interessa sapere qualcosa di più su uv e imparare ad usarlo, vi suggerisco l’ottimo tutorial di Corey Schafer.

Configurazione

Dato che l’applicazione non utilizza database e non prevede l’interazione con gli utenti, non c’è bisogno di un setup complicato e non serve impostare una secret key per la gestione dei cookie; pertanto in questo caso non è necessario creare un file .env con queste informazioni.

Gunicorn

Come per Django, anche questa applicazione Flask sarà eseguita dall’application server Gunicorn, che provvediamo ad installare con uv pip install gunicorn. Questo server può essere eseguito manualmente oppure si può eseguire con l’ausilio di un tool chiamato supervisor, che si occupa di avviarlo, monitorarlo e riavviarlo senza interventi manuali. Il tool si può installare su distribuzioni basate su Debian col comando sudo apt install supervisor. La configurazione per la nostra applicazione sarà scritta nel file /etc/supervisor/conf.d/flask-test.conf:

[program:flask-test]
command=/srv/flask/flask-test/.venv/bin/gunicorn -b localhost:5000 -w 4 app:app
directory=/srv/flask/flask-test
user=gianluca
autostart=true
autorestart=true
stopasgroup=true
killasgroup=trueCode language: JavaScript (javascript)

Ricarichiamo la configurazione di Supervisor con sudo supervisorctl reload e la nostra applicazione dovrebbe essere in esecuzione.

Se si preferisce usare systemd al posto di Supervisor, si può configurare una service unit come fatto per l’applicazione Django.

NGINX

La configurazione di NGINX ricalca quelle viste per Django e Rails, con la sola differenza che in questo caso l’application server è in ascolto su una porta TCP anziché su un socket Unix:

server {
    server_name flask.verolinux.com;
    access_log /var/log/flask-test_access.log;
    error_log /var/log/flask-test_error.log;

    location / {
        # forward application requests to the gunicorn server
        proxy_pass http://localhost:5000;
        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /static {
        # handle static files directly, without forwarding to the application
        alias /srv/flask/flask-test/app/static;
        expires 30d;
    }

    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/flask.verolinux.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/flask.verolinux.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

}
server {
    if ($host = flask.verolinux.com) {
        return 301 https://$host$request_uri;
    }


    listen 80;
    server_name flask.verolinux.com;
    return 404;
}Code language: PHP (php)

La nostra applicazione Flask è finalmente in esecuzione e accessibile via web:

Screenshot di una pagina dell’applicazione Flask in produzione

Maggiori dettagli su questo setup li trovate su questa pagina del Flask Mega-Tutorial dell’ottimo Miguel Grinberg.

Conclusioni

L’articolo che avete appena letto offre una panoramica, superficiale ma utile, su una delle possibili modalità di deployment di un’applicazione web fatta con Django, Flask o Rails.

Si tratta del metodo probabilmente più “naturale”, che consiste nella copia del codice sul server pubblico e che non è esente da criticità.

Infatti, da qualche anno a questa parte, sembra che la modalità di deployment preferita dagli sviluppatori sia quella che fa uso di container Docker e strumenti di CI/CD adeguati. Sempre più spesso, infatti, applicazioni particolarmente complesse e professionali sono distribuite sotto forma di container perché l’installazione cosiddetta “bare-metal” non è alla portata di tutti. Alcuni esempi sono Discourse, Wiki.js, Mailcow e, da qualche giorno, anche Ghost.

Io sinceramente, per le mie piccole e banali applicazioni continuo a preferire la modalità tradizionale, ma da qualche settimana sto lavorando anche su Docker per capire se vale la pena fare questo passaggio.

Applicazioni demo

Le applicazioni utilizzate come esempio in questo articolo sono state messe effettivamente on-line per avere un riscontro reale del funzionamento di quanto illustrato, ma è probabile che verranno disattivate e rimosse per fare spazio sul VPS, che intendo riutilizzare per altri scopi 😏

Powered by atecplugins.com