Cristhian Villegas
DevOps13 min read7 views

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

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

Bienvenido al Curso de Docker - Parte 10 de 10

Logo de Docker

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.

Prerequisitos: Este articulo asume que completaste las Partes 1 a 9 del curso. Usaras conceptos de cada articulo anterior. Si algo no queda claro, regresa a la parte correspondiente para repasar.

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:

bash
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

json
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)

dockerfile
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
Tip: La etapa de build usa 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

javascript
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

dockerfile
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

sql
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);
Nota: PostgreSQL ejecuta automaticamente los archivos .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.

bash
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)

yaml
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
Importante: La red 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)

yaml
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)

yaml
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

bash
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

bash
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

bash
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:

bash
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
bash
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-privileges habilitado
  • 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-stopped o always)
  • 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)
  • .dockerignore excluye 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.example comiteado en control de versiones
  • .env esta 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 1Que es Docker? Instalacion y primer contenedor
Parte 2Imagenes Docker: pull, build y gestion
Parte 3Dockerfile: construir imagenes personalizadas
Parte 4Volumenes Docker: almacenamiento persistente
Parte 5Redes Docker: comunicacion entre contenedores
Parte 6Docker Compose: aplicaciones multi-contenedor
Parte 7Multi-stage builds: optimizacion de imagenes
Parte 8Seguridad en Docker: mejores practicas y hardening
Parte 9Docker en CI/CD: automatizacion con GitHub Actions
Parte 10Proyecto 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).
Consejo final: Docker es una habilidad fundamental que todo desarrollador e ingeniero DevOps debe conocer. La mejor forma de solidificar tu conocimiento es containerizar tus propios proyectos. Toma una aplicacion existente, escribe su Dockerfile, configura Docker Compose, agrega health checks y hardening de seguridad, y despliegala. Cada proyecto que containerices te hara mas seguro y eficiente. ¡Sigue construyendo y nunca dejes de aprender!

Para mas detalles, consulta la documentacion oficial de Docker: https://docs.docker.com/get-started/docker-overview/

Share:
CV

Cristhian Villegas

Software Engineer specializing in Java, Spring Boot, Angular & AWS. Building scalable distributed systems with clean architecture.

Comments

Sign in to leave a comment

No comments yet. Be the first!

Related Articles