Skip to content

ci cd y despliegue

Janio Isacura edited this page Oct 16, 2026 · 1 revision

CI/CD y despliegue

Esta página acompaña a la guía de estudio; allí están el mapa general y la ruta recomendada. Aquí no se estudia Angular: se explica cómo llega todo lo que se escribe hasta internet (las tres aplicaciones y esta misma wiki) y cómo está montado. Sirve para repasar y para contarlo en una entrevista o en una charla técnica.


Módulo P1: El panorama completo

El problema que resuelve

Hay dos repositorios, tres aplicaciones, una wiki privada, otra pública y un sitio web de la wiki. Si no se ve el conjunto, parece magia: se edita un archivo y minutos después cambia una página en internet. Este módulo pone todas las piezas en un solo dibujo.

Los dos repositorios

Repositorio Visibilidad Qué contiene
Reskyon/angular-ufh Privado El código de los proyectos (01-… a 04-…), la GitHub Action mirror-wiki.yml y su wiki privada, que es la fuente de verdad del contenido
Reskyon/angular-wiki Público Una copia de sólo lectura de la wiki, más lo necesario para construir el sitio web: Dockerfile, wiki-ref y la carpeta site/
Reskyon/angular-ufh  (privado)
├── 01-typescript-intro/          no se despliega (se ve en la consola)
├── 02-bases/                     ──► angular-basics.reskyon.com
├── 03-gifs-app/                  ──► angular-gifs.reskyon.com
├── 04-country-app/               ──► angular-country.reskyon.com
├── .github/workflows/
│   └── mirror-wiki.yml           copia la wiki y avisa al sitio
└── (wiki privada)                Reskyon/angular-ufh.wiki   ← se edita aquí

Reskyon/angular-wiki  (público)
├── (wiki pública)                copia de sólo lectura
├── Dockerfile                    lo usa Coolify
├── wiki-ref                      commit de la wiki que hay que publicar
└── site/
    ├── mkdocs.yml                configuración del sitio y del tema
    ├── nginx.conf                cómo se sirve
    └── scripts/prepare.py        convierte la wiki al formato de MkDocs
                                  ──► angular-wiki.reskyon.com

El mapa de todo

                         ┌──────────────────────────────┐
   tú (VS Code, git) ──► │  Reskyon/angular-ufh (main)  │
                         └───────┬──────────────┬───────┘
                                 │              │
                  push a main    │              │  editas la wiki privada
                                 ▼              ▼
                         ┌─────────────┐   ┌──────────────────────┐
                         │   Coolify   │   │  GitHub Action       │
                         │ (3 apps)    │   │  mirror-wiki.yml     │
                         └──────┬──────┘   └─────┬──────────┬─────┘
                                │                │          │
                                │                ▼          ▼
                                │       wiki pública   wiki-ref en
                                │      (copia, sólo    Reskyon/angular-wiki
                                │       lectura)       (commit a main)
                                │                           │
                                ▼                           ▼
                   angular-basics / -gifs /          ┌─────────────┐
                   -country .reskyon.com             │   Coolify   │
                                                     │ (sitio wiki)│
                                                     └──────┬──────┘
                                                            ▼
                                                 angular-wiki.reskyon.com

Dos flujos independientes, que sólo comparten dónde viven los archivos:

  1. El código (push a main) lo despliega Coolify directamente.
  2. El contenido (editar la wiki) lo copia una GitHub Action y, al cambiar wiki-ref, Coolify reconstruye el sitio.

Qué es CI y qué es CD aquí

Término Qué es Dónde ocurre en este proyecto
CI (integración continua) Verificar cada cambio automáticamente (compilar, probar, revisar estilo) El build en Coolify compila la app y la wiki (--strict). No hay todavía una etapa de pruebas ni de lint automática
CD (despliegue continuo) Publicar solo, sin pasos manuales, lo que está en la rama principal Coolify reconstruye y publica en cada push a main
Automatización de contenido Mover y transformar archivos entre repositorios La GitHub Action mirror-wiki.yml y el script prepare.py

Ser honesto con esta tabla es lo que más se valora en una entrevista: el CD existe y funciona; la parte de CI se apoya en que el build falla si algo está mal, y todavía no corre pruebas.

Preguntas de entrevista

1. ¿Por qué hay dos repositorios en vez de uno?

Respuesta:

Por visibilidad y por responsabilidad. El de estudio es privado y contiene el código y la fuente de la wiki. El público existe para exponer el contenido (copia de sólo lectura) y para alojar lo que se necesita para construir el sitio web. Así se publica la teoría sin abrir el código de los ejercicios.

2. ¿Dónde está la fuente de verdad del contenido?

Respuesta:

En la wiki privada de Reskyon/angular-ufh. La pública se sobrescribe con cada copia (push --force), por lo que cualquier edición hecha directamente en ella se pierde. Una sola fuente de verdad evita que dos copias diverjan.


Módulo P2: De la edición de la wiki al sitio web

El problema que resuelve

Quieres escribir en la wiki de siempre (Markdown, desde el editor) y que el sitio web se actualice solo, sin entrar a ningún panel. Además, sin que la copia pública pueda quedar desactualizada ni que un enlace roto llegue a producción.

El camino de un cambio, paso a paso

 1  Editas una página de la wiki privada
         │   (git push a Reskyon/angular-ufh.wiki, o desde la web de GitHub)
         ▼
 2  GitHub dispara la Action  "Mirror wiki"
         │   por: evento gollum | ejecución manual | cron diario 06:17 UTC
         ▼
 3  Job "mirror" (ubuntu-latest)
    ├─ a) clona la wiki privada            (token GITHUB_TOKEN)
    ├─ b) push --force a la wiki pública   (secreto WIKI_MIRROR_TOKEN)
    └─ c) escribe el commit en wiki-ref de Reskyon/angular-wiki
            ├─ si wiki-ref ya tiene ese commit → no hace nada
            └─ si es distinto → commit "chore: publish wiki <sha7>" a main
         ▼
 4  Coolify detecta el push a main de Reskyon/angular-wiki
         ▼
 5  docker build   (Dockerfile del repositorio público)
    ├─ etapa 1: python:3.13-slim + git + mkdocs 1.6.1 + mkdocs-material 9.7.7
    │     git clone de la wiki pública ──► git checkout <wiki-ref>
    │     python scripts/prepare.py /wiki docs
    │     mkdocs build --strict
    └─ etapa 2: nginx:alpine  ◄── copia sólo el HTML generado
         ▼
 6  Contenedor nuevo ──► angular-wiki.reskyon.com
        (si el build falla, queda publicada la versión anterior)

Piezas clave

Pieza Para qué sirve
on: gollum Evento de GitHub cuando se crea o edita una página de la wiki
workflow_dispatch Permite lanzar la Action a mano desde la pestaña Actions
schedule (cron) Red de seguridad diaria: por si una edición por git push no disparó gollum
concurrency con cancel-in-progress: false La ejecución en curso no se cancela: la nueva espera a que termine
WIKI_MIRROR_TOKEN Secreto con permiso de escritura sobre el repositorio público; GITHUB_TOKEN sólo alcanza para leer el privado
wiki-ref Un archivo con un solo commit: la versión exacta de la wiki que debe tener el sitio

Por qué existe wiki-ref

El sitio se construye a partir de la wiki, pero Coolify sólo sabe reaccionar a cambios en el repositorio (no en su wiki). wiki-ref es el puente: la Action escribe ahí el commit de la wiki, eso es un push a main del repositorio, y Coolify lo ve y reconstruye.

Dos ventajas:

  • Es idempotente: si el archivo ya contiene ese commit, la Action no crea ningún commit nuevo y no hay un despliegue de más.
  • Es reproducible: el sitio siempre se construye desde un commit concreto de la wiki (git checkout <wiki-ref>), no desde "lo último que haya".

Se puede ver funcionando: el wiki-ref actual de Reskyon/angular-wiki coincide con el último commit de la wiki privada.

Qué hace prepare.py y cómo se actualiza el menú

La wiki de GitHub y MkDocs no hablan el mismo idioma. El script los traduce, y sólo corre en la etapa de construcción:

/wiki  (Markdown de la wiki)               docs/  (lo que lee MkDocs)
──────────────────────────                 ─────────────────────────
Home.md                          ──►       index.md
study-guide.md                   ──►       study-guide.md
01-typescript-intro.md           ──►       01-typescript-intro.md
...
_Sidebar.md, _Footer.md          ──►       (no se publican)

(02-bases#ancla)                 ──►       (02-bases.md#ancla)

Además, reescribe el bloque nav: de mkdocs.yml:

nav:
  - Inicio                 ← fijo (index.md)
  - Guía de estudio        ← fijo
  - CI/CD y despliegue     ← página extra declarada en el script (EXTRA)
  - <H1 de 01-…>           ┐
  - <H1 de 02-…>           │ una entrada por cada página NN-nombre.md,
  - <H1 de 03-…>           │ ordenada por número, con su primer "# "
  - ...                    ┘ como título
  - Hoja de atajos         ← fijo (al final)

Consecuencias prácticas:

  • Una página nueva con nombre NN-nombre.md aparece sola en el menú, ordenada por su número.
  • Las páginas que no son una parte del curso (como esta, ci-cd-y-despliegue) se declaran en una lista del script (EXTRA) y salen entre la Guía de estudio y la Parte 1. Si la página no existe en la wiki, simplemente no entra.
  • Una página con otro nombre, que no está en esa lista, hace fallar el build (el script lo detiene a propósito para que nadie publique una página que no sale en el menú).
  • Una página sin título # también falla: no tendría texto para el menú.
  • El contenido de las páginas no se modifica; sólo cambian los enlaces entre páginas.

Por qué --strict

mkdocs build --strict convierte cualquier aviso en error: un enlace a una página que no existe, o a un ancla que no existe, rompe el build. El resultado práctico es que un error en la wiki nunca llega a producción: el despliegue falla y Coolify deja la versión anterior.

Esto ya ocurrió: un enlace con un ancla mal escrita hizo fallar el despliegue, y el sitio siguió sirviendo la versión previa hasta corregirlo.

Errores comunes

  • Editar la wiki pública a mano: se pierde en la siguiente copia (push --force).
  • Crear una página con un nombre fuera del patrón (notas.md): el build falla con la lista de páginas que no están en el menú.
  • Un ancla mal escrita: las anclas siguen las reglas de GitHub (minúsculas, sin signos de puntuación, espacios a guiones). input.required pasa a inputrequired.
  • Cambiar la versión de MkDocs sin revisar Material: MkDocs 2 todavía no es compatible con el tema, por eso el Dockerfile fija mkdocs==1.6.1.
  • Esperar un cambio al instante: son varios pasos (copia, commit, build); tarda unos minutos.

Resumen

Editar la wiki privada dispara una Action que la copia a la pública y escribe el commit en wiki-ref. Ese cambio en main hace que Coolify construya una imagen: descarga la wiki en ese commit, prepare.py la adapta y regenera el menú, mkdocs build --strict la convierte en HTML y Nginx la sirve. Si algo no cuadra, el build falla y el sitio anterior sigue en pie.

Preguntas de entrevista

1. ¿Cómo se actualiza la wiki pública y el sitio web cuando se edita una página?

Respuesta:

Una GitHub Action (disparada por la edición de la wiki, por ejecución manual o por un cron diario) clona la wiki privada, la sube a la pública con push --force y escribe el commit resultante en el archivo wiki-ref del repositorio público. Ese commit a main dispara el redespliegue en Coolify, que construye el sitio a partir de ese commit exacto.

2. ¿Para qué sirve wiki-ref?

Respuesta:

Es el puente entre dos mundos: Coolify sólo reacciona a cambios en el repositorio, no en su wiki. Al guardar el commit de la wiki en un archivo del repositorio, un cambio en el contenido se convierte en un push que Coolify entiende. Además hace el build reproducible (siempre sale de un commit concreto) e idempotente (si no cambió, no hay commit nuevo).

3. ¿Qué hace prepare.py y por qué hace falta?

Respuesta:

Convierte la wiki de GitHub al formato que necesita MkDocs: renombra Home.md a index.md, reescribe los enlaces entre páginas para que incluyan .md y regenera el bloque nav: de mkdocs.yml a partir de las páginas NN-nombre.md. Hace falta porque la wiki enlaza sin extensión y MkDocs necesita el archivo para resolver el enlace; y para que el menú no se mantenga a mano.

4. ¿Qué pasa si una edición deja un enlace roto?

Respuesta:

mkdocs build --strict falla, el despliegue no se completa y Coolify mantiene la versión anterior del sitio. El error aparece en el log de Coolify con el enlace o el ancla concreta, y se arregla editando la wiki, lo que dispara un nuevo intento.

5. ¿Por qué cancel-in-progress está en false?

Respuesta:

Porque la ejecución en curso puede estar a medias (copiada la wiki pero sin escribir wiki-ref). Cancelarla dejaría la copia pública y el sitio inconsistentes. Con false, la ejecución en curso termina y la nueva espera su turno.


Módulo P3: Cómo se configura todo en Coolify

El problema que resuelve

Coolify es la plataforma que construye y publica. Aunque se configura con formularios, cada campo tiene una razón. Entenderlas es lo que permite reproducir el montaje o arreglarlo cuando falla.

Cuatro aplicaciones, un mismo patrón

Aplicación Repositorio Base directory Dominio
02-bases Reskyon/angular-ufh /02-bases angular-basics.reskyon.com
03-gifs-app Reskyon/angular-ufh /03-gifs-app angular-gifs.reskyon.com
04-country-app Reskyon/angular-ufh la carpeta del proyecto angular-country.reskyon.com
Sitio de la wiki Reskyon/angular-wiki raíz del repositorio angular-wiki.reskyon.com

Campos comunes de cada aplicación:

Campo en Coolify Valor Para qué
Repositorio / rama Reskyon/angular-ufh, main De dónde se clona
Build pack Dockerfile Usa el Dockerfile del proyecto en vez de adivinar
Base directory La carpeta del proyecto Hace que COPY y npm ci trabajen dentro de esa carpeta
Dockerfile location /Dockerfile Relativa al Base directory
Puerto expuesto 80 El de Nginx dentro del contenedor
Dominio https://angular-….reskyon.com Coolify configura el proxy y el certificado
Watch paths Vacío Cualquier push a main redespliega

Por qué Base directory y no un Dockerfile en la raíz

El repositorio tiene varios proyectos independientes, cada uno con su package.json. Un solo Dockerfile en la raíz tendría que decidir cuál compilar. Con un Dockerfile por carpeta y Base directory, un mismo repositorio publica varias aplicaciones, cada una con su propia configuración en Coolify.

El Dockerfile de las aplicaciones Angular

etapa 1  node:24-alpine
         ├─ npm ci            instala exactamente lo del lockfile
         └─ npm run build     genera dist/<proyecto>/browser
etapa 2  nginx:alpine
         ├─ nginx.conf        fallback de SPA (try_files … /index.html)
         └─ copia sólo dist/<proyecto>/browser
  • Dos etapas: la imagen final no tiene Node, ni node_modules, ni el código fuente: sólo archivos estáticos y Nginx.
  • nginx.conf con try_files … /index.html: sin él, recargar una ruta de Angular (/country) daría 404, porque en el servidor esa ruta no es un archivo.
  • Por qué node:24-alpine y no el modo por defecto de Coolify: el generador automático (Nixpacks) instala un Node más viejo que el mínimo que pide Angular 22.

Variables de entorno y claves (el caso de 03-gifs-app)

La clave de Giphy no se sube a git. Se crea en Coolify como variable de build con el prefijo NG_APP_; un script (set-env.mjs) la convierte en un archivo que la app importa. La explicación completa, con su porqué, está en el Módulo G5 y el despliegue de esa app en el Módulo G8.

Del dominio al contenedor

navegador ──► Cloudflare (DNS + proxy, HTTPS)
                    │  SSL Full (strict)
                    ▼
              servidor (VPS)
                    │
                    ▼
              Traefik  (proxy de Coolify, certificado Let's Encrypt)
                    │  según el dominio
                    ▼
              contenedor de la app (Nginx, puerto 80)
  • Cloudflare: registro A con proxy hacia el servidor.
  • Traefik: lo gestiona Coolify; enruta cada dominio a su contenedor.
  • Un error 526 de Cloudflare casi siempre significa que el despliegue falló y no hay contenedor: el certificado que ve Cloudflare es el de Traefik por defecto. Se revisa el log de Coolify, no Cloudflare.

Qué ocurre en cada despliegue

push a main
    │
    ▼
Coolify clona el repositorio en esa rama
    │
    ▼
docker build  (Dockerfile del Base directory)
    │ falla ──► se descarta el contenedor nuevo, queda el anterior
    ▼
arranca el contenedor nuevo ──► el proxy cambia a él

Cosas que se pueden mejorar (para una entrevista)

Conocer los límites del montaje es parte de la respuesta:

Hoy Mejora posible
Cualquier push a main redespliega las tres apps (Watch paths vacíos) Poner Watch paths por carpeta (03-gifs-app/**)
No hay pruebas ni lint automáticos antes de publicar Una GitHub Action en cada pull request con ng test, npm run lint y npm run format:check
Se publica directo desde main Ramas de preview o un entorno de staging
Sin health check Activarlo, para que Coolify sólo cambie al contenedor nuevo cuando responda
Claves en variables de build Para secretos reales, un backend (en el front, todo es público)

Errores comunes

  • Poner la URL con tree/main/... como repositorio: es el repositorio completo; la subcarpeta se indica en Base directory.
  • Olvidar el puerto: Coolify no sabe a qué puerto del contenedor enviar el tráfico.
  • No marcar una variable como disponible en build: existe, pero no llega al npm run build.
  • Cambiar la carpeta de salida de ng build (el nombre del proyecto de Angular) y no el COPY del Dockerfile: el build se corta porque no encuentra la carpeta.

Resumen

Un repositorio con varios proyectos publica varias aplicaciones gracias a un Dockerfile por carpeta y a Base directory en Coolify. Cada push a main construye una imagen en dos etapas (Node compila, Nginx sirve), y si el build falla, queda la versión anterior. El dominio pasa por Cloudflare y por Traefik hasta el contenedor.

Preguntas de entrevista

1. ¿Cómo publicas varias aplicaciones desde un único repositorio?

Respuesta:

Con un Dockerfile dentro de cada carpeta de proyecto y una aplicación de Coolify por cada una, indicando en cada caso el Base directory. Coolify clona el mismo repositorio, entra en esa subcarpeta y construye con su Dockerfile. Cada aplicación tiene su dominio, su puerto y sus variables.

2. ¿Por qué un Dockerfile en dos etapas?

Respuesta:

La primera etapa tiene todo lo necesario para compilar (Node, dependencias, código fuente). La segunda sólo lleva el resultado (archivos estáticos) y un servidor ligero. La imagen final es mucho más pequeña, arranca antes y tiene menos superficie de ataque, porque no incluye herramientas de desarrollo.

3. Una SPA de Angular da 404 al recargar /country. ¿Qué pasa y cómo se arregla?

Respuesta:

La ruta /country existe sólo en el navegador (la define el enrutador de Angular); en el servidor no hay un archivo con ese nombre. Hay que decirle a Nginx que, si no encuentra el archivo, entregue index.html y deje que Angular resuelva la ruta: try_files $uri $uri/ /index.html.

4. El dominio muestra un error de certificado de Cloudflare tras desplegar. ¿Qué miras primero?

Respuesta:

El log de despliegue en Coolify, no Cloudflare. Ese error casi siempre indica que el despliegue falló y no hay contenedor corriendo: el proxy responde con su certificado por defecto, que Cloudflare (en modo Full strict) rechaza. Se arregla el build, no el DNS.

5. ¿Qué mejorarías de este flujo de despliegue?

Respuesta:

Añadiría una etapa de CI antes de publicar (pruebas, lint y comprobación de formato en cada pull request), Watch paths por proyecto para no redesplegar todo con cada cambio, un health check para que sólo se cambie de contenedor cuando el nuevo responda, y un entorno de preview o staging. Y dejaría claro que las claves del front son públicas, y que los secretos reales necesitan un backend.


Módulo P4: Cómo contarlo en una entrevista o charla

El problema que resuelve

Saber que algo funciona no es lo mismo que poder explicarlo en pocos minutos. Este módulo es un guion para contar el montaje completo, a distintos niveles de detalle.

La versión de 30 segundos

Tengo un repositorio privado con tres aplicaciones Angular y su teoría en una wiki. Cada push a main lo despliega Coolify con un Dockerfile por proyecto. La wiki se publica sola: una GitHub Action la copia a un repositorio público y deja anotado el commit en un archivo, lo que hace que Coolify reconstruya el sitio con MkDocs.

La versión de 2 minutos (con el dibujo)

  1. Qué hay: dos repositorios (privado con el código y la wiki fuente; público con la copia y el sitio) y cuatro sitios publicados.
  2. El código: un push a main → Coolify clona, usa el Dockerfile de la carpeta (Base directory), construye en dos etapas y publica detrás de Traefik y Cloudflare.
  3. El contenido: editar la wiki → GitHub Action → copia a la wiki pública y wiki-ref → Coolify → prepare.py + mkdocs --strict → Nginx.
  4. Qué protege: el build estricto (nada roto llega a producción) y que el sitio sale de un commit concreto de la wiki.
  5. Qué mejoraría: pruebas en CI, Watch paths por proyecto y health check.

Los tres dibujos que conviene saber de memoria

Código            push a main ──► Coolify ──► docker build ──► app publicada

Contenido         editar wiki ──► Action ──► copia + wiki-ref ──► Coolify ──► sitio

Dentro del build  wiki @ wiki-ref ──► prepare.py ──► docs/ ──► mkdocs --strict ──► HTML ──► Nginx

Preguntas que suelen hacer

Pregunta Idea de la respuesta
"¿Qué es CI/CD en tu proyecto?" CD: Coolify publica en cada push. CI: el build falla si algo está mal; falta añadir pruebas automáticas
"¿Cómo manejas los secretos?" En el front no hay secretos: todo es público. Lo que no se quiere en git entra por variables de build; un secreto real necesita un backend
"¿Qué pasa si falla un despliegue?" Coolify descarta el contenedor nuevo y sigue sirviendo el anterior; el log dice qué falló
"¿Por qué Docker y no Nixpacks?" Para fijar la versión de Node que exige Angular 22 y controlar cómo se construye
"¿Cómo evitas que la copia de la wiki quede vieja?" La Action se dispara con cada edición, y hay un cron diario como red de seguridad
"¿Cómo escalaría esto?" Watch paths por proyecto, pruebas en pull requests, entornos de preview y health checks

Para mostrarlo en una charla técnica

  1. Empieza por el problema, no por las herramientas: «quería escribir la teoría en Markdown y que se publicara sola».
  2. Enseña el dibujo del panorama (Módulo P1) y deja que el público lo mire unos segundos.
  3. Haz una demostración corta: editar una frase en la wiki y mostrar la Action y el despliegue en Coolify.
  4. Cuenta un error real, como el del ancla rota que detuvo un despliegue: muestra que el build estricto cumple su función.
  5. Cierra con lo que mejorarías. Demuestra criterio.

Resumen

El montaje se explica con dos flujos (código y contenido), un archivo puente (wiki-ref) y un build estricto que protege producción. Lo que más suma en una entrevista es contarlo con un dibujo, reconocer las limitaciones (todavía no hay pruebas automáticas) y saber qué se mejoraría.

Preguntas de entrevista

1. Resume en un minuto cómo se publica tu proyecto.

Respuesta:

Hay dos flujos. El código vive en un repositorio privado y cada push a main lo despliega Coolify con un Dockerfile por proyecto: Node compila y Nginx sirve. El contenido vive en una wiki; una GitHub Action la copia a un repositorio público y anota el commit en un archivo, lo que hace que Coolify reconstruya el sitio con MkDocs. El build es estricto: si hay un enlace roto, no se publica y queda la versión anterior.

2. ¿Qué parte te parece más frágil y cómo la mejorarías?

Respuesta:

La falta de pruebas automáticas antes de publicar: ahora el único control es que el build compile. Añadiría una Action en cada pull request que ejecute pruebas, lint y la comprobación de formato, y activaría los health checks en Coolify. También acotaría los Watch paths para que un cambio en un proyecto no redespliegue los demás.

Clone this wiki locally