Skip to content

[Actualizado a 14 de octubre de 2026]

Contenedores🔗

Este manual es muy, muy resumido con el único objeto de tener una herramienta de partida para correr contenedores y crear imágenes del C.E. de Desarrollo de Aplicaciones en Lenguaje Python. Para cualquier otro objetivo revisar las fuentes listadas y la documentación oficial.

1. Instalación🔗

Con Docker Desktop

¿Funciona?🔗

Abra una terminal y ejecuta docker --version.

2. Comando docker🔗

El comando docker tiene la siguiente estructura:

docker <comando> <argumentos> {{imagen}}[:tag] <COMANDO>

Vamos a analizarlo por partes:

  1. docker: indica al sistema que vamos a pasar un comando al servicio docker y no al shell donde estemos corriendo (PS, CMD, Bash, etc).
  2. <comando>: que acción queremos realizar. Lo analizamos más adelante.
  3. <argumentos>: modificadores de aislamiento del contenedor. Son opcionales, pero recuerda que el contenedor se ejecutará completamente aislado, excepto aquellos elementos que explícitamente le indiquemos aquí.
  4. {{imagen}}: es la base con la que deseamos trabajar. Habitualmente la obtendremos de docker hub u otro repositorio público o privado.
  5. :tag: nos permite elegir la versión concreta de la imagen (ver opciones en Docker Hub.
  6. <COMANDO>: Las imágenes tienen pre-cargado el comando a ejecutar al arrarncar... pero podemos seleccionar cualquier otro que se encuentre disponible dentro de la imagen.

2.1. <comando>🔗

En realidad sólo necesitamos recordar dos comandos: docker run y docker network.

2.1.1. docker run🔗

Es un comando mágico para trabajar con contenedores que realiza 3 funciones:

  • Busca la imagen en el "almacén local" (caché local). Si no existe la busca en el almacen original, por defecto docker hub, y la descarga.
  • La instancia (crea un contenedor ejecutable a partir de ella).
  • Lo corre (el contenedor).

El resto de comandos podemos manejarlos gráficamente con Docker Desktop, pero los mencionamos para que el alumnado investigue si lo desea: ps, logs, start, stop, rm, exec, pull, push, cp...

2.1.2. docker network🔗

Es un comando para gestión de redes que nos permite:

  • docker network create para crear una red 1.
  • docker network ls para listar redes disponibles.

Info

Las redes en docker nos permite tener contenedores conectados entre sí y aislados del resto del mundo (o no).

2.2. <argumentos>🔗

Esta parte es la que más debemos meditar ya que NO podremos modificarla luego y tiene implicaciones de usabilidad y seguridad.

2.2.1. Volúmenes🔗

Permite que el host y el contenedor compartan datos (archivos y directorios).

docker run -v {{DIR_DEL_HOST}}:{{DIR_DE_LA_IMAGEN}} ...

El directorio del host puede especificarse de forma absoluta (p.e. "C:/Users/Luis/mis_datos") o relativa al directorio actual:

  • En Bash: ./mis_datos, $(pwd) o ${PWD}
  • En Powershell: ${PWD}
  • y en CMD: %cd%

Warning

El {{DIR_DE_LA_IMAGEN}} lo marca el creador de la imagen y NO podemos modificarlo.

Podemos consultar los volúmenes en Docker Desktop-Containers-{{Contenedor}}-Bind mounts.

Podemos "montar" varios volúmenes y los mismos pueden conectar archivos o directorios.

2.2.2. Puertos🔗

Permite "abrir" puertos al exterior o escuchar en puertos del host.

docker run -p 8080:80 nginx

Conectará el puerto 8080 del host con el 80 del contenedor, pudiendo especificar el puerto que queramos del host que esté disponible.

Warning

El puerto del contenedor es fijado por el creador de la imagen y NO podemos cambiarlo.

2.2.3. Redes🔗

Conexión a redes con --net {{NOMBRE_DE_RED}}. El contenedor será creado con una conexión a esta red y podrá comunicarse con todos los contenedores que estén en ella.

Algunas redes pre-creadas son:

  • host: el contenedor tendrá pleno acceso a la red y al host. La más flexible e insegura.
  • none: el contenedor está aislado. La más rígida y segura.
  • bridge: podremos conectar si especificamos los puertos. Es la red por defecto.

Cualquier otra red creada permitirá la conexión entre contenedores y su conexión desde el exterior será según drivers utilizados.

2.2.4. Otros🔗

  • Elegir entre "interactivo" -i o desatendido -d (segundo plano).
  • Usar un emulador de terminal... -t
  • Nombrar contenedor --name {{NOMBRE}}
  • Autoborrar el contenedor al finalizar --rm
  • Elegir el directorio de trabajo -w {{DIRECTORIO_DEL_CONTENEDOR}}

2.3. Imagen🔗

Ver en docker hub

2.4. <COMANDO>🔗

Podemos modificar el comportamiento del contenedor lanzando comandos diferentes al por defecto:

PowerShell
1
2
3
4
5
6
docker run -it `
           --rm `
           --name python-3.13 `
           -v ${PWD}/:/home `
           -w /home `
           python:3.13 python

Note

Para separar en varias líneas la introducción de un comando de PowerShell tenemos que añadir un backtrick "`" al final de la misma.

En Bash lo sustituímos por una "contrabarra" \:

bash
1
2
3
4
5
6
docker run -it \ 
       --rm \ 
           --name python-3.13 \
           -v ${PWD}/:/home \
           -w /home \
           python:3.13 python

2.5. Ejemplos🔗

Probar nuestra aplicación en otra versión de python🔗

  1. Comprobamos: docker run -it --rm --name python-3.13 -v ${PWD}/:/home -w /home python:3.13 python --version

  2. Ejecutamos: docker run -it --rm --name python-3.13 -v ${PWD}/:/home -w /home python:3.13 python app.py

Aislamiento de red🔗

Aislamiento de red con --net none o --network none:

# Ejecución `default` (como `bridge`). Sólo expuesto lo indicado
# **explícitamente**, pero el contenedor tiene salida:

## Probamos salida
docker run -it \
           --rm \
           --name alpine-cli \
           -v $(pwd):/app \
           -w /app \
           arangodb/alpineutils-x86_64 \
           ping marca.com

## Probamos entrada
## 1. Lanzamos servicio
docker run -it \
           --rm \
           --name python-3.13 \
           -v ${PWD}/:/home \
           -w /home \
           python:3.13 \
           python -m http.server

## 2. Consultamos servicio desde el host con curl o navegador web:
curl localhost:8000

# `bridge` con reenvío:
## 1. Lanzamos servicio
docker run -p 8000:8000 \
           -it \
           --rm \
           --name python-3.13 \
           -v ${PWD}/:/home \
           -w /home \
           python:3.13 python -m http.server

## 2. Consultamos
curl localhost:8000

# `bridge` con reenvío de puerto:
## 1. Lanzamos servicio
docker run -p 80:8000 \
           -it \
           --rm \
           --name python-3.13 \
           -v ${PWD}/:/home \
           -w /home \
           python:3.13 python -m http.server

## 2. Consultamos
curl localhost


# Ejecución como `host`. Todo el contenedor expuesto:
## 1. Lanzamos servicio
docker run --net host \
           -it \
           --rm \
           --name python-3.13 \
           -v ${PWD}/:/home \
           -w /home \
           python:3.13 python -m http.server

## 2. Consultamos
curl localhost:8000

# Ejecución completamente **aislada** con `-net none`:
docker run --net none \
           -it \
           --rm \
           --name alpine \
           -v $(pwd):/app \
           -w /app \
           arangodb/alpineutils-x86_64 \
           ping marca.com

Gestión de variables de entorno -e:🔗

Demostrar cómo inyectar configuración sin modificar código Python.

  1. Creamos la aplicación:
    app.py
       import os
    
       print(f'Corriendo en {os.getenv("ENTORNO")} en puerto {os.getenv("PUERTO")}')
    
  2. La ejecutamos
       docker run --rm \ 
               -e ENTORNO="Produccion" \
               -e PUERTO=5000 \
               -v ${PWD}/app.py:/work/app.py \
               -w /work \
               python:3.13 \
               python app.py
    

Si nuestra aplicación tuviera más de un archivo y estuviera en el directorio work_dir...

   docker run --rm \ 
           -e ENTORNO="Produccion" \
           -e PUERTO=5000 \
           -v ${PWD}/work_dir:/app \
           -w /app \
           python:3.13 \
           python app.py

Comunicación de contenedores🔗

Comunicación entre dos contenedores mediante una red bridge personalizada:

# 1. Crear una red personalizada en modo bridge (las redes predeterminadas no tienen DNS interno)
docker network create red-extremadura

# 2. Arrancar el contenedor "caceres" en segundo plano (-d) dentro de la red
docker run -d \
  --name caceres \
  --network red-extremadura \
  alpine sleep 60

# 3. Arrancar el contenedor "badajoz" e interactuar para hacer ping a "caceres" por su nombre
docker run -it \
  --rm \
  --name badajoz \
  --network red-extremadura \
  alpine ping -c 4 caceres

# 4. Limpieza (opcional)
docker stop caceres
docker network rm red-extremadura

2.6. Ejercicios🔗

Ejercicio 1: Primeros pasos e inspección (Fácil)🔗

Despliega un contenedor interactivo con la imagen python:3.13-slim. Accede a la REPL (consola interactiva de Python), calcula la potencia de $2^{100}$ y sal del contenedor asegurándote de que el contenedor se elimine automáticamente al cerrar.

Ejercicio 2: Ejecución de scripts locales en entorno aislado (Fácil)🔗

Crea en el host un archivo hola.py con el contenido print("Hola desde el contenedor"). Ejecútalo usando un contenedor de Python sin copiar el archivo dentro de la imagen, utilizando un volumen relativo.

Ejercicio 3: Prueba de compatibilidad entre versiones de Python (Fácil - Medio)🔗

Dado un script check_version.py con el contenido import sys; print(sys.version), ejecútalo usando contenedores con Python 3.9, 3.11 y 3.13 comprobando la versión reportada en cada caso.

Ejercicio 4: Servidor HTTP rápido con redirección de puertos (Medio)🔗

Usa el módulo nativo de Python http.server para servir los archivos de tu directorio local en el puerto 8080 del Host, sabiendo que el servidor de Python escucha internamente en el puerto 8000.

Ejercicio 5: Pasar variables de configuración al contenedor (Fácil)🔗

Dado el script Python config_check.py que imprime las variables de entorno DATABASE_URL y DEBUG. Lánzalo dentro de un contenedor asignándole los valores postgres://user:pass@db:5432/app a DATABASE_URL y True A `DEBUG.

config_check.py
import os

print(os.getenv('DATABASE_URL'))
print(os.getenv('DEBUG'))

Ejercicio 6: Generación de artefactos persistentes (Medio)🔗

Ejecuta un script dentro de un contenedor que genere un archivo de texto resultados.txt con los números del 1 al 10. Verifica que, al finalizar el contenedor, el archivo permanece disponible en tu equipo host.

Ejercicio 7: Redes personalizadas entre servicios (Medio - Avanzado)🔗

  1. Crea una red personalizada de Docker llamada red-app.
  2. Lanza un servidor HTTP de Python en esa red con el nombre servidor-web.
  3. Lanza un segundo contenedor interactivo en la misma red y utiliza curl o urllib en Python para obtener la página principal del servidor-web usando su nombre de contenedor como hostname.
bash
# 1. Crear la red

# 2. Servidor

# 3. Cliente consultando por DNS de Docker

# Limpieza

Ejercicio 8: Contenedor en aislamiento de seguridad total (Avanzado)🔗

Simula la ejecución de un script de análisis de datos sensible analisis.py que no debe tener acceso a internet ni a la red local. Utiliza la bandera de red adecuada para garantizar aislamiento total y verifica que no puede conectarse a un servidor externo.

Ejercicio 9: Inyección y prueba con base de datos en memoria (Avanzado)🔗

Lanza un contenedor de Redis (redis:alpine) expuesto en el puerto 6379. Luego, ejecuta un script efímero en Python que instale la librería redis, se conecte al servidor, guarde una clave materia: "python" y la lea.

bash
# 1. Base de datos

# 2. Script ejecutor

# Limpieza

Para la conexión y guardado emplea el siguiente PROMT:

Creame una app python que se conecte a un servidor redis con el FQDN `redis` en el puerto almacenado en la variable de entorno `PUERTO` y que almacene el par: `materia: "python"`. Debe mostrar si finalizó con éxito.

Después debe leerla y mostrarla por pantalla.

Además:
1. Lead with the next action.
2. Number multi-step tasks.
3. End with one concrete next step.
4. Suppress tangents.
5. Restate state every turn.
6. Specific time estimates (minutes, not "a bit").
7. Make wins visible.
8. Matter-of-fact errors.
9. Cap lists to 5 items.
10. No preamble. No recap. No closers.

Ejercicio 10: Automatización multi-paso mediante CLI (Avanzado / Experto)🔗

Escribe un comando en Bash/PowerShell que descargue un dataset CSV público desde un script de Python en un contenedor con acceso a internet, lo guarde en un volumen compartido, y luego procese dicho CSV con un segundo contenedor con la red deshabilitada (--network none).

bash
# Paso 1: Descarga y guardado en volumen

# Paso 2: Procesamiento aislado

3. "Recetas" docker compose🔗

A medida que nuestras aplicaciones crecen, escribir comandos docker run extremadamente largos con múltiples volúmenes, variables de entorno, puertos y redes se vuelve propenso a errores y difícil de mantener.

Docker Compose es una herramienta que permite definir y ejecutar aplicaciones multicontenedor mediante un único archivo declarativo escrito en formato YAML (habitualmente llamado docker-compose.yml o compose.yaml).

3.1. Sintaxis Básica de un archivo compose.yaml🔗

Un archivo de Compose traduce directamente las banderas de docker run a clave-valor:

Banderas docker run Equivalente en compose.yaml
--name mi-app container_name: mi-app
-p 8000:8000 ports: - "8000:8000"
-v ${PWD}/:/app volumes: - ./:/app
-e ENTORNO=prod environment: - ENTORNO=prod
--net mi-red networks: - mi-red
--restart always restart: always

3.2. De docker run a docker compose con composerize🔗

La herramienta web Composerize permite transformar comandos CLI de Docker directamente a archivos YAML.

Ejemplo de conversión:🔗

Comando Docker CLI:

docker cli
docker run -d \
           --name api-python \
           -p 5000:5000 \
           -v ${PWD}/:/app \ 
           -e FLASK_ENV=development \
           --net red-datos \
           python:3.13-slim python /app/main.py

Resultado generado:

compose.yml
version: '3.8'
services:
  api-python:
    container_name: api-python
    ports:
      - 5000:5000
    volumes:
      - ${PWD}/:/app
    environment:
      - FLASK_ENV=development
    networks:
      - red-datos
    image: python:3.13-slim
    command: python /app/main.py

networks:
  red-datos:
    driver: bridge

3.3. Comandos Esenciales de Docker Compose🔗

Para gestionar el ciclo de vida de la "receta", ejecuta desde el directorio donde se encuentra el archivo compose.yaml:

  • Levantar los servicios en segundo plano:

    docker compose up -d
    

  • Ver los logs de los contenedores en tiempo real:

    docker compose logs -f
    

  • Detener y eliminar contenedores, redes y volúmenes creados:

    docker compose down
    

  • Ejecutar un comando dentro de un servicio en ejecución:

    docker compose exec api-python python --version
    

3.4. Recetas Frecuentes para Desarrollo en Python🔗

Receta 1: Entorno de Desarrollo Web Python (FastAPI / Flask) con Recarga en Vivo🔗

compose.yml
services:
  web:
    image: python:3.13-slim
    container_name: python-web-dev
    ports:
      - "8000:8000"
    volumes:
      - ${PWD}/:/app
    working_dir: /app
    command: sh -c "pip install -r requirements.txt && uvicorn main:app --host 0.0.0.0 --reload"

Receta 2: Pila Completa (Python API + Base de Datos PostgreSQL)🔗

compose.yml
services:
  app:
    image: python:3.13-slim
    container_name: python-app
    ports:
      - "8000:8000"
    volumes:
      - ${PWD}/:/app
    working_dir: /app
    environment:
      - DB_HOST=db
      - DB_NAME=app_db
      - DB_USER=postgres
      - DB_PASS=secret
    depends_on:
      - db
    command: python main.py

  db:
    image: postgres:16-alpine
    container_name: postgres-db
    environment:
      POSTGRES_DB: app_db
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  pgdata:

4. Creando imágenes🔗

Plantillas Dockerfile

4.1 Ejercicios🔗

Basado en las plantillas habituales de Dockerfile para desarrollo en Python (Slim, Alpine, Multi-stage y Poetry/UV), aquí tienes la batería de ejercicios de desarrollo de imágenes:

Ejercicio 4.1: Mi primer Dockerfile para un Script CLI🔗

Crea un proyecto con un archivo app.py que salude e imprima la hora actual. Crea un Dockerfile usando python:3.13-slim, construye la imagen con el tag mi-script:v1 y ejecútala.

Ejercicio 4.2: Optimización de Caché de Capas (COPY estructurado)🔗

Dado un proyecto Python con requirements.txt y código fuente, escribe un Dockerfile estructurado de forma que la capa de instalación de dependencias (pip install) no vuelva a ejecutarse cuando modifiques únicamente el código fuente de la aplicación.

Ejercicio 4.3: Usuario Sin Privilegios (non-root user) por Seguridad🔗

Modifica un Dockerfile de Python para que la aplicación no se ejecute como el usuario root. Crea un grupo y un usuario del sistema llamado appuser, asígnale los permisos del directorio /app y cambia a dicho usuario antes de definir la instrucción CMD.

  • Dockerfile:
    Dockerfile
    # ...
    # Crear usuario sin privilegios
    RUN addgroup --system appgroup && adduser --system --group appuser
    
    # Dar propiedad de la carpeta al nuevo usuario
    RUN chown -R appuser:appgroup /app
    
    # Cambiar al usuario seguro
    USER appuser
    
    #...
    

Ejercicio 4.4: Contenerización de una API Web con FastAPI y .dockerignore🔗

  1. Crea un archivo .dockerignore para excluir entornos virtuales (.venv), archivos de Git (.git), y cachés (__pycache__).
  2. Escribe un Dockerfile para empaquetar una API en FastAPI que escuche en el puerto 8000 usando uvicorn.

Ejercicio 4.5: Imagen de Producción Reducida con Multi-Stage Build (Avanzado)🔗

Diseña una imagen multi-etapa (multi-stage build) para una aplicación Python con dependencias pesadas compiladas.

  • La etapa builder debe incluir herramientas de compilación (build-essential).
  • La etapa runner debe partir de python:3.13-slim, copiar solo las librerías instaladas y el código, manteniendo un tamaño de imagen final óptimo.

5. Jupyter-server🔗

En bash podemos crear el archivo de variables de entorno con un token único:

bash
1
2
3
4
echo -n 'JUPYTER_TOKEN=' > .env \
  && head -c 16 /dev/urandom \
  | base64 \
  | tr -dc 'a-zA-Z0-9' >> .env

O en powershell:

PowerShell
$token = [Convert]::ToBase64String((1..16 | ForEach-Object { Get-Random -Minimum 0 -Maximum 256 })) -replace '[^a-zA-Z0-9]', ''
"JUPYTER_TOKEN=$token" | Out-File -FilePath .env -Encoding utf8

Luego:

compose.yml
services:
  jupyter:
    container_name: jupyter-server
    image: quay.io/jupyter/base-notebook
    ports:
      - "8888:8888"
    volumes:
      - .:/datos
    working_dir: /datos
    command: start-notebook.py --NotebookApp.token=${JUPYTER_TOKEN}

Lanzamos y ejecutamos docker exec -it jupyter-server jupyter server list para obtener el enlace directo al servidor.

Ahora podemos:

  1. Entrar con el enlace a la interfaz web del servidor o,
  2. Invocar desde VS Code con el plugin Jupyter de Microsoft

Instalando requerimientos en el servidor🔗

Desgraciadamente si corremos cualquiera de los notebooks veremos enseguida que tenemos algunos errores ya que no tenemos las librerías empleadas en los notebooks.

Para instalarlas:

Dockerfile
FROM quay.io/jupyter/base-notebook:latest

# Copiar el archivo de requerimientos e instalar dependencias
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

Y ahora empleamos el compose:

compose.yml
services:
  jupyter:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: jupyter-server
    ports:
      - "8888:8888"
    volumes:
      - .:/datos
    working_dir: /datos
    command: start-notebook.py --NotebookApp.token=${JUPYTER_TOKEN}

El archivo de requisitos:

requirements.txt
pandas
numpy
matplotlib
statsmodels

Creamos la imagen con docker compose build y luego lo levantamos con docker compose up -d

Warning

Recuerda regenerar la imagen después de cada modificación del archivo de requisitos requirements.txt

Ejercicios de ampliación🔗

Fuentes🔗


  1. Aunque tenemos varios modos de red nos bastará con el modo bridge (el por defecto). ↩