Docker no es para escalar: es para que el servidor deje de ser un misterio
Mi servidor no tiene PHP instalado. Ni Node, ni Composer, ni psql. Y aun asi corre cinco plataformas Laravel. Las tres ventajas de Docker que se cobran solas, contadas con la imagen de 282 MB que despliegue ayer.
Ayer desplegué una aplicación nueva de Laravel en mi servidor. Corrí cuatro comandos y quedó sirviendo. Después, por curiosidad, corrí esto en la máquina anfitriona:
for b in php node npm composer psql; do command -v $b || echo "$b: no instalado"; done
Los cinco salieron no instalado. En ese servidor corren cinco plataformas Laravel, tres bots de n8n, un microservicio en Java y almacenamiento S3. Y no hay un solo PHP en el sistema operativo.
Eso es lo que Docker me devuelve. No es escalar —no escalo nada, es un VPS de 3.5 GB—, es que el servidor dejó de ser un lugar donde se acumulan cosas que nadie recuerda haber instalado.
1. La imagen se construye igual hoy que en noviembre
El Dockerfile de esa aplicación tiene tres etapas. La primera resuelve las dependencias de PHP, la segunda compila el frontend, y la tercera —la única que se queda— se lleva el resultado:
# ---------- 1. Dependencias PHP ----------
FROM php:8.4-cli-alpine AS vendor
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
RUN composer install --no-dev --prefer-dist --no-interaction
# ---------- 2. Assets ----------
FROM php:8.4-cli-alpine AS assets
RUN apk add --no-cache nodejs npm
COPY --from=vendor /app ./
RUN npm ci && npm run build && rm -rf node_modules
# ---------- 3. Runtime ----------
FROM php:8.4-fpm-alpine AS runtime
COPY --from=assets /app ./
CMD ["php-fpm"]
La imagen final pesa 282 MB y no tiene dentro ni npm, ni composer, ni node_modules. Tiene el vendor/ resuelto, el public/build/ compilado y php-fpm. Nada más.
Esto importa por una razón que no es el tamaño: lo que se compiló es lo que corre. No hay un paso manual entre "funcionó en mi máquina" y "está en el servidor". El día que alguien clone el repo dentro de tres meses y construya, va a obtener el mismo composer.lock y el mismo package-lock.json resueltos con las mismas versiones base de PHP 8.4 y Alpine.
La alternativa —que es como trabajaba antes— es un servidor donde alguien corrió composer install en octubre, otro actualizó npm en enero, y nadie sabe por qué el build de marzo salió distinto.
2. Las versiones no se pelean porque no se conocen
Mis tres aplicaciones más nuevas corren PHP 8.4.24. Las tres reportan exactamente lo mismo:
$ docker exec hm-app php -v | head -1
PHP 8.4.24 (cli) (built: Jul 30 2026 22:47:50) (NTS)
Ahora bien: si mañana un cliente necesita quedarse en PHP 8.2 seis meses más, cambio una línea en su Dockerfile. No hay update-alternatives, no hay dos versiones de PHP peleándose por /usr/bin, no hay que decidir cuál gana. Cada aplicación trae su propio intérprete y no sabe que las demás existen.
Lo mismo con Postgres. Cada stack tiene el suyo, en su contenedor, con su volumen:
db:
image: postgres:17-alpine
volumes:
- pgdata:/var/lib/postgresql/data
Cuesta memoria —eso es real y lo pago todos los días— pero el día que una migración deja una base a medias, restauro esa base y las otras cuatro ni se enteran.
3. Mover todo a otro servidor es copiar tres cosas
Esta semana me preguntaron si podíamos llevar uno de los bots a otro VPS. La respuesta corta fue sí, y la lista de lo que hay que copiar cabe en un renglón: el compose.yml, el .env y el volumen de datos.
No hay que copiar la aplicación. La imagen se vuelve a bajar o se vuelve a construir. No hay que reinstalar dependencias del sistema, ni acordarse de qué extensiones de PHP hacían falta, ni averiguar qué versión de Node tenía la máquina vieja: todo eso está escrito en el Dockerfile, versionado, y se ejecuta solo.
Compáralo con migrar un servidor tradicional. Ahí la lista es "todo lo que alguien instaló alguna vez, y ojalá esté documentado".
Lo que Docker no te regala
Para que esto no suene a folleto, tres cosas que sigo pagando:
- Memoria. Cinco Postgres pesan más que uno. En una máquina con 3.5 GB eso se siente.
- Una capa más que entender. Cuando algo falla, ahora hay tres lugares donde mirar: la aplicación, el contenedor y el anfitrión. Un 502 puede venir de cualquiera de los tres.
- Los volúmenes son su propio tema. Docker solo copia el contenido de la imagen a un volumen cuando el volumen está vacío. Si no lo sabes, un día despliegas código nuevo y el sitio sigue sirviendo el viejo, sin ningún error.
Ninguna de las tres me ha hecho volver atrás. Pero las tres me costaron una tarde la primera vez.