Dev Containers es una especificación abierta para describir un entorno de desarrollo completo (imagen base, herramientas, servicios, extensiones del editor) en un fichero .devcontainer/devcontainer.json dentro del repositorio. Cualquier persona que clone el proyecto obtiene el mismo entorno en un contenedor, con las mismas versiones, sin instalar nada más que Docker. En este tutorial prepararás un servidor Ubuntu 24.04 como máquina de desarrollo, crearás un Dev Container para un proyecto Node.js, le añadirás una base de datos PostgreSQL con Docker Compose y lo usarás tanto desde la CLI oficial como desde VS Code por SSH.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor o equipo con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 2 vCPU y 4 GB de RAM (la primera construcción de imágenes consume bastante).
  • Un usuario no root con privilegios sudo.
  • Docker Engine y el plugin Docker Compose instalados desde el repositorio oficial de Docker, con tu usuario en el grupo docker.
  • Opcional, para el paso 6: VS Code en tu equipo local y acceso SSH al servidor.

Comprueba que Docker funciona sin sudo:

docker run --rm hello-world

Paso 1: Instalar Node.js y la CLI de Dev Containers

La CLI de referencia de la especificación, @devcontainers/cli, es la misma que usa VS Code por debajo y se distribuye como paquete npm. Ubuntu 24.04 trae Node.js 18, que ya está fuera de soporte, así que instala Node.js 22 LTS desde el repositorio de NodeSource:

sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt update
sudo apt install -y nodejs

Instala la CLI de forma global:

sudo npm install -g @devcontainers/cli

Comprueba ambas versiones:

node --version
devcontainer --version
v22.x.x
0.x.x

Paso 2: Crear un Dev Container básico

Crea un proyecto de ejemplo con su directorio .devcontainer:

mkdir -p ~/demo/.devcontainer
cd ~/demo

Crea el fichero de configuración:

nano ~/demo/.devcontainer/devcontainer.json
{
  "name": "demo",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:1-22-bookworm",
  "forwardPorts": [3000],
  "postCreateCommand": "node --version && npm --version",
  "containerEnv": {
    "NODE_ENV": "development"
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode"
      ]
    }
  }
}

Lo que define cada propiedad:

  • image: la imagen base. Las imágenes de mcr.microsoft.com/devcontainers/ ya incluyen un usuario no root (node en este caso) con sudo, Git y utilidades comunes. La etiqueta 1-22-bookworm fija la versión mayor de la imagen, Node.js 22 y Debian 12.
  • forwardPorts: puertos del contenedor que el editor reenvía a tu equipo.
  • postCreateCommand: se ejecuta una sola vez, al crear el contenedor. Es el sitio para npm install o migraciones.
  • containerEnv: variables de entorno del contenedor.
  • customizations.vscode.extensions: extensiones que VS Code instala dentro del contenedor. La CLI las ignora.

Levanta el contenedor:

devcontainer up --workspace-folder .

La primera vez descarga la imagen. Al terminar, la última línea es un JSON con el resultado:

{"outcome":"success","containerId":"5d1f0c...","remoteUser":"node","remoteWorkspaceFolder":"/workspaces/demo"}

El proyecto está montado en /workspaces/demo. Ejecuta comandos dentro del contenedor con devcontainer exec:

devcontainer exec --workspace-folder . node --version
devcontainer exec --workspace-folder . whoami
v22.x.x
node

Para abrir una shell interactiva, usa devcontainer exec --workspace-folder . bash.

Paso 3: Añadir herramientas con features

Las features son paquetes reutilizables que instalan una herramienta en cualquier imagen base, sin escribir un Dockerfile. Se publican como artefactos OCI; las oficiales están en ghcr.io/devcontainers/features/. Añade GitHub CLI y acceso al Docker del anfitrión editando devcontainer.json:

nano ~/demo/.devcontainer/devcontainer.json
{
  "name": "demo",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:1-22-bookworm",
  "features": {
    "ghcr.io/devcontainers/features/github-cli:1": {},
    "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
  },
  "forwardPorts": [3000],
  "postCreateCommand": "node --version && npm --version",
  "containerEnv": {
    "NODE_ENV": "development"
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode"
      ]
    }
  }
}

docker-outside-of-docker monta el socket de Docker del anfitrión, de modo que los comandos docker dentro del contenedor gestionan los contenedores del servidor. Es más ligero que docker-in-docker, pero da al contenedor control total sobre Docker del anfitrión: úsalo solo en máquinas de desarrollo.

Los cambios en devcontainer.json no se aplican a un contenedor existente. Recréalo:

devcontainer up --workspace-folder . --remove-existing-container

Verifica que las herramientas están disponibles:

devcontainer exec --workspace-folder . gh --version
devcontainer exec --workspace-folder . docker ps

El segundo comando debe listar los contenedores del servidor, incluido el propio Dev Container.

Paso 4: Añadir PostgreSQL con Docker Compose

Casi cualquier aplicación real necesita servicios auxiliares. Cuando devcontainer.json apunta a un fichero Compose, la CLI levanta todos los servicios y te conecta al que indiques. Primero, crea un Dockerfile para el contenedor de trabajo con el cliente de PostgreSQL:

nano ~/demo/.devcontainer/Dockerfile
FROM mcr.microsoft.com/devcontainers/javascript-node:1-22-bookworm

RUN apt-get update \
    && apt-get install -y --no-install-recommends postgresql-client \
    && rm -rf /var/lib/apt/lists/*

Crea el fichero Compose:

nano ~/demo/.devcontainer/compose.yaml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ..:/workspaces/demo:cached
    command: sleep infinity
    environment:
      DATABASE_URL: postgresql://app:your_dev_password@db:5432/app
    depends_on:
      - db

  db:
    image: postgres:17
    restart: unless-stopped
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: your_dev_password
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Sustituye your_dev_password por una contraseña propia en los dos sitios. Aspectos importantes:

  • ..:/workspaces/demo monta la raíz del proyecto (el directorio padre de .devcontainer) en el contenedor.
  • command: sleep infinity mantiene vivo el contenedor app; sin un proceso en primer plano se detendría al arrancar.
  • El servicio db no publica puertos en el servidor: app lo alcanza por su nombre en la red interna de Compose.
  • El volumen pgdata conserva los datos aunque recrees el entorno.

Sustituye devcontainer.json para que use Compose en lugar de una imagen:

nano ~/demo/.devcontainer/devcontainer.json
{
  "name": "demo",
  "dockerComposeFile": "compose.yaml",
  "service": "app",
  "workspaceFolder": "/workspaces/demo",
  "shutdownAction": "stopCompose",
  "remoteUser": "node",
  "features": {
    "ghcr.io/devcontainers/features/github-cli:1": {}
  },
  "forwardPorts": [3000],
  "postCreateCommand": "psql --version",
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode"
      ]
    }
  }
}

service indica en qué contenedor se trabaja y shutdownAction: stopCompose hace que VS Code pare todos los servicios al cerrar la ventana. La feature docker-outside-of-docker se ha quitado porque este entorno no la necesita; puedes mantenerla si trabajas con contenedores desde dentro.

Elimina el contenedor del paso anterior, que usaba otra configuración, y levanta el entorno nuevo:

docker rm -f $(docker ps -aq --filter "label=devcontainer.local_folder=$HOME/demo")
devcontainer up --workspace-folder .

Comprueba la conexión desde el contenedor de trabajo. Las comillas simples evitan que tu shell expanda $DATABASE_URL en el anfitrión, donde no existe:

devcontainer exec --workspace-folder . bash -c 'psql "$DATABASE_URL" -c "select version();"'
                                  version
----------------------------------------------------------------------------
 PostgreSQL 17.x (Debian 17.x-1.pgdg120+1) on x86_64-pc-linux-gnu, ...
(1 row)

Si la base de datos aún se está inicializando la primera vez, espera unos segundos y repite el comando.

Paso 5: Parar y limpiar el entorno

La CLI no tiene un comando para detener el entorno; se hace con Docker. Para un entorno basado en Compose, localiza el proyecto que ha creado la CLI:

docker compose ls
NAME               STATUS       CONFIG FILES
demo_devcontainer  running(2)   /home/your_user/demo/.devcontainer/compose.yaml

Páralo o elimínalo con ese nombre:

docker compose -p demo_devcontainer stop
docker compose -p demo_devcontainer down

down borra los contenedores pero conserva el volumen pgdata; añade -v solo si quieres empezar con una base de datos vacía. Un nuevo devcontainer up recrea todo en segundos porque las imágenes ya están en caché.

Paso 6: Usar el Dev Container desde VS Code por SSH

Con el servidor preparado, puedes programar desde tu equipo con VS Code mientras todo se ejecuta en el servidor:

  1. En VS Code, instala las extensiones Remote - SSH (ms-vscode-remote.remote-ssh) y Dev Containers (ms-vscode-remote.remote-containers).
  2. Abre la paleta de comandos (Ctrl+Shift+P) y ejecuta Remote-SSH: Connect to Host..., con your_user@your_server_ip.
  3. Abre la carpeta ~/demo en el servidor.
  4. Ejecuta Dev Containers: Reopen in Container.

VS Code usa el Docker del servidor para construir y arrancar el mismo entorno que has creado con la CLI, instala las extensiones de customizations.vscode dentro del contenedor y reenvía el puerto 3000 a tu equipo. La terminal integrada ya se abre dentro del contenedor, como usuario node, en /workspaces/demo.

Después de editar devcontainer.json, compose.yaml o el Dockerfile, aplica los cambios con Dev Containers: Rebuild Container.

Paso 7: Compartir el entorno con el equipo

El directorio .devcontainer debe versionarse junto al código. Unas pautas para que el entorno sea reproducible de verdad:

  • Fija versiones: usa etiquetas concretas de imagen (1-22-bookworm, postgres:17) y de features (:1), nunca latest.
  • Pon los pasos de preparación (npm ci, migraciones) en postCreateCommand, para que un entorno nuevo quede listo sin instrucciones manuales.
  • No guardes secretos en devcontainer.json ni en compose.yaml; las contraseñas de este ejemplo solo sirven para la base de datos local.

Si la construcción tarda mucho, puedes construir la imagen una vez y publicarla en tu registro. Inicia sesión antes con docker login:

devcontainer build --workspace-folder . --image-name registry.your_domain/team/demo-dev:2026.09 --push

Por último, puedes ejecutar los tests en CI dentro del mismo entorno que usan los desarrolladores. En GitHub Actions basta con instalar la CLI en el runner:

name: CI
on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm install -g @devcontainers/cli
      - run: devcontainer up --workspace-folder .
      - run: devcontainer exec --workspace-folder . npm test

Solución de problemas

  • permission denied while trying to connect to the Docker daemon socket: tu usuario no está en el grupo docker. Ejecuta sudo usermod -aG docker "$USER" y vuelve a iniciar sesión.
  • La construcción falla y no se ve por qué: repite con devcontainer up --workspace-folder . --log-level trace para ver cada comando de Docker. En VS Code, usa Dev Containers: Show Container Log.
  • Los cambios de devcontainer.json no aparecen: el contenedor existente se reutiliza. Recréalo con --remove-existing-container o Rebuild Container.
  • Ficheros creados en el contenedor pertenecen a otro usuario en el servidor: en Linux, la CLI ajusta el UID del usuario remoto (node) al de tu usuario del anfitrión al crear el contenedor. Si trabajas con varios usuarios o has cambiado remoteUser, recrea el contenedor; no uses chmod 777 ni remoteUser: root como solución.
  • psql: could not translate host name "db": el contenedor no está en la red de Compose, normalmente porque devcontainer.json todavía usa image en lugar de dockerComposeFile, o porque el contenedor antiguo sigue en marcha.

Conclusión

Has instalado la CLI de Dev Containers en Ubuntu 24.04, has definido un entorno Node.js con features y una base de datos PostgreSQL mediante Docker Compose, y lo has usado desde la línea de comandos y desde VS Code por SSH. Todo el entorno queda descrito en .devcontainer, así que cualquier persona del equipo, o tu pipeline de CI, puede reproducirlo con un solo comando.

Como siguientes pasos puedes:

  • Crear una feature propia para las herramientas internas de tu equipo, siguiendo la plantilla del repositorio devcontainers/feature-starter.
  • Añadir Redis u otros servicios al mismo compose.yaml.
  • Publicar la imagen preconstruida en un registro privado y referenciarla en devcontainer.json para acelerar el primer arranque.