Cristhian Villegas
DevOps12 min read0 views

Curso Docker #3: Dockerfile — Construye Tus Propias Imagenes

Curso Docker #3: Dockerfile — Construye Tus Propias Imagenes

Bienvenido al Curso de Docker - Parte 3 de 10

Logo de Docker

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.

Nota: Este articulo asume que ya dominas los conceptos de imagenes y contenedores de los articulos 1 y 2. Si aun no los has leido, te recomiendo hacerlo primero.

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:

  1. Escribes un Dockerfile con las instrucciones
  2. Ejecutas docker build para construir la imagen
  3. Docker lee el Dockerfile y ejecuta cada instruccion
  4. El resultado es una nueva imagen que puedes usar con docker run

Veamos un ejemplo minimo:

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

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

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

dockerfile
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/
Tip: Prefiere 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:

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

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

dockerfile
1EXPOSE 3000
2EXPOSE 8080

CMD y ENTRYPOINT — Comando de inicio

Definen que comando se ejecuta cuando arranca el contenedor:

dockerfile
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"]
Importante: Siempre usa la forma exec (con corchetes) en lugar de la forma shell. 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:

bash
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 .env con credenciales

Construir una imagen con docker build

Una vez que tienes tu Dockerfile, construyes la imagen con:

bash
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

javascript
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

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

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

bash
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
Tip: Nota como copiamos 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

python
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)
bash
1# requirements.txt
2flask==3.0.0

Paso 2: El Dockerfile

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

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

dockerfile
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

dockerfile
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

dockerfile
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"]
Seguridad: Ejecutar contenedores como root es un riesgo de seguridad. Si un atacante compromete la aplicacion, tendria permisos de root dentro del contenedor. Siempre crea un usuario no-root para ejecutar tu aplicacion.

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!

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