====== 02 - Docker ======
Los proyectos no viven solo del código: alrededor hay una base de datos, un almacenamiento de ficheros, un proxy... Cada pieza es un servidor ajeno, con su instalación y su configuración. **Docker** permite empaquetar cada pieza con todo lo que necesita y ejecutarla aislada y reproducible, en cualquier máquina y sin instalar nada más.
===== 1. Entornos que no coinciden =====
Instalar y configurar cada servidor a mano tiene dos problemas: el trabajo no se traslada (lo que funciona en una máquina no va igual en otra: versiones y configuraciones distintas) y las piezas comparten el sistema, así que una actualización toca a todas. **Docker** resuelve ambos: cada servidor se empaqueta en una **imagen** con su versión exacta y corre en un **contenedor** aislado del resto.
===== 2. Imagen, contenedor y registro =====
^ Concepto ^ Qué es ^
| **Imagen** | Plantilla inmutable con el sistema de ficheros y el comando de arranque; tiene una versión (*tag*) |
| **Contenedor** | Proceso aislado en ejecución, creado a partir de una imagen |
| **Registro** | Repositorio de imágenes; **Docker Hub** es el público más usado |
La imagen es la plantilla; el contenedor, la instancia. De una imagen salen tantos contenedores como se quieran.
^ ^ Máquina virtual ^ Contenedor ^
| Qué incluye | SO invitado completo + aplicación | Solo la aplicación y sus dependencias |
| Arranque | Minutos (arranca un SO) | Milisegundos (es un proceso) |
| Coste | Gigabytes por VM | Megabytes; comparte el kernel del host |
Ambos aíslan; el contenedor comparte el **kernel** del host y por eso es tan ligero. Para servidores de aplicación es el enfoque dominante.
Los nombres de imagen siguen el patrón **usuario/imagen:tag**: ''usuario'' identifica al publicador y es opcional —si falta, la imagen es **oficial**—; ''imagen'' es obligatorio; ''tag'' señala la versión y también es opcional. Sin ''tag'' se descarga ''latest'', que no siempre es la más nueva: es la versión que los creadores etiquetaron como tal. Fijar la versión (''mariadb:11'', no ''mariadb'') es lo reproducible.
Las piezas del tema y su sitio, antes de entrar en el detalle:
@startuml
!theme cerulean
skinparam backgroundColor transparent
skinparam shadowing false
skinparam ArrowThickness 1
skinparam classAttributeIconSize 0
skinparam class {
BackgroundColor #FEFEFE
BorderColor #344883
ArrowColor #344883
FontColor #343A40
}
skinparam classAttributeFontColor #343A40
skinparam stereotypeCBackgroundColor #344883
skinparam stereotypeCBorderColor #273768
skinparam package {
BackgroundColor transparent
BorderColor #344883
FontColor #343A40
}
skinparam ArrowColor #344883
skinparam participant {
BackgroundColor #FEFEFE
BorderColor #344883
FontColor #343A40
}
skinparam actor {
BackgroundColor #FEFEFE
BorderColor #344883
FontColor #343A40
}
skinparam sequence {
ArrowColor #344883
LifeLineBorderColor #94A3B8
GroupBorderColor #344883
GroupBackgroundColor #E9EDF7
}
skinparam note {
BackgroundColor #FEF9C3
BorderColor #B6C2DE
FontColor #343A40
}
skinparam activity {
BackgroundColor #FEFEFE
BorderColor #344883
FontColor #343A40
StartColor #344883
EndColor #344883
DiamondBackgroundColor #FEFEFE
DiamondBorderColor #344883
DiamondFontColor #343A40
}
skinparam component {
BackgroundColor #FEFEFE
BorderColor #344883
FontColor #343A40
}
skinparam database {
BackgroundColor #FEFEFE
BorderColor #344883
FontColor #343A40
}
skinparam node {
BackgroundColor #FEFEFE
BorderColor #344883
FontColor #343A40
}
skinparam cloud {
BackgroundColor transparent
BorderColor #344883
FontColor #344883
}
skinparam rectangle {
BackgroundColor #FEFEFE
BorderColor #344883
FontColor #343A40
}
skinparam state {
BackgroundColor #FEFEFE
BorderColor #344883
FontColor #343A40
}
skinparam usecase {
BackgroundColor #FEFEFE
BorderColor #344883
FontColor #343A40
}
cloud "Registro\n(Docker Hub)" as Registro
node "Máquina (host)" as Host {
artifact "Imagen\nnginx:alpine" as Img
node "Contenedor\nweb" as Web
node "Contenedor\ndb" as Db
database "Volumen\ndb-data" as Vol
}
Registro --> Img : docker pull
Img --> Web : docker run
Host --> Web : -p 8080:80
Db --> Vol : -v db-data:/var/lib/mysql
Web <--> Db : red interna
@enduml
===== 3. Imágenes =====
**Docker Hub** es el registro público: en la web se busca y se distinguen las imágenes oficiales. En una terminal sin entorno gráfico, lo mismo lo hace ''docker search''.
^ Comando ^ Qué hace ^
| ''docker search '' | busca imágenes con ese nombre en el registro |
| ''docker pull '' | descarga la imagen |
| ''docker images'' | lista las imágenes del sistema local |
| ''docker rmi '' | elimina una imagen local, por ''REPOSITORY:TAG'' o ''IMAGE ID'' |
| ''docker tag'' | crea un alias de una imagen existente |
| ''docker save'' / ''docker load'' | exporta o importa imágenes en un fichero ''.tar'', sin registro |
$ docker search mariadb
$ docker pull mariadb:11
$ docker images
$ docker rmi mariadb:11
* La salida de ''docker images'' muestra ''REPOSITORY'', ''TAG'', ''IMAGE ID'', fecha y tamaño.
* Los comandos tienen variantes largas equivalentes (''docker image pull'', ''docker image rm''...); las cortas son de uso habitual.
* Una imagen en uso por algún contenedor no se deja eliminar: primero hay que eliminar el contenedor.
===== 4. Contenedores =====
Los verbos de Docker se confunden con facilidad; el ciclo de vida los ordena:
^ Acción ^ Comando ^ Qué pasa ^
| Descargar la imagen | ''docker pull'' | Del registro al disco local; no crea contenedores |
| Crear y arrancar | ''docker run'' | Contenedor nuevo; si la imagen no está en local, antes la descarga |
| Parar | ''docker stop'' | Detiene el proceso; el contenedor sigue creado, con todo lo que guardó |
| Arrancar el mismo | ''docker start'' | Reanuda el contenedor ya creado: mismo nombre y mismos datos |
| Eliminar | ''docker rm'' | Lo destruye, junto a su contenido |
La clave: ''run'' es ''create'' + ''start'' con un contenedor **nuevo**; para reanudar uno que ya existe, ''start'' (repetir ''run'' crearía otro más). Los parados aparecen con ''docker ps -a''.
Las opciones de ''run'' completan la configuración del contenedor en el momento de crearlo:
^ Opción ^ Efecto ^
| ''-d'' | en segundo plano (*detached*), sin ocupar la terminal |
| ''-p host:contenedor'' | publica el puerto del contenedor en el host |
| ''--name'' | nombre para referirse al contenedor sin depender del ID |
| ''-e VAR=valor'' | variable de entorno: así se configura el servidor (contraseñas, rutas...) |
| ''-v volumen:ruta'' | guarda datos fuera del contenedor (sección 5) |
| ''--restart always'' | lo vuelve a arrancar si el contenedor o la máquina se reinician |
$ docker run -d -p 8080:80 nginx # un servidor web en el 8080 del host
$ docker run -d --name db \
-e MARIADB_ROOT_PASSWORD=secreto \
-p 3306:3306 mariadb:11
El contenedor se para y se arranca sin perderse; se elimina y ya no hay nada:
$ docker ps # contenedores en ejecución (con -a, también los parados)
$ docker stop db # lo detiene
$ docker start db # lo vuelve a arrancar: el mismo contenedor, con lo que tiene dentro
$ docker logs -f db # sigue la salida del contenedor
$ docker rm db # lo elimina (con -f también si está en marcha)
* Solo ''rm'' destruye el contenedor y con él lo que guardó dentro; los datos valiosos van en volúmenes (sección 5).
''docker exec'' ejecuta comandos en un contenedor en marcha, sin detenerlo:
$ docker exec -it db bash # una terminal dentro del contenedor (para salir, exit)
$ docker exec db ls /app # un comando suelto, sin terminal
* ''-it'' asigna un terminal interactivo: es lo que permite abrir una shell.
* El contenedor debe estar en ejecución; si no, no se puede hacer ''exec''.
===== 5. Puertos y volúmenes =====
**Puertos:** ''-p 9000:9000'' conecta el puerto del host con el del contenedor. Sin él, el servicio no es accesible desde fuera; entre contenedores no hace falta: se ven por la red interna (tema 08).
**Volúmenes:** el sistema de ficheros del contenedor es **efímero**: al eliminar el contenedor se pierde lo que guardó dentro. Un **volumen** vive fuera del contenedor y persiste:
$ docker run -d --name db -v db-data:/var/lib/mysql mariadb:11
Los datos de MariaDB quedan en el volumen ''db-data'' y sobreviven al contenedor.
^ Forma ^ Sintaxis ^ Cuándo usarla ^
| Volumen nombrado | ''-v db-data:/var/lib/mysql'' | datos que deben sobrevivir al contenedor; los gestiona Docker y no los liga a una ruta del host |
| Bind mount | ''-v ./html:/usr/share/nginx/html'' | trabajar con una carpeta concreta de la máquina: el host es dueño de los ficheros |
$ docker volume create db-data # crear el volumen por adelantado
$ docker volume inspect db-data # sus datos: el Mountpoint es la carpeta real del host
* Los **nombrados** son la forma normal de persistir datos de servidores (bases de datos, almacenamiento).
* Los **bind mounts** sirven para compartir una carpeta del host con el contenedor: configuración, código, contenido estático.
===== 6. Dockerfile =====
Los servidores oficiales ya vienen empaquetados; la **aplicación** propia se define en un **Dockerfile**: un fichero de texto que describe cómo se construye una imagen —de qué parte, qué añade y con qué comando arranca.
^ Instrucción ^ Qué hace ^
| ''FROM'' | imagen base de la que se parte; toda imagen empieza aquí |
| ''RUN'' | ejecuta un comando durante la construcción (instalar paquetes...) |
| ''COPY'' / ''ADD'' | copia ficheros del host a la imagen (''ADD'' admite URLs y descomprime) |
| ''WORKDIR'' | directorio de trabajo para las siguientes instrucciones |
| ''ENV'' | variables de entorno fijas de la imagen |
| ''EXPOSE'' | documenta el puerto que usa la aplicación; no lo publica (eso lo hace ''-p'') |
| ''CMD'' | comando por defecto al arrancar el contenedor; se puede sobreescribir |
| ''ENTRYPOINT'' | el programa principal, fijo; lo que se añade en ''docker run'' son sus argumentos |
FROM python:3
WORKDIR /app
COPY app.py .
CMD ["python", "app.py"]
$ docker build -t miapp:1.0 . # construye la imagen desde este directorio
$ docker run miapp:1.0
* La imagen base oficial de **python** ya trae el intérprete; el Dockerfile solo añade el código y el arranque.
* El fichero se llama exactamente ''Dockerfile'', sin extensión: Docker lo busca con ese nombre.
* ''-t'' fija nombre y *tag* de la imagen (''miapp:1.0''); sin ''tag'' tomaría ''latest''.
* La imagen construida corre igual en cualquier máquina. Si la aplicación espere peticiones, publica su puerto como cualquier contenedor (''-p'').
La forma no cambia aunque crezca lo que hay que instalar: el Dockerfile es la lista de pasos —base, añadidos, arranque— que sustituye a toda la instalación manual del servidor.
===== 7. Redes =====
Cada contenedor nace aislado en su propia red. Para que dos contenedores se vean, comparten una **red**: un conmutador virtual —de tipo **bridge** por defecto— donde se alcanzan por el **nombre del contenedor**, como un hostname.
$ docker network create mi-red
$ docker run -d --name db --network mi-red mariadb:11
$ docker run -d --name app --network mi-red alpine
$ docker exec app ping db # db responde a su nombre: la red los une
^ Comando ^ Qué hace ^
| ''network create'' | crea la red (bridge por defecto) |
| ''network connect'' / ''disconnect'' | añade o retira un contenedor en marcha a/de la red |
| ''network ls'' | lista las redes |
| ''network inspect'' | detalle de la red: driver, contenedores conectados y sus IP |
| ''network rm'' | elimina la red, sin contenedores conectados |
* Otros *drivers* existen —''host'' comparte la pila de red de la máquina, ''none'' aísla del todo, ''overlay'' conecta varias máquinas—; para el uso habitual basta el ''bridge'' de siempre.
* La clave del módulo está aquí: entre contenedores no son necesarias las direcciones ni los puertos publicados; los nombres bastan. Lo retoma la **arquitectura** del tema 08.
===== 8. docker compose =====
Con varios contenedores, los comandos manuales no escalan: hay que recordar orden, puertos y variables de cada uno. **Docker Compose** describe el sistema completo —servicios, redes y volúmenes— en un fichero **compose.yaml** y lo levanta con un solo comando:
services:
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html
db:
image: mariadb:11
environment:
MARIADB_ROOT_PASSWORD: secreto
MARIADB_DATABASE: miapp
volumes:
- db-data:/var/lib/mysql
volumes:
db-data:
* Cada entrada de ''services'' es un **servicio**: la receta de su contenedor —imagen, puertos, entorno, volúmenes—.
* Los volúmenes **nombrados** se declaran al final, una vez, y los servicios los comparten.
* El *bind mount* de la sección 5 se usa igual; en compose.yaml, una ruta que empieza por ''. '' es relativa al propio fichero.
* Compose crea una **red propia del proyecto** entre los servicios: ''web'' alcanza a ''db'' por su nombre, con la red de la sección 7 hecha automáticamente.
* Los servicios de aquí usan ''image'' (descargar del registro); la aplicación propia lleva en su lugar ''build: ./app'': construir la imagen con su **Dockerfile** (sección 6) en vez de descargarla del registro.
* ''depends_on'' declara dependencias entre servicios: los que se nombran se arrancan antes — la aplicación, después de la base de datos.
^ Comando ^ Qué hace ^
| ''docker compose up -d'' | crea, si faltan, y arranca todos los servicios, en segundo plano |
| ''docker compose start'' / ''stop'' | arranca o para los contenedores existentes, sin eliminarlos |
| ''docker compose ps'' | estado de los servicios |
| ''docker compose logs -f'' | la salida de todos, ordenada por servicio |
| ''docker compose down'' | para y elimina contenedores y red; con ''-v'', también los volúmenes |
''docker compose down'' elimina contenedores pero **conserva los volúmenes nombrados**: los datos sobreviven. Es el modo normal de reiniciar el sistema; ''-v'' solo si se quieren borrar de verdad.
Con un solo servicio Compose parece excesivo; su valor aparece al describir sistemas con varios servidores: es el caso del tema de **arquitectura** (tema 08).