Durante seis módulos has dado por supuesta una máquina con Docker instalado. En el módulo 6 esa máquina se multiplicó en un clúster y la pregunta dejó de ser cómoda: ¿de dónde salen los hosts, quién les instala Docker, quién los actualiza y quién responde cuando uno deja de arrancar? Esta lección empieza por la herramienta que lo intentó primero —Docker Machine, hoy archivada— y termina en cómo se hace en 2026.

Contenido

  1. El problema real del aprovisionamiento
  2. Qué fue Docker Machine y su modelo de drivers
  3. Los comandos característicos
  4. Por qué está archivado y qué hacer si lo encuentras
  5. Lo que sí sobrevivió: los contextos de Docker
  6. Un contexto aurora-prod sobre SSH
  7. Desplegar contra un host remoto sin cambiar de terminal
  8. SSH frente a exponer el daemon por TCP
  9. El mapa de alternativas actuales
  10. Instalación directa: el script idempotente
  11. cloud-init: que la máquina nazca con Docker
  12. Terraform / OpenTofu: infraestructura declarativa
  13. Ansible: configurar y mantener la flota
  14. Packer y los servicios gestionados
  15. El host como superficie de ataque
  16. ¿Hosts propios o gestionado? Criterios de decisión

  1. El problema real del aprovisionamiento

Entre «tengo cuenta en un proveedor de nube» y «docker compose up -d levanta Aurora Libros en producción» hay una lista que alguien tiene que ejecutar: crear la VM (tamaño, región, disco, red, grupo de seguridad), configurar acceso SSH y usuario sin privilegios, instalar Engine y el plugin de Compose, ajustar el daemon (data-root, logging, live-restore), endurecer el host (cortafuegos, parches automáticos), desplegar la aplicación, repetirlo idéntico en el segundo host y mantenerlo durante tres años. Los dos últimos puntos son los que separan una herramienta de un tutorial: Docker Machine atacaba los tres primeros y el resto quedaba fuera.

  1. Qué fue Docker Machine y su modelo de drivers

Docker Machine (docker-machine) fue un binario oficial, contemporáneo de Docker Toolbox (2015-2018), con un propósito concreto: crear una VM con Docker Engine ya instalado —en local o en la nube— y apuntar tu cliente local hacia ese daemon remoto.

Esa segunda mitad era la interesante. Recuerda 01-03: cliente y daemon son procesos separados que hablan por una API. Si el cliente alcanza un daemon remoto, docker ps lista los contenedores de un servidor de Frankfurt desde tu portátil. Docker Machine automatizaba el papeleo: crear la VM, instalar Engine, generar certificados TLS mutuos y exportar las variables que redirigían el cliente.

graph LR
    DM["docker-machine<br/>(tu portátil)"] -->|"1. crea la VM (API del proveedor)"| VM
    DM -->|"2. instala Engine y genera certificados"| VM
    DM -->|"3. exporta DOCKER_HOST y DOCKER_CERT_PATH"| CLI
    CLI["cliente docker<br/>(tu portátil)"] -->|"docker ps sobre TLS"| VM
    VM["VM 'aurora-prod'<br/>dockerd + TLS :2376"]

La pieza extensible era el driver: un adaptador por plataforma. El comando era el mismo; solo cambiaban --driver y sus opciones.

Driver Dónde creaba la máquina Estado en 2026
virtualbox, hyperv VM local Obsoleto (era Docker Toolbox)
amazonec2, digitalocean Instancia / droplet Archivado con el proyecto
azure, google, openstack VM en la nube pública o privada Archivado
generic Un host ya existente, vía SSH Su idea sobrevive (ver §10)
none Solo registraba una máquina externa Sustituido por docker context

El driver generic no creaba nada: se conectaba por SSH a una máquina existente e instalaba Docker en ella. Era un instalador remoto rudimentario, exactamente lo que hoy cubre mejor un gestor de configuración.

  1. Los comandos característicos

Los verás en documentación antigua y en scripts heredados. Reconócelos aunque no los ejecutes:

docker-machine create --driver digitalocean \
  --digitalocean-access-token "$DO_TOKEN" \
  --digitalocean-size s-2vcpu-4gb --digitalocean-region fra1 \
  aurora-prod

docker-machine ls                 # máquinas y estado
docker-machine ip aurora-prod     # IP pública
docker-machine ssh aurora-prod    # sesión SSH sin buscar la clave
eval "$(docker-machine env aurora-prod)"   # ← el comando clave
docker ps                         # ¡lista los contenedores REMOTOS!
docker-machine rm aurora-prod     # destruye la VM en el proveedor

El eval era la magia y también el problema. Exportaba DOCKER_TLS_VERIFY=1, DOCKER_HOST=tcp://203.0.113.42:2376 y DOCKER_CERT_PATH apuntando a los certificados generados.

A partir de ahí, cada comando docker de esa terminal iba al servidor remoto, y abrir otra pestaña te devolvía al daemon local. Más de un incidente célebre nació de un docker compose down en la terminal equivocada.

  1. Por qué está archivado y qué hacer si lo encuentras

Docker archivó el repositorio en 2021. No hay parches de seguridad, los drivers usan APIs que han cambiado y la instalación descarga versiones de Engine que ya no existen.

Motivo del abandono Qué lo sustituyó
Cada proveedor publicó su CLI y su API aws, gcloud, az, más Terraform
La infraestructura como código se volvió estándar Terraform / OpenTofu, Pulumi
Configurar el host es un problema aparte Ansible, cloud-init
La VM local se resolvió en el escritorio Docker Desktop (07-03)
Cambiar de daemon no exige crear máquinas docker context
La orquestación multinodo se la llevó Kubernetes Módulo 6

Regla práctica: si un tutorial lo usa, tiene más de cinco años y hay que verificar todo lo demás que dice. Si aparece en un script heredado de tu empresa, no lo ejecutes: identifica qué máquinas gestiona, traslada esa información a contextos o a Terraform y planifica su retirada con quien mantenga la infraestructura.

  1. Lo que sí sobrevivió: los contextos de Docker

La idea que valía la pena —un cliente, varios daemons— es hoy una función de primera clase: los contextos. Un contexto es un destino con nombre. A diferencia del eval, el cambio es global y persistente, no por terminal.

Comando Qué hace
docker context create <n> --docker host=... Define un destino nuevo
docker context ls Lista los contextos; * marca el activo
docker context use <n> Cambia el activo (persistente)
docker context inspect <n> Muestra endpoint y configuración TLS
docker --context <n> <cmd> Ejecuta un solo comando en otro destino
docker context rm <n> Elimina el contexto (no toca el servidor)

La distinción importante: docker-machine rm destruía una máquina; docker context rm solo olvida una dirección. Los contextos no crean, no instalan y no mantienen nada: solo apuntan. Por eso siguen siendo útiles y por eso no sustituyen al aprovisionamiento.

  1. Un contexto aurora-prod sobre SSH

Aurora Libros tiene un host en libros.example.com con un usuario deploy del grupo docker. Primero, que SSH funcione con clave y sin contraseña, y una entrada en ~/.ssh/config que haga legible el resto:

ssh-keygen -t ed25519 -C "deploy@aurora" -f ~/.ssh/aurora_deploy
ssh-copy-id -i ~/.ssh/aurora_deploy.pub [email protected]
Host aurora-prod
    HostName libros.example.com
    User deploy
    IdentityFile ~/.ssh/aurora_deploy
    IdentitiesOnly yes

El contexto es una línea, y la comprobación no requiere activarlo:

docker context create aurora-prod \
  --description "Producción Aurora Libros (fra1)" \
  --docker "host=ssh://aurora-prod"

docker --context aurora-prod version --format '{{.Server.Version}}'
# 27.5.1
docker --context aurora-prod ps --format '{{.Names}}\t{{.Status}}'
# aurora-web     Up 9 days
# aurora-api     Up 9 days (healthy)
# aurora-cache   Up 9 days
# aurora-db      Up 9 days (healthy)

Cuatro contenedores, nueve días en marcha, sin abrir una sola sesión SSH manual.

  1. Desplegar contra un host remoto sin cambiar de terminal

Compose respeta los contextos, así que el despliegue completo es esto:

docker --context aurora-prod compose \
  -f compose.yaml -f compose.prod.yaml \
  --env-file .env.prod up -d

docker --context aurora-prod compose ps
docker --context aurora-prod compose logs -f --tail=50 api

Tres detalles cambian el resultado:

Detalle Comportamiento
Rutas de volumes Se resuelven en el host remoto, no en tu portátil
build: El contexto de build se empaqueta y sube por SSH: lento
.env y env_file Se leen en tu máquina, y los valores viajan por la red
ports: "8080:8080" Publica en la IP del servidor, no en tu localhost

De la segunda fila sale una regla de oro: en producción no se construye, se despliega una imagen ya publicada. Es lo que hace tu compose.prod.yaml, que referencia ghcr.io/auroralibros/aurora-api:2.0.0 por digest en lugar de un build:. El pipeline de 06-02 construye, firma y publica; el host solo hace pull.

Patrón defensivo: nunca uses docker context use para producción. Deja default activo y escribe --context aurora-prod de forma explícita, o como mucho un alias dprod='docker --context aurora-prod'. Es más largo de teclear, y esa es exactamente la ventaja.

  1. SSH frente a exponer el daemon por TCP

Docker Machine usaba tcp://IP:2376 con TLS mutuo. Puedes hacerlo hoy, pero casi nunca deberías.

Aspecto ssh:// tcp:// con TLS tcp:// sin TLS
Puerto expuesto 22 (ya lo estaba) 2376 2375
Autenticación Claves SSH ya gestionadas Certificados propios a mantener Ninguna
Cifrado Sí Sí No
Rotación de credenciales Procedimiento existente CA propia, renovaciones, CRL —
Auditoría Registros de SSH del host Solo los del daemon —
Riesgo si se filtra Acceso de ese usuario root en el host root para cualquiera
Recomendación Por defecto Solo con justificación Nunca

La fila del riesgo es la que hay que interiorizar. Ya lo viste en 05-03: quien habla con el daemon puede lanzar docker run -v /:/host --privileged y es root en la máquina. Un 2375 abierto a Internet es una máquina regalada; hay bots escaneando ese puerto de forma continua y el patrón habitual es minar criptomonedas en tu servidor durante semanas.

Si alguna herramienta exige la API por TCP, la respuesta correcta es un túnel SSH local, no abrir el puerto:

ss -lntp | grep -E ':237[56]'    # no debería devolver nada expuesto
ssh -N -L 2375:/var/run/docker.sock deploy@aurora-prod &
DOCKER_HOST=tcp://127.0.0.1:2375 docker ps

  1. El mapa de alternativas actuales

Herramienta Qué resuelve Idempotente Cuándo elegirla
Script SSH propio Instalar y configurar Si lo escribes bien 1-2 hosts
cloud-init Configurar en el primer arranque Se ejecuta una vez Máquinas efímeras
Terraform / OpenTofu Crear la infraestructura Sí Infra revisable en Git
Ansible Configurar y mantener la flota Sí 3+ hosts duraderos
Packer Construir la imagen de máquina Sí Autoescalado, flotas
docker context Apuntar a hosts existentes N/A Operar, no aprovisionar
Gestionados Evitar el host N/A No querer mantener hosts

No compiten: se apilan. Un montaje sano de 2026 es Terraform crea + cloud-init arranca + Ansible mantiene + contextos operan.

  1. Instalación directa: el script idempotente

La versión honesta del driver generic. La clave es que se pueda ejecutar diez veces con el mismo resultado.

#!/usr/bin/env bash
# provision-host.sh — instala Docker Engine en Debian/Ubuntu. Idempotente.
set -euo pipefail

command -v docker >/dev/null 2>&1 \
  || curl -fsSL https://get.docker.com | sh   # Engine + plugins compose/buildx

id -u deploy >/dev/null 2>&1 || sudo useradd -m -s /bin/bash deploy
sudo usermod -aG docker deploy

sudo install -d -m 0755 /etc/docker
sudo tee /etc/docker/daemon.json >/dev/null <<'JSON'
{ "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" },
  "live-restore": true }
JSON
sudo systemctl enable --now docker && sudo systemctl reload docker
docker --version && docker compose version

Ventaja: cero dependencias y lo entiende cualquiera. Límites: no controla la deriva entre hosts, no sabe qué versión hay en cada uno y crece hasta volverse ilegible. A partir del tercer host, Ansible. Y un apunte: get.docker.com es cómodo, pero es un curl | sh contra Internet; en producción se usa el repositorio APT oficial con su clave GPG fijada, que es lo que el script hace por debajo.

  1. cloud-init: que la máquina nazca con Docker

cloud-init es el estándar de facto en imágenes de nube: lee un user-data en el primer arranque y configura la máquina antes de que nadie inicie sesión. Es el sustituto más directo de docker-machine create.

#cloud-config
# cloud-config.yaml — nodo de aplicación de Aurora Libros
hostname: aurora-app-01
timezone: Europe/Madrid
package_update: true
package_upgrade: true
packages: [ca-certificates, curl, gnupg, ufw, unattended-upgrades, fail2ban]

users:
  - name: deploy
    groups: [sudo]
    shell: /bin/bash
    sudo: "ALL=(ALL) NOPASSWD:ALL"
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... deploy@aurora

write_files:
  - path: /etc/docker/daemon.json
    permissions: "0644"
    content: |
      { "log-driver": "json-file",
        "log-opts": { "max-size": "10m", "max-file": "3" },
        "live-restore": true }
  - path: /etc/apt/apt.conf.d/20auto-upgrades
    content: |
      APT::Periodic::Update-Package-Lists "1";
      APT::Periodic::Unattended-Upgrade "1";

runcmd:   # repositorio oficial con clave GPG fijada, nunca curl | sh
  - install -m 0755 -d /etc/apt/keyrings
  - curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
  - chmod a+r /etc/apt/keyrings/docker.asc
  - echo "deb [signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" > /etc/apt/sources.list.d/docker.list
  - apt-get update
  - DEBIAN_FRONTEND=noninteractive apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  - usermod -aG docker deploy        # el grupo existe tras instalar Engine
  - systemctl enable --now docker
  - ufw default deny incoming && ufw allow 22/tcp && ufw allow 443/tcp && ufw --force enable
final_message: "Nodo Aurora listo tras $UPTIME segundos"

Se pasa como --user-data al crear la instancia, y se valida sin gastar dinero con cloud-init schema --config-file cloud-config.yaml; ya en el host, cloud-init status --wait espera a que termine y /var/log/cloud-init-output.log cuenta qué pasó. Su limitación es estructural: se ejecuta una sola vez. No sirve para cambiar algo en cincuenta máquinas ya en marcha.

  1. Terraform / OpenTofu: infraestructura declarativa

Terraform describe la infraestructura deseada y calcula el plan para llegar a ella. OpenTofu es su bifurcación de código abierto bajo la Linux Foundation, nacida tras el cambio de licencia de 2023; para lo que verás aquí son intercambiables (tofu en vez de terraform).

# main.tf — un nodo de aplicación de Aurora Libros
terraform {
  required_providers {
    digitalocean = { source = "digitalocean/digitalocean", version = "~> 2.40" }
  }
}
provider "digitalocean" { token = var.do_token }

resource "digitalocean_droplet" "app" {
  name      = "aurora-app-01"
  image     = "debian-12-x64"
  region    = "fra1"
  size      = "s-2vcpu-4gb"
  ssh_keys  = [digitalocean_ssh_key.deploy.fingerprint]
  user_data = file("${path.module}/cloud-config.yaml")   # ← §11 encaja aquí
  tags      = ["aurora", "produccion"]
}

resource "digitalocean_firewall" "app" {
  name        = "aurora-app-fw"
  droplet_ids = [digitalocean_droplet.app.id]
  inbound_rule { protocol = "tcp"  port_range = "22"
                 source_addresses = ["203.0.113.0/24"] }    # solo la oficina
  inbound_rule { protocol = "tcp"  port_range = "443"
                 source_addresses = ["0.0.0.0/0", "::/0"] }
}
terraform init && terraform plan -out=plan.bin && terraform apply plan.bin

terraform plan es el motivo por el que esto sustituye a los scripts: te dice qué va a pasar antes de que pase. Y como vive en Git, el aprovisionamiento se revisa en un pull request como cualquier código. El fichero de estado (terraform.tfstate) contiene secretos en claro: va en un backend remoto con bloqueo, nunca en el repositorio.

  1. Ansible: configurar y mantener la flota

Terraform crea; Ansible mantiene. Se conecta por SSH, no necesita agente y sus módulos son idempotentes: describes el estado final y solo actúa si hace falta.

# inventario.ini
[aurora_app]
aurora-app-01 ansible_host=203.0.113.42
aurora-app-02 ansible_host=203.0.113.43
[aurora_app:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/aurora_deploy
# playbook.yml — instala Docker y despliega el stack de Aurora Libros
- name: Preparar y desplegar Aurora Libros
  hosts: aurora_app
  become: true
  tasks:
    - name: Repositorio de Docker
      ansible.builtin.deb822_repository:
        name: docker
        uris: https://download.docker.com/linux/debian
        suites: bookworm
        components: stable
        signed_by: https://download.docker.com/linux/debian/gpg

    - name: Docker Engine y plugins
      ansible.builtin.apt:
        name: [docker-ce, docker-ce-cli, containerd.io,
               docker-buildx-plugin, docker-compose-plugin, python3-docker]
        state: present
        update_cache: true
      notify: reiniciar docker

    - name: Configuración del daemon
      ansible.builtin.copy:
        dest: /etc/docker/daemon.json
        mode: "0644"
        content: '{ "log-driver": "json-file", "log-opts": { "max-size": "10m" } }'
      notify: reiniciar docker

    - name: Descriptores de la pila
      ansible.builtin.copy: { src: "{{ item }}", dest: /opt/aurora/, mode: "0640" }
      loop: [compose.yaml, compose.prod.yaml]

    - name: Autenticarse en ghcr.io
      community.docker.docker_login:
        registry_url: ghcr.io
        username: auroralibros
        password: "{{ ghcr_token }}"      # cifrado con ansible-vault

    - name: Levantar la pila
      community.docker.docker_compose_v2:
        project_src: /opt/aurora
        files: [compose.yaml, compose.prod.yaml]
        pull: always
        state: present

  handlers:
    - name: reiniciar docker
      ansible.builtin.systemd: { name: docker, state: restarted, enabled: true }
ansible-playbook -i inventario.ini playbook.yml --check   # simulacro (= plan)
ansible-playbook -i inventario.ini playbook.yml
ansible-playbook -i inventario.ini playbook.yml --limit aurora-app-02

El ghcr_token va cifrado con ansible-vault encrypt_string, jamás en claro. Y en la segunda ejecución verás changed=0: eso es la idempotencia funcionando de verdad.

  1. Packer y los servicios gestionados

En lugar de instalar Docker en cada arranque, Packer construye una vez una imagen de máquina (AMI, snapshot) que ya lo trae todo, y las instancias arrancan en segundos. Es el razonamiento de las imágenes de contenedor un nivel más abajo.

Enfoque Tiempo hasta útil Deriva entre hosts Cuándo compensa
cloud-init en cada arranque 2-4 min Posible (los repos cambian) Pocos hosts
Imagen con Packer 15-40 s Ninguna: bit a bit idéntica Autoescalado, flotas

El coste es un pipeline más: cada parche obliga a reconstruir y redistribuir la imagen. Con autoescalado, se paga solo. Pero la mejor forma de mantener un host sigue siendo no tenerlo:

Opción Qué gestionas tú Qué desaparece Contrapartida
Instancias con contenedores La configuración de la app Instalar Docker Sigue habiendo VM
ECS + Fargate Definición de tarea El host entero Atado al proveedor
Cloud Run / Container Apps La imagen y sus variables Host y orquestador Menos control de red
Kubernetes gestionado Los manifiestos (módulo 6) El plano de control Los nodos siguen siendo tuyos

Para una imagen como ghcr.io/auroralibros/aurora-api:2.0.0 —sin estado, doce factores, sondas y apagado ordenado, justo lo que preparaste en 06-01— un servicio tipo Cloud Run es candidato serio: le das imagen, puerto y variables. Que puedas plantearlo es consecuencia directa del módulo 6, no una casualidad.

  1. El host como superficie de ataque

En 05-03 endureciste el contenedor. Todo aquello es inútil si el host lleva catorce meses sin parches.

Vector Medida mínima Verificación
Kernel y paquetes sin parchear unattended-upgrades activo sudo unattended-upgrade --dry-run -d
Puertos abiertos de más Denegar por defecto sudo ufw status verbose
API de Docker expuesta Nunca 2375/2376 públicos ss -lntp | grep 237
SSH con contraseña o fuerza bruta PasswordAuthentication no, fail2ban sudo sshd -T | grep -i password
Claves que nunca caducan Rotación anual documentada Auditar authorized_keys
El grupo docker = root Solo el usuario de despliegue getent group docker
Disco lleno de logs e imágenes Rotación + prune programado docker system df

Y una advertencia que no es técnica: acuerda por escrito quién mantiene esas máquinas. La pregunta «¿quién aplica los parches del kernel del host de producción?» debe tener un nombre por respuesta antes de que llegue el primer cliente. Si hay equipo de infraestructura o responsable de sistemas, la propiedad del host, la ventana de mantenimiento y el procedimiento de acceso se pactan con él; si no lo hay, el responsable eres tú, y conviene que esté escrito para que nadie lo descubra durante un incidente.

  1. ¿Hosts propios o gestionado? Criterios de decisión

Criterio Apunta a hosts propios Apunta a gestionado
Tamaño del equipo y guardias Hay alguien de sistemas y turnos 24×7 Solo desarrolladores, sin guardias
Coste a escala Tráfico alto, constante y previsible Tráfico irregular o bajo
Cumplimiento normativo Datos que deben estar en tu hardware Bastan los del proveedor
Necesidad de control Kernel, GPU, red o discos concretos La aplicación y poco más
Dependencia de proveedor Preocupa mucho Asumible
Ubicación Sitios sin servicio gestionado Regiones estándar
Tiempo hasta producción Hay semanas Se necesitan días

Para Aurora Libros en 2026 la respuesta razonable es mixta: base de datos gestionada —nadie quiere ser el responsable de la recuperación a un punto en el tiempo de PostgreSQL a las tres de la mañana— y aplicación en hosts propios o en Kubernetes gestionado según el tamaño. Que es justo la decisión que analiza la lección siguiente.

Errores Comunes y Consejos

  • Seguir un tutorial con docker-machine en 2026. Archivado desde 2021; si el tutorial lo usa, todo lo demás también está caducado.
  • Exponer 2375 «solo un momento para probar». Hay bots escaneando ese puerto de forma permanente. Un daemon abierto es root regalado, y el minado empieza en minutos.
  • Confundir contexto activo con máquina de trabajo. docker context use aurora-prod y olvidarlo es la receta del compose down en producción. Usa --context explícito siempre.
  • Construir imágenes en el host de producción. El contexto de build viaja por SSH, la caché no se reutiliza y el servidor gasta CPU en algo que le toca al pipeline. Publica y haz pull.
  • Scripts no idempotentes. Si ejecutarlo dos veces rompe algo, no es aprovisionamiento: es un accidente aplazado.
  • Guardar terraform.tfstate en Git o secretos en el user-data. Ambos son legibles: el primero por cualquiera con acceso al repositorio, el segundo desde el servicio de metadatos. Backend remoto con bloqueo y gestor de secretos.
  • Consejo: un contexto por entorno con descripción clara, y default siempre activo. La fricción de teclear --context es deliberada.
  • Consejo: empieza por cloud-init aunque solo tengas un host; el día que necesites el segundo, ya está resuelto. Y documenta en el README quién mantiene cada host, con qué ventana de parcheo y a quién se avisa: esa página vale más que cualquier script.

Ejercicios

Ejercicio 1 — Un contexto y una auditoría remota. Crea un contexto aurora-lab que apunte a un host por SSH (vale una VM local u otro portátil). Sin activarlo nunca como contexto por defecto, comprueba la versión del daemon remoto, lista sus contenedores, verifica que los puertos 2375 y 2376 no están a la escucha y muestra cuánto disco consume Docker. Escríbelo como auditar-host.sh, que debe devolver código 1 si detecta el daemon expuesto y 2 si no alcanza el host.

Ejercicio 2 — cloud-init para un nodo de Aurora Libros. Escribe un cloud-config.yaml que prepare un nodo con: usuario deploy con tu clave pública y en el grupo docker, Docker Engine desde el repositorio oficial, rotación de logs a 10 MB y 3 ficheros, cortafuegos que solo permita 22 y 443, SSH sin contraseña, actualizaciones desatendidas y /opt/aurora listo para el compose.yaml. Valida la sintaxis sin arrancar ninguna máquina.

Ejercicio 3 — Decisión razonada. Aurora Libros abre mercado en México y necesita una réplica de la plataforma. Datos: cuatro desarrolladores sin nadie de sistemas, sin guardias nocturnas, unas 40 peticiones por segundo con picos ×8 en campaña, copias de la base de datos de 30 días, y la dirección exige poder cambiar de proveedor «si suben los precios». Escribe una recomendación con la opción elegida, dos alternativas descartadas con su motivo y las tres preguntas que harías al responsable de infraestructura antes de ejecutar nada.

Soluciones

Solución 1.

docker context create aurora-lab --description "Laboratorio M7" \
  --docker "host=ssh://[email protected]"
#!/usr/bin/env bash
# auditar-host.sh — auditoría básica de un host Docker remoto
set -uo pipefail
CTX="${1:-aurora-lab}"; FALLO=0

docker --context "$CTX" version --format 'Servidor: {{.Server.Version}} ({{.Server.Arch}})' \
  || { echo "ERROR: no se alcanza el daemon"; exit 2; }
docker --context "$CTX" ps --format '{{.Names}}\t{{.Image}}\t{{.Status}}'
docker --context "$CTX" system df

HOST=$(docker context inspect "$CTX" --format '{{.Endpoints.docker.Host}}' | sed -E 's#ssh://##')
if ssh "$HOST" 'ss -lntp 2>/dev/null | grep -E ":(2375|2376)\b"'; then
  echo "CRÍTICO: la API de Docker escucha en TCP"; FALLO=1
else
  echo "OK: el daemon no escucha en 2375/2376"
fi
exit "$FALLO"

Claves: --context explícito en cada comando (nunca use); docker context inspect --format deduce el host sin duplicar configuración; y los códigos de salida distinguen «no llego» (2) de «llego y está mal configurado» (1), que es lo que permite meter el script en un cron o en un pipeline.

Solución 2.

Partiendo del fichero de §11, hay que añadir tres bloques y una línea:

#cloud-config
hostname: aurora-mx-01
# ... users, packages, write_files y runcmd de §11 ...

write_files:
  - path: /etc/ssh/sshd_config.d/99-aurora.conf     # ← SSH sin contraseña
    content: |
      PasswordAuthentication no
      PermitRootLogin no

runcmd:
  # ... instalación de Engine idéntica a §11 ...
  - usermod -aG docker deploy                       # tras instalar Engine
  - install -d -o deploy -g deploy -m 0750 /opt/aurora   # ← destino del compose
  - ufw default deny incoming && ufw allow 22/tcp && ufw allow 443/tcp && ufw --force enable
  - systemctl restart ssh
cloud-init schema --config-file cloud-config.yaml
# Valid schema cloud-config.yaml

Tres detalles marcan la diferencia: el usermod -aG docker deploy va en runcmd y después de instalar Engine, porque antes el grupo no existe; ufw deniega entrante por defecto y solo abre 22 y 443, nunca 2375/2376; y no hay un solo secreto en el fichero, porque el user-data es legible desde el servicio de metadatos por cualquiera que entre en la máquina.

Solución 3. Opción elegida: aplicación en Kubernetes gestionado (o Cloud Run) + PostgreSQL gestionado en la región de México.

Dato del enunciado Implicación
Cuatro desarrolladores, nadie de sistemas Nadie puede parchear hosts ni responder de noche
Sin guardias nocturnas Un host propio caído a las 3:00 no tiene dueño
40 req/s con picos ×8 Exige autoescalado real; el HPA de 06-06 ya está escrito
Copias de 30 días La recuperación a un punto en el tiempo gestionada evita un rol inexistente
«Poder cambiar de proveedor» Empuja a Kubernetes + imagen OCI, no a servicios propietarios

Descartadas: (a) hosts propios con Compose, que aguantaría 40 req/s pero obligaría a escalar a mano en el peor momento del año y dejaría el sistema operativo sin dueño; (b) Fargate o App Service, que resuelven la operación pero chocan con la exigencia de portabilidad, mientras que Kubernetes conserva los manifiestos del módulo 6 casi tal cual.

Preguntas previas: ¿quién es el propietario declarado de la plataforma mexicana y cuál es su ventana de mantenimiento?; ¿hay requisito legal de residencia de datos que impida replicar a Europa?; ¿cuál es el presupuesto mensual máximo y se prefiere pagar más por no contratar a alguien de sistemas? La tercera suele decidir de verdad, y no es una pregunta técnica.

Conclusión

Has cerrado el hueco que quedaba bajo la plataforma: las máquinas. Sabes qué fue Docker Machine, qué resolvía con sus drivers y su eval $(docker-machine env), y por qué está archivado desde 2021; si lo ves en un tutorial o en un script heredado, sabes leerlo y sabes cómo retirarlo.

Lo que sobrevivió lo tienes en las manos: los contextos. Creaste aurora-prod sobre SSH, comprobaste el daemon remoto sin abrir sesión, desplegaste con docker --context aurora-prod compose up -d y entendiste las tres trampas —volúmenes que se resuelven en el servidor, contextos de build que viajan por la red y puertos que se publican donde no miras—, con la regla que se deriva de ellas: en producción no se construye, se hace pull de una imagen firmada. Y tienes claro por qué ssh:// gana a tcp://, y por qué un 2375 abierto no es un descuido sino una máquina regalada.

Del lado moderno dominas el reparto de papeles: cloud-init para que el host nazca configurado, Terraform u OpenTofu para crearlo de forma declarativa y revisable en un pull request, Ansible para mantener la flota sin deriva y desplegar la pila con módulos idempotentes, Packer cuando el arranque debe durar segundos, y los servicios gestionados cuando la mejor forma de cuidar un host es no tenerlo. Con la tabla de criterios puedes defender la elección con argumentos, no con gustos. Y te llevas algo que no es un comando: el host es superficie de ataque —se parchea, se cortafuega, se audita— y alguien con nombre y apellidos tiene que ser su responsable antes de que llegue el primer cliente.

En la lección siguiente ponemos frente a frente las dos formas de desplegar que ya sabes usar: Docker Compose y Kubernetes. No como un duelo, sino como una decisión con criterios, con equivalencias exactas entre ambos formatos y con una respuesta honesta a la pregunta que casi nadie hace en voz alta: cuánto cuesta de verdad mantener un clúster.

Docker: De Principiante a Avanzado

Módulo 1: Introducción a Docker

Módulo 2: Trabajando con Imágenes Docker

Módulo 3: Contenedores Docker

Módulo 4: Docker Compose

Módulo 5: Conceptos Avanzados de Docker

Módulo 6: Docker en Producción

Módulo 7: Ecosistema y Herramientas de Docker

© Copyright 2026. Todos los derechos reservados