Requisitos
Usa una versión soportada de Ubuntu de 64 bits, acceso sudo, al menos 2 GB de RAM para aprender y espacio para imágenes y logs. En producción, calcula la RAM según los contenedores.
Instalar desde el repositorio oficial
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc Crea el repositorio oficial usando el formato DEB822 y después instala Docker Engine y los plugins de Buildx y Compose:
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker run --rm hello-world Consulta la documentación oficial si cambia el formato del repositorio; no descargues scripts copiados de blogs desconocidos.
Ejecutar un contenedor
sudo docker run -d --name web -p 8080:80 --restart unless-stopped nginx:alpine
sudo docker ps
sudo docker logs web El puerto 8080 queda publicado en todas las interfaces. Para limitarlo al propio servidor usa 127.0.0.1:8080:80 y coloca Nginx o Traefik delante.
Persistencia y redes
Los datos escritos solo dentro del contenedor se pierden al recrearlo. Utiliza volúmenes:
sudo docker volume create datos_app
sudo docker network create red_app Guarda bases de datos en volúmenes y respáldalas mediante herramientas consistentes con cada motor. Un volumen no es un backup.
Docker sin sudo
Añadir un usuario al grupo docker equivale prácticamente a darle privilegios root:
sudo usermod -aG docker "$USER" Cierra sesión y vuelve a entrar. Hazlo solo con administradores confiables; no es una medida de aislamiento.
Seguridad y operación
No publiques bases de datos, fija versiones de imágenes en producción, escanea dependencias, define límites de memoria y rota logs. Docker puede insertar reglas de iptables que pasan por alto UFW, por lo que debes revisar la cadena DOCKER-USER o el firewall del proveedor.
Usa Docker Compose para describir varios servicios y Portainer si necesitas una interfaz. Los VPS KVM de Terranode permiten instalar Docker sin las restricciones típicas de contenedores de sistema.
Qué aísla Docker y qué sigue compartiendo
Un contenedor aísla procesos y su vista del sistema, pero comparte el kernel Linux del VPS. Por eso arranca más rápido y ocupa menos que una máquina virtual completa, aunque tampoco ofrece la misma frontera. La imagen define archivos y dependencias; el contenedor añade una capa de escritura temporal; los volúmenes conservan lo que debe sobrevivir.
Esta distinción cambia cómo se opera el servidor:
- Reiniciar un contenedor no reinicia el VPS.
- Reemplazar un contenedor es normal; editarlo manualmente crea cambios imposibles de reproducir.
- El volumen no se incorpora a la imagen y necesita su propio respaldo.
- La CPU, la memoria, el disco y el kernel siguen siendo recursos compartidos por todos los contenedores.
Después de la instalación, revisa cliente, daemon y plugin Compose por separado:
docker version
docker info
docker compose version
systemctl is-enabled docker
systemctl is-active docker docker version muestra tanto el cliente como el servidor. Si solo aparece el cliente, el comando existe pero no puede hablar con el daemon. docker info también revela el directorio raíz, el controlador de almacenamiento y advertencias que conviene resolver antes de desplegar datos.
Elegir imágenes que puedas identificar y actualizar
Una imagen de producción debe proceder de una fuente conocida y tener una versión explícita. nginx:alpine es cómodo para una prueba, pero una etiqueta móvil puede apuntar mañana a contenido diferente. Para un servicio estable usa una versión que tu equipo haya probado y registra el cambio cuando la actualices.
Examina una imagen antes de ejecutarla:
docker image pull nginx:1.28-alpine
docker image inspect nginx:1.28-alpine
docker history --no-trunc nginx:1.28-alpine El Dockerfile de una aplicación propia debe producir una imagen, no descargar dependencias cada vez que arranca. Así una versión probada es la misma que llega al VPS. Un ejemplo mínimo para una aplicación Node separa la instalación de dependencias del código para aprovechar la caché:
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"] No copies .env, llaves SSH ni credenciales al contexto de construcción. Añádelos a .dockerignore y pásalos en tiempo de ejecución. Una credencial copiada y luego borrada puede permanecer en una capa anterior de la imagen.
Ejecutar un contenedor con límites y comprobaciones
Un servicio sin límites puede consumir toda la memoria del VPS y derribar procesos que no tienen relación con él. Para una primera aplicación web puedes fijar memoria, reinicio y una comprobación de salud desde el inicio:
docker run -d \
--name web \
--restart unless-stopped \
--memory 512m \
--cpus 1.0 \
--health-cmd='wget -qO- http://127.0.0.1/ || exit 1' \
--health-interval=30s \
--log-opt max-size=10m \
--log-opt max-file=3 \
-p 127.0.0.1:8080:80 \
nginx:1.28-alpine Comprueba los límites efectivos y no solo la línea que ejecutaste:
docker inspect web --format '{{.HostConfig.Memory}} {{.HostConfig.NanoCpus}}'
docker ps
docker inspect web --format '{{json .State.Health}}'
curl -I http://127.0.0.1:8080/ Un límite muy bajo puede provocar reinicios bajo un pico legítimo. Empieza con observación, mide el consumo estable y deja margen. La meta no es estrangular cada contenedor, sino evitar que un fallo local absorba el host completo.
Publicar puertos sin exponer servicios internos
La opción -p crea una entrada desde el host hacia el contenedor; no hace falta para la comunicación entre contenedores de una misma red. -p 8080:80 escucha normalmente en todas las interfaces. -p 127.0.0.1:8080:80 limita el acceso al host, ideal si Nginx recibe HTTPS y reenvía a Docker.
Revisa la publicación de dos maneras:
docker port web
sudo ss -ltnp | grep 8080 Después pruébala desde otra red. Una base de datos, Redis o el panel de administración no deberían exponerse solo porque resulta cómodo conectarse desde el portátil. Para administración remota usa una VPN, un túnel SSH o reglas por origen; para la aplicación, conecta los servicios por red Docker.
Docker puede modificar reglas de filtrado. Esto significa que ufw status no es una prueba suficiente de que el puerto quedó cerrado. Combina el enlace a loopback con el firewall perimetral del proveedor y una comprobación externa.
Redes de aplicación y resolución por nombre
Una red definida por el usuario permite que los contenedores se encuentren por nombre y separa grupos que no necesitan hablar entre sí. Crea una red y levanta dos servicios sin publicar la base:
docker network create tienda_backend
docker run -d --name tienda_db --network tienda_backend \
-e POSTGRES_PASSWORD='CAMBIA_ESTA_CLAVE' \
-v tienda_postgres:/var/lib/postgresql/data \
postgres:17-alpine
docker run -d --name tienda_app --network tienda_backend \
-p 127.0.0.1:8080:3000 \
-e DATABASE_URL='postgresql://postgres:CAMBIA_ESTA_CLAVE@tienda_db:5432/postgres' \
ghcr.io/empresa/tienda:1.7.2 La aplicación usa tienda_db como host. localhost dentro de tienda_app designa a la propia aplicación. Si borras y recreas la base con el mismo nombre en la red, Docker actualiza su resolución; codificar la IP interna rompería ese comportamiento.
Inspecciona membresía y direcciones cuando una conexión falla:
docker network inspect tienda_backend
docker exec tienda_app getent hosts tienda_db No conectes todos los contenedores a una red universal. Separa frontal, backend y monitoreo según lo que de verdad deba comunicarse; así un servicio comprometido tiene menos rutas directas hacia los demás.
Persistencia: volumen o directorio del host
Los datos importantes no deben vivir únicamente en la capa de escritura del contenedor. Si ejecutas docker rm, esa capa desaparece. Un volumen nombrado es apropiado para la base; un bind mount es útil cuando necesitas editar o versionar un archivo desde el host.
docker volume create tienda_postgres
docker volume inspect tienda_postgres No dependas de la ruta interna que Docker muestra para copiar archivos en caliente. Una base puede mantener páginas pendientes, WAL o bloqueos; copiar su directorio mientras escribe no garantiza consistencia. Genera un volcado lógico:
mkdir -p "$HOME/backups"
docker exec tienda_db pg_dump -U postgres -Fc postgres \
> "$HOME/backups/tienda-$(date +%F).dump"
test -s "$HOME/backups/tienda-$(date +%F).dump" Mueve esa copia fuera del VPS y ensaya la restauración en un contenedor temporal o un entorno de pruebas. No borres un volumen hasta listar qué contenedores lo usan:
docker ps -a --filter volume=tienda_postgres
docker volume inspect tienda_postgres Logs y disco: el crecimiento silencioso
Las imágenes antiguas, capas de construcción y logs pueden llenar el disco aunque la aplicación siga respondiendo. Observa el uso antes de limpiar:
docker system df -v
sudo du -sh /var/lib/docker
docker inspect web --format '{{.LogPath}}'
sudo du -h "$(docker inspect web --format '{{.LogPath}}')" El driver json-file no rota por sí solo con límites útiles en todas las configuraciones. Define max-size y max-file al crear el contenedor o como valores predeterminados del daemon. Si cambias /etc/docker/daemon.json, valida el JSON y entiende que los límites nuevos se aplican a contenedores recreados.
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
} sudo dockerd --validate --config-file=/etc/docker/daemon.json
sudo systemctl restart docker Un reinicio del daemon merece una ventana controlada. Las políticas de reinicio y la función live-restore cambian el comportamiento de los contenedores, así que prueba tu configuración antes de asumir que no habrá corte.
Actualizar imágenes con una salida de emergencia
Actualizar no significa ejecutar pull y borrar inmediatamente la versión anterior. Registra la imagen actual, descarga una etiqueta concreta, prueba y conserva la posibilidad de volver.
docker inspect web --format '{{.Config.Image}} {{.Image}}'
docker pull nginx:1.28-alpine
docker stop web
docker rename web web-anterior
docker run -d --name web --restart unless-stopped \
-p 127.0.0.1:8080:80 nginx:1.28-alpine
curl -fsSI http://127.0.0.1:8080/ En una aplicación con estado, recrea el contenedor nuevo montando el mismo volumen solo si la versión es compatible con sus datos. Si la prueba falla, detén el nuevo, recupera el nombre anterior y arráncalo. Docker Compose automatiza mejor esta descripción y reduce errores de opciones olvidadas.
No uses docker system prune -a --volumes como mantenimiento rutinario. Su alcance incluye recursos que no estén asociados a un contenedor activo y puede destruir tanto el rollback como datos que alguien conservaba deliberadamente.
Diagnosticar un contenedor que no funciona
Empieza por el estado y el error del proceso, no por reiniciar el VPS. La secuencia de menor impacto es:
docker ps -a
docker logs --tail=200 --timestamps web
docker inspect web --format '{{.State.Status}} exit={{.State.ExitCode}} error={{.State.Error}}'
docker stats --no-stream
docker top web Si el contenedor termina de inmediato, ejecuta la imagen de forma interactiva solo en un entorno seguro para inspeccionar archivos o variables:
docker run --rm -it --entrypoint sh nginx:1.28-alpine | Síntoma | Causa que debes comprobar | Herramienta |
|---|---|---|
Exited (1) | Configuración o comando inválido | docker logs e inspect |
Exited (137) | Terminación forzada o presión de memoria | Kernel y docker stats |
| Puerto no responde | Publicación, proceso o firewall | docker port, ss, curl |
| No resuelve otro contenedor | Redes diferentes o nombre incorrecto | docker network inspect |
| Permiso denegado en volumen | UID/GID y modo del directorio | docker exec id y stat |
| Reinicios continuos | Healthcheck no reinicia por sí solo; revisar política y app | inspect y logs |
Un contenedor marcado healthy prueba solo el comando definido. Añade además una solicitud desde el proxy y, para un sitio, una verificación desde internet. Eso diferencia que el proceso esté vivo de que el servicio completo sea utilizable.
Reducir privilegios sin romper la aplicación
El daemon Docker controla recursos del host, por lo que su socket y el grupo docker son accesos administrativos. No montes /var/run/docker.sock dentro de una aplicación común y evita ejecutar contenedores con --privileged para resolver permisos.
Prueba estas restricciones cuando la imagen las admita:
docker run --rm \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--security-opt no-new-privileges:true \
--cap-drop ALL \
nginx:1.28-alpine nginx -t Mantén actualizado el kernel del VPS y el motor: el aislamiento depende de ambos. Revisa las imágenes, elimina paquetes innecesarios y ejecuta como usuario no root dentro del contenedor cuando sea posible. Estas medidas reducen impacto, pero no convierten código no confiable en seguro para compartir un mismo kernel.
Controlar el contexto de build y la procedencia del artefacto
Todo archivo enviado al contexto puede influir en el build o terminar expuesto si el Dockerfile lo copia. Mide y revisa el contexto antes de construir en el VPS:
du -sh .
find . -maxdepth 2 -type f -size +20M -print
cat .dockerignore
docker build --progress=plain -t tienda:1.7.2 . Incluye en .dockerignore repositorios, copias, dependencias locales y archivos de entorno. Esto reduce tiempo y evita que un COPY . . incorpore secretos o gigabytes innecesarios.
.git
.env
.env.*
node_modules
backups
*.log Guarda la relación entre commit e imagen. Una etiqueta comercial sin commit obliga a adivinar qué código contiene durante un incidente:
docker build \
--label org.opencontainers.image.revision="$(git rev-parse HEAD)" \
-t ghcr.io/empresa/tienda:1.7.2 .
docker inspect ghcr.io/empresa/tienda:1.7.2 \
--format '{{index .Config.Labels "org.opencontainers.image.revision"}}' Si usas un registro, autentica el VPS con una credencial de solo lectura cuando únicamente descarga imágenes. Un token capaz de publicar o borrar todo el registro amplía innecesariamente el impacto de comprometer ese servidor.
Mantener el daemon sin borrar recursos desconocidos
La limpieza empieza por un inventario, no por un prune general. Observa imágenes, contenedores detenidos y volúmenes:
docker system df -v
docker ps -a --size
docker image ls --digests
docker volume ls Elimina por nombre recursos que hayas identificado. Un contenedor detenido puede conservar la configuración necesaria para rollback; un volumen sin consumidor activo puede contener una base que alguien detuvo intencionalmente.
Revisa la configuración del daemon y su registro:
sudo dockerd --validate --config-file=/etc/docker/daemon.json
sudo journalctl -u docker --since '24 hours ago' --no-pager Programa actualizaciones del motor como cambios del host, no como actualizaciones ordinarias de una aplicación. Confirma compatibilidad, respaldo y comportamiento al reiniciar daemon. Después prueba que el proxy alcanza los servicios, que las redes existen y que los contenedores con política de reinicio volvieron.
No almacenar configuración única dentro de un contenedor
Los cambios hechos con docker exec desaparecen cuando el contenedor se recrea. Usar una shell para inspeccionar es válido; editar paquetes, código o certificados dentro convierte esa instancia en irrepetible. Lleva la corrección al Dockerfile, al archivo Compose o a un volumen definido.
Puedes detectar diferencias de la capa escribible:
docker diff NOMBRE_CONTENEDOR Las marcas A, C y D muestran archivos añadidos, cambiados o eliminados respecto de la imagen. No todo cambio es sospechoso: aplicaciones escriben PID, caché y temporales. La pregunta es si alguno contiene configuración necesaria que no está declarada en otro lugar.
Para recuperar un archivo durante un diagnóstico:
docker cp NOMBRE_CONTENEDOR:/ruta/archivo ./archivo-investigacion No uses esa copia como despliegue definitivo. Corrige la fuente, construye una nueva etiqueta y demuestra que un contenedor creado desde cero funciona con sus volúmenes previstos.
Decidir si necesitas Docker rootless
El modo rootless reduce la autoridad del daemon y los contenedores al ejecutarlos bajo un usuario sin root, pero cambia red, almacenamiento y algunas funciones. Puede ser útil en entornos donde usuarios distintos ejecutan cargas separadas y aceptan esas limitaciones. No lo actives como interruptor mágico sobre una instalación existente.
Evalúa compatibilidad con puertos bajos, cgroups, montajes y herramientas de operación. Mantén actualizado el host: rootless reduce impacto de ciertos errores, pero los procesos siguen compartiendo kernel. Para un VPS administrado por un único equipo, un daemon convencional bien restringido puede ser más simple; documenta por qué eliges cada modelo.
Probar el reinicio completo del host
Un contenedor que funciona después de docker run puede no volver tras mantenimiento del VPS. La política unless-stopped ayuda, pero debes verificar daemon, redes, volúmenes y dependencias externas durante una ventana autorizada.
Antes del reinicio registra:
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
docker inspect web --format '{{.HostConfig.RestartPolicy.Name}}'
systemctl is-enabled docker Reinicia el servidor solo si tienes consola y un periodo seguro. Al volver:
uptime
systemctl is-active docker
docker ps -a
docker logs --since=10m web
curl -fsSI http://127.0.0.1:8080/ Que el contenedor aparezca Up no basta: comprueba datos persistentes, resolución DNS y proxy público. Si depende de un montaje de red que tarda más que Docker, crea una dependencia del servicio o reintentos de aplicación en lugar de confiar en el orden accidental.
Registra cuánto tarda en recuperar servicio. Esa medición establece una expectativa real de recuperación ante parches de kernel o reinicio inesperado. Si el negocio no tolera ese tiempo o el único host, la solución requiere redundancia fuera de ese VPS, no una política de reinicio más agresiva.
Comprobar hora y DNS antes de culpar al registro
Descargas y autenticación pueden fallar si el VPS no resuelve nombres o tiene el reloj desajustado. Antes de regenerar tokens revisa:
timedatectl status
getent hosts registry-1.docker.io
curl -I https://registry-1.docker.io/v2/ Una respuesta HTTP de autenticación puede demostrar que red y TLS llegan al registro, aunque todavía falten credenciales. Un error de certificado con fechas incoherentes apunta al reloj; un nombre sin dirección apunta a DNS.
No desactives verificación TLS ni configures un registro inseguro para superar el síntoma. Si existe un proxy corporativo, instala su autoridad según la política y documenta alcance. Las imágenes son código ejecutable: perder autenticidad del canal convierte una reparación de conectividad en un riesgo para todo el host.
Instala Docker desde su repositorio oficial, comprueba hello-world y aprende puertos, volúmenes y redes antes de desplegar. La comodidad del contenedor no elimina las responsabilidades del servidor.
Hosting con LiteSpeed, NVMe y soporte 24/7 desde $3/mes.