I am Lino
16 de abril de 2026

Docker basico: de 'en mi maquina funciona' a entender que estas haciendo

Publicado el 16 de abril de 2026  •  16 minutos  • 3258 palabras
Table of contents

Docker es una de esas herramientas que todo el mundo “usa” y casi nadie se para a entender. Y es una pena, porque una vez que la entiendes te cambia la vida como desarrollador: pasas de “en mi máquina funciona” a “funciona en mi máquina, en la tuya, en el CI y en producción, y además puedo demostrarlo”.

La idea de este tutorial es sencilla: si nunca has tocado Docker, que al acabar puedas moverte con soltura por los conceptos básicos, ejecutar contenedores, trabajar con volúmenes y entender qué estás haciendo. No copiar y pegar comandos como un mono amaestrado, sino saber por qué funcionan.

Qué es Docker de verdad (más allá del hype)

Técnicamente, Docker se apoya en mecanismos de virtualización a nivel de sistema operativo: namespaces, cgroups y compañía. Eso significa que no estás levantando una máquina virtual completa con su propio kernel (como harías con VirtualBox, VMware o similares), sino procesos aislados que comparten el kernel del host pero viven en su “propio mundo”: su sistema de archivos, sus interfaces de red, sus procesos.

Antes de tocar nada, hay dos conceptos que necesitas tener claros o todo lo demás te va a sonar a ruido de fondo.

Una imagen es una plantilla inmutable: un sistema de archivos congelado junto con sus metadatos (qué comando ejecutar por defecto, variables de entorno, puertos expuestos…).

Un contenedor es una instancia en ejecución de esa imagen: un proceso (o grupo de procesos) que arranca a partir de esa plantilla. La imagen es la receta; el contenedor es el plato servido. Si el plato sale malo, tiras el contenedor y sirves otro desde la misma receta sin que nadie se entere.

Si vienes de programación orientada a objetos, la analogía es directa:

Y si tu referencia mental son servidores clásicos:

Tu primer contenedor: hello-world

Instalado Docker (Docker Desktop en Mac/Windows, Docker Engine en Linux; si necesitas una guía paso a paso, la tienes en el Artículo de instalación de Docker ), vamos con el ritual iniciático:

docker run hello-world

A nivel superficial parece magia: escribes eso, salen unos logs simpáticos y todo se acaba solo.

Por debajo, la cosa es más interesante de lo que parece. Docker mira primero si ya tiene la imagen hello-world en tu máquina local. Como no la encuentra (es tu primera vez, nadie nace sabiendo), se la pide al registro por defecto (Docker Hub ), la descarga, la almacena localmente y crea a partir de ella un contenedor. Dentro de ese contenedor se ejecuta el comando que la imagen tiene definido —en este caso, imprimir unos mensajes de bienvenida y terminar—. Cuando el proceso termina, el contenedor pasa al estado “Exited” y se queda ahí, ocupando un poquito de disco pero sin hacer nada más, como un jarrón decorativo al que se le han secado las flores.

No has escrito stop en ningún momento porque no hay nada que “parar” manualmente: el proceso nació, hizo su trabajo (imprimir el texto), y murió. Un contenedor no es un “ser inmortal”; si el proceso principal termina, el contenedor también. Puedes comprobarlo:

docker ps -a

Verás algo como:

CONTAINER ID   IMAGE         COMMAND    STATUS
abcd1234       hello-world   "/hello"   Exited (0) ...

Ya has hecho tu primera pull + run sin darte cuenta.

Imágenes, tags y registros: de dónde sale todo esto

Una imagen no es solo un nombre suelto; tiene estructura:

<registry>/<namespace>/<nombre_imagen>:<tag>

Ejemplos:

Si no especificas :tag, Docker asume :latest, que es cómodo en desarrollo pero una trampa en entornos serios: “latest” significa “lo último que alguien subió”, no “lo que tú crees que es”.

Qué es un registro

Un registry es un servicio que almacena imágenes Docker y te permite hacer:

Los hay públicos y remotos —Docker Hub (docker.io), GitHub Container Registry (ghcr.io), GitLab Container Registry, y los de cada proveedor de cloud: AWS ECR, Azure ACR, GCP Artifact Registry…—, y los hay privados, que son los que montas en tu propia infraestructura con herramientas como registry:2 de Docker, Artifactory o Harbor. La diferencia es quién paga el hosting y quién controla el acceso, pero conceptualmente todos hacen exactamente lo mismo: guardar imágenes y dejar que las subas y las bajes.

Para hablar con un registro autenticado necesitas hacer login:

docker login
# te pedirá usuario + password o token

Si trabajas con ECR, ACR, GCR u otros registries cloud, el login suele hacerse con el CLI del cloud, que genera un token temporal y se lo pasa a Docker por debajo. Consulta la documentación del registry ya que suelen incluir instrucciones para hacer login y subir y bajar imágenes.

Trabajando con imágenes: ver, descargar, borrar

Comandos básicos de supervivencia:

# Ver las imágenes que tienes en local
docker images

# Descargar explícitamente una imagen
docker pull nginx:alpine

# Borrar una imagen (si no la usa ningún contenedor)
docker rmi nginx:alpine

Nada impide que tengas varias versiones de lo mismo:

docker pull postgres:15-alpine
docker pull postgres:14-alpine

Al final, tus imágenes locales son tu cache: todo lo que Docker encuentre ahí no lo tendrá que volver a bajar del registro.

docker run: un contenedor “vivo”

Vamos a ejecutar algo un poco menos cutre y simple que hello-world. Por ejemplo, Nginx:

docker run --name mi-nginx -p 8080:80 -d nginx:alpine

Traduciendo a lenguaje humano:

Abre http://localhost:8080 y verás la página de bienvenida de Nginx.

Revisa qué se está ejecutando:

docker ps

Cuando te canses de jugar con ese container tan bonito pero francamente bastante inútil:

docker stop mi-nginx
docker rm mi-nginx

Concepto importante: los contenedores son recursos desechables. Trátalos como vasos de plástico, no como la vajilla buena de tu abuela.

Detener un contenedor no borra la imagen. Borrarlo (rm) no borra el volumen (si lo hay). Borrar la imagen no toca el registro remoto. Son capas independientes: puedes cargarte una sin que las otras se enteren.

“Abrir la tapa”: entrar en un contenedor con exec

La tentación de “ver qué hay dentro” es natural y hasta sana. Destripar la tecnología es el primer paso para entenderla.

Si tienes un contenedor en ejecución, puedes abrirle una shell:

docker exec -it mi-nginx sh

o, si lleva bash:

docker exec -it mi-contenedor bash

-it es la combinación de -i (modo interactivo, que mantiene STDIN abierto para que puedas escribir) y -t (pseudo-TTY, para que la shell se comporte como una terminal de verdad y no como un grifo que gotea caracteres sin control). Sin esos dos flags, tu sesión interactiva tendría la usabilidad de un teclado mojado.

Una vez dentro, lo que ves es el filesystem del contenedor: puedes listar /, ver procesos (ps aux), revisar rutas de configuración, o ejecutar cualquier comando.

Para salir de la shell del contendor se usa:

exit

No estás “logándote en una máquina remota”; sigues en tu host, pero abriendo una sesión interactiva en un proceso aislado.

Aunque puedes ejecutar cualquier comando dentro del contenedor e incluso modificar cosas, los cambios solo vivirán en ese contenedor concreto, no en la imagen original. Es como dibujar bigotes en una fotocopia: puedes destrozarla a gusto, pero el original sigue intacto en la fotocopiadora.

Volúmenes: que tus datos no mueran con el contenedor

Si lanzas un contenedor, escribes datos dentro y luego lo borras, esos datos desaparecen. Eso es una virtud cuando quieres entornos efímeros, pero es poco útil para cosas como bases de datos o incluso servidores web.

Ahí entran en juego los volúmenes y los bind mounts.

Bind mounts: tu carpeta –> carpeta del contenedor

Vamos a tratar un caso bastante habitual: tienes una carpeta con HTML estático y quieres servirlo con Nginx:

mkdir -p ~/web-demo
echo "Hola Docker" > ~/web-demo/index.html

docker run --name nginx-web \
  -p 8080:80 \
  -v ~/web-demo:/usr/share/nginx/html:ro \
  -d nginx:alpine

El flag -v host_path:container_path:ro conecta una carpeta de tu máquina (host_path) con una ruta dentro del contenedor (container_path). El :ro la monta en solo lectura, que es buena idea cuando el contenedor no necesita escribir ahí —siempre es mejor prevenir que explicarle a tu yo del lunes por qué la carpeta de desarrollo está vacía—.

Si cambias index.html en tu host, el cambio se ve al refrescar el navegador. Esto es muy útil en desarrollo: no tienes que reconstruir la imagen cada vez que tocas un fichero.

Volúmenes gestionados por Docker

Cuando no te importa la ruta física en el host, pero sí que los datos persistan, usas volúmenes Docker:

docker volume create datos-mysql

docker run --name mysql-demo \
  -e MYSQL_ROOT_PASSWORD=secret \
  -v datos-mysql:/var/lib/mysql \
  -d mysql:8.0

En este ejemplo, datos-mysql es un volumen que Docker creará y gestionará por ti —tú no necesitas saber dónde vive físicamente en el disco, solo que existe—. Y /var/lib/mysql es la ruta donde MySQL guarda sus datos dentro del contenedor.

Si mañana borras el contenedor, el volumen sigue ahí con los datos intactos, esperando pacientemente a que levantes otro que lo necesite.

Puedes inspeccionar y listar:

docker volume ls
docker volume inspect datos-mysql
docker volume rm datos-mysql   # cuidado: borras los datos

¿Cuándo usar cada uno?

Para el código de tu app durante desarrollo, bind mounts: editas en tu IDE, refrescas el navegador, y ves los cambios al instante sin reconstruir nada.

Para datos de servicios como bases de datos o colas de mensajes, volúmenes Docker: necesitas que los datos sobrevivan al contenedor, pero te da exactamente igual en qué rincón del disco vivan.

De tu código a una imagen: el mínimo Dockerfile digno

Hasta ahora hemos usado imágenes de otros. Pero la gracia de Docker es empaquetar tu aplicación.

Un ejemplo mínimo para una API Node.js:

# Dockerfile
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

EXPOSE 3000

CMD ["npm", "start"]

Para construir y ejecutar:

# Construir
docker build -t mi-usuario/mi-api:dev .

# Ver la imagen
docker images | grep mi-usuario

# Ejecutar
docker run --name mi-api -p 3000:3000 mi-usuario/mi-api:dev

Merece la pena entender qué hace cada línea, porque no son conjuros mágicos.

FROM elige la imagen base, tu punto de partida con sistema operativo y runtime incluidos. WORKDIR establece el directorio de trabajo dentro de la imagen, para que los comandos siguientes sepan dónde están parados. COPY y RUN van apilando capas sobre esa base: copias ficheros, instalas dependencias, preparas el terreno. Y CMD define el proceso principal que arrancará cada vez que alguien lance un contenedor a partir de esta imagen. Piénsalo como una receta de cocina donde cada instrucción va añadiendo un ingrediente, y CMD es el momento en que metes el plato al horno.

Capas: por qué Docker no reconstruye todo cada vez

Cada instrucción del Dockerfile que modifica el sistema de archivos (FROM, COPY, RUN…) genera una capa. Una imagen no es un bloque monolítico: es una pila de capas apiladas una encima de otra, como las hojas de un hojaldre. La primera capa es la imagen base (FROM node:20-alpine), la siguiente es el resultado de copiar tus package*.json, la siguiente es lo que queda tras ejecutar npm ci, y así sucesivamente.

Lo interesante es que cada capa es inmutable y tiene un hash único. Cuando Docker reconstruye tu imagen, compara cada instrucción con lo que ya tiene cacheado: si la instrucción no ha cambiado y los ficheros que usa tampoco, Docker reutiliza la capa anterior y se ahorra el trabajo. Solo reconstruye desde la primera capa que haya cambiado hacia abajo.

Por eso en el Dockerfile de ejemplo copiamos primero package*.json y ejecutamos npm ci antes de copiar el resto del código. Si solo has tocado un fichero .js, Docker reutiliza las capas de dependencias (que suelen tardar bastante) y solo reconstruye la capa del COPY . . final. Si lo hicieras al revés —copiar todo y luego instalar—, cualquier cambio en tu código invalidaría también la capa de npm ci, y te tocaría esperar cada vez a que se reinstalen todas las dependencias. La diferencia entre un build de 3 segundos y uno de 2 minutos suele estar en entender cómo funcionan las capas.

Cuando haces docker push, Docker solo sube las capas que el registro no tiene ya. Y cuando haces docker pull, solo baja las que te faltan. Esa reutilización es lo que hace que mover imágenes sea mucho más rápido de lo que el tamaño total sugeriría.

No hace falta entrar en multi-stage builds, optimizaciones de capas o seguridad todavía. Lo importante es que ya sabes construir una imagen desde tu repo y hacerla funcionar.

Subir y bajar tus imágenes: push/pull y registries

En algún momento vas a querer usar esa imagen fuera de tu portátil: en otra máquina, en un pipeline de CI, en un cluster de Kubernetes, etc. Ahí entran los registries.

1. Nombrar bien la imagen

Cuando construyes una imagen con docker build -t mi-api:dev ., ese nombre solo tiene sentido en tu máquina. Para que un registry remoto la acepte, el nombre de la imagen tiene que incluir la ruta completa del destino: tu usuario o namespace en el registro, el nombre del repositorio y un tag de versión. Es como poner el remitente y la dirección en un paquete antes de llevarlo a correos: sin eso, el cartero no sabe qué hacer con él.

Supongamos que tienes mi-api:dev localmente y quieres subirla a Docker Hub:

docker tag mi-api:dev miusuario/mi-api:1.0.0

Esto no copia la imagen ni la duplica; simplemente le pone un alias nuevo que el registry entenderá. Ahora miusuario/mi-api:1.0.0 apunta a los mismos bytes que mi-api:dev, pero con un nombre que Docker Hub reconoce como “esto va a la cuenta miusuario, repositorio mi-api, versión 1.0.0”.

2. Login y push

docker login    # si es Docker Hub

docker push miusuario/mi-api:1.0.0

Docker hablará con el registro, autentica y sube las capas que no estén allí ya.

En otra máquina:

docker pull miusuario/mi-api:1.0.0
docker run --rm -p 3000:3000 miusuario/mi-api:1.0.0

Para otros registries el ritual es idéntico, solo cambia quién te pide las credenciales.

En AWS ECR, primero sacas un token con aws ecr get-login-password | docker login ... y luego push/pull con URLs tipo 123456789012.dkr.ecr.eu-west-1.amazonaws.com/mi-api:1.0.0.

En Azure ACR, haces az acr login --name miregistry y trabajas con miregistry.azurecr.io/mi-api:1.0.0.

En GitHub Container Registry, login con docker login ghcr.io y la URL será ghcr.io/mi-org/mi-api:1.0.0.

Si aprendes uno, has aprendido todos: la única diferencia real es a quién le llega la factura a final de mes.

Varios servicios a la vez: una pincelada de docker compose

Levantar una API y una base de datos con varios docker run seguidos es factible. Mantener eso en un equipo de más de una persona (tú) sin que alguien acabe llorando en Slack es otra historia. Para eso existe docker compose.

docker-compose.yml muy sencillo:

version: "3.9"

services:
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_PASSWORD: example
      POSTGRES_USER: app
      POSTGRES_DB: appdb
    volumes:
      - datos-db:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  api:
    build: .
    depends_on:
      - db
    environment:
      DATABASE_URL: postgres://app:example@db:5432/appdb
    ports:
      - "3000:3000"

volumes:
  datos-db:

Y los cuatro comandos que separan el caos del orden:

docker compose up      # levanta todo en foreground
docker compose up -d   # levanta en background
docker compose logs -f # sigue logs de todos los servicios
docker compose down    # para y borra contenedores (volúmenes solo si lo pides)

Lo bonito de esto es que tu entorno de desarrollo ahora es declarativo. Alguien clona el repo, ejecuta docker compose up, y en menos de lo que tarda en prepararse un café ya está trabajando con la misma topología que el resto del equipo.

Nada de “a mí me falta esta variable de entorno” ni “¿qué versión de Postgres llevas?”. Todo está en el fichero, y el fichero no miente (al menos no tanto como la documentación de la wiki).

Cómo saber si ya “te manejas” con Docker

Si después de leer esto puedes explicar en voz alta la diferencia entre imagen y contenedor, lanzar un docker run con puertos y volúmenes sin vivir pegado a los apuntes, listar, parar y borrar contenedores (y abrirles una shell con exec cuando te pique la curiosidad), escribir un Dockerfile para tu app, construir la imagen, moverla con push/pull entre registros, y levantar una API con su base de datos usando docker compose up … ya no eres “alguien que copia comandos de Stack Overflow”. Eres un desarrollador que entiende lo suficiente de Docker como para usarlo con criterio, y eso en esta industria ya te pone bastante por delante de lo que imaginas.

A partir de aquí, el siguiente nivel ya es meterse con redes, seguridad, multi-stage builds, imágenes mínimas, buenas prácticas de producción y orquestadores como Kubernetes. Pero eso ya es otro tutorial.


Glosario rápido

Si al leer el artículo has pensado “me estás hablando en arameo, pero de la variante técnica”, aquí va el traductor.


Fuentes y referencias

Los enlaces que habrías buscado tú de todas formas, pero ya agrupados y sin tener que pelear con Google.

Sígueme

Escribo y opino sobre tecnología, desarrollo de software y lo que se me pase por la cabeza.