Curso Docker #3: Dockerfile — Construye Tus Propias Imagenes
Bienvenido al Curso de Docker - Parte 3 de 10

Fuente: Wikimedia Commons
¡Bienvenido a la Parte 3 del Curso de Docker! En los articulos anteriores aprendiste a usar imagenes existentes de Docker Hub. Ahora llega el momento mas emocionante: construir tus propias imagenes con Dockerfile.
Un Dockerfile es el archivo donde defines, paso a paso, como se construye tu imagen. Es como una receta que Docker sigue al pie de la letra para crear una imagen personalizada con tu aplicacion y todas sus dependencias.
Que es un Dockerfile
Un Dockerfile es un archivo de texto plano (sin extension) que contiene una serie de instrucciones que Docker ejecuta en orden para construir una imagen. Cada instruccion crea una nueva capa en la imagen.
El flujo basico es:
- Escribes un
Dockerfilecon las instrucciones - Ejecutas
docker buildpara construir la imagen - Docker lee el Dockerfile y ejecuta cada instruccion
- El resultado es una nueva imagen que puedes usar con
docker run
Veamos un ejemplo minimo:
1# Mi primer Dockerfile
2FROM ubuntu:22.04
3RUN apt-get update && apt-get install -y curl
4CMD ["curl", "--version"]
Este Dockerfile hace tres cosas:
- FROM: Usa Ubuntu 22.04 como imagen base
- RUN: Instala curl dentro de la imagen
- CMD: Define el comando que se ejecuta cuando arranca el contenedor
Instrucciones fundamentales del Dockerfile
Estas son las instrucciones que usaras en el 90% de tus Dockerfiles:
FROM — Imagen base
Todo Dockerfile debe comenzar con FROM. Define la imagen base sobre la que construiras:
1# Usar Node.js 20 basado en Alpine
2FROM node:20-alpine
3
4# Usar Python 3.12 slim
5FROM python:3.12-slim
6
7# Usar una imagen vacia (para binarios compilados)
8FROM scratch
WORKDIR — Directorio de trabajo
Establece el directorio donde se ejecutaran los comandos siguientes. Si no existe, Docker lo crea automaticamente:
1WORKDIR /app
2# A partir de aqui, todos los comandos se ejecutan dentro de /app
COPY y ADD — Copiar archivos
Copian archivos desde tu maquina (build context) al interior de la imagen:
1# Copiar un archivo especifico
2COPY package.json .
3
4# Copiar todo el directorio actual
5COPY . .
6
7# ADD tiene funcionalidad extra: descomprime archives y acepta URLs
8ADD app.tar.gz /app/
COPY sobre ADD a menos que necesites descomprimir archivos. COPY es mas explicita y predecible. Esta es una recomendacion de las mejores practicas oficiales de Dockerfile.
RUN — Ejecutar comandos
Ejecuta comandos dentro de la imagen durante el proceso de build. Cada RUN crea una nueva capa:
1# Instalar dependencias del sistema
2RUN apt-get update && apt-get install -y \
3 curl \
4 git \
5 vim \
6 && rm -rf /var/lib/apt/lists/*
7
8# Instalar dependencias de Node.js
9RUN npm install
10
11# Instalar dependencias de Python
12RUN pip install --no-cache-dir -r requirements.txt
ENV y ARG — Variables
ENV define variables de entorno disponibles tanto en el build como en el contenedor en ejecucion. ARG define variables solo disponibles durante el build:
1# Variable de entorno (disponible en el contenedor)
2ENV NODE_ENV=production
3ENV PORT=3000
4
5# Argumento de build (solo durante la construccion)
6ARG APP_VERSION=1.0.0
7
8# Usar ARG en el build
9RUN echo "Building version $APP_VERSION"
EXPOSE — Documentar puertos
Documenta que puertos usa la aplicacion. No abre los puertos — eso lo hace -p en docker run:
1EXPOSE 3000
2EXPOSE 8080
CMD y ENTRYPOINT — Comando de inicio
Definen que comando se ejecuta cuando arranca el contenedor:
1# CMD: comando por defecto (puede ser sobreescrito con docker run)
2CMD ["node", "server.js"]
3
4# ENTRYPOINT: comando fijo (los argumentos de docker run se anaden)
5ENTRYPOINT ["python", "app.py"]
6
7# Combinacion comun: ENTRYPOINT fijo + CMD como argumentos por defecto
8ENTRYPOINT ["python"]
9CMD ["app.py"]
CMD ["node", "server.js"] es correcto. CMD node server.js funciona pero ejecuta el comando dentro de un shell, lo que causa problemas con senales del sistema operativo y con el cierre limpio del contenedor.
El archivo .dockerignore
Cuando ejecutas docker build, Docker envia todo el contenido del directorio actual (el build context) al Docker daemon. El archivo .dockerignore te permite excluir archivos que no necesitas en la imagen:
1# .dockerignore
2node_modules
3npm-debug.log
4.git
5.gitignore
6.env
7.env.local
8Dockerfile
9docker-compose.yml
10README.md
11.vscode
12coverage
13dist
14*.md
Beneficios de usar .dockerignore:
- El build es mas rapido (se envian menos archivos al daemon)
- La imagen es mas pequena (no incluye archivos innecesarios)
- Evitas copiar archivos sensibles como
.envcon credenciales
Construir una imagen con docker build
Una vez que tienes tu Dockerfile, construyes la imagen con:
1# Sintaxis basica (el punto indica el build context = directorio actual)
2docker build -t mi-imagen:1.0 .
3
4# Explicacion:
5# -t mi-imagen:1.0 = nombre y tag de la imagen
6# . = build context (directorio donde esta el Dockerfile)
7
8# Especificar un Dockerfile diferente
9docker build -t mi-imagen:1.0 -f Dockerfile.prod .
10
11# Build con argumentos (ARG)
12docker build -t mi-imagen:1.0 --build-arg APP_VERSION=2.0.0 .
13
14# Build sin cache (fuerza reconstruccion de todas las capas)
15docker build -t mi-imagen:1.0 --no-cache .
Ejemplo practico: aplicacion Node.js
Vamos a crear una imagen Docker para una aplicacion Node.js real. Primero, crea estos archivos:
Paso 1: La aplicacion
1// server.js
2const http = require('http');
3
4const PORT = process.env.PORT || 3000;
5
6const server = http.createServer((req, res) => {
7 res.writeHead(200, { 'Content-Type': 'application/json' });
8 res.end(JSON.stringify({
9 message: 'Hola desde Docker!',
10 timestamp: new Date().toISOString(),
11 nodeVersion: process.version
12 }));
13});
14
15server.listen(PORT, () => {
16 console.log('Servidor corriendo en puerto ' + PORT);
17});
Paso 2: El package.json
1{
2 "name": "docker-node-demo",
3 "version": "1.0.0",
4 "description": "Demo Node.js app para el curso de Docker",
5 "main": "server.js",
6 "scripts": {
7 "start": "node server.js"
8 }
9}
Paso 3: El Dockerfile
1# Usar Node.js 20 basado en Alpine (imagen pequena)
2FROM node:20-alpine
3
4# Crear directorio de trabajo
5WORKDIR /app
6
7# Copiar package.json primero (para aprovechar cache de capas)
8COPY package.json .
9
10# Instalar dependencias
11RUN npm install --production
12
13# Copiar el codigo de la aplicacion
14COPY server.js .
15
16# Documentar el puerto
17EXPOSE 3000
18
19# Definir variable de entorno
20ENV NODE_ENV=production
21
22# Comando para iniciar la aplicacion
23CMD ["node", "server.js"]
Paso 4: Construir y ejecutar
1# Construir la imagen
2docker build -t mi-node-app:1.0 .
3
4# Ejecutar el contenedor
5docker run -d -p 3000:3000 --name node-demo mi-node-app:1.0
6
7# Probar la aplicacion
8curl http://localhost:3000
9# {"message":"Hola desde Docker!","timestamp":"2025-...","nodeVersion":"v20..."}
10
11# Ver logs
12docker logs node-demo
13
14# Limpiar
15docker rm -f node-demo
package.json y ejecutamos npm install ANTES de copiar el codigo. Esto aprovecha el cache de capas de Docker: si tu codigo cambia pero package.json no, Docker reutiliza la capa de npm install sin reinstalar dependencias.
Ejemplo practico: aplicacion Python
Ahora hagamos lo mismo con una aplicacion Python usando Flask:
Paso 1: Los archivos de la aplicacion
1# app.py
2from flask import Flask, jsonify
3from datetime import datetime
4import platform
5
6app = Flask(__name__)
7
8@app.route('/')
9def home():
10 return jsonify({
11 'message': 'Hola desde Docker con Python!',
12 'timestamp': datetime.now().isoformat(),
13 'python_version': platform.python_version()
14 })
15
16@app.route('/health')
17def health():
18 return jsonify({'status': 'OK'})
19
20if __name__ == '__main__':
21 app.run(host='0.0.0.0', port=5000)
1# requirements.txt
2flask==3.0.0
Paso 2: El Dockerfile
1# Usar Python 3.12 slim
2FROM python:3.12-slim
3
4# Crear directorio de trabajo
5WORKDIR /app
6
7# Copiar dependencias primero (cache de capas)
8COPY requirements.txt .
9
10# Instalar dependencias sin cache de pip
11RUN pip install --no-cache-dir -r requirements.txt
12
13# Copiar el codigo
14COPY app.py .
15
16# Documentar el puerto
17EXPOSE 5000
18
19# Variable de entorno para Flask
20ENV FLASK_APP=app.py
21ENV FLASK_ENV=production
22
23# Comando de inicio
24CMD ["python", "app.py"]
Paso 3: Construir y ejecutar
1# Construir
2docker build -t mi-python-app:1.0 .
3
4# Ejecutar
5docker run -d -p 5000:5000 --name python-demo mi-python-app:1.0
6
7# Probar
8curl http://localhost:5000
9curl http://localhost:5000/health
10
11# Limpiar
12docker rm -f python-demo
Cache de capas y mejores practicas
Entender el cache de capas es crucial para escribir Dockerfiles eficientes. Docker cachea cada capa y la reutiliza si la instruccion y los archivos no han cambiado.
Orden importa
Coloca las instrucciones que cambian con menos frecuencia al inicio y las que cambian mas al final:
1# CORRECTO: dependencias primero, codigo despues
2FROM node:20-alpine
3WORKDIR /app
4COPY package.json package-lock.json ./ # Cambia poco
5RUN npm ci # Se cachea si package.json no cambio
6COPY . . # Tu codigo cambia frecuentemente
7CMD ["node", "server.js"]
8
9# INCORRECTO: todo junto invalida el cache constantemente
10FROM node:20-alpine
11WORKDIR /app
12COPY . . # Cualquier cambio invalida TODO
13RUN npm install
14CMD ["node", "server.js"]
Combinar comandos RUN
1# CORRECTO: un solo RUN = una sola capa
2RUN apt-get update && apt-get install -y \
3 curl \
4 git \
5 && rm -rf /var/lib/apt/lists/*
6
7# INCORRECTO: multiples RUN = multiples capas innecesarias
8RUN apt-get update
9RUN apt-get install -y curl
10RUN apt-get install -y git
11RUN rm -rf /var/lib/apt/lists/*
Usar usuario no-root
1# Buena practica: no ejecutar como root
2FROM node:20-alpine
3WORKDIR /app
4COPY . .
5RUN npm ci
6
7# Crear usuario sin privilegios
8RUN addgroup -S appgroup && adduser -S appuser -G appgroup
9USER appuser
10
11CMD ["node", "server.js"]
Resumen y proximo articulo
En este tercer articulo del curso hemos aprendido:
- Que es un Dockerfile y como funciona
- Las instrucciones fundamentales: FROM, WORKDIR, COPY, RUN, ENV, ARG, EXPOSE, CMD, ENTRYPOINT
- Como usar .dockerignore para excluir archivos
- Como construir imagenes con docker build
- Ejemplo completo con Node.js y Python (Flask)
- El cache de capas y como optimizarlo
- Mejores practicas: orden de instrucciones, combinar RUN, usuario no-root
En el proximo articulo (Parte 4 de 10) aprenderemos sobre volumenes Docker para persistir datos. Veras como evitar que tus datos se pierdan cuando un contenedor se destruye, con ejemplos practicos usando PostgreSQL.
¡Nos vemos en el siguiente articulo!
Comments
Sign in to leave a comment
No comments yet. Be the first!