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
- Qué es Docker de verdad (más allá del hype)
- Tu primer contenedor:
hello-world - Imágenes, tags y registros: de dónde sale todo esto
- Trabajando con imágenes: ver, descargar, borrar
docker run: un contenedor “vivo”- “Abrir la tapa”: entrar en un contenedor con
exec - Volúmenes: que tus datos no mueran con el contenedor
- Subir y bajar tus imágenes: push/pull y registries
- Cómo saber si ya “te manejas” con Docker
- Glosario rápido
- Fuentes y referencias
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:
- Imagen ~ clase.
- Contenedor ~ objeto.
Y si tu referencia mental son servidores clásicos:
- Imagen ~ snapshot muy bien definido de un servidor.
- Contenedor ~ servidor “encendido” basado en ese snapshot, pero mucho más ligero y desechable.
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:
ubuntu:22.04–> registry implícito (Docker Hub), namespace implícito (library), tag22.04.nginx:alpine.mycompany/api:1.3.0.ghcr.io/mi-org/mi-servicio:02a3f7d(GitHub Container Registry con un tag basado en commit).
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:
docker pull nombre:tag–> bajar una imagen.docker push nombre:tag–> subir una imagen.
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:
--name mi-nginx: quiero poder referirme a este contenedor por un nombre decente.-p 8080:80: mapea el puerto 80 del contenedor al 8080 de tu máquina.-d: “detached”, arráncalo en background.nginx:alpine: imagen base.
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.
- imagen: plantilla inmutable que contiene un sistema de archivos congelado más metadatos (comando por defecto, variables de entorno, puertos). De ella se crean contenedores.
- contenedor: instancia en ejecución de una imagen. Un proceso (o grupo de procesos) aislado que arranca a partir de esa plantilla y que, cuando termina, se puede descartar sin dejar rastro.
- registry (registro): servicio que almacena y distribuye imágenes Docker. Docker Hub es el registro público por defecto; los hay también privados (ECR, ACR, Harbor…).
- tag: etiqueta que identifica una versión concreta de una imagen (por ejemplo,
python:3.12onginx:alpine). Si no la indicas, Docker usalatest. - bind mount: montaje directo de una carpeta del host dentro del contenedor. Los cambios se reflejan en ambos lados en tiempo real.
- volumen: espacio de almacenamiento gestionado por Docker, independiente del ciclo de vida del contenedor. Ideal para persistir datos de bases de datos o colas.
- namespace / cgroup: mecanismos del kernel Linux que permiten aislar procesos (namespaces) y limitar sus recursos –CPU, memoria, red– (cgroups). Son la base sobre la que Docker construye los contenedores.
- Dockerfile: fichero de texto que describe, instrucción a instrucción, cómo construir una imagen Docker. Cada instrucción (
FROM,COPY,RUN,CMD…) añade una capa al resultado final. - docker compose: herramienta que permite definir y levantar varios contenedores a la vez a partir de un fichero YAML (
docker-compose.yml). Ideal para entornos de desarrollo donde necesitas varios servicios coordinados (API + base de datos, por ejemplo). - capa (layer): cada instrucción del Dockerfile que modifica el sistema de archivos genera una capa inmutable. Las imágenes son pilas de capas reutilizables, lo que permite que los builds y las transferencias (push/pull) sean incrementales.
- host: la máquina física o virtual donde corre Docker. Cuando se habla de “puerto del host” o “carpeta del host”, se refiere a tu ordenador real, no al entorno aislado del contenedor.
Fuentes y referencias
Los enlaces que habrías buscado tú de todas formas, pero ya agrupados y sin tener que pelear con Google.
- Artículo de instalación de Docker - iamlino.net. Guía paso a paso para instalar Docker sin dolor.
- Docker Desktop - Docker Inc. Instalación y uso de Docker Desktop en Mac y Windows.
- Docker Engine - Install - Docker Inc. Guía de instalación de Docker Engine en Linux.
- Docker Hub - Docker Inc. Registro público de imágenes Docker.
- Dockerfile reference - Docker Inc. Referencia completa de instrucciones para Dockerfile.
- Docker Compose - Docker Inc. Documentación oficial de Docker Compose.
