Hai un backend Node.js e un frontend React. In locale tutto gira liscio. Poi arriva il momento di far funzionare il tutto su un server, o di far collaborare un collega. E iniziano i problemi: "sulla mia macchina funziona", dipendenze mancanti, versioni di Node diverse, la build di React che non parte. Ci siamo passati anche noi, di Meteora Web, gestendo progetti per clienti che spaziano dal chatbot AI a dashboard SaaS. La soluzione si chiama Docker: un modo per impacchettare ogni componente con il suo ambiente esatto, e farli parlare tra loro senza attriti.
In questa guida operativa ti mostriamo come containerizzare un'app full-stack con Node.js e React, usando Docker e Docker Compose. Non serve essere esperti: partiamo dal problema e arriviamo a un setup che puoi copiare e adattare subito.
Perché containerizzare un'app full-stack con Docker?
Immagina di avere due strumenti che devono vivere sulla stessa macchina, ma con dipendenze diverse. Il backend Node.js usa la versione 18, il frontend React invece ha bisogno di Node 20 per i tool di build. Senza container, sei costretto a installare versioni multiple con nvm, gestire conflitti di porta, e pregare che non si rompa nulla quando aggiorni un pacchetto.
Con Docker, ogni servizio viene isolato nel suo contenitore. Puoi avere Node 18 per il backend e Node 20 per il frontend (anche se nel nostro caso useremo un build multi-stage per React, che produce file statici serviti da Nginx). Non c'è conflitto. E se un collega clona il repo e fa docker-compose up, tutto parte identico.
Noi usiamo Docker su ogni progetto che supera il prototipo — e anche per i prototipi, se c'è un backend. Il motivo è semplice: un sito si misura in fatturato, non in complimenti. Se devi perdere tempo a configurare ambienti, non stai sviluppando. Stai spegnendo incendi.
In più, Docker ti prepara per la produzione. Il setup che vedrai qui può essere deployato su qualsiasi server Linux (ma anche su VPS o servizi come AWS) con qualche accorgimento.
Sponsored Protocol
Come strutturare i Dockerfile per Node.js e React?
Partiamo dai mattoni: i Dockerfile. Un Dockerfile dice a Docker come costruire l'immagine del contenitore. Per un'app full-stack servono due immagini: una per il backend (Node.js/Express) e una per il frontend (React).
Dockerfile per il backend Node.js
Ecco un esempio che usiamo nei nostri progetti. Usiamo multi-stage build per tenere l'immagine finale leggera: prima installiamo le dipendenze di sviluppo (es. TypeScript), poi compiliamo e infine copiamo solo i file necessari nella produzione.
# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Se usi TypeScript o un build tool
RUN npm run build 2>/dev/null || true
# Stage 2: Production
FROM node:18-alpine
WORKDIR /app
# Copia solo node_modules e codice compilato
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
Perché Alpine? Riduce la dimensione dell'immagine: da ~1GB a ~200MB. Meno spazio su disco, più veloce da scaricare. Attenzione però: alcune dipendenze native (es. bcrypt) richiedono build tools. In tal caso usa node:18 (Debian slim) e installa g++ make python3 nel builder.
Errore comune: copiare tutto il codice di sviluppo (node_modules interi, file di configurazione IDE) nell'immagine finale. Usa .dockerignore per escludere:
node_modules
.git
.gitignore
.env.local
.env.development
Dockerfile per il frontend React
Il frontend React produce file statici (HTML, JS, CSS). In produzione li serviamo con Nginx, molto più performante di un server Node. Usiamo un build multi-stage anche qui.
Sponsored Protocol
# Stage 1: Build React
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Serve with Nginx
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
# Copia un'eventuale configurazione personalizzata
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Nota sulla configurazione Nginx: per un'app React con routing lato client (React Router), devi gestire il fallback a index.html. Ecco un nginx.conf minimo:
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# Cache aggressiva per asset statici
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Come orchestrare i servizi con Docker Compose?
Ora che abbiamo i Dockerfile, dobbiamo farli lavorare insieme. Docker Compose è lo strumento giusto: definisci i servizi (backend, frontend, database) in un unico file docker-compose.yml e li avvii con un comando.
version: '3.8'
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: app-backend
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_HOST=db
- DB_PORT=5432
- DB_NAME=mydb
- DB_USER=user
- DB_PASSWORD=secret
depends_on:
db:
condition: service_healthy
volumes:
# volume per log o file upload
- backend_data:/app/uploads
networks:
- app-network
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: app-frontend
ports:
- "80:80"
depends_on:
- backend
networks:
- app-network
db:
image: postgres:15-alpine
container_name: app-db
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: secret
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
volumes:
backend_data:
postgres_data:
networks:
app-network:
driver: bridge
Spiegazione:
Sponsored Protocol
- backend e frontend vengono costruiti dai rispettivi Dockerfile.
- depends_on assicura che il database parta prima del backend (con healthcheck per sicurezza).
- volumes persistenti: i dati del database e gli upload del backend non si perdono quando ricrei i contenitori.
- network comune: i servizi comunicano tra loro tramite nomi host (es.
backenddal frontend,dbdal backend). - Le variabili d'ambiente (DB_HOST, password) vanno gestite con file
.envo segreti di Docker Swarm in produzione. Mai hard-coded.
Avvia tutto con:
docker-compose up --build -d
Esempio reale da un nostro progetto: un cliente e-commerce aveva immagini da 5MB. Con Docker abbiamo isolato il backend Express per l'elaborazione immagini e un frontend React per il catalogo. Il setup con Nginx e caching ha ridotto i tempi di caricamento del 40%. Lo abbiamo visto nei report di Google Search Console.
Come gestire variabili d'ambiente e volumi in produzione?
In locale, docker-compose con le variabili scritte nel file va bene. In produzione, devi separare la configurazione dall'immagine. Due strade:
1. File .env per docker-compose
Docker Compose legge automaticamente un file .env nella stessa directory. Puoi mettere lì le variabili sensibili e non includerle nel repository. Attenzione: su server di produzione, usa un file separato con permessi ristretti (chmod 600).
Sponsored Protocol
2. Docker Secrets (Swarm o Compose con secret)
Per ambienti più sicuri, usa i secret. Docker Swarm ha il comando docker secret create. In docker-compose v3.8 puoi dichiarare:
secrets:
db_password:
file: ./secrets/db_password.txt
services:
backend:
secrets:
- db_password
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
Poi nel codice Node.js leggi il file. Non esporre mai le password in env del processo.
Volumi persistenti
I volumi nominati (come postgres_data) sono gestiti da Docker. Fai backup periodici. Noi, di Meteora Web, automatizziamo i backup con uno script che salva i volumi in un bucket S3 ogni notte. Lo vediamo come parte della sicurezza: la sicurezza nelle PMI italiane è sistematicamente sottovalutata.
Come debug di un'app containerizzata?
Quando qualcosa non funziona, i log sono i tuoi migliori amici:
# Visualizza log di tutti i servizi
docker-compose logs -f
# Log solo del backend
docker-compose logs backend -f
# Accedi al contenitore in esecuzione
docker exec -it app-backend sh
Per il frontend, verifica che Nginx stia servendo correttamente: fai un curl interno al contenitore. Se il backend non risponde, controlla la rete e le variabili d'ambiente. Un errore comune: il frontend chiama http://localhost:3000/api ma in Docker localhost si riferisce al proprio contenitore. Il frontend deve usare il nome del servizio: http://backend:3000/api. Questo vale solo all'interno della rete Docker. Per l'esterno (browser), userai il proxy di Nginx o il reverse proxy.
Quali best practice adottare per il deploy?
Docker Compose è ottimo per ambienti di sviluppo e staging. Per produzione, considera:
- Orchestrazione: Docker Swarm (nativo, semplice) o Kubernetes (complesso, potente). Noi spesso usiamo Swarm per PMI: bastano 3 comandi per scalare.
- Reverse proxy: aggiungi un servizio Nginx o Traefik in cima per gestire HTTPS (Let's Encrypt) e routing.
- Healthcheck: implementa endpoint
/healthnel backend e configuralo in Compose. - Limitazioni risorse: assegna
deploy.resources.limitsper evitare che un contenitore mangi tutta la RAM.
Ecco un esempio di risorse limitate in docker-compose:
Sponsored Protocol
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
Noi, di Meteora Web, abbiamo costruito una piattaforma proprietaria proprio sfruttando Docker per isolare ogni cliente. Ogni stack (Node + React + DB) è un set di contenitori separati. Aggiornamenti indipendenti, nessun conflitto. È lo stesso approccio che puoi adottare per il tuo progetto.
Cosa fare adesso
- Scarica Docker Desktop o installa Docker Engine su Linux.
- Crea una struttura di cartelle:
backend/,frontend/,docker-compose.yml. Inserisci i Dockerfile sopra. - Aggiungi il file .gitignore e .dockerignore per escludere node_modules e file sensibili.
- Avvia con docker-compose up --build. Verifica che backend e frontend rispondano.
- Personalizza: sostituisci il database PostgreSQL con MySQL o MongoDB a seconda del tuo stack. Modifica le variabili d'ambiente.
- Leggi il nostro pillar su Docker e containerizzazione per una visione completa: Docker e containerizzazione — dalla prototipazione alla produzione per PMI italiane.
- Documentazione ufficiale: Docker Compose per approfondire.
Ora hai un setup funzionante per containerizzare un'app full-stack con Node.js e React. Non devi più preoccuparti di ambienti divergenti. Il codice che produci oggi sarà identico su ogni macchina. E quando il cliente ti dirà "funziona in produzione?", potrai rispondere "sì, perché abbiamo testato proprio quella immagine".