Notas sobre el curso Docker avanzado de 30 horas organizado por el Cluster TIC de Galicia.
A lo largo del curso el alumno aprenderá los principales retos a los que se enfrenta cuando implementa soluciones basadas en contenedores desde el punto de vista de la seguridad y la escalabilidad
Profesores:
El profe usa Visual Studio Code para escribir los Dockerfiles.
Los contenedores de Docker son una de las tecnologías más en auge en el mundo del desarrollo, ofreciendo métodos más sencillos, rápidos y robustos que los conocidos anteriormente para desarrollar, desplegar y distribuir software.
A lo largo del curso el alumno aprenderá por qué los contenedores son tan importantes y cómo hacer que sean parte de su proceso de desarrollo.
La formación trata desde las bases de Docker hasta la ejecución de docenas de contenedores sobre un sistema multi-host con networking y scheduling, además de revisar los pasos necesarios para desarrollar, testear y desplegar aplicaciones web con Docker.
Requisitos para seguir el curso:
Objetivos del curso:
El objetivo de los contenedores es “hacer despliegues”.
¿Qué es una aplicación web?
En la ingeniería de software se denomina aplicación web a aquellas herramientas que los usuarios pueden utilizar accediendo a un servidor web a través de internet o de una intranet mediante un navegador.
En otras palabras, es un programa que se codifica en un lenguaje interpretable por los navegadores web en la que se confía la ejecución al navegador.
Es un proceso de ingeniería:
La elaboración de una aplicación es un proceso de ingeniería dividido en fases. Cada fase involucra diferentes roles con necesidades de conocimiento dispares pero complementarios
Las fases pueden ejecutarse de forma secuencial una única vez (metodología en Cascada) o pueden ejecutarse N veces de forma repetitiva (metodología en espiral).
Múltiples tecnologías para el desarrollo:
Múltiples tecnologías para el despliegue:
Partiendo de una máquina física, en un sistema operativo se instala un hipervisor donde se crearán máquinas virtuales.
La escalabilidad vertical: ampliar capacidades de un servidor (más RAM, más disco…)
La escalabilidad horizontal: duplicar una instancia / elemento.
No es necesario añadir sistemas operativos virtualizados sino lo que necesite cada aplicación: bibliotecas / runtimes y el propio código de la aplicación.
Las imágenes base salen de los repositorios/registros. El oficial es el Docker Hub.
| Virtualización | Contenerización | |
| Tamaño | Elevado debido a que tiene que embeber el SO huésped | Reducido al no necesitar SO huésped |
| Reserva de recursos | Se hace previa al despliegue y es bloqueante | Es dinámica en función de las necesidades y la gestiona el engine |
| Tiempo de despliegue | Varios minutos | Unos pocos segundos |
| Portabilidad entre OS host | Permitida | NO se puede ejecutar contenedores / aplicaciones que no sean para ese mismo SO host |
| Complejidad | Relativamente sencillo de empezar | La complejidad crece a medida que se requieren despliegues más escalabels |
La migración en Docker debería hacerse de los volúmenes y no del docker en sí.
Podman, Buildah y skopeo es la alternativa que ofrece Red Hat a Docker containers. Docker y Podman respetan el estándar OCI. La gran diferencia es que en Podman no existe un demonio como Docker Engine.
En Windows y macOS los instaladores incluyen una plataforma de virtualización transparente para incorporar el núcleo Linux necesario para la ejecución del entorno de Docker: Docker Desktop.
En Linux tenemos instrucciones diferentes según la distro que escojamos:
Requisitos:
Para comprobar si la instalación ha sido correcta y tenemos funcionando Docker:
docker info
Ejemplo de salida:
Client: Docker Engine - Community
Version: 24.0.6
Context: default
Debug Mode: false
Plugins:
compose: Docker Compose (Docker Inc.)
Version: v2.21.0
Path: /usr/libexec/docker/cli-plugins/docker-compose
Server:
Containers: 31
Running: 22
Paused: 0
Stopped: 9
Images: 53
Server Version: 24.0.6
Storage Driver: overlay2
Backing Filesystem: extfs
Supports d_type: true
Using metacopy: false
Native Overlay Diff: true
userxattr: false
Logging Driver: json-file
Cgroup Driver: systemd
Cgroup Version: 2
Plugins:
Volume: local
Network: bridge host ipvlan macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
Swarm: inactive
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 61f9fd88f79f081d64d6fa3bb1a0dc71ec870532
runc version: v1.1.9-0-gccaecfc
init version: de40ad0
Security Options:
seccomp
Profile: builtin
cgroupns
Kernel Version: 6.1.0-0.deb11.11-amd64
Operating System: Debian GNU/Linux 11 (bullseye)
OSType: linux
Architecture: x86_64
CPUs: 8
Total Memory: 15.45GiB
Name: omv
ID: PW6N:HZDR:DKQF:HCWR:Z6GD:BSLC:K3TE:YEQF:6OAG:QWTR:FLHU:VJXE
Docker Root Dir: /srv/dev-disk-by-uuid-d8848615-8168-4fba-8cce-d868e42e9fdb/docker
Debug Mode: false
Experimental: false
Insecure Registries:
127.0.0.0/8
Live Restore Enabled: false
Docker Desktop limita el uso de CPU, RAM y disco, por defecto.
También podemos ver la versión instalada con:
docker version
Ejemplo de salida:
docker version Client: Docker Engine - Community Version: 24.0.6 API version: 1.43 Go version: go1.20.7 Git commit: ed223bc Built: Mon Sep 4 12:32:16 2023 OS/Arch: linux/amd64 Context: default Server: Docker Engine - Community Engine: Version: 24.0.6 API version: 1.43 (minimum version 1.12) Go version: go1.20.7 Git commit: 1a79695 Built: Mon Sep 4 12:32:16 2023 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.6.24 GitCommit: 61f9fd88f79f081d64d6fa3bb1a0dc71ec870523 runc: Version: 1.1.9 GitCommit: v1.1.9-0-gccaecfc docker-init: Version: 0.19.0 GitCommit: de40ad0
El comando docker es nuestra herramienta para interactuar con toda la arquitectura de Docker. La sintaxis es la siguiente:
docker [opción] [comando] [argumentos]
Si introducimos únicamente el comando docker en la terminal, nos responderá con la lista de todos los subcomandos de los que dispone:
docker
https://labs.play-with-docker.com/
El comando docker solo puede ser ejecutado por root o un usuario que pertenezca al grupo docker (se crea automáticamente durante la instalación).
Lo que vamos a administrar entra dentro de los siguientes grupos:
Todo empieza con un “hello, world”:
docker run hello-world
hello-world.
Podríamos ejecutar de nuevo docker run y se creará un nuevo contenedor, pero la imagen será la misma.
Normalmente, además de la imagen se le indica el nombre de la versión / etiqueta (tag). Por ejemplo, hello-world:latest. Cuando no ponemos la etiqueta, Docker coge latest.
Para que un contenedor se mantenga en ejecución debe existir un proceso que quede en primer plano (si ejecutásemos un programa en modo demonio, el contenedor también se cerraría).
Como las imágenes, los contenedores también tienen nombres / identificadores únicos.
Podemos darle un nombre al contenedor que queremos crear con la opción --name:
docker run --name mi-contenedor -it ubuntu /bin/bash
De esta manera, si volvemos a ejecutar el comando, no nos dejará porque no podemos tener dos contenedores con el mismo nombre. Recordemos que tanto el nombre como el ID identifica un contenedor.
Crear un contenedor en modo “background” (de fondo) y ejecutando un bucle infinito para que no se pare:
docker run --name mydaemon -d ubuntu /bin/sh -c "while true; do echo hello world; sleep 1; done"
Miramos que esté en ejecución el contenedor anterior:
docker ps
Podemos ver la salida del contenedor:
docker logs mydaemon
Para ver en tiempo real la salida:
docker logs -ft 10 mydaemon
Para ver la lista de procesos dentro del contendor (sin tener que entrar en él):
docker top mydaemon
Comando que da información extendida sobre un contenedor.
docker inspect mi-contenedor
Ejemplo de salida:
[
{
"Id": "08d1311242a61620b8dd03b951ef66f5dc63d82d35ba17fda9cdf80d3d9844d6",
"Created": "2023-10-09T18:26:20.92818159Z",
"Path": "/bin/sh",
"Args": [
"-c",
"while true; do echo hello world; sleep 1; done"
],
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 192214,
"ExitCode": 0,
"Error": "",
"StartedAt": "2023-10-09T18:26:21.354420029Z",
"FinishedAt": "0001-01-01T00:00:00Z"
},
"Image": "sha256:3565a89d9e81a4cb4cb2b0d947c7c11227a3f358dc216d19fc54bfd77cd5b542",
"ResolvConfPath": "/srv/dev-disk-by-uuid-d8848615-8168-4fba-8cce-d868e42e9fdb/docker/containers/08d1311242a61620b8dd03b951ef66f5dc63d82d35ba17fda9cdf80d3d9844d6/resolv.conf",
"HostnamePath": "/srv/dev-disk-by-uuid-d8848615-8168-4fba-8cce-d868e42e9fdb/docker/containers/08d1311242a61620b8dd03b951ef66f5dc63d82d35ba17fda9cdf80d3d9844d6/hostname",
"HostsPath": "/srv/dev-disk-by-uuid-d8848615-8168-4fba-8cce-d868e42e9fdb/docker/containers/08d1311242a61620b8dd03b951ef66f5dc63d82d35ba17fda9cdf80d3d9844d6/hosts",
"LogPath": "/srv/dev-disk-by-uuid-d8848615-8168-4fba-8cce-d868e42e9fdb/docker/containers/08d1311242a61620b8dd03b951ef66f5dc63d82d35ba17fda9cdf80d3d9844d6/08d1311242a61620b8dd03b951ef66f5dc63d82d35ba17fda9cdf80d3d9844d6-json.log",
"Name": "/mydaemon",
"RestartCount": 0,
"Driver": "overlay2",
"Platform": "linux",
"MountLabel": "",
"ProcessLabel": "",
"AppArmorProfile": "",
"ExecIDs": null,
"HostConfig": {
"Binds": null,
"ContainerIDFile": "",
"LogConfig": {
"Type": "json-file",
"Config": {}
},
"NetworkMode": "default",
"PortBindings": {},
"RestartPolicy": {
"Name": "no",
"MaximumRetryCount": 0
},
"AutoRemove": false,
"VolumeDriver": "",
"VolumesFrom": null,
"ConsoleSize": [
36,
188
],
"CapAdd": null,
"CapDrop": null,
"CgroupnsMode": "private",
"Dns": [],
"DnsOptions": [],
"DnsSearch": [],
"ExtraHosts": null,
"GroupAdd": null,
"IpcMode": "private",
"Cgroup": "",
"Links": null,
"OomScoreAdj": 0,
"PidMode": "",
"Privileged": false,
"PublishAllPorts": false,
"ReadonlyRootfs": false,
"SecurityOpt": null,
"UTSMode": "",
"UsernsMode": "",
"ShmSize": 67108864,
"Runtime": "runc",
"Isolation": "",
"CpuShares": 0,
"Memory": 0,
"NanoCpus": 0,
"CgroupParent": "",
"BlkioWeight": 0,
"BlkioWeightDevice": [],
"BlkioDeviceReadBps": [],
"BlkioDeviceWriteBps": [],
"BlkioDeviceReadIOps": [],
"BlkioDeviceWriteIOps": [],
"CpuPeriod": 0,
"CpuQuota": 0,
"CpuRealtimePeriod": 0,
"CpuRealtimeRuntime": 0,
"CpusetCpus": "",
"CpusetMems": "",
"Devices": [],
"DeviceCgroupRules": null,
"DeviceRequests": null,
"MemoryReservation": 0,
"MemorySwap": 0,
"MemorySwappiness": null,
"OomKillDisable": null,
"PidsLimit": null,
"Ulimits": null,
"CpuCount": 0,
"CpuPercent": 0,
"IOMaximumIOps": 0,
"IOMaximumBandwidth": 0,
"MaskedPaths": [
"/proc/asound",
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware"
],
"ReadonlyPaths": [
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
]
},
"GraphDriver": {
"Data": {
"LowerDir": "/srv/dev-disk-by-uuid-d8848615-8168-4fba-cc8e-d868e42e9fdb/docker/overlay2/befdb5ff531519676951b62dcbaa449dc9a2fce322b05ba049f13475bc8132cf-init/diff:/srv/dev-disk-by-uuid-d8848615-8168-4fba-8cce-d868e42e9fdb/docker/overlay2/37e0fab9210ae886a0eff914fa60be829e336c6f4b201dd5141d6839f5898d79/diff",
"MergedDir": "/srv/dev-disk-by-uuid-d8848615-8168-4fba-cc8e-d868e42e9fdb/docker/overlay2/befdb5ff531519676951b62dcbaa449dc9a2fce322b05ba049f13475bc8132cf/merged",
"UpperDir": "/srv/dev-disk-by-uuid-d8848615-8168-4fba-cc8e-d868e42e9fdb/docker/overlay2/befdb5ff531519676951b62dcbaa449dc9a2fce322b05ba049f13475bc8132cf/diff",
"WorkDir": "/srv/dev-disk-by-uuid-d8848615-8168-4fba-cc8e-d868e42e9fdb/docker/overlay2/befdb5ff531519676951b62dcbaa449dc9a2fce322b05ba049f13475bc8132cf/work"
},
"Name": "overlay2"
},
"Mounts": [],
"Config": {
"Hostname": "08d1311242a6",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"while true; do echo hello world; sleep 1; done"
],
"Image": "ubuntu",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {
"org.opencontainers.image.ref.name": "ubuntu",
"org.opencontainers.image.version": "22.04"
}
},
"NetworkSettings": {
"Bridge": "",
"SandboxID": "4a5cbec72d8ed785cab1167730d4872c9c5bf627b435566dee54548ea74cc8e9",
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"Ports": {},
"SandboxKey": "/var/run/docker/netns/4a5cbec72d8e",
"SecondaryIPAddresses": null,
"SecondaryIPv6Addresses": null,
"EndpointID": "5cc996b631769af9085216ccc8ec1009c676e5033b7126612d72317482aa313c",
"Gateway": "172.17.0.1",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"MacAddress": "02:42:ac:11:00:02",
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "3030bfecb10961fecc8ea4930975a55e4c4a7f5bd6e36bbf6682c5b1c8896696",
"EndpointID": "5cc996b631769af90cc8e6c161ac1009c676e5033b7126612d72317482aa313c",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:11:00:02",
"DriverOpts": null
}
}
}
}
]
Para coger una parte de esa salida, por ejemplo, la dirección IP del contenedor:
docker inspect -f='{{.NetworkSettings.IPAddress}}' mydaemon
La sintaxis es JSONPath.
Busybox es una imagen Docker mínima que nos permite trastear de forma rápida.
Para ejecutar un único comando dentro de una imagen podemos usar lo siguiente:
docker run busybox echo hola mundo
Lo que escribamos a continuación del nombre de la imagen, se lanzaría en una terminal del contenedor en ejecución.
docker run siempre crea un contenedor cada vez que lo ejecutamos
Si lo que queremos es abrir una terminal para trastear más en detalle:
docker run -it busybox sh
-i: modo interactivo.-t: reserva un TTY.Podemos crear contenedores de diferentes imágenes para:
Contenedores activos:
docker ps
Para ver todos los contenedores (tanto activos como no):
docker ps -a
Si queremos mostrar solo el identificador, usamos la opción -q:
docker ps -aq
Mientras exista un contenedor, los cambios que hagamos en él se mantendrán.
Si está parado:
docker start <ID_CONTENEDOR>|<NOMBRE_CONTENEDOR>
La versión larga del comando de arriba: docker container start…
Para saber el identificador del contenedor:
docker ps -a
A diferencia de las máquinas virtuales, los contenedores están pensados para mantenerlos en ejecución mientras se necesiten y eliminarlos cuando no sean necesarios. Veremos cómo gestionar la persistencia de los datos con los volúmenes (una carpeta en algún sistema de ficheros).
Parar un contenedor:
docker stop <ID_CONTENEDOR>|<NOMBRE_CONTENEDOR>
Para entrar en un contenedor existente usamos docker exec:
docker exec -it <ID_CONTENEDOR> sh
docker rm <ID_CONTENEDOR>|<NOMBRE_CONTENEDOR>
Eliminar todos los contenedores:
docker rm $(docker ps -aq)
Docker avisará que no puede eliminar los contenedores que estén en ejecución
Para ver las modificaciones de un contenedores a partir de su imagen base:
docker diff <ID_CONTENEDOR>|<NOMBRE_CONTENEDOR>
Esto nos puede servir para saber qué perderíamos si eliminamos el contenedor.
docker run ubuntu /bin/bash
A diferencia de docker exec, docker attach nos permite conectar a un contenedor en ejecución e interactura con el proceso del contenedor que tenga PID 1.
Con docker exec crearmos un nuevo proceso en el contenedor.
docker run -it ubuntu /bin/bashhostnamecat /etc/hostsifconfig (hay que instalar el paquete net-tools)vim:apt-get updateapt-get install vimexit y repite el paso 1. Tenemos el comando vim?
Al volver a ejecutar docker run -it ubuntu /bin/bash se crea un nuevo contenedor por lo que no tendremos ninguno de los cambios que hicimos en el contenedor previo.
Docker monta un puente para poder acceder a la IP de los contenedores desde nuestra máquina anfitriona.
Por defecto, los contenedores que levantemos estarán en la red 172.17.0.x.
Obtener información de las imágenes descargadas:
docker images ls