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
- El problema real del aprovisionamiento
- Qué fue Docker Machine y su modelo de drivers
- Los comandos característicos
- Por qué está archivado y qué hacer si lo encuentras
- Lo que sí sobrevivió: los contextos de Docker
- Un contexto
aurora-prodsobre SSH - Desplegar contra un host remoto sin cambiar de terminal
- SSH frente a exponer el daemon por TCP
- El mapa de alternativas actuales
- Instalación directa: el script idempotente
- cloud-init: que la máquina nazca con Docker
- Terraform / OpenTofu: infraestructura declarativa
- Ansible: configurar y mantener la flota
- Packer y los servicios gestionados
- El host como superficie de ataque
- ¿Hosts propios o gestionado? Criterios de decisión
- 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.
- 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.
- 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 proveedorEl 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.
- 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.
- 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.
- Un contexto
aurora-prod sobre SSH
aurora-prod sobre SSHAurora 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 yesEl 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.
- 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 apiTres 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.
- 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
- 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.
- 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 versionVentaja: 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.
- 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.
- 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 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.
- 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-02El 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.
- 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.
- 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.
- ¿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-machineen 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-prody olvidarlo es la receta delcompose downen producción. Usa--contextexplí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.tfstateen Git o secretos en eluser-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
defaultsiempre activo. La fricción de teclear--contextes 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
READMEquié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 sshTres 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
- ¿Qué es Docker?
- Instalando Docker
- Arquitectura de Docker
- Comandos Básicos de Docker
- Entendiendo las Imágenes de Docker
- Creando tu Primer Contenedor Docker
- El Proyecto del Curso: la Plataforma Aurora Libros
Módulo 2: Trabajando con Imágenes Docker
- Docker Hub y Repositorios
- Construyendo Imágenes Docker
- Conceptos Básicos de Dockerfile
- Instrucciones Avanzadas del Dockerfile
- Gestionando Imágenes Docker
- Etiquetado y Publicación de Imágenes
Módulo 3: Contenedores Docker
- Ejecutando Contenedores
- Ciclo de Vida del Contenedor
- Gestionando Contenedores
- Inspección y Depuración de Contenedores
- Redes en Docker
- Persistencia de Datos con Volúmenes
- Límites de Recursos y Políticas de Reinicio
Módulo 4: Docker Compose
- Introducción a Docker Compose
- Definiendo Servicios en Docker Compose
- Comandos de Docker Compose
- Aplicaciones Multi-Contenedor
- Variables de Entorno en Docker Compose
- Perfiles, Overrides y Múltiples Entornos
- Desarrollo Local con Docker Compose
Módulo 5: Conceptos Avanzados de Docker
- Profundización en Redes Docker
- Opciones de Almacenamiento Docker
- Mejores Prácticas de Seguridad en Docker
- Optimizando Imágenes Docker
- Builds Avanzadas con BuildKit y Buildx
- Registro y Monitoreo en Docker
- El Runtime por Dentro: Namespaces, Cgroups y Capas
Módulo 6: Docker en Producción
- Preparar una Imagen para Producción
- CI/CD con Docker
- Orquestando Contenedores con Docker Swarm
- Introducción a Kubernetes
- Desplegando Contenedores Docker en Kubernetes
- Escalado y Balanceo de Carga
- Estrategias de Despliegue y Rollback
