TL;DR
- Bedrock solo reorganiza WordPress con Composer y configuración basada en el entorno. No necesita Trellis, Ansible ni un VPS: Docker en local más hosting compartido funciona bien.
- El detalle que nadie documenta: mantienes dos archivos
.envseparados para siempre, porque elDB_HOSTque funciona en local nunca funciona en producción y viceversa. - Despliega con
git pull(vendor/,web/wp/,uploads/y.envquedan fuera de git), apunta el document root aweb/, y conoce qué cambiaDISALLOW_FILE_MODSen tu flujo de producción.
La primera vez que configuré WordPress Bedrock en un hosting compartido, pasé dos horas depurando un error de conexión a la base de datos antes de entender por qué fallaba. Todos los tutoriales que encontré mostraban el mismo archivo .env. Todos se detenían en docker compose up. Ninguno mencionaba que el valor de DB_HOST que necesitas localmente nunca funcionará en producción, y el que funciona en producción nunca funcionará en local. Son dos archivos distintos con dos valores distintos que gestionas por separado para siempre. No es complicado una vez que lo sabes. Lo que pasa es que nadie lo escribe.
Este artículo cubre exactamente el setup de WordPress Bedrock con Docker que usa este sitio: Docker Compose en local, hosting compartido de Hostinger en producción, despliegues con git pull, sin Trellis, sin Ansible, sin VPS. Las tres secciones que no existen en ningún otro sitio son el problema de los dos .env, el despliegue en producción con PHP puro y lo que DISALLOW_FILE_MODS hace realmente a tu flujo de trabajo. Todo lo demás está aquí por completitud, pero esos tres puntos son la razón de que exista este artículo.

Qué hace Bedrock (y qué no hace)
Bedrock es un boilerplate de WordPress creado por Roots que reorganiza la estructura estándar, instala WordPress core y plugins mediante Composer y sustituye wp-config.php por configuración basada en entornos. Eso es todo. No gestiona servidores, no ejecuta despliegues y no requiere Trellis, a pesar de lo que sugiere la documentación de Roots.
La estructura de directorios relevante:
your-project/
config/
application.php # reads .env, defines WP constants
environments/
development.php # debug on, file mods allowed
production.php # debug off, DISALLOW_FILE_MODS
web/ # document root — point your server here
wp/ # WordPress core (Composer-installed)
app/ # wp-content equivalent
themes/
plugins/
mu-plugins/
uploads/
wp-config.php # minimal bootstrap, loads application.php
index.php
composer.json # pins WP core + plugins as packages
.env # never committedLo que queda fuera de git: vendor/, web/wp/, web/app/uploads/ y .env. Todo lo demás está bajo control de versiones, incluido composer.lock.
El detalle clave para hosting compartido: el document root debe apuntar a web/, no a la raíz del repositorio. Si tu servidor ve la raíz del repositorio, los visitantes obtendrán un listado de directorios. Más adelante, en la sección de producción, se explica cómo configurarlo en Hostinger.
Requisitos previos
- Docker Desktop u OrbStack (Mac) — OrbStack es más rápido y ligero si usas Apple Silicon
- Composer 2.x en tu máquina (solo se necesita para el bootstrap; Docker gestiona el runtime de PHP)
- Repositorio Git en GitHub o GitLab
- Acceso SSH en el hosting compartido — el plan Business de Hostinger y superiores incluyen SSH
- Comodidad básica con archivos
.envy ejecución de comandos en terminal
Setup local con Docker Compose
Primero, crea el proyecto:
composer create-project roots/bedrock your-project
cd your-projectDespués crea tu docker-compose.yml. Este es el que usa este sitio en local:
services:
app:
image: php:8.4-apache
platform: linux/amd64
volumes:
- .:/var/www/html
ports:
- "8880:80"
depends_on:
- db
command: >
bash -c "
apt-get update -qq &&
apt-get install -y -qq libpng-dev libjpeg-dev libzip-dev zip unzip &&
docker-php-ext-install pdo_mysql gd zip &&
sed -i 's|/var/www/html|/var/www/html/web|g' /etc/apache2/sites-enabled/000-default.conf &&
a2enmod rewrite &&
apache2-foreground
"
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress
volumes:
- db_data:/var/lib/mysql
volumes:
db_data:Merece la pena nombrar algunas cosas explícitamente. La línea platform: linux/amd64 evita problemas de compatibilidad ARM en Apple Silicon. El paso docker-php-ext-install pdo_mysql no es opcional: sin él, PHP no puede comunicarse con MySQL y WordPress mostrará “Error establishing a database connection” independientemente de lo que tengas en el .env. El override del document root apunta Apache a web/, que es la estructura de Bedrock. El nombre del servicio db es lo que pones en DB_HOST, no localhost ni 127.0.0.1. Es el error más común al empezar, así que tiene su propia sección más abajo.
Arranca el stack:
docker compose up -dEjecuta WP-CLI y Composer a través del contenedor. Si tienes un Makefile, estos wrappers ahorran mucha escritura:
.PHONY: up down shell wp composer logs
up:
docker compose up -d
down:
docker compose down
shell:
docker compose exec app bash
wp:
docker compose exec app wp $(CMD) --allow-root
composer:
docker compose exec app composer $(CMD)
logs:
docker compose logs -f appInstala las dependencias dentro del contenedor, no en tu máquina, para evitar incompatibilidades de versiones de PHP:
make composer CMD="install"Después instala WordPress:
make wp CMD="core install --url=http://localhost:8880 --title='My Site' --admin_user=admin --admin_password=admin [email protected]"El sitio corre en http://localhost:8880. No montes vendor/ ni web/wp/ como volúmenes; son paquetes gestionados por Composer y no deben ser sobreescritos por archivos del host.
El problema de los dos .env
Esta es la sección que todos los tutoriales se saltan. Tu .env local y el de producción son archivos completamente distintos con valores incompatibles entre sí. Ninguno se sube a git. Los gestionas de forma independiente.
Por qué no pueden compartir valores:
| Variable | Local (Docker Compose) | Producción (Hostinger) |
|---|---|---|
DB_HOST | db (el nombre del servicio en Compose) | 127.0.0.1 o el host interno de cPanel |
DB_NAME | wordpress | u336386_prod (prefijo de cPanel) |
DB_USER | wordpress | u336386_wp |
WP_HOME | http://localhost:8880 | https://yourdomain.com |
WP_SITEURL | http://localhost:8880/wp | https://yourdomain.com/wp |
WP_ENV | development | production |
DISALLOW_FILE_MODS | no definido (o false) | true |
DB_HOST=db funciona en local porque Docker Compose crea una red privada donde los servicios se encuentran por nombre. Fuera de esa red, db no resuelve nada. Si pones DB_HOST=localhost en local, WordPress intenta conectarse por un socket Unix que no existe dentro del contenedor para el servicio MySQL. El error es “Error establishing a database connection” sin más detalle. La solución es una palabra: cambiar localhost por db.
Gestiona los dos archivos así: haz commit de un .env.example con valores de ejemplo y comentarios explicando qué debe ir en cada variable según el entorno. Guarda los valores reales en un gestor de contraseñas. Nunca los pongas en el repositorio, en Slack ni en un documento compartido.
# .env.example
DB_NAME=''
DB_USER=''
DB_PASSWORD=''
# Local Docker: use 'db' (the Compose service name)
# Production: use 127.0.0.1 or the cPanel database host
DB_HOST=''
# Local: http://localhost:8880
# Production: https://yourdomain.com
WP_HOME=''
WP_SITEURL="${WP_HOME}/wp"
WP_ENV='development'
# Salts — generate at https://roots.io/salts.html
AUTH_KEY=''
SECURE_AUTH_KEY=''
LOGGED_IN_KEY=''
NONCE_KEY=''
AUTH_SALT=''
SECURE_AUTH_SALT=''
LOGGED_IN_SALT=''
NONCE_SALT=''Producción: PHP puro en Hostinger (sin Docker)
El hosting compartido no ejecuta Docker. Hostinger Business y superiores te dan SSH, PHP 8.x, MySQL y Composer, pero no hay runtime de contenedores. No pasa nada. Bedrock es PHP puro. No necesita contenedores para funcionar; los contenedores son solo un envoltorio cómodo para el entorno local.
Configuración inicial: apuntar el document root a web/
Entra en hPanel, ve a Hosting, encuentra tu dominio y busca los ajustes de “Document root” o “Website directory”. Cámbialo de public_html a donde esté tu directorio web/ dentro de la raíz del dominio. En Hostinger la ruta tendrá una forma similar a:
/home/u336386691/domains/yourdomain.com/webEste es un paso único que la mayoría de tutoriales se salta por completo. Si no lo haces, tus visitantes llegarán a la raíz del repositorio y verán una página en blanco o un listado de directorios. Todos los demás pasos del despliegue dependen de que esto esté bien configurado.
Primer despliegue
- Conéctate al servidor por SSH:
ssh your-user@your-host -p 65002(Hostinger usa un puerto SSH no estándar). - Ve al directorio raíz del dominio y clona el repositorio:
cd /home/u336386691/domains/yourdomain.com git clone [email protected]:youruser/yourrepo.git . - Ejecuta Composer para instalar WordPress core y los plugins:
composer install --no-dev --optimize-autoloaderSi Composer no está en
$PATH, descárgalo directamente:php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" php composer-setup.php php composer.phar install --no-dev --optimize-autoloader - Crea el
.envde producción manualmente:nano .envPega los valores de producción. El nombre de la base de datos y el usuario llevarán el prefijo de la cuenta de cPanel. Encuentra el host exacto y las credenciales en hPanel bajo Databases.
- Comprueba que el sitio carga en tu dominio. Si ves una página en blanco, verifica que el document root apunta a
web/y no a la raíz del repositorio.
Despliegues posteriores
Cada despliegue siguiente sigue la misma secuencia. Ya me la sé de memoria:
- Sube tus cambios a
origin/main. - Conéctate por SSH.
- Haz pull:
git pull origin main - Ejecuta Composer solo si
composer.lockha cambiado:composer install --no-dev --optimize-autoloader - Vacía las cachés:
wp cache flush && wp litespeed-purge all - Si has cambiado slugs, reglas de reescritura o la estructura de permalinks:
wp rewrite flush --hard
Ese es el despliegue completo. Con buena conexión tarda menos de dos minutos. El flag --no-dev es importante: omite paquetes como kint, whoops y herramientas de testing que nunca deberían ejecutarse en un servidor en producción.
Qué hace DISALLOW_FILE_MODS a tu flujo de trabajo
Pon DISALLOW_FILE_MODS=true en tu .env de producción y los instaladores de plugins y temas de wp-admin desaparecen. WordPress no permitirá instalar, actualizar ni borrar plugins o temas desde el navegador. Para ser exactos: DISALLOW_FILE_MODS bloquea todas las escrituras en el sistema de archivos desde el navegador. Las actualizaciones automáticas del core son un interruptor aparte (AUTOMATIC_UPDATER_DISABLED), pero en la práctica ambos se ponen a true en un sitio Bedrock en producción porque todos los cambios pasan por Composer y git de todas formas.
Esto es intencionado y es la decisión correcta para un setup con Bedrock. Cada cambio pasa por Composer y git. Obtienes un historial de cambios limpio, despliegues reproducibles y sin deriva de configuración provocada por alguien que hace clic en “actualizar” en wp-admin un viernes por la tarde.
El cambio práctico en tu flujo de trabajo: añadir un plugin implica editar composer.json, hacer commit y desplegar. Para un plugin del registro de WordPress Packagist:
composer require wpackagist-plugin/redirectionHaz commit del composer.json y el composer.lock actualizados, sube los cambios, haz pull en el servidor, ejecuta composer install --no-dev y activa el plugin con WP-CLI:
wp plugin activate redirectionLa activación de plugins sigue funcionando: WordPress puede activar un plugin ya instalado, simplemente no puede descargar ni escribir archivos nuevos. Lo que hay que documentar para cualquier colaborador o cliente: si intentan instalar un plugin desde wp-admin y no pasa nada, no está roto. Está bloqueado intencionalmente.
No pongas DISALLOW_FILE_MODS=true en local. Tu entorno de desarrollo (configurado mediante WP_ENV=development, que carga config/environments/development.php) debe dejar las modificaciones de archivos habilitadas para que puedas probar plugins con libertad antes de añadirlos a Composer.
Saltarse Trellis: a qué renuncias realmente
Trellis es la herramienta de aprovisionamiento de servidores basada en Ansible de Roots. La documentación de Roots lo presenta como el complemento natural de Bedrock, pero es opcional y no encaja bien con el hosting compartido.
El balance honesto:
| Trellis + Bedrock | Docker local + PHP puro (este artículo) | |
|---|---|---|
| Aprovisionamiento del servidor | Automatizado mediante playbooks de Ansible | Manual, configuración única en hPanel |
| Despliegues sin downtime | Incluido (intercambio de symlinks) | No disponible; un git pull aplica los cambios en vivo de inmediato |
| SSL | Automático mediante Let’s Encrypt | Gestionado por hPanel de Hostinger (un clic) |
| Infraestructura necesaria | VPS o servidor dedicado | Cualquier hosting compartido con SSH y Composer |
| Curva de aprendizaje | Ansible, Vagrant o Multipass, configuración de Trellis | Fundamentos de Docker Compose, SSH |
| Tamaño de equipo | 3 o más desarrolladores, proyectos de clientes que necesitan rollbacks | Dev en solitario o equipo pequeño, proyectos personales |
Usa Trellis cuando tengas un VPS, necesites rollbacks automatizados o gestiones varios sitios de clientes desde un mismo servidor. Omítelo cuando estés en hosting compartido, trabajes solo y un despliegue con git pull sea suficientemente rápido. Para un portfolio personal o un sitio pequeño en producción, la sobrecarga de Trellis es real y los beneficios son marginales.
Depurar los fallos más comunes
“Error establishing a database connection” en local
Revisa DB_HOST en tu .env local. Si pone localhost o 127.0.0.1, cámbialo a db, el nombre del servicio en Compose. Así es como los contenedores en la misma red de Compose se encuentran entre sí. Nada más resuelve dentro del contenedor.
Composer falla en el servidor con “command not found”
Hostinger no siempre incluye Composer en $PATH por defecto. Ejecuta primero which composer. Si no devuelve nada, descarga el instalador directamente y usa php composer.phar para esa sesión, o añade el binario de Composer a tu path de usuario en ~/.bashrc.
WordPress carga pero faltan plugins
Hiciste pull pero no ejecutaste composer install. Los plugins no son archivos que se suban al repositorio: son paquetes de Composer. Si composer.lock cambió en el pull, los paquetes instalados están desactualizados hasta que ejecutes composer install --no-dev.
wp-admin no tiene instalador de plugins
DISALLOW_FILE_MODS=true está configurado en tu .env de producción. Es el comportamiento esperado. Instala los plugins mediante Composer y actívalos con WP-CLI.
Los uploads no aparecen tras el despliegue
web/app/uploads/ está en el gitignore. Los archivos subidos por los usuarios solo existen en el sistema de archivos del servidor, no en tu repositorio. Para migrar uploads entre entornos, usa SFTP o un plugin como WP Offload Media. Es una limitación fundamental de los despliegues basados en git, no algo específico de Bedrock.
El .env de producción fue subido al repositorio por error
Añade .env al .gitignore de inmediato si no está ya (el .gitignore por defecto de Bedrock lo incluye). Rota todos los secretos del archivo: contraseña de la base de datos, salts de WordPress y cualquier clave de API. Un .env con credenciales reales subido al repositorio debe tratarse como completamente comprometido, aunque el repositorio sea privado. Los repositorios privados acaban filtrándose.
Preguntas frecuentes
- ¿Necesito Trellis para usar Bedrock en producción?
- No. Trellis es una herramienta opcional de aprovisionamiento con Ansible del mismo equipo. Bedrock es PHP puro y funciona en cualquier servidor con PHP 8.x, MySQL y un document root configurado correctamente. Un hosting compartido con acceso SSH es suficiente.
- ¿Por qué DB_HOST tiene que ser “db” y no “localhost” en Docker Compose?
- Docker Compose crea una red privada entre tus servicios. Los servicios se encuentran por nombre, no por IP. El contenedor MySQL se llama
dben el archivo Compose, así que ese es el hostname.localhostdentro del contenedor PHP hace referencia al propio contenedor PHP, donde no hay ningún proceso MySQL. - ¿Puedo ejecutar Composer en el hosting compartido de Hostinger?
- Sí, en el plan Business y superiores, que incluyen acceso SSH. Ejecuta
which composerdespués de conectarte. Si Composer no está en$PATH, descarga el instalador conphp -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"e invócalo comophp composer.phar. - ¿Qué rompe DISALLOW_FILE_MODS=true en WordPress?
- Elimina los instaladores de plugins y temas de wp-admin y bloquea todas las escrituras en el sistema de archivos desde el navegador. La activación de plugins sigue funcionando: WordPress puede activar un plugin ya instalado, simplemente no puede descargar ni escribir archivos nuevos. Todo pasa por Composer y git.
- ¿Cómo gestiono los uploads de WordPress entre local y producción?
web/app/uploads/está en el gitignore. Para trabajar con los archivos de producción en local, usa SFTP para descargar el directorio de uploads, o sincronízalo conrsync. Para un sitio en producción serio, WP Offload Media mueve los uploads a S3 o almacenamiento de objetos compatible, accesible desde ambos entornos.- ¿Puedo usar este setup con Polylang u otros mu-plugins?
- Sí. Los plugins que requieren cargarse como mu-plugin funcionan exactamente igual: añade un archivo loader en
web/app/mu-plugins/y haz commit al repositorio. Polylang Pro, por ejemplo, vive enweb/app/mu-plugins/polylang-pro/con unpolylang-pro-loader.phpjunto a él. Ambos archivos están en el repositorio; no hace falta ninguna configuración especial de Docker.
Artículos relacionados
- El CSS funciona en local pero falla en producción — el problema de LiteSpeed UCSS que aparece cuando Bedrock ya está funcionando en Hostinger.
- Agentes de IA en el flujo de trabajo de desarrollo — el ciclo de despliegue y verificación seguro tras cada
git pull.




