Docker
Los comandos con los que se opera un contenedor de principio a fin: mirar qué hay, arrancarlo y pararlo, entrar dentro, publicarle puertos, darle almacenamiento persistente y construir su imagen con un Dockerfile.
Ver qué hay: info, version, ps e images
Lo primero al sentarse delante de un anfitrión ajeno es enterarse de qué contiene. Hay cuatro comandos de consulta que se preguntan y que conviene distinguir bien, porque dos miran contenedores y dos miran el motor.
| Comando | Qué muestra |
|---|---|
| docker info | El estado global del motor: cuántos contenedores hay y en qué estado, cuántas imágenes, el registro configurado y el directorio de datos. |
| docker version | La versión del cliente y la del demonio. |
| docker ps | Los contenedores en ejecución. Con -a, también los parados. |
| docker images | Las imágenes descargadas, con su repositorio, etiqueta, identificador, antigüedad y tamaño. |
# Estado del motor: incluye el registro por defecto y /var/lib/docker
docker info
# Contenedores en marcha
docker ps
# Todos los contenedores, tambien los parados
docker ps -a
# Imagenes disponibles en local
docker images
docker image ls # forma moderna y equivalente
# Ultimas lineas de la salida de un contenedor, siguiendo en vivo
docker logs -f api-temarioDocker mantiene dos formas de escribir casi todo. La antigua es plana (docker images, docker ps), y la moderna agrupa por objeto (docker image ls, docker container ls). Las dos siguen funcionando y significan lo mismo, así que en un examen pueden aparecer indistintamente.
docker ps sin más solo enseña lo que está corriendo. Un contenedor que se ha caído no aparece, y por eso la primera reacción ante un contenedor «desaparecido» es repetir el comando con -a.
Para el examen
docker ps: solo los contenedores EN MARCHA
docker ps -a: también los parados
docker images: las imágenes descargadas
El ciclo de vida de un contenedor
Un contenedor pasa por estados y hay un comando para cada transición. La confusión clásica está entre create y run: create prepara el contenedor a partir de una imagen pero lo deja parado, mientras que run hace las dos cosas, lo crea y lo arranca, y devuelve su identificador.
| Comando | Qué hace |
|---|---|
| docker pull imagen | Descarga una imagen del registro sin ejecutar nada. |
| docker create imagen | Crea el contenedor a partir de la imagen, pero no lo arranca. |
| docker run imagen | Crea y arranca. Devuelve el identificador del contenedor. |
| docker start id | Arranca un contenedor que estaba parado. |
| docker stop id | Lo detiene ordenadamente, dándole tiempo a cerrar. |
| docker kill id | Lo detiene de forma inmediata, sin margen para cerrar. |
| docker rm id | Borra el contenedor. Tiene que estar parado. |
| docker rmi imagen | Borra una imagen local. No puede haber contenedores que la usen. |
# Arrancar en segundo plano, con nombre propio
docker run -d --name api-temario registro.llegandoalcorte.com/api-temario:2.4.1
# Arrancar en modo interactivo con una consola dentro
# OJO: las opciones van SIEMPRE antes del nombre de la imagen
docker run -it debian:12 /bin/bash
# Parar, volver a arrancar y matar
docker stop api-temario
docker start api-temario
docker kill api-temario
# Borrar el contenedor y despues su imagen
docker rm api-temario
docker rmi registro.llegandoalcorte.com/api-temario:2.4.1| Opción de run | Para qué |
|---|---|
| -d | Arranque en segundo plano (detached). El contenedor sigue vivo tras devolver el control. |
| -i | Mantiene abierta la entrada estándar. |
| -t | Asigna un terminal. Casi siempre se combinan como -it. |
| --name | Le da un nombre legible, para no manejar identificadores. |
| --rm | Borra el contenedor automáticamente en cuanto termine. |
| -e | Define una variable de entorno dentro del contenedor. |
stop pide al proceso que se cierre y espera; kill lo remata en el acto. En un servicio con datos abiertos, kill puede dejar el trabajo a medias, así que no es un stop más rápido: es un stop más brusco.
Para el examen
run: equivale a pull más create más start
stop y kill: parada con margen y parada forzada
rm y rmi: borra el contenedor y borra la imagen
Entrar y operar dentro: exec y attach
Para diagnosticar un problema hace falta muchas veces mirar desde dentro del contenedor. Hay dos comandos que parecen lo mismo y no lo son, y la diferencia se pregunta.
| Comando | Qué hace | Riesgo |
|---|---|---|
| docker exec | Lanza un proceso NUEVO dentro de un contenedor que ya está en marcha, por ejemplo una consola. | Ninguno: al salir, el contenedor sigue igual. |
| docker attach | Conecta la entrada, la salida y el error del terminal al proceso principal que ya se está ejecutando. | Alto: lo que se teclee llega al proceso principal, y una interrupción puede detener el contenedor. |
# Abrir una consola dentro de un contenedor en marcha (lo habitual)
docker exec -it api-temario bash
# Ejecutar una sola orden sin abrir consola
docker exec api-temario cat /etc/hosts
# Conectarse al proceso principal del contenedor
docker attach api-temario
# Guardar un contenedor entero en un tar, como copia
docker export api-temario > api-temario-copia.tarEn la práctica, casi todo se hace con exec, porque abre una consola independiente que se puede cerrar sin tocar el servicio. Attach se reserva para ver en directo lo que el proceso principal escribe por pantalla, y hoy para eso suele bastar con docker logs.
exec crea un proceso nuevo; attach se engancha al que ya había. Salir de un exec no afecta al contenedor; salir mal de un attach puede pararlo.
Para el examen
exec: lanza un proceso NUEVO dentro
attach: se engancha al proceso principal
Riesgo de attach: salir de él puede parar el contenedor
Publicar puertos y conectar redes
Un contenedor tiene su propia pila de red y sus propios puertos, que por defecto nadie ve desde fuera. Para que un servicio de dentro sea accesible desde el anfitrión hay que publicar el puerto, es decir, decirle a Docker que redirija un puerto del anfitrión hacia un puerto del contenedor. Se hace con la opción -p y el formato es PUERTO_ANFITRION:PUERTO_CONTENEDOR: el primero es el real, el que se marca desde fuera.
# El 8080 del anfitrion lleva al 3000 del contenedor
docker run -d -p 8080:3000 --name web-temario web-temario:2.4.1
# Varios puertos a la vez
docker run -d -p 8080:3000 -p 9090:9090 --name web-temario web-temario:2.4.1
# Publicar solo en el bucle local del anfitrion, no en toda la red
docker run -d -p 127.0.0.1:8080:3000 --name web-temario web-temario:2.4.1Por debajo, esa redirección es una traducción de puertos, la misma idea del PAT de los encaminadores: se cambia el puerto de destino de los paquetes que entran para llevarlos al contenedor correcto. Conviene no confundir esto con la instrucción EXPOSE de un Dockerfile, que es solo documentación: declara qué puerto usa la aplicación, pero no abre nada por sí sola.
Para que dos contenedores hablen entre sí no hace falta publicar nada: basta con ponerlos en la misma red de Docker. Al crear una red propia, Docker les da resolución de nombres entre ellos, de modo que un contenedor puede llamar a otro por su nombre en lugar de por una dirección IP que cambiará en el siguiente arranque.
| Tipo de red | Comportamiento |
|---|---|
| bridge | La opción por defecto. Red privada virtual del anfitrión, con traducción de direcciones hacia fuera. |
| host | El contenedor usa directamente la pila de red del anfitrión. No hay aislamiento ni hace falta publicar puertos. |
| none | Sin red. El contenedor queda incomunicado. |
# Crear una red propia y conectar dos contenedores a ella
docker network create red-temario
docker run -d --network red-temario --name bd-temario postgres:16
docker run -d --network red-temario --name api-temario -p 8080:3000 api-temario:2.4.1
# Desde api-temario, la base de datos se alcanza por su nombre
docker exec api-temario ping -c 1 bd-temarioEn -p el primer número es el del anfitrión y el segundo el del contenedor. Invertirlos es el error más frecuente y da un servicio que parece arrancado pero al que no responde nadie.
Para el examen
Orden en -p: anfitrión:contenedor, de fuera hacia dentro
Sin publicar el puerto: el servicio no se ve desde fuera del anfitrión
Volúmenes: los datos que sobreviven al contenedor
Un contenedor es desechable por diseño: se borra y se vuelve a crear sin pensarlo. Eso choca de frente con cualquier servicio que guarde datos, porque todo lo que escriba en su propia capa desaparece con él. La solución es el volumen: un almacenamiento que vive fuera del contenedor y se le monta dentro en una ruta concreta.
| Forma | Qué es | Cuándo se usa |
|---|---|---|
| Volumen gestionado | Un espacio que administra el propio Docker bajo su directorio de datos, con nombre propio. | Datos de producción: bases de datos, ficheros subidos por los usuarios. |
| Bind mount | Un directorio concreto del anfitrión montado dentro del contenedor. | Desarrollo y configuración: código fuente en caliente, ficheros de configuración del anfitrión. |
| tmpfs | Un espacio en memoria que no llega a tocar el disco. | Datos temporales o sensibles que no deben persistir. |
# Volumen gestionado: Docker se encarga de donde vive
docker volume create datos-temario
docker run -d --name bd-temario -v datos-temario:/var/lib/postgresql/data postgres:16
# Bind mount: se indica una ruta del anfitrion
# Formato RUTA_ANFITRION:RUTA_CONTENEDOR
docker run -d --name web-temario -v /srv/config/web.conf:/etc/web/web.conf:ro web-temario:2.4.1
# Ver y limpiar volumenes
docker volume ls
docker volume rm datos-temarioEl formato de la opción -v es el mismo que el de los puertos: primero el lado del anfitrión y después el del contenedor. El sufijo ro monta el volumen en solo lectura, lo que es buena práctica para todo lo que sea configuración. Borrar un contenedor no borra sus volúmenes: hay que hacerlo aparte, y eso es una red de seguridad, no un descuido del diseño.
Regla que resume la administración de contenedores con estado: el contenedor es sustituible, el volumen no. Cuando se actualiza la versión de una base de datos se destruye el contenedor y se crea otro apuntando al mismo volumen.
Para el examen
Sin volumen: los datos se van con el contenedor
Volumen: lo gestiona Docker
Bind mount: monta una ruta concreta del anfitrión
Construir imágenes: docker build y el Dockerfile
El Dockerfile es el fichero de texto donde se escribe, paso a paso, cómo se construye una imagen. Cada instrucción genera una capa, y el resultado es reproducible: quien tenga el Dockerfile puede reconstruir la misma imagen sin que nadie le explique nada. El comando que lo procesa es docker build, y la opción -t le pone nombre y etiqueta al resultado.
| Instrucción | Qué hace |
|---|---|
| FROM | Imagen base de la que se parte, con su etiqueta. Es siempre la primera. Con FROM scratch se parte de cero, sin ninguna base. |
| ENV | Define una variable de entorno que existe al construir y también al ejecutar. |
| ARG | Define una variable que solo existe durante la construcción. |
| COPY | Copia ficheros del contexto de construcción hacia dentro de la imagen. |
| RUN | Ejecuta órdenes durante la construcción: instalar paquetes, crear directorios. |
| WORKDIR | Fija el directorio de trabajo para las instrucciones siguientes. |
| USER | Usuario con el que se ejecutan las acciones. Si no se indica, es root. |
| EXPOSE | Declara qué puerto usa la aplicación. Es documentación: no publica nada. |
| VOLUME | Declara un directorio cuyo contenido debe ser persistente. |
| CMD | Orden por defecto al arrancar un contenedor de esta imagen. Solo puede haber una. |
| ENTRYPOINT | Igual que CMD, pero no se puede sustituir desde la línea de comandos. |
La pareja CMD y ENTRYPOINT es la que más se pregunta y la que más se confunde. CMD fija lo que se ejecutará al arrancar, pero si al lanzar el contenedor se escribe otra orden detrás del nombre de la imagen, esa otra orden sustituye a la de CMD. ENTRYPOINT no se puede sustituir así: lo que se escriba detrás se le pasa como argumentos. De ahí la costumbre de poner en ENTRYPOINT el programa y en CMD sus parámetros por defecto.
FROM node:22-alpine
# Variables: ENV sobrevive en el contenedor, ARG solo existe al construir
ENV NODE_ENV=production
ARG VERSION_TEMARIO=2.4.1
WORKDIR /app
# Primero las dependencias, para aprovechar la cache de capas
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# Y despues el codigo, que cambia mucho mas a menudo
COPY src ./src
# El servicio no debe correr como root
RUN adduser -D corte
USER corte
EXPOSE 3000
VOLUME ["/app/informes"]
ENTRYPOINT ["node", "src/servidor.js"]
CMD ["--puerto", "3000"]# Construir la imagen desde el Dockerfile del directorio actual
docker build -t api-temario:2.4.1 .
# Pasar un ARG en tiempo de construccion
docker build --build-arg VERSION_TEMARIO=2.5.0 -t api-temario:2.5.0 .
# Congelar como imagen nueva el estado de un contenedor en marcha
docker commit api-temario api-temario:parcheadoLa elección de la imagen base decide buena parte del tamaño y de la superficie de ataque del resultado. Por eso se prefieren bases mínimas como Alpine o BusyBox, que rondan los cinco megabytes frente a los cientos de una distribución completa, y en el extremo está FROM scratch, que parte de una imagen vacía para meter dentro solo un ejecutable. Menos base significa menos paquetes que actualizar y menos vulnerabilidades heredadas, y hay herramientas de análisis, como docker scout, que revisan capa por capa qué vulnerabilidades conocidas arrastra una imagen.
Ese último comando, docker commit, existe y hay que conocerlo, pero es un mal camino: congela el estado de un contenedor en una imagen sin dejar constancia de cómo se llegó a él, y con eso se pierde justamente lo que se buscaba, la reproducibilidad. La forma correcta de cambiar una imagen es cambiar su Dockerfile y volver a construirla.
El orden de las instrucciones importa por la caché: lo que cambia poco (las dependencias) va arriba y lo que cambia mucho (el código) va abajo. Al revés, cada compilación reinstala todo desde cero.
Para el examen
COPY: solo copia
ADD: además descomprime y admite URL
Recomendación: usar COPY salvo que se necesite lo otro
Cada instrucción: crea una capa
Un Dockerfile de servicio con base de datos, comentado
Merece la pena leer un Dockerfile algo más largo, de los que preparan un servicio de sistema y no una aplicación sencilla, porque aparecen juntas casi todas las instrucciones y se ve cómo encajan.
# Base: una distribucion con soporte largo
FROM debian:12
# Evita que los paquetes pidan confirmacion durante la construccion
ARG DEBIAN_FRONTEND=noninteractive
ENV PGDATA=/var/lib/postgresql/data
# Un solo RUN para instalar y limpiar: cada RUN es una capa,
# y limpiar en otra capa distinta no reduciria el tamano final
RUN apt-get update \
&& apt-get install -y --no-install-recommends postgresql postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# La configuracion se copia desde el contexto de construccion
COPY conf/postgresql.conf /etc/postgresql/postgresql.conf
COPY conf/pg_hba.conf /etc/postgresql/pg_hba.conf
# A partir de aqui nada se ejecuta como root
USER postgres
# Puerto que usa el servicio: es documentacion, no lo publica
EXPOSE 5432/tcp
# Directorios cuyo contenido tiene que sobrevivir al contenedor
VOLUME ["/var/lib/postgresql/data", "/var/log/postgresql"]
# El proceso principal, en primer plano y con su configuracion
CMD ["postgres", "-D", "/var/lib/postgresql/data", \
"-c", "config_file=/etc/postgresql/postgresql.conf"]- Los RUN se agrupan a propósito: cada uno crea una capa, y lo que se borra en una capa posterior sigue ocupando espacio en la anterior.
- USER postgres cambia el usuario a partir de ese punto. Todo lo que venga después se ejecuta sin privilegios, que es lo que hay que buscar.
- VOLUME declara qué directorios llevan datos que no pueden morir con el contenedor, pero no crea el volumen: eso se decide al arrancar.
- El proceso de CMD tiene que quedarse en primer plano. Si se lanza como servicio en segundo plano, el contenedor se cerrará en cuanto la orden termine.
Un contenedor vive mientras viva su proceso principal. Por eso dentro de un contenedor no se usa systemctl ni se lanzan servicios en segundo plano: el programa se ejecuta en primer plano y ese programa es el contenedor.
Para el examen
CMD: órdenes por defecto, sustituibles al arrancar
ENTRYPOINT: fija el ejecutable
EXPOSE: solo documenta: no publica el puerto