🐋 The Hitchhiker's Guide to Docker Deployment Part 2: Multi-Container Architecture - Don't Panic!

Frontend React ottimizzato, backend Node.js e MongoDB Atlas - il passo successivo

Dal Single Container all'Architettura Reale

Nel primo articolo abbiamo deployato una semplice app Node.js single-container. Ora affrontiamo il caso reale: un'applicazione completa Goals App con frontend React, backend Express API e database MongoDB.

L'architettura finale sarà:

Il Codice Reale: Goals App

L'applicazione è una semplice todo list dove puoi aggiungere e cancellare obiettivi. Il backend espone API REST:

// backend/app.js - Le route principali
app.get("/goals", async (req, res) => {
  const goals = await Goal.find();
  res.status(200).json({ goals });
});

app.post("/goals", async (req, res) => {
  const goal = new Goal({ text: req.body.text });
  await goal.save();
  res.status(201).json({ message: "Goal saved", goal });
});

app.delete("/goals/:id", async (req, res) => {
  await Goal.deleteOne({ _id: req.params.id });
  res.status(200).json({ message: "Deleted goal!" });
});Code language: JavaScript (javascript)

Il Problema degli Environment: Development vs Production

Database Connection - La Sfida

Il backend deve connettersi a MongoDB, ma la stringa di connessione cambia completamente tra sviluppo e produzione:

// backend/app.js - Connection string universale
mongoose.connect(
  `mongodb+srv://${process.env.MONGODB_USERNAME}:${process.env.MONGODB_PASSWORD}` +
  `@${process.env.MONGODB_URL}/${process.env.MONGODB_NAME}` +
  `?retryWrites=true&w=majority&appName=Cluster0`
);Code language: JavaScript (javascript)

Development (docker-compose locale):

# backend/Dockerfile
ENV MONGODB_USERNAME=root
ENV MONGODB_PASSWORD=secret
ENV MONGODB_URL=mongodb        # Nome container locale
ENV MONGODB_NAME=goals-dev
Code language: PHP (php)

Production (MongoDB Atlas):

# Su ECS, environment variables del task definition
MONGODB_USERNAME=your_db_user
MONGODB_PASSWORD=your-secure-password
MONGODB_URL=cluster0.xyz123.mongodb.net  # Atlas cluster
MONGODB_NAME=goals
Code language: PHP (php)

Frontend URL - Il Problema React

Il frontend deve chiamare il backend, ma l'URL cambia tra sviluppo e produzione. React ha una peculiarità: gira nel browser, non nel container, quindi le environment variables Docker non funzionano.

La soluzione:

// frontend/src/App.js - URL dinamico basato su NODE_ENV
const backendUrl =
  process.env.NODE_ENV === "development"
    ? "http://localhost"
    : "http://goals-alb-123456789.eu-west-1.elb.amazonaws.com";

// Chiamate API
const response = await fetch(backendUrl + "/goals");
Code language: JavaScript (javascript)

Perché funziona:

  • NODE_ENV è disponibile in build-time
  • Create React App lo espone automaticamente
  • Non richiede variabili Docker custom

Multi-Stage Build: Due Dockerfile per Due Mondi

Development - Live Reloading

# frontend/Dockerfile - Per sviluppo
FROM node
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
EXPOSE 3000
CMD [ "npm", "start" ]
Code language: PHP (php)

Production - Ottimizzato

# frontend/Dockerfile.prod - Per produzione
FROM node:14-alpine as build
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
RUN npm run build

# Ci serve node solo per servire i file statici
# Ogni istruzione FROM crea un nuovo stage di build
FROM nginx:stable-alpine

# vogliamo usare i file ottimizzati e servirli con nginx

COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Code language: PHP (php)

Risultato del build:

  • npm run build compila JSX → JavaScript
  • Minifica CSS e JS
  • Ottimizza immagini
  • Crea cartella /build con static files

Task Definitions Reali: Il Cuore dell'Architettura

Perché Servizi ECS Separati

Il punto cruciale: entrambi i container usano porta 80. In ECS, due container nella stessa task non possono mappare la stessa porta host. Soluzione: servizi ECS separati, ciascuno con la propria task definition.

Frontend Task Definition

{
  "family": "goals-react",
  "cpu": "512",
  "memory": "1024", 
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "containerDefinitions": [{
    "name": "goals-react",
    "image": "pandagandocker/goals-react",
    "portMappings": [{
      "containerPort": 80,
      "hostPort": 80,
      "protocol": "tcp",
      "appProtocol": "http"
    }],
    "essential": true,
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/goals-react",
        "awslogs-create-group": "true",
        "awslogs-region": "eu-west-1",
        "awslogs-stream-prefix": "ecs"
      }
    }
  }],
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole"
}
Code language: JSON / JSON with Comments (json)

Resource allocation reasoning:

  • 512 CPU, 1024 MB: Nginx serve static files, richiede pochissime risorse
  • awsvpc networkMode: Ogni task ha il proprio IP, elimina port conflicts

Backend Task Definition

{
  "family": "deploy-app",
  "cpu": "1024", 
  "memory": "3072",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "containerDefinitions": [{
    "name": "backend",
    "image": "pandagandocker/goals-backend",
    "portMappings": [{
      "containerPort": 80,
      "hostPort": 80,
      "protocol": "tcp",
      "appProtocol": "http"
    }],
    "environment": [
      {"name": "MONGODB_NAME", "value": "goals"},
      {"name": "MONGODB_PASSWORD", "value": "your-secure-password"},
      {"name": "MONGODB_USERNAME", "value": "your_db_user"},
      {"name": "MONGODB_URL", "value": "cluster0.xyz123.mongodb.net"}
    ],
    "essential": true,
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/deploy-app",
        "awslogs-create-group": "true",
        "awslogs-region": "eu-west-1", 
        "awslogs-stream-prefix": "ecs"
      }
    }
  }],
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole"
}
Code language: JSON / JSON with Comments (json)

Resource allocation reasoning:

  • 1024 CPU, 3072 MB: Node.js runtime + MongoDB connections richiedono più risorse
  • Environment variables: Connection string per MongoDB Atlas completa

MongoDB Atlas: La Connection String Reale

Dalle environment variables vediamo la configurazione Atlas effettiva:

// Risulta nella connection string:
mongodb+srv://your_db_user:your-secure-password@cluster0.xyz123.mongodb.net/goals?retryWrites=true&w=majority&appName=Cluster0
Code language: JavaScript (javascript)

Setup Atlas steps:

  1. Cluster creato su MongoDB Atlas
  2. Database user configurato con password sicura
  3. Database name: goals
  4. IP whitelist: 0.0.0.0/0 (necessario per Fargate)

Application Load Balancer: Due Load Balancer Separati

Il vantaggio principale: URL stabili invece di IP che cambiano ad ogni deploy.

Il Problema degli IP Pubblici

Senza load balancer, ogni task ECS ha un IP pubblico che cambia ad ogni aggiornamento:

  • Update servizio → Nuovo task → Nuovo IP
  • Devi aggiornare l'URL hardcoded nel frontend
  • Downtime durante la transizione

La Soluzione: Load Balancer Stabili

Contrariamente a un singolo ALB con routing rules, l'architettura utilizza due load balancer indipendenti:

Load Balancer Backend (goals-alb)

  • Target Group: goals-tg
  • Porta: 80, HTTP
  • Punta a: ECS Service Backend (deploy-app-service)
  • URL stabile: goals-alb-123456789.eu-west-1.elb.amazonaws.com

Load Balancer Frontend (goals-react-lb)

  • Target Group: react-tg
  • Porta: 80, HTTP
  • Punta a: ECS Service Frontend (goals-react-service)
  • URL stabile: goals-react-lb-123456789.eu-west-1.elb.amazonaws.com

Perché Due Load Balancer Separati?

  • URL stabili: Non cambiano mai, indipendentemente dai deploy
  • Isolamento completo tra frontend e backend
  • Scaling indipendente dei load balancer
  • Zero downtime durante rolling updates

Il frontend React chiama sempre lo stesso URL del backend:

const backendUrl = "http://goals-alb-123456789.eu-west-1.elb.amazonaws.com";Code language: JavaScript (javascript)

Questo approccio elimina la necessità di aggiornare URL ad ogni deploy, mantenendo l'applicazione sempre raggiungibile.

Deploy Multi-Container

Build e Push Separati

# Backend - stesso Dockerfile dev/prod
docker buildx build --platform linux/amd64 \
  -t pandagandocker/goals-backend:latest \
  ./backend --push

# Frontend - Dockerfile.prod specifico
docker buildx build --platform linux/amd64 \
  -t pandagandocker/goals-react:latest \
  -f frontend/Dockerfile.prod \
  ./frontend --push
Code language: PHP (php)

Deploy Indipendenti

Con architettura multi-service, i deploy diventano indipendenti:

  • Posso aggiornare solo il frontend senza toccare il backend
  • Posso aggiornare solo il backend senza toccare il frontend
  • Ogni servizio ha il suo ciclo di vita separato

Conclusioni: Architettura Production-Ready

Il setup multi-container con task separate rappresenta l'architettura standard per applicazioni moderne:

Vantaggi dimostrati:

  • Zero port conflicts: awsvpc networking isola completamente i container
  • Resource optimization: 512MB per Nginx, 3GB per Node.js
  • Deploy selettivi: Aggiorno solo il componente modificato
  • Monitoring integrato: CloudWatch logging automatico
  • Database managed: MongoDB Atlas elimina overhead operativo

Lezioni chiave:

  • Multi-stage build è obbligatorio per frontend moderni
  • Environment variables richiedono strategie diverse per React vs Node.js
  • Servizi ECS separati sono necessari per port conflicts
  • URL stabili via load balancer eliminano problemi di IP dinamici

Il prossimo step: CI/CD pipeline per automatizzare completamente build e deploy.


Gli screenshot e i dettagli tecnici sono tratti dai miei appunti del corso "Docker & Kubernetes: The Practical Guide" di Maximilian Schwarzmüller su Udemy - un corso che consiglio vivamente a chiunque voglia andare oltre i tutorial base di Docker e capire davvero come funziona in scenari reali.

Related Post

Novembre 30, 2025
Lo sapevate che…oggi ho mandato in crisi la mia memoria muscolare. Due volte.

Sì, avete letto bene: due volte. Perché quando decido di complicarmi la vita, lo faccio con una certa eleganza: passare a una tastiera meccanica split con layout americano mentre costruisco un workflow da tastiera con Linux e Neovim. Prima mossa geniale: passare a una tastiera meccanica split, quelle divise in due metà che ti fanno […]

Novembre 17, 2025
Il mio sabato al GDG Catania: tra codice, filosofia e senso di appartenenza

Ore 3 di mattina. Gli occhi si aprono nel buio. Non riprenderò più sonno, lo so. La sveglia è programmata per le 5, ma ormai sono sveglia. Perché una sveglia così presto di sabato? Per prendere un autobus alle 7 e partecipare al Google Development Group di Catania! Quando arrivo in stazione, ancora assonnata, i […]

veronicaschembri
Copyright © Veronica Schembri

Privacy Policy
Cookie Policy
💬