====== 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. ===== 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 ===== ''docker run'' crea y arranca un contenedor desde una imagen; si no está en local, la descarga antes. Sus opciones 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. ^ 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).