Módulo 4: Entornos dev, staging y prod en self-hosted
4. Variables de entorno y claves de cifrado
Descripción
Al terminar esta lección vas a poder darle a cada entorno de Cumbre su propia configuración a través de un archivo .env: vas a entender a fondo qué es un archivo .env y cómo lo lee Docker Compose, vas a generar una N8N_ENCRYPTION_KEY distinta por entorno y a explicar con precisión qué se rompe si la compartes o la cambias, y vas a configurar el WEBHOOK_URL y el host propios de cada entorno para que sus URLs no se crucen. Vas a cerrar, además, el reflejo de seguridad del módulo: el .env con valores reales siempre fuera del repositorio, el .env.example dentro como contrato de configuración.
Esto importa porque el .env es donde vive todo lo que distingue a un entorno de otro. En la lección 3 dejaste un plano común lleno de huecos ${...}; esta lección los rellena, y cada hueco mal rellenado es un problema real: una clave compartida es una grieta de seguridad, un WEBHOOK_URL equivocado son webhooks rotos, un .env commiteado es un incidente. Y hay una razón más profunda: la N8N_ENCRYPTION_KEY es la pieza más delicada de todo el sistema. Entenderla bien —qué cifra, qué pasa si se pierde, por qué cada entorno tiene la suya— es lo que separa a quien opera entornos de verdad de quien copia comandos y reza.
Conexión con el módulo: la lección 3 montó el esqueleto —tres carpetas, un plano común, el .gitignore—. Esta lo amuebla con el .env de cada entorno. Retoma directamente el hilo del Módulo 3, lección 3, donde conociste la N8N_ENCRYPTION_KEY y la regla de "los secretos fuera del repo"; aquí esa regla se vuelve operativa por entorno. La lección 5 va a apoyarse en lo que aprendas aquí sobre la clave de cifrado para explicar por qué las credenciales se recrean en cada entorno en vez de copiarse. Piensa en esta lección como la que convierte tres esqueletos idénticos en tres entornos con identidad propia.
Qué es un archivo .env, con manzanas
Empecemos por el objeto central de la lección. Un archivo .env —se lee "punto env", de environment, entorno— es un archivo de texto plano donde escribes, una por línea, variables con el formato NOMBRE=valor. Nada más: nombres a la izquierda del =, valores a la derecha, un par por renglón.
POSTGRES_USER=cumbre
POSTGRES_PASSWORD=k7Rx9mQ2vL8pN4wT
N8N_PORT=5678
Su propósito es separar la configuración de el código. El docker-compose.yml —el plano— no debe cambiar entre entornos; lo que cambia son los valores. El .env es donde viven esos valores. Cuando Docker Compose lee el plano y encuentra un hueco como ${N8N_PORT}, va al .env de esa carpeta, busca una línea que empiece con N8N_PORT=, y rellena el hueco con lo que haya a la derecha del =.
Piénsalo como un formulario para llenar espacios en blanco. El plano es el formulario impreso: "Nombre: ____, Puerto: ____, Contraseña: ____". El mismo formulario sirve para muchas personas. El .env es el formulario ya llenado por una persona concreta: el de dev pone unos valores, el de prod pone otros. Mismo formulario, respuestas distintas. Por eso un solo plano sirve para tres entornos: cada uno trae su propio .env con sus respuestas.
Tres propiedades del .env que conviene tener claras desde ya:
- Docker Compose lo lee automáticamente si se llama exactamente
.envy está en la misma carpeta desde donde corresdocker compose. No tienes que decirle "usa este archivo"; si está ahí, lo toma. (También puedes pasárselo a mano con--env-file, pero el automático es lo cómodo.) - Es texto plano, sin comillas ni punto y coma.
N8N_PORT=5678, noN8N_PORT="5678";. Las líneas que empiezan con#son comentarios y se ignoran. - Contiene secretos de verdad. El
.envreal lleva las contraseñas y la clave de cifrado sin cifrar, a la vista. Por eso —y esto es la regla que no se rompe— el.envnunca se sube al repositorio. Lo vas a ver en detalle al final de la lección, pero grábalo desde ahora: el.enves un archivo privado de cada máquina, no del repo.
La N8N_ENCRYPTION_KEY: repaso y profundización
Ya conociste la clave de cifrado en el Módulo 3. Vamos a repasarla en una frase y luego a profundizar en lo que este módulo agrega.
La N8N_ENCRYPTION_KEY es la llave maestra de una instancia de n8n: la cadena de caracteres con la que n8n cifra todas las credenciales antes de guardarlas en su base de datos, y con la que las descifra cuando un workflow las necesita. Es una sola llave por instancia, y cifra todos los secretos de esa instancia. Recuerda la analogía del Módulo 3: la clave es la combinación de una caja fuerte, y las credenciales cifradas son lo que está adentro. La combinación y la caja juntas abren todo; por separado, cada una es inútil.
Según la documentación oficial, hay dos formas de que una instancia tenga su clave:
- Automática: si no le das ninguna, n8n genera una clave aleatoria la primera vez que arranca y la guarda en la carpeta
~/.n8n(en su archivo de configuración). No tienes que hacer nada; aparece sola. En nuestro stack con Docker, esa carpeta~/.n8nvive dentro del volumenn8n_storage, así que la clave generada persiste ahí. - Explícita: le pasas tu propia clave por la variable de entorno
N8N_ENCRYPTION_KEYantes del primer arranque, y n8n usa esa en vez de generar una. Es exactamente lo que hace nuestrodocker-compose.yml:N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}toma la clave del.env.
¿Por qué preferimos la explícita, si la automática "funciona sola"? Por dos razones que son el corazón de este módulo:
Primera: control. Si dejas que cada entorno genere su clave solo, no sabes cuál es, vive escondida en un volumen, y si algún día ese volumen se pierde o se recrea, la clave se va con él y no tienes copia. Con la clave explícita en el .env, tú la conoces, la respaldas en tu lugar seguro, y puedes recrear el entorno sin perder acceso a las credenciales cifradas.
Segunda: la haces distinta por entorno a propósito. Este es el punto nuevo del módulo. Si dejas que cada entorno genere una clave aleatoria por su cuenta, tus tres entornos van a tener claves distintas —bien—, pero por accidente, y sin que tú las controles. Poniéndolas tú en cada .env, las haces distintas deliberadamente, sabes cuáles son, y las tratas como los secretos que son.
Por qué cada entorno tiene su propia clave, y por qué compartirla es peligroso
Esta es la idea que este módulo agrega sobre el Módulo 3, y merece detenerse. Cada entorno de Cumbre tiene su propia N8N_ENCRYPTION_KEY, distinta de las otras dos. No es un descuido: es una decisión de seguridad. Veamos las dos consecuencias, una deseada y una que hay que evitar.
La consecuencia deseada: los secretos no cruzan entre entornos. Como dev y prod cifran con llaves distintas, una credencial cifrada en dev es basura ilegible en prod, y viceversa. Aunque alguien copiara la base de datos entera de dev a prod, las credenciales de dev no se podrían usar en prod, porque la llave de prod no las descifra. Esto es una pared: aísla los secretos de un entorno de los otros. Si la llave de dev —la menos protegida, la que más gente toca— se filtrara, no le sirve de nada a un atacante para leer los secretos de prod, porque prod cifra con otra llave. Claves distintas = compartimentos estancos.
La consecuencia que hay que evitar: compartir la clave junta los compartimentos. Si usaras la misma N8N_ENCRYPTION_KEY en los tres entornos, romperías esa pared. Una credencial cifrada en dev se podría descifrar en prod, y —peor— el día que la clave se filtre por el entorno más débil (dev, donde experimentas, donde pruebas cosas, donde el cuidado es menor), esa misma clave abre los secretos de prod. Compartir la clave hace que la seguridad de tu producción dependa de la seguridad de tu entorno de juguete. Es exactamente al revés de lo que quieres. Por eso: una clave por entorno, distintas a propósito, nunca compartidas.
Qué pasa cuando la clave cambia (el error que aterra a los juniors)
Hay un escenario que causa pánico y conviene entender antes de que te pase. Si la N8N_ENCRYPTION_KEY de una instancia cambia después de que ya cifró credenciales, esas credenciales dejan de poder descifrarse.
El mecanismo es directo: las credenciales están cifradas con la clave vieja. Si arrancas la instancia con una clave nueva, n8n intenta descifrarlas con la nueva, no coincide, y falla. En la interfaz vas a ver errores del estilo "la credencial no se pudo descifrar", y los workflows que dependían de esas credenciales dejan de autenticarse. La base de datos está intacta, los workflows están intactos, pero los secretos quedaron encerrados con una llave que ya no tienes.
Esto pasa más de lo que crees, por descuidos concretos:
- Levantas el stack con la clave automática (n8n la generó y la guardó en el volumen), y más tarde le pones una clave explícita distinta en el
.env. La nueva no coincide con la que cifró las credenciales. - Borras el volumen
n8n_storage(donde vivía la clave automática) y recreas la instancia: la clave nueva no descifra lo viejo. - Copias el
.envde un entorno a otro y, con él, su clave, sobre una instancia que ya tenía credenciales cifradas con otra.
La lección práctica es doble. Una: fija la clave explícita en el .env desde el primer arranque de cada entorno, y no la cambies. Dos: respalda cada clave en tu lugar seguro (un gestor de contraseñas), porque si la pierdes, pierdes el acceso a todas las credenciales de ese entorno. La clave no es un detalle de configuración; es la única copia de la llave de tu caja fuerte. (n8n tiene un procedimiento oficial para rotar la clave de forma controlada, que re-cifra las credenciales con la llave nueva; eso es distinto de cambiarla a lo bruto, que es lo que rompe todo.)
Ejemplo trabajado: generar y colocar una clave por entorno
Vamos a generar tres claves —una por entorno— y a colocarlas en sus .env. Recuerda: tú corres estos comandos; la guía no los ejecuta.
Paso 1 — Genera una clave aleatoria. Una clave de cifrado no es una palabra que inventas; es una cadena larga y aleatoria, imposible de adivinar. La forma estándar de generarla es con openssl, una herramienta de criptografía que casi seguro ya tienes instalada:
openssl rand -hex 32
Desmenucemos el comando: openssl rand genera bytes aleatorios; -hex los muestra como caracteres hexadecimales (0-9 y a-f), fáciles de copiar sin ambigüedad; 32 es la cantidad de bytes, que en hexadecimal se traduce en una cadena de 64 caracteres. Qué esperar: una línea como esta (la tuya será distinta, es aleatoria):
9f2c8a1b4e7d60039f2c8a1b4e7d60039f2c8a1b4e7d60039f2c8a1b4e7d6003
Paso 2 — Córrelo tres veces, una por entorno. Cada entorno necesita su propia clave, así que corres el comando tres veces y obtienes tres cadenas distintas. Qué esperar: tres cadenas de 64 caracteres, todas diferentes. Esa diferencia es la que aísla los secretos entre entornos.
Paso 3 — Coloca cada clave en el .env de su entorno. Recuerda que en la lección 3 creaste los .env.example. Ahora, en cada carpeta de entorno, creas el .env real a partir del ejemplo y lo rellenas. Para dev:
# environments/dev/.env (este archivo NO se sube al repo)
COMPOSE_PROJECT_NAME=cumbre-dev
N8N_PORT=5678
N8N_HOST=localhost
WEBHOOK_URL=http://localhost:5678/
N8N_ENCRYPTION_KEY=9f2c8a1b4e7d6003... # la PRIMERA cadena que generaste
POSTGRES_USER=cumbre
POSTGRES_PASSWORD=k7Rx9mQ2vL8pN4wT # otra cadena aleatoria, distinta por entorno
POSTGRES_DB=n8n
Y para prod, el mismo formulario con otras respuestas:
# environments/prod/.env (este archivo NO se sube al repo)
COMPOSE_PROJECT_NAME=cumbre-prod
N8N_PORT=5680
N8N_HOST=localhost
WEBHOOK_URL=http://localhost:5680/
N8N_ENCRYPTION_KEY=3d5e0a2f9c4b7108... # la TERCERA cadena: DISTINTA a la de dev
POSTGRES_USER=cumbre
POSTGRES_PASSWORD=Zq2Wp9Lm4Xn7Vb1 # otra contraseña, distinta a la de dev
POSTGRES_DB=n8n
Mira las dos diferencias que importan entre dev y prod: la N8N_ENCRYPTION_KEY es otra cadena (compartimentos estancos), y el POSTGRES_PASSWORD también (bases separadas, con credenciales de acceso separadas). El POSTGRES_USER y el POSTGRES_DB pueden coincidir sin problema, porque cada entorno tiene su propia base de datos aislada; lo que no puede coincidir es lo que da acceso o descifra secretos.
Paso 4 — Verifica que Compose lee el .env. Cuando entres en la carpeta del entorno y levantes el stack, Compose toma el .env de esa carpeta automáticamente. Puedes confirmar que las variables se resolvieron bien, sin arrancar nada, con:
docker compose config
config le pide a Compose que muestre el plano ya con los huecos rellenados por el .env, sin levantar contenedores. Qué esperar: el docker-compose.yml impreso con ${N8N_PORT} reemplazado por 5678, etc. Si ves los huecos todavía como ${...} o vacíos, el .env no se está leyendo (revisa que se llame exactamente .env y esté en esa carpeta). Es una forma segura de comprobar la configuración antes de encender el motor.
Las URLs propias de cada entorno: N8N_HOST y WEBHOOK_URL
La clave de cifrado no es lo único que cambia por entorno. Las URLs también, y aquí hay un detalle que rompe a mucha gente cuando separa entornos por primera vez.
order-triage empieza con un nodo Webhook. Un webhook es una URL que n8n crea para que un servicio externo pueda "tocar la puerta" y disparar el workflow: cuando llega un pedido, el sistema de Cumbre hace una petición a esa URL, y n8n arranca order-triage. La URL del webhook la construye n8n a partir de su configuración de host y protocolo, y por eso es distinta en cada entorno.
Dos variables gobiernan esto:
N8N_HOST— el nombre de host donde n8n cree que está corriendo. En local eslocalhost. En un servidor de verdad sería el dominio (n8n.cumbre.com), pero recuerda que aquí levantamos entornos locales, así quelocalhostpara los tres. Su valor por defecto eslocalhost.WEBHOOK_URL— la dirección base que n8n usa para construir las URLs de los webhooks. Esta es la crítica. Si no la fijas bien, n8n arma las URLs de webhook con el host y puerto que él supone, y en un entorno que corre en un puerto no estándar, esa suposición falla.
El problema concreto: dev corre en 5678, staging en 5679, prod en 5680. Si en staging dejaras WEBHOOK_URL apuntando a 5678, n8n le mostraría a los servicios externos una URL de webhook que apunta a dev, no a staging. Un pedido dirigido a staging tocaría la puerta de dev, o la petición fallaría porque la URL no corresponde. La URL del webhook y el puerto real del entorno tienen que contar la misma historia.
Por eso en cada .env el WEBHOOK_URL acompaña al puerto:
| Entorno | N8N_PORT | WEBHOOK_URL |
|---|---|---|
dev | 5678 | http://localhost:5678/ |
staging | 5679 | http://localhost:5679/ |
prod | 5680 | http://localhost:5680/ |
La regla mecánica: cada vez que cambias el puerto de un entorno, cambia también su WEBHOOK_URL. Van en pareja. Es de las cosas más fáciles de olvidar y de las que más confusión causan, porque el síntoma —"mi webhook apunta al lugar equivocado"— no grita "es el WEBHOOK_URL"; hay que saber dónde mirar. Ahora lo sabes.
Otros secretos del .env y una lista de verificación por entorno
La N8N_ENCRYPTION_KEY es el secreto estrella del .env, pero no el único. Conviene conocer los demás para no dejar ninguno con un valor de ejemplo por descuido.
POSTGRES_PASSWORD— la contraseña de la base de datos de ese entorno. Como cada entorno tiene su propia base aislada, cada uno lleva su propia contraseña, distinta y aleatoria. Aunque la base no esté expuesta a internet (recuerda: no tiene puerto público), una contraseña aleatoria es la práctica correcta; no dejespassword.N8N_USER_MANAGEMENT_JWT_SECRET— si tu stack lo usa (aparece en el Starter Kit oficial), es la cadena con la que n8n firma las sesiones de los usuarios que inician sesión en el editor. Como la clave de cifrado, es un secreto aleatorio que conviene fijar propio y distinto por entorno, no dejar el valor de ejemplo. Su detalle exacto conviene confirmarlo en la doc de tu versión.
La regla que unifica a todos: cualquier valor del .env que sea una contraseña, una llave o un secreto se genera aleatorio, se hace distinto por entorno, y jamás se deja en el valor de ejemplo. Lo que puede repetirse sin problema entre entornos es la configuración no secreta: POSTGRES_USER, POSTGRES_DB, N8N_HOST. Lo que da acceso o cifra, nunca.
Para no dejar cabos sueltos, una lista de verificación que puedes correr mentalmente sobre el .env de cada entorno antes de levantarlo:
-
COMPOSE_PROJECT_NAMEes único de este entorno (cumbre-dev/cumbre-staging/cumbre-prod). -
N8N_PORTes el de este entorno (5678/5679/5680) y no choca con otro. -
WEBHOOK_URLapunta al mismo puerto queN8N_PORT. -
N8N_ENCRYPTION_KEYes una cadena aleatoria, distinta de la de los otros entornos, y está respaldada en tu gestor de contraseñas. -
POSTGRES_PASSWORDes aleatoria y distinta por entorno. - Ningún valor secreto quedó con el marcador de ejemplo (
password,super-secret-key, vacío donde debería ir un secreto). - Este
.envno aparece engit status(lo cubre el.gitignore).
Correr esta lista sobre los tres .env toma un minuto y evita los tres errores más caros del módulo: un puerto que choca, una clave compartida, y un secreto sin cambiar. Un minuto de checklist contra una tarde de depuración: el trato es bueno.
El reflejo de seguridad del módulo: .env fuera, .env.example dentro
Llegamos al punto que, como en el Módulo 3, importa más que cualquier otro de la lección. Y no es exageración repetirlo, porque es el error que hunde a un junior.
El .env con valores reales nunca, jamás, bajo ninguna circunstancia, se sube al repositorio. Contiene la clave de cifrado y las contraseñas de la base de datos en texto plano. Si ese archivo llega a Git —y peor, a GitHub—, has publicado las llaves maestras de tus entornos. Los bots que rastrean secretos en repositorios públicos las encuentran en minutos.
Lo que sí va al repositorio es el .env.example: el mismo formulario, con los nombres de todas las variables pero sin los valores secretos. Es el contrato de configuración: le dice a cualquiera que clone el repo "estas son las variables que tienes que llenar para levantar este entorno", sin filtrar ni una llave. Alguien que reciba el repo copia .env.example a .env, rellena sus propios valores, y arranca. El molde viaja; el contenido, no.
La distinción, otra vez, es la misma del Módulo 3:
| Archivo | ¿Qué contiene? | ¿Va al repo? |
|---|---|---|
.env | Los valores reales: clave de cifrado, contraseñas | Nunca |
.env.example | Los nombres de las variables, sin valores | Sí, es el contrato |
Y la red de seguridad que hace esto a prueba de descuidos es el .gitignore que ya pusiste en la lección 3:
# Cualquier .env en cualquier carpeta, incluidos los de environments/*/
**/.env
**/.env.*
!**/.env.example
El **/.env ignora los .env de las tres carpetas de entorno; el !**/.env.example rescata las plantillas. Así, aunque hagas un git add . distraído, los .env reales son invisibles para Git.
El reflejo, en tres pasos, que debes correr antes de cada commit:
- Corre
git status. - Confirma que aparecen los
docker-compose.ymly los.env.example, y que no aparece ningún.env. - Si ves un
.enven la lista, detente: el.gitignoreno está bien, y estás a ungit addde publicar una clave. Arréglalo antes de seguir.
Qué esperar al correr git status con todo bien puesto: ves environments/dev/docker-compose.yml, environments/dev/.env.example y sus equivalentes de staging y prod, pero ninguno de los tres .env. Los secretos existen en tu disco, pero son invisibles para Git. Esa invisibilidad es la señal de que hiciste bien tu trabajo.
Errores comunes
Compartir la N8N_ENCRYPTION_KEY entre entornos (conceptual, y grave). Qué pasa: alguien genera una sola clave y la pone igual en los tres .env, pensando "así es más simple". Rompe la pared entre entornos: un secreto de dev se descifra en prod, y el día que la clave se filtre por dev —el entorno más expuesto—, abre también los secretos de prod. Por qué pasa: tener una sola clave que recordar es cómodo, y el peligro es invisible mientras nada se filtra. Cómo detectarlo: compara la línea N8N_ENCRYPTION_KEY= de los tres .env; si son iguales, tienes el problema. Cómo corregirlo: una clave distinta por entorno, generada con openssl rand -hex 32, respaldada por separado. La comodidad de una sola clave no vale poner la seguridad de tu producción en manos de tu entorno de pruebas.
Cambiar la clave sobre una instancia que ya cifró credenciales (práctico, y causa pánico). Qué pasa: alguien arranca un entorno, crea credenciales, y después cambia la N8N_ENCRYPTION_KEY en el .env (o borra el volumen donde vivía la clave automática). Al reiniciar, las credenciales "no se pueden descifrar" y los workflows fallan a autenticar. Por qué pasa: no se entiende que las credenciales están atadas a la clave con que se cifraron, y cambiar la clave las deja encerradas. Cómo detectarlo: errores de "credencial no se pudo descifrar" tras un cambio de clave o un borrado de volumen. Cómo corregirlo: fija la clave explícita desde el primer arranque y no la cambies; respáldala. Si de verdad necesitas cambiarla, usa el procedimiento oficial de rotación, que re-cifra las credenciales, no un cambio a lo bruto.
Olvidar sincronizar WEBHOOK_URL con el puerto (práctico). Qué pasa: alguien copia el .env de dev a staging, cambia el N8N_PORT a 5679, pero deja WEBHOOK_URL=http://localhost:5678/. Los webhooks de staging muestran URLs que apuntan a dev, y las peticiones caen en el entorno equivocado o fallan. Por qué pasa: son dos variables separadas que en realidad van en pareja, y es fácil cambiar una y olvidar la otra. Cómo detectarlo: si la URL de webhook que muestra n8n no coincide con el puerto por el que entras al editor, están desincronizadas. Cómo corregirlo: cada vez que toques N8N_PORT, toca también WEBHOOK_URL para que apunten al mismo puerto. Van juntas, siempre.
Perder la clave por no respaldarla (práctico). Qué pasa: alguien deja que n8n genere la clave automática, no la anota en ningún lado, y un día recrea el volumen o migra de máquina. La clave se fue con el volumen, y ahora ninguna credencial de ese entorno se puede descifrar. Por qué pasa: la clave automática es invisible, "funciona sola", y por eso se olvida que existe y que es irremplazable. Cómo detectarlo: pregúntate "¿sé cuál es la clave de cifrado de prod y tengo una copia en lugar seguro?". Si la respuesta es no, estás a un accidente de perder tus credenciales. Cómo corregirlo: usa clave explícita en el .env, y respalda cada clave en tu gestor de contraseñas. La clave es la única copia de la combinación de tu caja fuerte: sin ella, lo cifrado se pierde.
Ejercicios
Ejercicio 1 — Diagnostica las credenciales rotas. Un compañero te escribe: "Levanté prod, creé la credencial del CRM, todo funcionaba. Hoy reinicié y n8n dice que la credencial no se puede descifrar. No cambié el workflow." Le preguntas qué tocó, y admite que ayer editó el .env para 'ordenarlo' y de paso reemplazó la N8N_ENCRYPTION_KEY por una nueva más bonita. ¿Qué pasó, exactamente, y se puede recuperar la credencial?
Ver solución
Lo que pasó: la credencial del CRM se cifró con la clave original. Al reemplazar la N8N_ENCRYPTION_KEY por una nueva, n8n ahora intenta descifrar con la clave nueva, que no coincide con la que cifró la credencial, y falla. La base de datos y el workflow están intactos; el secreto quedó encerrado con una llave que ya no está en el .env.
¿Se puede recuperar? Solo si tu compañero todavía tiene la clave original en algún lado (un respaldo, el historial del editor, la terminal). Si la anotó o la puede recuperar, la vuelve a poner en el .env, reinicia, y la credencial se descifra de nuevo. Si la clave original se perdió del todo, la credencial cifrada es irrecuperable: hay que recrearla —entrar al CRM, obtener la llave, y volver a crear la credencial en n8n con la clave nueva ya fija—.
Por qué funciona: este caso te enseña la regla más importante de la clave —no se cambia sobre credenciales ya cifradas— viviendo el pánico en cabeza ajena. Y te deja la lección de respaldar la clave: si tu compañero la hubiera tenido guardada, la recuperación era trivial.
Ejercicio 2 — Llena los tres .env (las líneas que cambian). Escribe, para los tres entornos, solo las cinco líneas que cambian entre ellos: COMPOSE_PROJECT_NAME, N8N_PORT, WEBHOOK_URL, N8N_ENCRYPTION_KEY y POSTGRES_PASSWORD. Para las claves y contraseñas no hace falta generarlas de verdad; escribe un marcador que deje claro que cada una es distinta (por ejemplo <clave-dev>, <clave-staging>, <clave-prod>). Después señala cuáles de esas cinco líneas son secretos que jamás van al repo.
Ver solución
dev:
COMPOSE_PROJECT_NAME=cumbre-dev
N8N_PORT=5678
WEBHOOK_URL=http://localhost:5678/
N8N_ENCRYPTION_KEY=<clave-dev>
POSTGRES_PASSWORD=<password-dev>
staging:
COMPOSE_PROJECT_NAME=cumbre-staging
N8N_PORT=5679
WEBHOOK_URL=http://localhost:5679/
N8N_ENCRYPTION_KEY=<clave-staging>
POSTGRES_PASSWORD=<password-staging>
prod:
COMPOSE_PROJECT_NAME=cumbre-prod
N8N_PORT=5680
WEBHOOK_URL=http://localhost:5680/
N8N_ENCRYPTION_KEY=<clave-prod>
POSTGRES_PASSWORD=<password-prod>
Los secretos que jamás van al repo son las dos últimas líneas de cada entorno: N8N_ENCRYPTION_KEY y POSTGRES_PASSWORD. Las otras tres (COMPOSE_PROJECT_NAME, N8N_PORT, WEBHOOK_URL) no son secretas —son configuración— y de hecho aparecen con sus valores en el .env.example. Los secretos aparecen en el .env.example solo con el nombre y el = vacío.
Por qué funciona: separar "las cinco que cambian" de "las que además son secretas" es justo el criterio que decide qué va al .env.example (todo, pero con los secretos vacíos) y qué va solo al .env real (los valores secretos). Si lo tienes claro, no vas a filtrar una clave por accidente.
Ejercicio 3 — Verifica el blindaje. En tu esqueleto de cumbre-automations, con los .gitignore y .env.example en su lugar, crea a mano tres archivos .env vacíos (uno en cada carpeta de entorno) para simular el peligro. Corre git status y anota qué aparece y qué no. Explica por qué los .env.example sí se ven y los .env no, refiriéndote a las líneas del .gitignore.
Ver solución
git status debería mostrar los tres docker-compose.yml y los tres .env.example, pero ninguno de los tres .env.
La razón está en las líneas del .gitignore: **/.env ignora cualquier archivo llamado exactamente .env en cualquier subcarpeta, así que los tres .env de las carpetas de entorno quedan fuera. La excepción !**/.env.example rescata específicamente los archivos de ejemplo, así que esos tres sí los ve Git y los puedes versionar como contrato. El ** es lo que hace que el patrón alcance las tres carpetas environments/*/, no solo la raíz.
Por qué funciona: ver con tus propios ojos que git status esconde los .env —aun estando ahí, en el disco— es lo que convierte el .gitignore de una idea abstracta en una red de seguridad en la que confías. Y confirmar que el ** alcanza las subcarpetas te evita el error clásico de un .gitignore que solo protege la raíz y deja expuestos los .env de environments/.
Resumen y siguiente paso
En esta lección amueblaste los tres esqueletos con su .env. Entendiste que un archivo .env es un formulario de "llenar espacios en blanco" —NOMBRE=valor, una línea por variable— que Docker Compose lee para rellenar los huecos ${...} del plano, y que por eso un solo plano sirve para tres entornos con respuestas distintas. Profundizaste en la N8N_ENCRYPTION_KEY: qué cifra, cómo la genera n8n (automática en ~/.n8n, o explícita por variable), por qué la fijamos explícita para controlarla y respaldarla, y por qué cada entorno tiene la suya, distinta a propósito —para que los secretos no crucen y para que un filtrado en dev no comprometa prod—. Grabaste el escenario que aterra: si la clave cambia sobre credenciales ya cifradas, esas credenciales dejan de descifrarse, así que se fija desde el primer arranque, no se cambia, y se respalda. Generaste claves con openssl rand -hex 32, configuraste N8N_HOST y el WEBHOOK_URL propios de cada entorno —que van en pareja con el puerto—, y cerraste el reflejo de seguridad del módulo: .env fuera del repo, .env.example dentro como contrato, con el .gitignore de **/.env como red.
Antes de avanzar deberías poder: explicar qué hace Docker Compose con un .env; decir por qué la clave de cifrado es distinta por entorno y qué pasa si se comparte o se cambia; generar una clave con openssl; y explicar por qué WEBHOOK_URL cambia junto con el puerto.
La lección 5 toma el hilo de la clave de cifrado y lo lleva a las credenciales por entorno. Ahora que cada entorno cifra con su propia llave, vas a entender por qué una credencial no se copia de un entorno a otro sino que se recrea, cómo un mismo workflow referencia la "misma" credencial aunque su valor cambie entre entornos, y por qué las llaves reales del CRM de Cumbre viven solo en prod mientras dev y staging usan cuentas de prueba. Es donde la separación de entornos se vuelve concreta en el día a día de trabajar con order-triage.
Recursos
- Set a custom encryption key — n8n Docs — qué es la
N8N_ENCRYPTION_KEY, cómo la genera n8n en~/.n8ny cómo fijar la tuya por variable de entorno. - Rotate encryption keys — n8n Docs — el procedimiento oficial para rotar la clave sin perder las credenciales, distinto de cambiarla a lo bruto.
- Configure webhook URLs — n8n Docs — cómo n8n construye las URLs de webhook a partir del host y el puerto, y por qué se fija
WEBHOOK_URL; confirma aquí los nombres de las variables para tu versión. - Environment variables in Compose — Docker Docs — cómo Compose lee el archivo
.envy resuelve las variables${...}; incluyedocker compose configpara verificar. - gitignore — Git Documentation — la referencia del formato, incluidos el comodín
**para subcarpetas y la excepción con!.