Dockerfile: come creare un'immagine personalizzata con Flask
Dalla teoria alla pratica: costruiamo una vera Web App Python, scriviamo il nostro primo Dockerfile, creiamo l'immagine e facciamo il push su Docker Hub.
1. Introduzione
Uno dei punti cardine di Docker è il Dockerfile, un semplice file di testo che contiene le istruzioni necessarie per costruire un'immagine personalizzata.
Personalizzare un'immagine tramite un Dockerfile permette di automatizzare la configurazione dell'ambiente di esecuzione della propria applicazione, rendendolo riproducibile su qualsiasi sistema compatibile con Docker.
Ragazzi, fino ad ora abbiamo usato immagini già pronte da Docker Hub. Ma in azienda non troverete mai l'immagine perfetta per la vostra app. Dovete costruirvela. Il Dockerfile è il vostro "libretto di istruzioni" che dice a Docker come assemblare il vostro ambiente. Imparare a scriverlo bene è quello che distingue chi usa Docker da chi lo capisce davvero. E all'esame è una delle domande più gettonate.
2. Cos’è un Dockerfile?
Un Dockerfile è un file di testo che contiene una serie di comandi, in un linguaggio dichiarativo, che Docker interpreta per costruire un'immagine. È senza estensione, si chiama proprio Dockerfile con la D maiuscola.
3. Struttura di base del Dockerfile
Ecco le direttive principali:
| Istruzione | Descrizione |
|---|---|
| FROM | Specifica l’immagine di base da cui partire |
| RUN | Esegue comandi all’interno dell’immagine durante la sua costruzione |
| COPY / ADD | Copia file o directory nella nuova immagine |
| WORKDIR | Imposta la directory di lavoro all'interno del container |
| CMD / ENTRYPOINT | Definisce il comando eseguito all'avvio del container |
| EXPOSE | Espone una porta per l’applicazione containerizzata |
Io dico sempre ai miei alunni: FROM è "da dove parto", COPY è "cosa ci metto dentro", RUN è "cosa ci installo", WORKDIR è "dove lavoro", EXPOSE è "che porta apro", CMD è "cosa faccio partire". Se dite questa filastrocca all'orale, non vi dimenticate nulla.
4. Creazione di un’immagine personalizzata Docker + Flask
Introduzione a Flask
Flask è un micro framework web per Python, leggero ma estremamente potente, ideale per creare API REST e applicazioni web in modo rapido e modulare.
Caratteristiche principali
- Architettura minimalista e flessibile
- Routing semplice tramite decoratori (@app.route)
- Supporto per template HTML (Jinja2)
- Facilità di integrazione con database e librerie esterne
Per i nostri scopi didattici Flask è perfetto: è minimalista, in 10 righe avete un server web funzionante. Django è potentissimo ma ha già database, admin, ORM... troppo per capire Docker. Con Flask vedete subito il risultato. In molte startup e per i microservizi si usa proprio Flask per questo motivo.
5. Obiettivo
Creare un’immagine Docker personalizzata che:
- Contenga una semplice web app Flask
- Risponda sulla porta 5000 con visualizzazione della home page
- Sia pubblicabile su Docker Hub
6. Struttura del progetto e creazione file
Creiamo la nostra struttura. Fare riferimento alla guida precedente all'interno della quale avevamo già creato una directory per i nostri progetti "Applicazioni con Docker". In questa Directory creiamo la sottodirectory flask-docker-app al cui interno creiamo le directory static e templates che conterranno, rispettivamente, un file css per lo stile della nostra Web application e la home page.
Dopo aver creato la struttura, aprire VSC ed aprire la directory "flask-docker-app" oppure posizionarsi nella directory, aprire il terminale ed eseguire il comando:
File templates/index.html
Nella cartella templates creare un file index.html come ad esempio:
File static/css/stile.css
Nella cartella /static/css creare un file stile.css come ad esempio:
Tutti i file di questo progetto sono disponibili nella sezione DOWNLOAD del corso. Se volete, potete partire da quelli.
7. Creazione del file app.py
Il file app.py gestisce il routing delle pagine della Web application (URL che gestiscono le richieste HTTP), contiene il codice per creare l'istanza dell'applicazione Flask, definisce le view (funzioni che gestiscono il contenuto da restituire) e altri elementi essenziali per il funzionamento dell'applicazione.
Questa è la struttura base di un file app.py. La prima riga importa il framework flask e la funzione render_template (serve per le view dei template html), poi viene definita l'app e poi c'è il decorator @app.route. Queste righe di codice consentono di visualizzare la pagina index.html ogni volta che viene fatta una richiesta HTTP su localhost:5000.
host="0.0.0.0" è fondamentale e obbligatorio in Docker! Se scrivete host="127.0.0.1" o non lo mettete, il server Flask ascolta solo dentro il container e non risponde all'esterno. Con 0.0.0.0 dite "ascolta da tutte le interfacce". È l'errore che blocca il 90% degli studenti al primo tentativo. E debug=True serve a vedere subito le modifiche senza ricreare il container.
8. Creazione del file requirements.txt
Il file requirements.txt è un file di testo utilizzato nei progetti Python per elencare tutte le dipendenze (librerie e pacchetti) necessarie al funzionamento dell'applicazione. È uno standard molto diffuso in ambito Python, specialmente in ambienti virtuali, container Docker e deploy su server.
Questo blocca la versione e garantisce stabilità. Se scrivessimo solo flask, oggi scaricherebbe la 2.3.3, tra 6 mesi la 3.0 che potrebbe rompere tutto.
9. Creazione del Dockerfile
Il Dockerfile è un file testuale senza estensione
Spiegazione riga per riga:
- FROM python:3.14-rc-alpine --> costruiamo la nostra immagine sulla base di una esistente ed, attualmente, priva di vulnerabilità. Alpine è super leggera (circa 5MB contro 900MB di una Debian completa)
- WORKDIR /app --> creo una directory di lavoro all'interno del container
- COPY requirements.txt . --> copio i requisiti
- RUN pip install --no-cache-dir -r requirements.txt --> questa è la parte più importante: RUN crea in effetti un nuovo layer sull'immagine di base. In questo caso sull'immagine Python di base monto un layer con il framework Flask.
- COPY app.py . --> copio il file app.py nella nuova immagine
- EXPOSE 5000 --> espongo la porta 5000
- CMD ["python", "app.py"] --> comando di esecuzione all'avvio
| Parte del comando | Significato |
|---|---|
| pip install | Comando per installare pacchetti Python tramite pip |
| -r requirements.txt | Legge l’elenco dei pacchetti da un file (-r = requirements) |
| --no-cache-dir | Disattiva l'uso della cache locale di pip per mantenere leggera l'immagine ed avere installazione pulita |
Normalmente pip conserva una cache locale dei pacchetti scaricati per velocizzare le installazioni future. Ma in Docker non ci serve: il container è effimero, ogni build parte da zero. Se teniamo la cache, l'immagine diventa più pesante di 50-100MB inutili. Con --no-cache-dir teniamo l'immagine pulita e leggera. In produzione questa attenzione fa risparmiare gigabyte quando avete centinaia di immagini.
10. Creazione dell'immagine personalizzata
Come di consueto l'help di Docker ci dice che per la creazione di immagini dobbiamo usare il comando build. Per sapere come usarlo facciamo l'help di questo comando. Ci sono diverse opzioni possibili ma quella di cui necessitiamo è la possibilità di taggare la nostra immagine. Questo è possibile farlo con -t
Scegliamo quindi il nome della nostra immagine in questo modo: nome_utente/nome_immagine:tag
- nome utente = nome utente di Docker Hub (nel mio caso m76g)
- nome immagine = nome cartella di progetto (nel mio caso flask-docker-app)
- tag = versione (nel mio caso v1.0)
Quindi il comando per la creazione diventa:
Il punto finale rappresenta la directory corrente per cui occorre lanciare il comando nella directory che contiene il Dockerfile. Possiamo farlo da VSC. Docker costruisce l'immagine la cui presenza possiamo verificarla in Docker Desktop. Da Docker Desktop, se clicchiamo sull'immagine appena creata, possiamo vedere effettivamente i layer aggiunti all'immagine base.
Quando fate docker build, Docker scarica l'immagine base python:alpine solo la prima volta. Le volte successive usa la cache e ci mette 2 secondi. Se modificate solo app.py, ricostruisce solo l'ultimo layer. È velocissimo. Se invece modificate requirements.txt, ricostruisce da lì in poi. Per questo motivo si copia prima requirements.txt e si fa pip install, e solo dopo si copia app.py: così cambiate spesso app.py senza reinstallare Flask ogni volta.
11. Creazione di un container basato sull'immagine personalizzata
Non ci resta che testare la nostra immagine creando un'istanza dell'immagine stessa cioè un container:
Controlliamo che il container sia stato creato e che funzioni da Docker Desktop. Clicchiamo sul link 5000:5000 che punta a index.html e vediamo la nostra pagina. Si può verificare che qualsiasi modifica fatta viene riflessa automaticamente grazie al volume -v.
Qui usiamo -v .:/app per sviluppo, così modificate il codice in VS Code e vedete subito le modifiche nel browser senza rifare build. Ma in produzione NON si usa il volume: l'immagine deve contenere già tutto il codice. Altrimenti a cosa serve aver fatto COPY? Ricordate: volume per sviluppare, COPY per rilasciare.
12. PUSH dell'immagine su Docker Hub
Per eseguire il push della nostra immagine personalizzata dobbiamo fare il login a Docker Hub:
Ed utilizzare il comando push:
Verifichiamo che il PUSH sia andato a buon fine su Docker Hub. Di default l'immagine è pubblica per cui è visibile da chiunque acceda a Docker Hub. E' comunque possibile renderla privata e gestire le sue proprietà.
Ora la vostra immagine è su Docker Hub! Chiunque nel mondo può fare docker pull m76g/flask-docker-app:v1.0 e far partire la vostra app. È come aver pubblicato la vostra app sullo store. Potete mettere il link nel CV: è un ottimo portfolio per i colloqui.
13. Best Practices nella scrittura di Dockerfile
Scrivere un Dockerfile pulito, efficiente e manutenibile è essenziale per la sicurezza e le prestazioni dei container. Di seguito, alcune linee guida consigliate:
- Utilizzare immagini ufficiali e leggere: Preferire immagini ufficiali e versioni "slim" o "alpine" per ridurre le dimensioni e i rischi di sicurezza.
- Minimizzare il numero di layer: Ogni istruzione RUN, COPY e ADD genera un layer. Unire più comandi in una singola istruzione aiuta a mantenere l'immagine leggera.
- Utilizzare .dockerignore: Creare un file .dockerignore per escludere file inutili (es. .git, __pycache__, file temporanei), migliorando tempi di build e sicurezza.
- Specificare un WORKDIR: Evitare l'uso di cd nei comandi RUN. Usa WORKDIR, più chiaro e riutilizzabile.
- Evitare privilegi elevati: Evitare l'esecuzione come root se non strettamente necessario. Usare un utente dedicato.
- Specificare CMD in formato array: Evitare l’uso di shell per eseguire comandi: l’approccio array (exec form) è più sicuro e gestisce meglio i segnali (es. shutdown)
- Versionare le dipendenze: Bloccare le versioni nel requirements.txt evita comportamenti imprevedibili
Far girare tutto come root dentro il container è come navigare sempre come amministratore su Windows: se entra un malware ha tutti i permessi. In produzione si crea sempre un utente non privilegiato appuser. È una best practice che fa la differenza tra un Dockerfile da studente e uno da professionista. Segnatevela per il colloquio DevOps.
14. Conclusioni
Ora che abbiamo a disposizione gli strumenti per costruire le nostre immagini personalizzate potremmo, ad esempio, costruire un ambiente per lo sviluppo di Web Application con Apache-PHP, MySQL, PHPmyadmin
In questo caso, però, è sconsigliato l'uso di Dockerfile per costruire un'immagine complessa perché:
- phpMyAdmin è un’app separata che si connette al servizio MySQL
- PHP + Apache possono stare in un container (immagine php:apache)
- MySQL richiede un container dedicato per la gestione dei dati e volumi
Quindi, sebbene si possa tentare di creare un ambiente complesso con Dockerfile non è una best practice per vari motivi:
- Violazione del principio "1 container = 1 processo"
- Modularità e manutenibilità ridotta
- Problemi di gestione dei processi multipli
- Complicazioni con il networking interno
Soluzione corretta: Docker Compose che sarà l'argomento della prossima lezione.
Vuoi consultare il video relativo a questa lezione? Accedi cliccando il pulsante qui sotto.
🎬 Video DockerfileVuoi passare all'azione? Accedi al laboratorio cliccando il pulsante qui sotto.
🧪 Laboratorio Pratico - App PythonVuoi consultare tutti i laboratori di questa sezione? Accedi cliccando il pulsante qui sotto.
🧪 Laboratori Pratici - Docker📚 Compito per casa
Provate a modificare stile.css cambiando il colore dell'header, rifate docker build -t m76g/flask-docker-app:v1.1 . e rilanciate il container. Poi fate docker push della v1.1. Avrete così due versioni della vostra app su Docker Hub. Nella prossima lezione vedremo come orchestrare Apache, PHP, MySQL e phpMyAdmin con un unico file docker-compose.yml.