Saltar al contenido

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.

ComandoQué muestra
docker infoEl 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 versionLa versión del cliente y la del demonio.
docker psLos contenedores en ejecución. Con -a, también los parados.
docker imagesLas imágenes descargadas, con su repositorio, etiqueta, identificador, antigüedad y tamaño.
bash
# 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-temario

Docker 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.

ComandoQué hace
docker pull imagenDescarga una imagen del registro sin ejecutar nada.
docker create imagenCrea el contenedor a partir de la imagen, pero no lo arranca.
docker run imagenCrea y arranca. Devuelve el identificador del contenedor.
docker start idArranca un contenedor que estaba parado.
docker stop idLo detiene ordenadamente, dándole tiempo a cerrar.
docker kill idLo detiene de forma inmediata, sin margen para cerrar.
docker rm idBorra el contenedor. Tiene que estar parado.
docker rmi imagenBorra una imagen local. No puede haber contenedores que la usen.
bash
# 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 runPara qué
-dArranque en segundo plano (detached). El contenedor sigue vivo tras devolver el control.
-iMantiene abierta la entrada estándar.
-tAsigna un terminal. Casi siempre se combinan como -it.
--nameLe da un nombre legible, para no manejar identificadores.
--rmBorra el contenedor automáticamente en cuanto termine.
-eDefine 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.

ComandoQué haceRiesgo
docker execLanza un proceso NUEVO dentro de un contenedor que ya está en marcha, por ejemplo una consola.Ninguno: al salir, el contenedor sigue igual.
docker attachConecta 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.
bash
# 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.tar

En 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.

bash
# 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.1

Por 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 redComportamiento
bridgeLa opción por defecto. Red privada virtual del anfitrión, con traducción de direcciones hacia fuera.
hostEl contenedor usa directamente la pila de red del anfitrión. No hay aislamiento ni hace falta publicar puertos.
noneSin red. El contenedor queda incomunicado.
bash
# 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-temario

En -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.

FormaQué esCuándo se usa
Volumen gestionadoUn 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 mountUn 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.
tmpfsUn espacio en memoria que no llega a tocar el disco.Datos temporales o sensibles que no deben persistir.
bash
# 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-temario

El 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ónQué hace
FROMImagen base de la que se parte, con su etiqueta. Es siempre la primera. Con FROM scratch se parte de cero, sin ninguna base.
ENVDefine una variable de entorno que existe al construir y también al ejecutar.
ARGDefine una variable que solo existe durante la construcción.
COPYCopia ficheros del contexto de construcción hacia dentro de la imagen.
RUNEjecuta órdenes durante la construcción: instalar paquetes, crear directorios.
WORKDIRFija el directorio de trabajo para las instrucciones siguientes.
USERUsuario con el que se ejecutan las acciones. Si no se indica, es root.
EXPOSEDeclara qué puerto usa la aplicación. Es documentación: no publica nada.
VOLUMEDeclara un directorio cuyo contenido debe ser persistente.
CMDOrden por defecto al arrancar un contenedor de esta imagen. Solo puede haber una.
ENTRYPOINTIgual 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.

dockerfile
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"]
bash
# 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:parcheado

La 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.

dockerfile
# 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