--- title: "Servidor de desarrollo con varios proyectos en Ubuntu 24.04" description: "Aloja varios proyectos de desarrollo en un mismo servidor Ubuntu 24.04 con Docker Compose, un subdominio por proyecto, Nginx como proxy inverso y HTTPS." category: "Casos de Uso" language: es source: https://cubepath.com/es/docs/casos-de-uso/servidor-de-desarrollo-con-multiples-proyectos updated: 2026-09-25 --- # Servidor de desarrollo con varios proyectos en Ubuntu 24.04 Cuando un equipo trabaja en varios proyectos a la vez, cada uno con su propio lenguaje, versión y base de datos, instalarlo todo directamente en el sistema acaba en conflictos de versiones y en un servidor imposible de mantener. En este tutorial montarás un servidor de desarrollo en Ubuntu 24.04 donde cada proyecto vive aislado en su propio stack de Docker Compose y se publica en un subdominio propio (`app1.dev.your_domain`, `app2.dev.your_domain`) a través de Nginx, con HTTPS de Let's Encrypt y acceso protegido por contraseña. Como ejemplo usarás dos proyectos con stacks distintos: una API en Node.js 22 y una aplicación PHP 8.3 con MariaDB. El mismo patrón sirve para cualquier otro proyecto que puedas meter en un contenedor. ## Requisitos previos Para seguir esta guía necesitas: - Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 2 GB de RAM (4 GB si vas a ejecutar más de tres o cuatro proyectos con base de datos). - Un usuario no root con privilegios `sudo`. - Un dominio con acceso a su DNS. En esta guía se usa `your_domain`; sustitúyelo por el tuyo. - Un registro DNS comodín de tipo A `*.dev.your_domain` que apunte a la IP pública del servidor (`your_server_ip`). Así cualquier proyecto nuevo tendrá subdominio sin tocar el DNS. - UFW activo con SSH permitido. Comprueba que el registro comodín resuelve antes de continuar: ```bash dig +short app1.dev.your_domain ``` ```output 203.0.113.10 ``` ## Paso 1: Instalar Docker Engine y Docker Compose Docker aísla las dependencias de cada proyecto: cada uno puede usar su propia versión de Node, PHP o base de datos sin afectar a los demás. Instálalo desde el repositorio oficial de Docker: ```bash sudo apt update sudo apt install 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 ``` Añade el repositorio: ```bash sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null < Importante: publica siempre los puertos de los contenedores en `127.0.0.1`. Docker añade sus propias reglas de iptables y un puerto publicado como `3001:3000` queda abierto a Internet aunque UFW no lo permita. Con `127.0.0.1:3001:3000` solo Nginx, desde el propio servidor, puede llegar a él. ## Paso 3: Crear el primer proyecto (Node.js) Crea una aplicación mínima de Node.js. Abre el archivo: ```bash nano /srv/projects/app1/server.js ``` Añade este contenido: ```javascript const http = require('node:http'); const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ project: 'app1', node: process.version, path: req.url })); }); server.listen(3000, '0.0.0.0', () => { console.log('app1 escuchando en el puerto 3000'); }); ``` Crea el archivo de Compose del proyecto: ```bash nano /srv/projects/app1/compose.yaml ``` ```yaml services: web: image: node:22-alpine working_dir: /app volumes: - ./:/app command: node --watch server.js ports: - "127.0.0.1:3001:3000" restart: unless-stopped ``` El código se monta como volumen y `node --watch` reinicia el proceso cada vez que cambia un archivo, así que puedes editar en el servidor (o sincronizar desde tu equipo) y ver los cambios al momento. Arranca el proyecto: ```bash cd /srv/projects/app1 docker compose up -d ``` Comprueba que responde en su puerto local: ```bash curl http://127.0.0.1:3001/ ``` ```output {"project":"app1","node":"v22.19.0","path":"/"} ``` ## Paso 4: Crear el segundo proyecto (PHP y MariaDB) El segundo proyecto usa otro lenguaje y necesita base de datos. Las credenciales van en un archivo `.env` que Compose lee automáticamente y que nunca debe subirse al repositorio: ```bash nano /srv/projects/app2/.env ``` ```ini MARIADB_DATABASE=app2 MARIADB_USER=app2 MARIADB_PASSWORD=your_strong_password MARIADB_ROOT_PASSWORD=your_strong_root_password ``` Sustituye las dos contraseñas por valores propios y restringe los permisos del archivo: ```bash chmod 600 /srv/projects/app2/.env ``` Crea una página PHP que se conecte a la base de datos: ```bash nano /srv/projects/app2/src/index.php ``` ```php query('SELECT VERSION()')->fetchColumn(); echo "app2: PHP " . PHP_VERSION . ", MariaDB " . $version . "\n"; ``` Como la imagen oficial de PHP no incluye la extensión `pdo_mysql`, crea un `Dockerfile` mínimo que la instale: ```bash nano /srv/projects/app2/Dockerfile ``` ```dockerfile FROM php:8.3-apache RUN docker-php-ext-install pdo_mysql ``` Ahora el archivo de Compose con los dos servicios: ```bash nano /srv/projects/app2/compose.yaml ``` ```yaml services: web: build: . volumes: - ./src:/var/www/html env_file: .env ports: - "127.0.0.1:3002:80" depends_on: - db restart: unless-stopped db: image: mariadb:11 env_file: .env volumes: - db_data:/var/lib/mysql restart: unless-stopped volumes: db_data: ``` La base de datos no publica ningún puerto: solo es accesible desde la red interna que Compose crea para este proyecto, donde el contenedor web la encuentra por el nombre `db`. Cada proyecto tiene su propia red, así que `app1` no puede ver la base de datos de `app2`. Construye y arranca el proyecto: ```bash cd /srv/projects/app2 docker compose up -d --build ``` MariaDB tarda unos segundos en inicializarse la primera vez. Comprueba el estado y prueba la página: ```bash docker compose ps curl http://127.0.0.1:3002/ ``` ```output app2: PHP 8.3.26, MariaDB 11.8.3-MariaDB-ubu2404 ``` ## Paso 5: Instalar Nginx como proxy inverso Nginx recibirá todas las peticiones en los puertos 80 y 443 y las repartirá a cada proyecto según el subdominio. Instálalo y abre los puertos web en UFW: ```bash sudo apt install nginx sudo ufw allow 'Nginx Full' ``` Como es un entorno de desarrollo, no debe ser público. Protege todos los proyectos con autenticación básica. Instala la utilidad `htpasswd` y crea el primer usuario (te pedirá la contraseña): ```bash sudo apt install apache2-utils sudo htpasswd -c /etc/nginx/.htpasswd your_user ``` Para añadir más usuarios después, repite el comando sin `-c` (esa opción sobrescribe el archivo). Crea un fragmento con la configuración de proxy común a todos los proyectos, para no repetirla en cada archivo: ```bash sudo nano /etc/nginx/snippets/dev-proxy.conf ``` ```nginx auth_basic "Entorno de desarrollo"; auth_basic_user_file /etc/nginx/.htpasswd; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # WebSockets (recarga en caliente de Vite, Next.js, etc.) proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; ``` La variable `$connection_upgrade` hay que definirla una vez a nivel `http`. Crea este archivo: ```bash sudo nano /etc/nginx/conf.d/connection-upgrade.conf ``` ```nginx map $http_upgrade $connection_upgrade { default upgrade; '' close; } ``` ## Paso 6: Publicar cada proyecto en su subdominio Crea un server block por proyecto. Empieza por `app1`: ```bash sudo nano /etc/nginx/sites-available/app1.dev.your_domain ``` ```nginx server { listen 80; listen [::]:80; server_name app1.dev.your_domain; location / { include snippets/dev-proxy.conf; proxy_pass http://127.0.0.1:3001; } } ``` Y el de `app2`, idéntico salvo el nombre y el puerto: ```bash sudo nano /etc/nginx/sites-available/app2.dev.your_domain ``` ```nginx server { listen 80; listen [::]:80; server_name app2.dev.your_domain; client_max_body_size 50m; location / { include snippets/dev-proxy.conf; proxy_pass http://127.0.0.1:3002; } } ``` Activa los dos sitios, desactiva el sitio por defecto, comprueba la sintaxis y recarga Nginx: ```bash sudo ln -s /etc/nginx/sites-available/app1.dev.your_domain /etc/nginx/sites-enabled/ sudo ln -s /etc/nginx/sites-available/app2.dev.your_domain /etc/nginx/sites-enabled/ sudo rm /etc/nginx/sites-enabled/default sudo nginx -t sudo systemctl reload nginx ``` ```output nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful ``` Prueba un proyecto sin credenciales y con ellas: ```bash curl -I http://app1.dev.your_domain curl -u your_user http://app1.dev.your_domain ``` ```output HTTP/1.1 401 Unauthorized ... {"project":"app1","node":"v22.19.0","path":"/"} ``` ## Paso 7: Activar HTTPS con Let's Encrypt Instala Certbot con su plugin de Nginx: ```bash sudo apt install certbot python3-certbot-nginx ``` Solicita un certificado para los dos subdominios. Certbot valida cada nombre por HTTP, modifica los server blocks para servir HTTPS y añade la redirección desde HTTP: ```bash sudo certbot --nginx -d app1.dev.your_domain -d app2.dev.your_domain ``` ```output Successfully deployed certificate for app1.dev.your_domain to /etc/nginx/sites-enabled/app1.dev.your_domain Successfully deployed certificate for app2.dev.your_domain to /etc/nginx/sites-enabled/app2.dev.your_domain Congratulations! You have successfully enabled HTTPS ``` El paquete instala un temporizador de systemd que renueva los certificados automáticamente. Comprueba que la renovación funciona: ```bash sudo certbot renew --dry-run ``` Abre `https://app1.dev.your_domain` y `https://app2.dev.your_domain` en el navegador: te pedirá usuario y contraseña y después mostrará cada proyecto. ## Paso 8: Añadir un proyecto nuevo Con esta base, añadir un proyecto son siempre los mismos cuatro pasos: 1. Crear `/srv/projects/nombre` con su `compose.yaml`, publicando el siguiente puerto libre en `127.0.0.1` (por ejemplo `127.0.0.1:3003`). 2. Arrancarlo con `docker compose up -d` y comprobarlo con `curl http://127.0.0.1:3003/`. 3. Copiar uno de los server blocks, cambiar `server_name` y `proxy_pass`, enlazarlo en `sites-enabled` y recargar Nginx. 4. Emitir su certificado con `sudo certbot --nginx -d nombre.dev.your_domain`. Para ver de un vistazo qué proyectos están en marcha y cuánto consumen: ```bash docker compose ls docker stats --no-stream ``` ```output NAME STATUS CONFIG FILES app1 running(1) /srv/projects/app1/compose.yaml app2 running(2) /srv/projects/app2/compose.yaml ``` Si un proyecto no se usa durante un tiempo, páralo con `docker compose stop` desde su carpeta para liberar memoria; los datos de sus volúmenes se conservan. ## Solución de problemas **`502 Bad Gateway`**: Nginx no puede conectar con el contenedor. Comprueba que el proyecto está en marcha con `docker compose ps` en su carpeta, que el puerto de `proxy_pass` coincide con el publicado y revisa los logs con `docker compose logs -f web`. **`SQLSTATE[HY000] [2002] Connection refused` en app2**: MariaDB aún se está inicializando o no ha arrancado. Revisa `docker compose logs db`. Si cambiaste las credenciales del `.env` después del primer arranque, ten en cuenta que MariaDB solo las aplica al crear el volumen: bórralo con `docker compose down -v` (se pierden los datos) y vuelve a arrancar. **Certbot falla con `unauthorized` o `NXDOMAIN`**: el subdominio no resuelve a tu servidor. Comprueba el registro comodín con `dig +short nombre.dev.your_domain` y que el puerto 80 está abierto en UFW. **La recarga en caliente del frontend no funciona**: falta la cabecera `Upgrade` para WebSockets. Asegúrate de que el server block incluye `snippets/dev-proxy.conf` y de que existe `/etc/nginx/conf.d/connection-upgrade.conf`. ## Conclusión Tienes un servidor de desarrollo en Ubuntu 24.04 donde cada proyecto corre aislado en su propio stack de Docker Compose, con sus versiones y su base de datos, y se publica en un subdominio propio con HTTPS y autenticación básica. Añadir un proyecto nuevo es cuestión de minutos y no afecta a los que ya están en marcha. Como siguientes pasos, puedes: - Conectar tu editor al servidor con la extensión Remote SSH de VS Code para trabajar directamente sobre `/srv/projects`. - Programar copias de seguridad de los volúmenes de base de datos con `docker compose exec db mariadb-dump`. - Automatizar el despliegue de cada rama desde tu sistema de CI con `docker compose up -d --build` por SSH.