Saltar al contenido
Infrastructure 2026-09-05 22 min de lectura

Docker en un VPS: guía completa desde cero

Docker empaqueta una aplicación y sus dependencias en contenedores reproducibles. En un VPS simplifica despliegues y aislamiento de servicios, pero no administra por ti backups, secretos, actualizaciones ni firewall.

Docker en un VPS: guía completa desde cero
#Docker#VPS#Ubuntu#Contenedores
T
Equipo Terranode
Editorial

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

bash
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:

bash
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

bash
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:

bash
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:

bash
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:

bash
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:

bash
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é:

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

bash
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:

bash
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:

bash
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:

bash
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:

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

bash
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:

bash
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:

bash
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:

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

json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
bash
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.

bash
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:

bash
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:

bash
docker run --rm -it --entrypoint sh nginx:1.28-alpine
SíntomaCausa que debes comprobarHerramienta
Exited (1)Configuración o comando inválidodocker logs e inspect
Exited (137)Terminación forzada o presión de memoriaKernel y docker stats
Puerto no respondePublicación, proceso o firewalldocker port, ss, curl
No resuelve otro contenedorRedes diferentes o nombre incorrectodocker network inspect
Permiso denegado en volumenUID/GID y modo del directoriodocker exec id y stat
Reinicios continuosHealthcheck no reinicia por sí solo; revisar política y appinspect 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:

bash
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:

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

text
.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:

bash
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:

bash
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:

bash
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:

bash
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:

bash
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:

bash
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:

bash
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:

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

¿Listo para llevar tu sitio al siguiente nivel?

Hosting con LiteSpeed, NVMe y soporte 24/7 desde $3/mes.

Ver planes de hosting

Preguntas frecuentes