2.5.-Creación de Imágenes
2.5. Creación de imágenes Docker¶
Hasta ahora hemos trabajado con imágenes predefinidas descargadas de Docker Hub. En esta sección aprenderemos a crear nuestras propias imágenes personalizadas para empaquetar nuestras aplicaciones.
1. Métodos de creación de imágenes¶
Existen dos métodos principales para crear imágenes Docker:
- Desde un contenedor: Modificar un contenedor y guardarlo como imagen
- Desde un Dockerfile: Definir la construcción mediante un archivo de texto (recomendado)
2. Crear imágenes desde un contenedor (docker commit)¶
Este método consiste en modificar manualmente un contenedor y luego guardarlo como imagen.
2.1. Proceso paso a paso¶
Paso 1: Crear contenedor base
- Crea un contenedor interactivo desde Ubuntu
- Te conecta a su shell
- Ahora estás "dentro" del contenedor
Paso 2: Instalar software (dentro del contenedor)
root@abc123:/# apt-get update
Get:1 http://archive.ubuntu.com/ubuntu jammy InRelease [270 kB]
...
Reading package lists... Done
root@abc123:/# apt-get install -y apache2
Reading package lists... Done
Building dependency tree... Done
The following NEW packages will be installed:
apache2 apache2-bin apache2-data apache2-utils
...
Setting up apache2 (2.4.52-1ubuntu4)
root@abc123:/# echo "Hola desde mi servidor" > /var/www/html/index.html
root@abc123:/# exit
exit
¿Qué hemos hecho?
- Actualizamos la lista de paquetes
- Instalamos Apache (servidor web)
- Creamos un archivo HTML personalizado
- Salimos del contenedor
Paso 3: Guardar los cambios como imagen
$ docker commit mi_contenedor mi_apache:v1
sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
docker commit: Crea una imagen desde un contenedormi_contenedor: Nombre del contenedor con las modificacionesmi_apache:v1: Nombre y etiqueta de la nueva imagen
¿Qué ha pasado?
Docker ha tomado todos los cambios del contenedor (Apache instalado + archivo HTML) y los ha guardado como una nueva imagen.
Paso 4: Verificar la imagen creada
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mi_apache v1 1234567890ab 5 seconds ago 200MB
ubuntu latest f63181f19b2f 2 weeks ago 72.9MB
Observa:
- La imagen mi_apache ocupa 200MB (Ubuntu 72.9MB + Apache + nuestros cambios)
- La imagen se creó hace 5 segundos
Paso 5: Probar la imagen creada
$ docker run -d -p 8080:80 --name test_apache mi_apache:v1 apache2ctl -D FOREGROUND
abc123def456
$ curl http://localhost:8080
Hola desde mi servidor
¡Funciona! Hemos creado nuestra primera imagen personalizada.
2.2. Añadir información a la imagen¶
$ docker commit \
--author "Tu Nombre <tu@email.com>" \
--message "Instalado Apache 2.4 con página personalizada" \
mi_contenedor \
mi_apache:v1
Opciones útiles:
--author: Quién creó la imagen--messageo-m: Descripción de los cambios--change: Modificar configuración (CMD, EXPOSE, etc.)
Ejemplo con --change:
$ docker commit \
--change='CMD ["apache2ctl", "-D", "FOREGROUND"]' \
--change='EXPOSE 80' \
mi_contenedor \
mi_apache:v2
Ahora la imagen v2 ya tiene definido el comando y el puerto.
2.3. Limitaciones y problemas¶
❌ Problema 1: No es reproducible
# Tu compañero no sabe cómo creaste la imagen
$ docker history mi_apache:v1
IMAGE CREATED CREATED BY SIZE COMMENT
1234567890ab 2 minutes ago /bin/bash 127MB # ¿Qué se hizo aquí?
f63181f19b2f 2 weeks ago ... 72.9MB
No hay registro de qué comandos ejecutaste.
❌ Problema 2: No es versionable
Si quieres modificar algo, debes: 1. Crear contenedor desde la imagen 2. Hacer cambios manualmente 3. Hacer commit de nuevo 4. Esperar recordar qué hiciste la vez anterior
❌ Problema 3: No es automatizable
No puedes incluirlo en CI/CD porque requiere intervención manual.
❌ Problema 4: Difícil de mantener
# Después de 3 meses...
$ docker images
REPOSITORY TAG
mi_apache v1 # ¿Qué había aquí?
mi_apache v2 # ¿Qué cambió de v1 a v2?
mi_apache v3 # ¿Esta es la buena?
2.4. ¿Cuándo usar docker commit?¶
✅ Casos válidos:
- Debugging rápido: Probar algo temporalmente
- Experimentación: Testear configuraciones
- Backup temporal: Guardar estado antes de cambios arriesgados
Ejemplo de debugging:
# Tu app falla en producción
$ docker commit contenedor_produccion debug:snapshot
$ docker run -it debug:snapshot bash
# Investigas el problema...
❌ NO usar para:
- Producción
- Compartir con el equipo
- Cualquier cosa que necesite mantenimiento
Resumen
docker commit es útil para experimentación y debugging, pero siempre usa Dockerfile para crear imágenes de producción o que necesites mantener en el tiempo.
3. Dockerfile (El método correcto)¶
Un Dockerfile es un archivo de texto que contiene instrucciones para construir una imagen Docker de forma automatizada y reproducible.
¿Por qué es mejor que docker commit?
- ✅ Reproducible: Cualquiera puede construir la misma imagen
- ✅ Versionable: Se guarda en Git con tu código
- ✅ Documentado: El archivo ES la documentación
- ✅ Automatizable: CI/CD puede construir automáticamente
- ✅ Mantenible: Fácil de modificar y actualizar
3.1. Estructura básica (ejemplo comentado)¶
Vamos a crear un servidor web Nginx paso a paso:
# Imagen base
FROM ubuntu:22.04
# Información del mantenedor
LABEL maintainer="tu@email.com"
# Actualizar sistema e instalar paquetes
RUN apt-get update && \
apt-get install -y nginx && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Copiar archivos
COPY index.html /var/www/html/
# Puerto a exponer
EXPOSE 80
# Comando al iniciar contenedor
CMD ["nginx", "-g", "daemon off;"]
Análisis línea por línea:
Línea 1-2: FROM
- Primera instrucción (obligatoria) - Define la imagen base - Todo lo que hagas se construye SOBRE esta imagen -ubuntu:22.04 = Ubuntu versión 22.04 LTS
Línea 4-5: LABEL
- Metadatos de la imagen (opcional pero recomendado) - Puedes añadir múltiples labels - Se puede ver condocker inspect
Línea 7-11: RUN
RUN apt-get update && \
apt-get install -y nginx && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
¿Por qué todo en un solo RUN?
Cada instrucción RUN crea una nueva CAPA en la imagen. Si lo hicieras así:
# ❌ MAL: Crea 4 capas innecesarias
RUN apt-get update # Capa 1: 40MB
RUN apt-get install nginx # Capa 2: 100MB
RUN apt-get clean # Capa 3: 0MB (pero la capa 2 sigue ocupando)
RUN rm -rf /var/lib/... # Capa 4: 0MB
Resultado: 140MB en la imagen final.
Con un solo RUN:
# ✅ BIEN: Una sola capa
RUN apt-get update && \
apt-get install -y nginx && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Resultado: 60MB en la imagen final (eliminó archivos temporales en la misma capa).
Línea 13-14: COPY
- Copia archivos de tu ordenador a la imagen -index.html: Archivo en el mismo directorio que el Dockerfile
- /var/www/html/: Destino dentro de la imagen
Línea 16-17: EXPOSE
- Documenta qué puertos usa el contenedor - NO abre el puerto automáticamente - Es solo informativo (pero importante)Línea 19-20: CMD
- Comando que se ejecuta al iniciar el contenedor - Formato JSON (recomendado):["ejecutable", "param1", "param2"]
- Solo puede haber UN CMD (si hay varios, solo el último cuenta)
3.2. Construir la imagen¶
Paso 1: Crear el Dockerfile
Crea un directorio de trabajo:
Crea el archivo Dockerfile (sin extensión):
Crea el archivo index.html:
Estructura del directorio:
Paso 2: Construir la imagen
$ docker build -t mi-nginx:v1 .
[+] Building 45.3s (9/9) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 234B 0.0s
=> [internal] load .dockerignore 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:22.04 1.2s
=> [1/4] FROM docker.io/library/ubuntu:22.04@sha256:... 5.3s
=> => resolve docker.io/library/ubuntu:22.04@sha256:... 0.0s
=> => sha256:... 72.9MB / 72.9MB 3.5s
=> [internal] load build context 0.0s
=> => transferring context: 52B 0.0s
=> [2/4] RUN apt-get update && apt-get install -y nginx... 35.2s
=> [3/4] COPY index.html /var/www/html/ 0.1s
=> exporting to image 3.3s
=> => exporting layers 3.3s
=> => writing image sha256:abc123def456... 0.0s
=> => naming to docker.io/library/mi-nginx:v1 0.0s
Análisis de la salida:
load build definition: Lee el Dockerfileload metadata: Obtiene info de la imagen base (ubuntu:22.04)FROM: Descarga Ubuntu (5.3s)RUN: Instala Nginx (35.2s - lo más lento)COPY: Copia index.html (0.1s - muy rápido)exporting: Guarda la imagen final
Explicación del comando:
$ docker build -t mi-nginx:v1 .
↑ ↑ ↑ ↑
│ │ │ └─ Contexto (directorio actual)
│ │ └───────── Etiqueta (tag)
│ └─────────────── Nombre de la imagen
└─────────────────────── Comando de construcción
.: Punto indica el directorio actual como contexto de build- Docker envía todos los archivos de este directorio al daemon
- Por eso el Dockerfile debe estar aquí
Paso 3: Verificar la imagen
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mi-nginx v1 abc123def456 2 minutes ago 168MB
ubuntu 22.04 f63181f19b2f 2 weeks ago 72.9MB
Paso 4: Ejecutar un contenedor
$ docker run -d -p 8080:80 --name test_nginx mi-nginx:v1
abc123def456
$ curl http://localhost:8080
<h1>¡Hola desde mi Dockerfile!</h1>
¡Funciona perfectamente!
Ventaja sobre docker commit:
Si tu compañero quiere la misma imagen:
# Le envías solo 2 archivos: Dockerfile + index.html
# Él ejecuta:
$ docker build -t mi-nginx:v1 .
# Y obtiene EXACTAMENTE la misma imagen
3.3. Instrucciones principales del Dockerfile (explicadas)¶
FROM - La imagen base¶
¿Qué hace FROM?
Define la imagen base sobre la que construir. Es como elegir el "sistema operativo" de tu contenedor.
Opciones comunes:
-
Distribuciones Linux:
-
Imágenes con lenguajes:
-
Imágenes con servidores:
¿Cuál elegir?
- Alpine: Mínima, rápida, pero puede tener problemas de compatibilidad
- Slim: Balance entre tamaño y compatibilidad
- Completa: Si necesitas todas las herramientas
Ejemplo de multi-stage (avanzado):
FROM node:18 AS builder # Etapa de construcción
# ... compilar código ...
FROM nginx:alpine # Etapa final (solo producción)
COPY --from=builder ... # Copiar solo lo necesario
LABEL - Metadatos de la imagen¶
¿Para qué sirve LABEL?
Añade metadatos (información) a la imagen. No afecta al funcionamiento, solo proporciona información.
Usos comunes:
# Información del mantenedor
LABEL maintainer="nombre@empresa.com"
# Versión de la aplicación
LABEL version="2.3.1"
# Descripción
LABEL description="API REST para gestión de usuarios"
# Múltiples labels en una línea
LABEL version="1.0" \
description="Mi app" \
author="Tu Nombre"
Ver los labels:
$ docker inspect mi-imagen | grep -A 10 Labels
"Labels": {
"maintainer": "nombre@empresa.com",
"version": "2.3.1",
"description": "API REST para gestión de usuarios"
}
Labels útiles para organización:
LABEL org.opencontainers.image.authors="equipo@empresa.com"
LABEL org.opencontainers.image.version="1.0.0"
LABEL org.opencontainers.image.created="2024-01-23"
LABEL org.opencontainers.image.source="https://github.com/user/repo"
RUN - Ejecutar comandos durante construcción¶
¿Qué hace RUN?
Ejecuta comandos DURANTE la construcción de la imagen (en tiempo de docker build).
⚠️ Concepto importante: CAPAS
Cada instrucción RUN crea una nueva capa en la imagen. Las capas son inmutables.
Ejemplo visual:
Imagen final
├── Capa 3: RUN npm install (100MB)
├── Capa 2: RUN pip install flask (50MB)
├── Capa 1: RUN apt-get update (40MB)
└── Capa 0: FROM ubuntu (72MB)
Total: 262MB
❌ MAL - Múltiples RUN separados:
FROM ubuntu:22.04
RUN apt-get update # Capa 1: 40MB
RUN apt-get install -y nginx # Capa 2: 100MB
RUN apt-get clean # Capa 3: 0MB
RUN rm -rf /var/lib/apt/lists/* # Capa 4: 0MB
# Problema: Las capas 2, 3 y 4 ocupan 100MB
# Aunque limpies en capas posteriores, la capa 2 sigue ahí
✅ BIEN - Un solo RUN combinado:
FROM ubuntu:22.04
RUN apt-get update && \
apt-get install -y nginx && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Una sola capa que ocupa solo 60MB
# Los archivos temporales se eliminan en la MISMA capa
Formato de RUN:
-
Shell form (usa
/bin/sh -c): -
Exec form (no usa shell):
Buenas prácticas con RUN:
# ✅ Ordenar paquetes alfabéticamente (fácil de mantener)
RUN apt-get update && apt-get install -y \
curl \
git \
nginx \
vim \
wget \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# ✅ Usar && para que falle si algo sale mal
RUN cd /app && npm install # Si cd falla, no ejecuta npm install
# ❌ Sin && puede ejecutar en lugar equivocado
RUN cd /app
RUN npm install # Esto se ejecuta donde sea que esté, no necesariamente en /app
# ✅ Usar cache de APT para acelerar builds
RUN --mount=type=cache,target=/var/cache/apt \
apt-get update && apt-get install -y nginx
Ejemplo completo Python:
FROM python:3.11-slim
# Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Instalar paquetes Python
RUN pip install --no-cache-dir \
flask==2.3.0 \
psycopg2-binary==2.9.5 \
gunicorn==20.1.0
¿Por qué --no-cache-dir en pip?
Sin --no-cache-dir, pip guarda archivos de caché que ocupan espacio innecesario en la imagen.
COPY - Copiar archivos a la imagen¶
¿Qué hace COPY?
Copia archivos/directorios de tu ordenador (contexto de build) a la imagen.
Sintaxis:
<origen>: Ruta relativa al directorio donde está el Dockerfile<destino>: Ruta absoluta dentro de la imagen
Ejemplos prácticos:
# Copiar un archivo
COPY app.py /usr/src/app/app.py
# Copiar con nombre diferente
COPY config.yml /etc/myapp/config.yaml
# Copiar directorio completo
COPY src/ /usr/src/app/
# Copiar todo el directorio actual
COPY . /usr/src/app/
# Copiar múltiples archivos
COPY requirements.txt package.json /app/
# Copiar con permisos de usuario específico
COPY --chown=www-data:www-data html/ /var/www/html/
¿Qué es el contexto de build?
mi-proyecto/
├── Dockerfile ← Aquí estamos
├── app.py
├── requirements.txt
└── src/
├── main.py
└── utils.py
Rutas relativas al contexto:
COPY app.py /app/ # Copia mi-proyecto/app.py
COPY src/main.py /app/ # Copia mi-proyecto/src/main.py
COPY ../fuera.txt /app/ # ❌ ERROR: No puede copiar fuera del contexto
Usar .dockerignore:
Crea un archivo .dockerignore para excluir archivos:
Esto acelera el build porque Docker no envía estos archivos al daemon.
Orden importa (caché):
# ❌ MAL: Invalida caché cada vez que cambias código
COPY . /app/
RUN pip install -r requirements.txt
# ✅ BIEN: Solo invalida caché si cambian dependencias
COPY requirements.txt /app/
RUN pip install -r /app/requirements.txt
COPY . /app/
¿Por qué es mejor?
- Build inicial:
- COPY requirements.txt ✓
- RUN pip install ✓ (tarda 2 min)
-
COPY . ✓
-
Cambias código (NO requirements.txt):
- COPY requirements.txt ✓ (usa caché)
- RUN pip install ✓ (usa caché - no tarda nada)
- COPY . ✓ (copia nuevo código)
ADD - COPY con superpoderes¶
ADD archivo.tar.gz /app/ # Descomprime automáticamente
ADD https://ejemplo.com/file.txt /tmp/ # Descarga URLs
Diferencias entre COPY y ADD:
| Característica | COPY | ADD |
|---|---|---|
| Copiar archivos locales | ✅ | ✅ |
| Descomprimir tar.gz | ❌ | ✅ |
| Descargar URLs | ❌ | ✅ |
| Simplicidad | ✅ | ❌ |
| Recomendado | ✅ | Solo si necesitas sus features |
Ejemplo ADD descomprimiendo:
# Tienes archivo.tar.gz en tu directorio
ADD archivo.tar.gz /app/
# ADD automáticamente:
# 1. Copia el archivo
# 2. Lo descomprime
# 3. El resultado queda en /app/
Ejemplo ADD descargando URL:
No se descomprime desde URL
ADD solo descomprime archivos locales, NO archivos descargados de URLs.
¿Cuándo usar ADD?
# ✅ Cuando necesites descomprimir
ADD app.tar.gz /usr/src/app/
# ❌ Para copiar archivos normales usa COPY
COPY app.py /usr/src/app/ # Más claro
# ❌ Para descargar URLs, mejor RUN con curl
RUN curl -L https://ejemplo.com/file.tar.gz | tar -xz -C /app/
Regla de oro
Usa COPY a menos que específicamente necesites descomprimir archivos locales con ADD.
WORKDIR - Directorio de trabajo¶
¿Qué hace WORKDIR?
Establece el directorio de trabajo para las instrucciones que vienen después (RUN, CMD, ENTRYPOINT, COPY, ADD).
Es como hacer cd pero mejor:
# ❌ MAL: Usar RUN cd
RUN cd /app
RUN ls # Esto NO está en /app, está donde estaba antes
# ✅ BIEN: Usar WORKDIR
WORKDIR /app
RUN ls # Esto SÍ está en /app
Ejemplo completo:
FROM node:18
# Crear y establecer directorio de trabajo
WORKDIR /usr/src/app
# Ahora COPY copia relativo a /usr/src/app
COPY package*.json ./
# RUN se ejecuta en /usr/src/app
RUN npm install
# COPY también usa /usr/src/app
COPY . .
# CMD se ejecuta en /usr/src/app
CMD ["node", "server.js"]
WORKDIR crea el directorio si no existe:
Usar múltiples WORKDIR:
¿Dónde estás si no usas WORKDIR?
Por defecto estás en la raíz (/).
ENV - Variables de entorno¶
¿Qué hace ENV?
Define variables de entorno que estarán disponibles: - Durante el build (en instrucciones posteriores) - En el contenedor cuando se ejecute
Sintaxis:
# Una variable por línea
ENV MI_VARIABLE=valor
# Múltiples variables
ENV VAR1=valor1 \
VAR2=valor2 \
VAR3=valor3
Usar variables definidas:
ENV APP_DIR=/usr/src/app
ENV CONFIG_DIR=/etc/myapp
WORKDIR ${APP_DIR}
COPY config.yml ${CONFIG_DIR}/
Ejemplos por lenguaje:
Python:
PYTHONUNBUFFERED=1: Logs en tiempo realPYTHONDONTWRITEBYTECODE=1: No crear archivos .pycPIP_NO_CACHE_DIR=1: Pip sin caché (imagen más pequeña)
Node.js:
Java:
Modificar PATH:
Diferencia entre ENV en Dockerfile y -e en docker run:
Ver variables en el contenedor:
EXPOSE - Documentar puertos¶
¿Qué hace EXPOSE?
⚠️ IMPORTANTE: EXPOSE NO abre puertos, solo los DOCUMENTA.
Lo que EXPOSE hace:
Es equivalente a escribir un comentario:
Lo que EXPOSE NO hace:
# Esto NO funciona solo con EXPOSE:
$ docker run mi-app
# No puedes acceder al puerto 80 desde tu navegador
Para realmente exponer el puerto:
¿Entonces para qué sirve EXPOSE?
- Documentación: Indica qué puertos usa la app
- Con -P: Mapea automáticamente puertos
$ docker run -P mi-app
# Docker automáticamente mapea TODOS los puertos en EXPOSE
# a puertos aleatorios del host
$ docker ps
PORTS
0.0.0.0:49153->80/tcp # EXPOSE 80 mapeado a 49153
0.0.0.0:49154->443/tcp # EXPOSE 443 mapeado a 49154
Sintaxis:
EXPOSE 80 # Por defecto TCP
EXPOSE 80/tcp # Explícitamente TCP
EXPOSE 53/udp # UDP
EXPOSE 8080-8090 # Rango de puertos
Ejemplo completo:
FROM nginx:alpine
# Documentar que usa puerto 80 (HTTP) y 443 (HTTPS)
EXPOSE 80 443
# Para usarlo:
# docker run -p 8080:80 -p 8443:443 mi-nginx
Buena práctica:
Aunque EXPOSE no sea técnicamente necesario, úsalo siempre para documentar:
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
# Documentar que la app usa puerto 3000
EXPOSE 3000
CMD ["node", "server.js"]
Cuando alguien use tu imagen, sabrá qué puerto necesita mapear.
VOLUME - Puntos de montaje¶
¿Qué hace VOLUME?
Define directorios que deben persistir fuera del contenedor.
Ejemplo:
Cuando ejecutes el contenedor, Docker creará automáticamente un volumen para ese directorio.
$ docker run -d postgres
$ docker volume ls
DRIVER VOLUME NAME
local abc123def456... # Volumen anónimo creado automáticamente
Es mejor usar volúmenes nombrados en docker run:
USER - Cambiar usuario¶
¿Qué hace USER?
Cambia el usuario para instrucciones posteriores y para cuando se ejecute el contenedor.
¿Por qué es importante?
Por defecto, los contenedores ejecutan como root. Esto es un riesgo de seguridad.
❌ MAL (ejecuta como root):
✅ BIEN (ejecuta como usuario sin privilegios):
FROM node:18
# Crear usuario
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY . .
RUN chown -R appuser:appuser /app
# Cambiar a usuario sin privilegios
USER appuser
CMD ["node", "server.js"] # Ejecuta como appuser
Ejemplo Python:
FROM python:3.11-slim
RUN useradd -m -u 1000 appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
USER appuser
CMD ["python", "app.py"]
CMD - Comando por defecto¶
¿Qué hace CMD?
Define el comando que se ejecuta cuando inicias el contenedor (si no especificas otro).
Formatos:
# Exec form (recomendado)
CMD ["ejecutable", "param1", "param2"]
# Shell form
CMD comando param1 param2
Ejemplo:
$ docker run mi-imagen
Hola desde CMD
$ docker run mi-imagen echo "Otro mensaje"
Otro mensaje # CMD se reemplaza
Solo puede haber UN CMD:
ENTRYPOINT - Punto de entrada¶
¿Diferencia entre CMD y ENTRYPOINT?
- CMD: Fácil de sobrescribir
- ENTRYPOINT: Difícil de sobrescribir (punto de entrada fijo)
Ejemplo CMD:
Ejemplo ENTRYPOINT:
$ docker run mi-imagen
Hola
$ docker run mi-imagen "Adiós"
Adiós # Solo cambias el argumento, no el comando
Patrón ENTRYPOINT + CMD:
# Ejecuta: python app.py
$ docker run mi-imagen
# Ejecuta: python otro.py
$ docker run mi-imagen otro.py
# Ejecuta: python -m pytest
$ docker run mi-imagen -m pytest
Caso de uso real - Script wrapper:
FROM postgres:15
COPY docker-entrypoint.sh /
RUN chmod +x /docker-entrypoint.sh
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["postgres"]
El script puede hacer inicialización antes de ejecutar postgres.
4. Construir imágenes¶
4.1. Comando docker build¶
# Sintaxis básica
$ docker build -t nombre:tag ruta_contexto
# Ejemplo
$ docker build -t mi_app:1.0 .
# Especificar Dockerfile diferente
$ docker build -t mi_app:1.0 -f Dockerfile.prod .
# Pasar argumentos de construcción
$ docker build --build-arg VERSION=1.0 -t mi_app .
Opciones útiles:
# Sin caché (construcción completa)
$ docker build --no-cache -t mi_app .
# Especificar plataforma
$ docker build --platform linux/amd64 -t mi_app .
# Ver construcción detallada
$ docker build --progress=plain -t mi_app .
4.2. Contexto de construcción¶
El contexto es el conjunto de archivos enviados al daemon de Docker para la construcción.
Archivo .dockerignore:
Excluir archivos del contexto:
5. Ejemplos prácticos¶
5.1. Sitio web estático con Nginx¶
Dockerfile:
FROM nginx:alpine
# Copiar archivos HTML
COPY html/ /usr/share/nginx/html/
# Copiar configuración personalizada
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Construcción:
5.2. Aplicación PHP¶
Dockerfile:
FROM php:8.2-apache
# Instalar extensiones PHP
RUN docker-php-ext-install mysqli pdo pdo_mysql
# Habilitar módulos Apache
RUN a2enmod rewrite
# Copiar código fuente
COPY src/ /var/www/html/
# Permisos
RUN chown -R www-data:www-data /var/www/html
EXPOSE 80
5.3. Aplicación Python con Flask¶
Dockerfile:
FROM python:3.11-slim
# Directorio de trabajo
WORKDIR /app
# Copiar requirements primero (mejor aprovechamiento del caché)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copiar código de la aplicación
COPY . .
# Usuario no privilegiado
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
# Puerto
EXPOSE 5000
# Variables de entorno
ENV FLASK_APP=app.py
ENV FLASK_ENV=production
# Comando de inicio
CMD ["flask", "run", "--host=0.0.0.0"]
requirements.txt:
5.4. Aplicación Node.js¶
Dockerfile:
FROM node:18-alpine
# Directorio de trabajo
WORKDIR /usr/src/app
# Copiar package.json y package-lock.json
COPY package*.json ./
# Instalar dependencias
RUN npm ci --only=production
# Copiar código de la aplicación
COPY . .
# Usuario no privilegiado
USER node
# Puerto
EXPOSE 3000
# Comando de inicio
CMD ["node", "server.js"]
6. Multi-stage builds¶
Los multi-stage builds permiten crear imágenes más pequeñas usando múltiples etapas.
Ejemplo: Aplicación Go
# Etapa 1: Construcción
FROM golang:1.20 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
# Etapa 2: Imagen final
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# Copiar solo el binario de la etapa anterior
COPY --from=builder /app/myapp .
CMD ["./myapp"]
Ventajas:
- Imagen final más pequeña (solo contiene lo necesario)
- Mayor seguridad (menos herramientas en producción)
- Separación construcción/ejecución
7. Buenas prácticas¶
7.1. Optimización de capas¶
# ❌ Mal: Múltiples capas
RUN apt-get update
RUN apt-get install -y package1
RUN apt-get install -y package2
# ✅ Bien: Una sola capa
RUN apt-get update && \
apt-get install -y \
package1 \
package2 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
7.2. Orden de instrucciones¶
# Primero lo que cambia menos (aprovecha caché)
FROM python:3.11-slim
WORKDIR /app
# Dependencias (cambian poco)
COPY requirements.txt .
RUN pip install -r requirements.txt
# Código de la aplicación (cambia frecuentemente)
COPY . .
CMD ["python", "app.py"]
7.3. Imágenes base pequeñas¶
# Alpine Linux es muy ligera
FROM python:3.11-alpine
FROM node:18-alpine
FROM nginx:alpine
# Imágenes "slim" también son una buena opción
FROM python:3.11-slim
7.4. Seguridad¶
# No ejecutar como root
RUN useradd -m -u 1000 appuser
USER appuser
# Mantener imágenes actualizadas
RUN apt-get update && apt-get upgrade -y
# No incluir secretos en la imagen
# Usar secrets de Docker o variables de entorno en runtime
8. Distribuir imágenes¶
8.1. Etiquetar imágenes¶
# Etiquetar para Docker Hub
$ docker tag mi_app:1.0 usuario/mi_app:1.0
$ docker tag mi_app:1.0 usuario/mi_app:latest
# Etiquetar para registro privado
$ docker tag mi_app:1.0 registry.ejemplo.com/mi_app:1.0
8.2. Subir a Docker Hub¶
8.3. Registro privado¶
# Ejecutar registro privado
$ docker run -d -p 5000:5000 --name registry registry:2
# Etiquetar y subir
$ docker tag mi_app:1.0 localhost:5000/mi_app:1.0
$ docker push localhost:5000/mi_app:1.0
# Descargar
$ docker pull localhost:5000/mi_app:1.0
9. Argumentos de construcción¶
Dockerfile:
FROM ubuntu:22.04
# Definir argumento
ARG VERSION=1.0
ARG USER=appuser
# Usar argumento
ENV APP_VERSION=${VERSION}
RUN useradd -m ${USER}
USER ${USER}
LABEL version=${VERSION}
Construcción:
Resumen¶
En esta sección hemos aprendido:
- ✅ Métodos para crear imágenes Docker
- ✅ Estructura y sintaxis de Dockerfiles
- ✅ Instrucciones principales del Dockerfile
- ✅ Construir imágenes con docker build
- ✅ Multi-stage builds para optimizar tamaño
- ✅ Buenas prácticas de construcción
- ✅ Distribuir imágenes en registros
Recursos adicionales¶
Fin del curso Docker
Has completado la introducción a Docker. Ahora estás preparado para containerizar tus aplicaciones y trabajar con contenedores en proyectos reales.