Curso Docker #10: Proyecto Final — App Full-Stack con Docker

Bienvenido al Curso de Docker - Parte 10 de 10

Fuente: Wikimedia Commons
¡Felicidades por llegar al articulo final del Curso de Docker! En este proyecto vas a construir una aplicacion full-stack completa con Docker: un frontend React servido por Nginx, una API Node.js/Express, una base de datos PostgreSQL y Redis como cache.
Este proyecto reune todo lo que has aprendido a lo largo del curso: Dockerfiles, multi-stage builds, Docker Compose, volumenes, redes, health checks, variables de entorno y mejores practicas de seguridad. Al final tendras una configuracion Docker lista para produccion que podras adaptar a tus propios proyectos.
Arquitectura del Proyecto
Nuestra aplicacion full-stack consiste en cuatro servicios:
| Servicio | Tecnologia | Puerto | Proposito |
|---|---|---|---|
| Frontend | React + Nginx | 80 | Interfaz de usuario (SPA) |
| API | Node.js + Express | 3000 | REST API backend |
| Base de datos | PostgreSQL 16 | 5432 | Almacenamiento persistente |
| Cache | Redis 7 | 6379 | Cache y sesiones |
Asi es como los servicios se comunican entre si:
- El navegador se conecta al frontend en el puerto 80
- Nginx sirve los archivos estaticos de React y hace proxy reverso de las peticiones
/api/*al backend - La API se conecta a PostgreSQL para datos persistentes y a Redis para cache
- PostgreSQL y Redis solo son accesibles por la API (red interna)
Estructura del Proyecto
Crea la siguiente estructura de directorios:
1mkdir -p fullstack-docker/{frontend,api,database,nginx}
2cd fullstack-docker
3
4# Estructura final:
5# fullstack-docker/
6# ├── docker-compose.yml
7# ├── docker-compose.dev.yml
8# ├── docker-compose.prod.yml
9# ├── .env.example
10# ├── .dockerignore
11# ├── Makefile
12# ├── frontend/
13# │ ├── Dockerfile
14# │ ├── package.json
15# │ ├── src/
16# │ └── public/
17# ├── api/
18# │ ├── Dockerfile
19# │ ├── package.json
20# │ └── src/
21# ├── database/
22# │ └── init.sql
23# └── nginx/
24# └── default.conf
El Frontend: React con Multi-Stage Build
Primero creamos una aplicacion React sencilla con un formulario de tareas.
package.json del frontend
1{
2 "name": "fullstack-frontend",
3 "version": "1.0.0",
4 "private": true,
5 "dependencies": {
6 "react": "^18.3.0",
7 "react-dom": "^18.3.0",
8 "react-scripts": "5.0.1"
9 },
10 "scripts": {
11 "start": "react-scripts start",
12 "build": "react-scripts build"
13 }
14}
Dockerfile del frontend (multi-stage)
1# ============================================
2# Etapa 1: Build de la app React
3# ============================================
4FROM node:20-alpine AS build
5
6WORKDIR /app
7
8# Copiar dependencias primero (cache de capas)
9COPY package*.json ./
10RUN npm ci --prefer-offline
11
12# Copiar codigo fuente y compilar
13COPY . .
14RUN npm run build
15
16# ============================================
17# Etapa 2: Servir con Nginx
18# ============================================
19FROM nginx:1.27-alpine
20
21# Copiar archivos compilados
22COPY --from=build /app/build /usr/share/nginx/html
23
24# Configuracion personalizada de Nginx
25COPY ../nginx/default.conf /etc/nginx/conf.d/default.conf
26
27EXPOSE 80
28
29HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
30 CMD wget -qO- http://localhost/health || exit 1
node:20-alpine (~180MB) pero la imagen final usa nginx:1.27-alpine (~40MB). El resultado es una imagen de ~45MB en lugar de ~1GB.
La API: Node.js con Express
La API maneja las operaciones CRUD para las tareas y se conecta a PostgreSQL y Redis.
Codigo principal de la API
1// api/src/index.js
2const express = require('express');
3const { Pool } = require('pg');
4const Redis = require('ioredis');
5
6const app = express();
7app.use(express.json());
8
9// Conexion a PostgreSQL
10const pool = new Pool({
11 host: process.env.DB_HOST || 'database',
12 port: parseInt(process.env.DB_PORT || '5432'),
13 database: process.env.DB_NAME || 'fullstack_app',
14 user: process.env.DB_USER || 'appuser',
15 password: process.env.DB_PASSWORD || 'apppassword',
16});
17
18// Conexion a Redis
19const redis = new Redis({
20 host: process.env.REDIS_HOST || 'cache',
21 port: parseInt(process.env.REDIS_PORT || '6379'),
22});
23
24redis.on('connect', () => console.log('Conectado a Redis'));
25
26// Health check
27app.get('/health', async (req, res) => {
28 try {
29 await pool.query('SELECT 1');
30 await redis.ping();
31 res.json({ status: 'healthy', db: 'connected', cache: 'connected' });
32 } catch (error) {
33 res.status(503).json({ status: 'unhealthy', error: error.message });
34 }
35});
36
37// Obtener todas las tareas (con cache)
38app.get('/api/tasks', async (req, res) => {
39 try {
40 const cached = await redis.get('tasks');
41 if (cached) {
42 return res.json(JSON.parse(cached));
43 }
44 const { rows } = await pool.query(
45 'SELECT * FROM tasks ORDER BY created_at DESC'
46 );
47 await redis.setex('tasks', 60, JSON.stringify(rows));
48 res.json(rows);
49 } catch (error) {
50 res.status(500).json({ error: error.message });
51 }
52});
53
54// Crear una tarea
55app.post('/api/tasks', async (req, res) => {
56 try {
57 const { title, description } = req.body;
58 const { rows } = await pool.query(
59 'INSERT INTO tasks (title, description) VALUES ($1, $2) RETURNING *',
60 [title, description || '']
61 );
62 await redis.del('tasks');
63 res.status(201).json(rows[0]);
64 } catch (error) {
65 res.status(500).json({ error: error.message });
66 }
67});
68
69// Completar una tarea
70app.patch('/api/tasks/:id', async (req, res) => {
71 try {
72 const { rows } = await pool.query(
73 'UPDATE tasks SET completed = NOT completed WHERE id = $1 RETURNING *',
74 [req.params.id]
75 );
76 if (rows.length === 0) return res.status(404).json({ error: 'No encontrada' });
77 await redis.del('tasks');
78 res.json(rows[0]);
79 } catch (error) {
80 res.status(500).json({ error: error.message });
81 }
82});
83
84// Eliminar una tarea
85app.delete('/api/tasks/:id', async (req, res) => {
86 try {
87 await pool.query('DELETE FROM tasks WHERE id = $1', [req.params.id]);
88 await redis.del('tasks');
89 res.status(204).send();
90 } catch (error) {
91 res.status(500).json({ error: error.message });
92 }
93});
94
95const PORT = process.env.PORT || 3000;
96app.listen(PORT, '0.0.0.0', () => {
97 console.log('API corriendo en puerto ' + PORT);
98});
Dockerfile de la API
1# ============================================
2# Etapa de dependencias
3# ============================================
4FROM node:20-alpine AS deps
5
6WORKDIR /app
7COPY package*.json ./
8RUN npm ci --only=production
9
10# ============================================
11# Etapa de runtime
12# ============================================
13FROM node:20-alpine
14
15RUN addgroup -S app && adduser -S app -G app
16
17WORKDIR /app
18
19# Copiar dependencias de la etapa anterior
20COPY --from=deps /app/node_modules ./node_modules
21COPY . .
22
23# Usuario non-root
24USER app
25
26EXPOSE 3000
27
28HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
29 CMD wget -qO- http://localhost:3000/health || exit 1
30
31CMD ["node", "src/index.js"]
La Base de Datos: PostgreSQL con Inicializacion
PostgreSQL se inicializa automaticamente con un script SQL que crea la tabla de tareas.
Script de inicializacion
1-- database/init.sql
2CREATE TABLE IF NOT EXISTS tasks (
3 id SERIAL PRIMARY KEY,
4 title VARCHAR(255) NOT NULL,
5 description TEXT DEFAULT '',
6 completed BOOLEAN DEFAULT FALSE,
7 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
8 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
9);
10
11-- Datos de prueba
12INSERT INTO tasks (title, description) VALUES
13 ('Aprender Docker', 'Completar el curso de Docker de 10 articulos'),
14 ('Configurar CI/CD', 'Crear pipeline con GitHub Actions'),
15 ('Desplegar en produccion', 'Usar Docker Compose en el servidor VPS');
16
17-- Indice para consultas frecuentes
18CREATE INDEX idx_tasks_completed ON tasks(completed);
19CREATE INDEX idx_tasks_created_at ON tasks(created_at DESC);
.sql ubicados en /docker-entrypoint-initdb.d/ la primera vez que el contenedor se inicia. Si el volumen ya tiene datos, el script NO se vuelve a ejecutar.
Configuracion de Nginx
Nginx actua como servidor web para React y como proxy reverso para la API.
1# nginx/default.conf
2upstream api_backend {
3 server api:3000;
4}
5
6server {
7 listen 80;
8 server_name localhost;
9
10 # Archivos estaticos de React
11 root /usr/share/nginx/html;
12 index index.html;
13
14 # Compresion gzip
15 gzip on;
16 gzip_vary on;
17 gzip_min_length 1024;
18 gzip_types text/plain text/css application/json application/javascript
19 text/xml application/xml text/javascript image/svg+xml;
20
21 # Cache de archivos estaticos
22 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
23 expires 30d;
24 add_header Cache-Control "public, immutable";
25 }
26
27 # Proxy reverso para la API
28 location /api/ {
29 proxy_pass http://api_backend;
30 proxy_set_header Host $host;
31 proxy_set_header X-Real-IP $remote_addr;
32 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
33 proxy_set_header X-Forwarded-Proto $scheme;
34 proxy_connect_timeout 30s;
35 proxy_read_timeout 60s;
36 }
37
38 # Health check del proxy
39 location /health {
40 proxy_pass http://api_backend/health;
41 }
42
43 # SPA fallback: cualquier ruta no encontrada -> index.html
44 location / {
45 try_files $uri $uri/ /index.html;
46 }
47}
Docker Compose: La Orquestacion Completa
Este es el corazon del proyecto. El archivo docker-compose.yml define los cuatro servicios y como se conectan entre si.
docker-compose.yml (base)
1# docker-compose.yml
2services:
3 frontend:
4 build:
5 context: ./frontend
6 dockerfile: Dockerfile
7 ports:
8 - "80:80"
9 depends_on:
10 api:
11 condition: service_healthy
12 networks:
13 - frontend-net
14 restart: unless-stopped
15
16 api:
17 build:
18 context: ./api
19 dockerfile: Dockerfile
20 environment:
21 - DB_HOST=database
22 - DB_PORT=5432
23 - DB_NAME=fullstack_app
24 - DB_USER=appuser
25 - DB_PASSWORD=apppassword
26 - REDIS_HOST=cache
27 - REDIS_PORT=6379
28 - NODE_ENV=production
29 depends_on:
30 database:
31 condition: service_healthy
32 cache:
33 condition: service_healthy
34 networks:
35 - frontend-net
36 - backend-net
37 restart: unless-stopped
38
39 database:
40 image: postgres:16-alpine
41 environment:
42 POSTGRES_DB: fullstack_app
43 POSTGRES_USER: appuser
44 POSTGRES_PASSWORD: apppassword
45 volumes:
46 - pgdata:/var/lib/postgresql/data
47 - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
48 healthcheck:
49 test: ["CMD-SHELL", "pg_isready -U appuser -d fullstack_app"]
50 interval: 10s
51 timeout: 5s
52 start_period: 30s
53 retries: 5
54 networks:
55 - backend-net
56 restart: unless-stopped
57
58 cache:
59 image: redis:7-alpine
60 command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
61 healthcheck:
62 test: ["CMD", "redis-cli", "ping"]
63 interval: 10s
64 timeout: 3s
65 retries: 5
66 networks:
67 - backend-net
68 restart: unless-stopped
69
70volumes:
71 pgdata:
72
73networks:
74 frontend-net:
75 driver: bridge
76 backend-net:
77 driver: bridge
78 internal: true
backend-net tiene internal: true, lo que significa que PostgreSQL y Redis NO tienen acceso a internet. Solo la API puede alcanzarlos. Esto es una practica de seguridad esencial.
docker-compose.dev.yml (desarrollo)
1# docker-compose.dev.yml — sobreescribe la configuracion base para desarrollo
2services:
3 frontend:
4 build:
5 target: build
6 ports:
7 - "3001:3000"
8 volumes:
9 - ./frontend/src:/app/src
10 command: ["npm", "start"]
11 environment:
12 - REACT_APP_API_URL=http://localhost:3000
13
14 api:
15 build:
16 target: deps
17 ports:
18 - "3000:3000"
19 - "9229:9229"
20 volumes:
21 - ./api/src:/app/src
22 command: ["node", "--inspect=0.0.0.0:9229", "src/index.js"]
23 environment:
24 - NODE_ENV=development
25
26 database:
27 ports:
28 - "5432:5432"
29
30 cache:
31 ports:
32 - "6379:6379"
docker-compose.prod.yml (produccion)
1# docker-compose.prod.yml — hardening para produccion
2services:
3 frontend:
4 deploy:
5 resources:
6 limits:
7 cpus: "0.5"
8 memory: 256M
9 logging:
10 driver: json-file
11 options:
12 max-size: "10m"
13 max-file: "3"
14
15 api:
16 read_only: true
17 tmpfs:
18 - /tmp:rw,noexec,nosuid,size=64m
19 deploy:
20 resources:
21 limits:
22 cpus: "1.0"
23 memory: 512M
24 reservations:
25 cpus: "0.25"
26 memory: 128M
27 logging:
28 driver: json-file
29 options:
30 max-size: "10m"
31 max-file: "5"
32
33 database:
34 read_only: true
35 tmpfs:
36 - /tmp:rw,noexec,nosuid,size=64m
37 - /run/postgresql:rw,noexec,nosuid,size=16m
38 deploy:
39 resources:
40 limits:
41 cpus: "1.0"
42 memory: 1G
43 reservations:
44 cpus: "0.5"
45 memory: 256M
46 logging:
47 driver: json-file
48 options:
49 max-size: "10m"
50 max-file: "5"
51
52 cache:
53 read_only: true
54 tmpfs:
55 - /tmp:rw,noexec,nosuid,size=16m
56 deploy:
57 resources:
58 limits:
59 cpus: "0.5"
60 memory: 256M
61 logging:
62 driver: json-file
63 options:
64 max-size: "5m"
65 max-file: "3"
Ejecucion y Pruebas
Ahora que tienes toda la configuracion, vamos a levantar la aplicacion.
Iniciar en modo desarrollo
1# Levantar con las configuraciones de desarrollo
2docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
3
4# Verificar que todos los servicios estan corriendo
5docker compose ps
6
7# Resultado esperado:
8# NAME SERVICE STATUS PORTS
9# frontend frontend running (healthy) 0.0.0.0:3001->3000/tcp
10# api api running (healthy) 0.0.0.0:3000->3000/tcp
11# database database running (healthy) 0.0.0.0:5432->5432/tcp
12# cache cache running (healthy) 0.0.0.0:6379->6379/tcp
Probar la API
1# Health check
2curl http://localhost:3000/health
3# {"status":"healthy","db":"connected","cache":"connected"}
4
5# Listar tareas
6curl http://localhost:3000/api/tasks
7# [{"id":1,"title":"Aprender Docker",...},...]
8
9# Crear una tarea
10curl -X POST http://localhost:3000/api/tasks \
11 -H "Content-Type: application/json" \
12 -d '{"title":"Mi nueva tarea","description":"Creada desde curl"}'
13
14# Completar una tarea
15curl -X PATCH http://localhost:3000/api/tasks/1
16
17# Eliminar una tarea
18curl -X DELETE http://localhost:3000/api/tasks/3
Iniciar en modo produccion
1# Levantar con las configuraciones de produccion
2docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
3
4# Verificar los recursos asignados
5docker stats --no-stream
6
7# Ver logs de un servicio especifico
8docker compose logs -f api
9
10# Acceder a la base de datos directamente
11docker compose exec database psql -U appuser -d fullstack_app
12
13# Acceder a Redis CLI
14docker compose exec cache redis-cli
Makefile: Comandos de Conveniencia
Para no tener que recordar los comandos largos de Docker Compose, crea un Makefile:
1# Makefile
2.PHONY: dev prod down logs clean test
3
4# Iniciar en modo desarrollo
5dev:
6 docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
7 @echo "Entorno de desarrollo iniciado!"
8 @echo "Frontend: http://localhost:3001"
9 @echo "API: http://localhost:3000"
10 @echo "Database: localhost:5432"
11
12# Iniciar en modo produccion
13prod:
14 docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
15 @echo "Entorno de produccion iniciado!"
16 @echo "App: http://localhost"
17
18# Detener todos los servicios
19down:
20 docker compose down
21
22# Ver logs
23logs:
24 docker compose logs -f
25
26# Limpiar todo (incluyendo volumenes!)
27clean:
28 docker compose down -v --rmi local
29 @echo "Todos los contenedores, volumenes e imagenes locales eliminados."
30
31# Ejecutar tests en contenedores
32test:
33 docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm api npm test
34
35# Shell de la base de datos
36db-shell:
37 docker compose exec database psql -U appuser -d fullstack_app
38
39# Redis CLI
40redis-cli:
41 docker compose exec cache redis-cli
1# Uso:
2make dev # Iniciar entorno de desarrollo
3make prod # Iniciar entorno de produccion
4make down # Detener todo
5make logs # Ver logs
6make clean # Eliminar todo incluyendo datos
7make test # Ejecutar tests
8make db-shell # Abrir shell de PostgreSQL
Checklist de Despliegue
Antes de desplegar tu aplicacion Dockerizada a produccion, revisa esta lista:
Seguridad
- Todos los contenedores corren como usuarios non-root
- Capabilities innecesarias eliminadas (
cap_drop: ALL) no-new-privilegeshabilitado- Filesystem read-only donde sea posible
- Sin secrets en las capas de la imagen
- Imagenes escaneadas con Trivy (sin vulnerabilidades CRITICAL)
- Base de datos no expuesta a internet (red interna)
- CORS configurado para el origen correcto
Confiabilidad
- Health checks configurados para todos los servicios
- Politica de reinicio configurada (
unless-stoppedoalways) - Limites de recursos configurados (CPU y memoria)
- Logging configurado con limites de tamano
- Datos de la base de datos persistidos con named volumes
- Estrategia de backup para volumenes de la base de datos
Rendimiento
- Multi-stage builds usados (imagenes finales minimas)
.dockerignoreexcluye archivos innecesarios- Compresion gzip habilitada en Nginx
- Assets estaticos con headers de cache
- Redis configurado con limites de memoria apropiados
Operaciones
- Pipeline CI/CD construye y publica imagenes automaticamente
- Estrategia de rollback definida (tag de imagen anterior)
- Monitoreo y alertas configurados
.env.examplecomiteado en control de versiones.envesta en.gitignore
Resumen del Curso y Proximos Pasos
¡Felicidades! Has completado el Curso de Docker desde Cero. A lo largo de 10 articulos, has pasado de los conceptos basicos de containerizacion a construir una aplicacion full-stack completa lista para produccion.
Este es un resumen de todo lo que aprendiste:
| Parte | Tema |
|---|---|
| Parte 1 | Que es Docker? Instalacion y primer contenedor |
| Parte 2 | Imagenes Docker: pull, build y gestion |
| Parte 3 | Dockerfile: construir imagenes personalizadas |
| Parte 4 | Volumenes Docker: almacenamiento persistente |
| Parte 5 | Redes Docker: comunicacion entre contenedores |
| Parte 6 | Docker Compose: aplicaciones multi-contenedor |
| Parte 7 | Multi-stage builds: optimizacion de imagenes |
| Parte 8 | Seguridad en Docker: mejores practicas y hardening |
| Parte 9 | Docker en CI/CD: automatizacion con GitHub Actions |
| Parte 10 | Proyecto final: app full-stack con Docker |
Que sigue despues?
Ahora que dominas Docker, estos son los siguientes pasos naturales en tu camino DevOps:
- Kubernetes: Aprende orquestacion de contenedores a escala. Kubernetes automatiza el despliegue, escalado y gestion de aplicaciones containerizadas en clusters de maquinas. Es el estandar de la industria para correr contenedores en produccion.
- Terraform: Gestiona tu infraestructura cloud como codigo. Aprovisiona servidores, redes y servicios en AWS, Azure o GCP con archivos de configuracion declarativos.
- Monitoreo: Configura observabilidad con Prometheus y Grafana para metricas, o el stack ELK (Elasticsearch, Logstash, Kibana) para logging centralizado.
- Service Mesh: Explora Istio o Linkerd para networking avanzado entre microservicios: gestion de trafico, seguridad y observabilidad.
- Cloud-native: Profundiza en patrones cloud-native como microservicios, arquitectura event-driven y contenedores serverless (AWS Fargate, Azure Container Instances, Google Cloud Run).
Para mas detalles, consulta la documentacion oficial de Docker: https://docs.docker.com/get-started/docker-overview/
Comments
Sign in to leave a comment
No comments yet. Be the first!