Apuntes DAM
Volver al inicio

Ejercicios resueltos de Despliegue de Aplicaciones Web

Los 31 ejercicios de Despliegue de Aplicaciones Web de la web, tema a tema: cada uno con su enunciado, los datos que necesitas (código de partida, ejemplos de entrada y salida o la base de datos) y, al final, la solución explicada.

Descargar el PDF

Arquitecturas de Despliegue y la Nube

1. ¿Cuántos nueves tiene tu arquitectura?

Medio · Arquitecturas de Despliegue y la Nube · apuntesdam.com/subject/despliegue/topic/arquitecturas-despliegue

La primera línea es «objetivo X» (la disponibilidad que exige el cliente, en %). Después, una línea por capa de la arquitectura: «capa nombre disponibilidad réplicas». Las réplicas de una capa están en paralelo (la capa solo cae si caen todas: 1 − (1 − a)^n) y las capas están en serie (la disponibilidad total es el producto). Escribe por capa «nombre: n × a % → capa %» (a con 2 decimales, capa con 6), después «Total: …» con 6 decimales, «Caída máxima al mes: X min» (mes de 43.200 minutos, 1 decimal) y «Cumple el objetivo» o «No cumple el objetivo del X %».

Código de partida (bash)
#!/bin/bashexport LC_ALL=C # TODO: calcula con awkcat > /dev/null

Ejemplo: Tienda con réplicas

Entrada

objetivo 99.95
capa balanceador 99.99 1
capa web 99.5 3
capa bd 99.9 2

Salida esperada

balanceador: 1 × 99.99 % → 99.990000 %
web: 3 × 99.50 % → 99.999988 %
bd: 2 × 99.90 % → 99.999900 %
Total: 99.989888 %
Caída máxima al mes: 4.4 min
Cumple el objetivo

Ejemplo: Todo en un servidor

Entrada

objetivo 99.9
capa vps 99.9 1
capa bd 99.5 1

Salida esperada

vps: 1 × 99.90 % → 99.900000 %
bd: 1 × 99.50 % → 99.500000 %
Total: 99.400500 %
Caída máxima al mes: 259.0 min
No cumple el objetivo del 99.90 %

2. Un balanceador ponderado con caídas

Difícil · Arquitecturas de Despliegue y la Nube · apuntesdam.com/subject/despliegue/topic/arquitecturas-despliegue

Nginx reparte las peticiones entre servidores con pesos distintos usando el algoritmo round robin ponderado «suave». Cada servidor tiene un peso w y un contador cw que empieza en 0. En cada petición: a todos los servidores en servicio se les suma su peso a cw; se elige el de mayor cw (a igualdad, el primero de la lista) y a ese se le resta la suma de los pesos en servicio. La entrada tiene líneas «nombre peso», una línea «---» y después eventos: «peticion», «caido nombre» o «vuelve nombre» (al caer o volver, su cw se pone a 0). Escribe «→ nombre» por petición, «nombre fuera de servicio», «nombre de vuelta», «503 sin servidores» si no queda ninguno y, al final, «Reparto: a=N, b=N…» en el orden de la lista.

Código de partida (bash)
#!/bin/bashdeclare -A peso cw activo cuentaorden=()while read -r a b && [ "$a" != "---" ]; do    orden+=("$a"); peso[$a]=$b; cw[$a]=0; activo[$a]=1; cuenta[$a]=0done while read -r evento nombre || [ -n "$evento" ]; do    # TODO    :done reparto=()for s in "${orden[@]}"; do reparto+=("$s=${cuenta[$s]}"); done(IFS=,; echo "Reparto: ${reparto[*]}" | sed 's/,/, /g')

Ejemplo: Pesos 5, 1 y 1

Entrada

a 5
b 1
c 1
---
peticion
peticion
peticion
peticion
peticion
peticion
peticion

Salida esperada

→ a
→ a
→ b
→ a
→ c
→ a
→ a
Reparto: a=5, b=1, c=1

Ejemplo: Caídas y vueltas

Entrada

web1 2
web2 1
---
peticion
peticion
peticion
caido web1
peticion
peticion
caido web2
peticion
vuelve web1
peticion
peticion

Salida esperada

→ web1
→ web2
→ web1
web1 fuera de servicio
→ web2
→ web2
web2 fuera de servicio
503 sin servidores
web1 de vuelta
→ web1
→ web1
Reparto: web1=4, web2=3

3. Auditoría de un servidor recién instalado

Muy difícil · Arquitecturas de Despliegue y la Nube · apuntesdam.com/subject/despliegue/topic/arquitecturas-despliegue

Antes de publicar una aplicación hay que bastionar el servidor. La entrada tiene tres bloques separados por «---»: el fichero sshd_config, la lista de puertos en escucha («protocolo dirección:puerto programa», como la resume ss -tlnp) y una línea «permitidos 22,80,443». Revisa, en este orden: PermitRootLogin yes (ALTO), PasswordAuthentication distinto de no (ALTO; si no aparece, su valor por defecto es yes), MaxAuthTries mayor que 4 (MEDIO; por defecto 6) y X11Forwarding yes (MEDIO). Ojo: en sshd_config las directivas no distinguen mayúsculas, se ignoran las líneas en blanco y los comentarios, y cuenta la PRIMERA aparición de cada directiva. Después, cada puerto escuchando en 0.0.0.0, [::] o * que no esté permitido es un ALTO («puerto N (programa) abierto a internet»), por orden de número y sin repetir (los que escuchan en 127.0.0.1 o ::1 no importan). Termina con «Resultado: N altos, M medios» o «Resultado: sin problemas».

Código de partida (bash)
#!/bin/bash# TODO: analiza los tres bloquescat > /dev/nullecho "Resultado: sin problemas"

Ejemplo: Instalación por defecto

Entrada

# Configuración de ejemplo
Port 22
PermitRootLogin yes
#PasswordAuthentication no
X11Forwarding yes
---
tcp 0.0.0.0:22 sshd
tcp 0.0.0.0:80 nginx
tcp 0.0.0.0:3306 mysqld
tcp [::]:3306 mysqld
tcp 127.0.0.1:6379 redis
---
permitidos 22,80,443

Salida esperada

ALTO: PermitRootLogin yes permite entrar como root con contraseña
ALTO: PasswordAuthentication permite contraseñas (usa solo claves)
MEDIO: MaxAuthTries 6 (recomendado 4 o menos)
MEDIO: X11Forwarding activado sin necesidad
ALTO: puerto 3306 (mysqld) abierto a internet
Resultado: 3 altos, 2 medios

Ejemplo: Bien configurado

Entrada

PermitRootLogin no
PasswordAuthentication no
MaxAuthTries 3
---
tcp 0.0.0.0:22 sshd
tcp 0.0.0.0:443 nginx
tcp 127.0.0.1:3306 mysqld
tcp [::1]:5432 postgres
---
permitidos 22,80,443

Salida esperada

Resultado: sin problemas

Servidores Web: Apache y Nginx

4. Generar un virtual host de Nginx

Fácil · Servidores Web: Apache y Nginx · apuntesdam.com/subject/despliegue/topic/servidores-web

La entrada es una línea «dominio raíz [puerto]». Genera el bloque server de Nginx con este formato exacto (4 espacios de sangría): «server {», « listen 80;», « server_name dominio www.dominio;» y, si no hay puerto, « root raíz;» y « index index.html;»; si hay puerto, en su lugar « location / {», « proxy_pass http://127.0.0.1:puerto;», « }». Termina con «}». Si falta el dominio o la raíz, escribe «Uso: dominio raíz [puerto]».

Código de partida (bash)
#!/bin/bashread -r dominio raiz puerto # TODOexit 0

Ejemplo: Sitio estático

Entrada

apuntes.es /var/www/apuntes

Salida esperada

server {
    listen 80;
    server_name apuntes.es www.apuntes.es;
    root /var/www/apuntes;
    index index.html;
}

Ejemplo: Aplicación con proxy

Entrada

tienda.com /srv/tienda 3000

Salida esperada

server {
    listen 80;
    server_name tienda.com www.tienda.com;
    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

Ejemplo: Datos incompletos

Entrada

solo-dominio.com

Salida esperada

Uso: dominio raíz [puerto]

5. Estadísticas de un access.log

Medio · Servidores Web: Apache y Nginx · apuntesdam.com/subject/despliegue/topic/servidores-web

La entrada es un fichero de registro de Nginx en formato combinado. Muestra «Peticiones: N», «Errores 4xx: N», «Errores 5xx: N» y «IP con más peticiones: IP (N)». Si hay empate, gana la IP menor alfabéticamente.

Código de partida (bash)
#!/bin/bashlog=$(cat) # TODO

Ejemplo: Registro de ejemplo

Entrada

203.0.113.5 - - [30/Sep/2025:10:01:02 +0200] "GET / HTTP/1.1" 200 5120 "-" "Mozilla/5.0"
198.51.100.7 - - [30/Sep/2025:10:01:05 +0200] "GET /login HTTP/1.1" 200 2048 "-" "Mozilla/5.0"
203.0.113.5 - - [30/Sep/2025:10:01:09 +0200] "POST /login HTTP/1.1" 302 0 "-" "Mozilla/5.0"
192.0.2.44 - - [30/Sep/2025:10:02:11 +0200] "GET /wp-admin HTTP/1.1" 404 512 "-" "curl/8.0"
192.0.2.44 - - [30/Sep/2025:10:02:12 +0200] "GET /.env HTTP/1.1" 404 512 "-" "curl/8.0"
203.0.113.5 - - [30/Sep/2025:10:03:40 +0200] "GET /api/pedidos HTTP/1.1" 500 128 "-" "Mozilla/5.0"
198.51.100.7 - - [30/Sep/2025:10:04:01 +0200] "GET /panel HTTP/1.1" 403 256 "-" "Mozilla/5.0"

Salida esperada

Peticiones: 7
Errores 4xx: 3
Errores 5xx: 1
IP con más peticiones: 203.0.113.5 (3)

6. Vigilar la caducidad de los certificados

Medio · Servidores Web: Apache y Nginx · apuntesdam.com/subject/despliegue/topic/servidores-web

Certbot renueva solo, hasta que un día no lo hace (un cambio de DNS, un puerto cerrado) y la web muestra un aviso de seguridad a todos los visitantes. La primera línea es «hoy AAAA-MM-DD» y las siguientes, «dominio fecha-de-caducidad». Escribe los dominios ordenados por días restantes (a igualdad, alfabéticamente) con su estado: «CADUCADO hace N días» si ya ha caducado, «CRÍTICO: caduca en N días» si quedan 7 o menos, «AVISO: caduca en N días» si quedan 30 o menos y «ok (N días)» en otro caso. Al final, «Resumen: N críticos, M avisos» (los caducados cuentan como críticos).

Código de partida (bash)
#!/bin/bashread -r _ hoy # TODOcat > /dev/null

Ejemplo: Varios dominios

Entrada

hoy 2026-09-30
apuntes.es 2026-12-01
tienda.com 2026-10-05
blog.tienda.com 2026-10-20
api.tienda.com 2026-09-25

Salida esperada

api.tienda.com: CADUCADO hace 5 días
tienda.com: CRÍTICO: caduca en 5 días
blog.tienda.com: AVISO: caduca en 20 días
apuntes.es: ok (62 días)
Resumen: 2 críticos, 1 avisos

Ejemplo: Empates y límites

Entrada

hoy 2026-02-20
b.es 2026-03-22
a.es 2026-03-22
c.es 2026-02-27
d.es 2026-02-20
e.es 2026-03-23

Salida esperada

d.es: CRÍTICO: caduca en 0 días
c.es: CRÍTICO: caduca en 7 días
a.es: AVISO: caduca en 30 días
b.es: AVISO: caduca en 30 días
e.es: ok (31 días)
Resumen: 2 críticos, 2 avisos

7. Cadenas y bucles de redirecciones

Difícil · Servidores Web: Apache y Nginx · apuntesdam.com/subject/despliegue/topic/servidores-web

Tras varias migraciones de una web, las redirecciones 301 se acumulan: /antiguo → /intermedio → /nuevo obliga al navegador (y a Google) a dar varios saltos, y un error puede crear un bucle infinito. La entrada tiene reglas «origen destino» (una por línea), una línea «---» y después URLs a comprobar. Para cada URL sigue las reglas y escribe: «url: 200 sin redirección» si no hay regla, «url: 301 → destino» si hay un solo salto, «url: CADENA de N saltos → final (redirige directamente)» si hay más, o «url: BUCLE a → b → … → a» si vuelve a una URL ya visitada (mostrando el camino hasta la repetición).

Código de partida (bash)
#!/bin/bashdeclare -A reglawhile read -r origen destino && [ "$origen" != "---" ]; do    regla[$origen]=$destinodone while read -r url || [ -n "$url" ]; do    [ -z "$url" ] && continue    # TODO    echo "$url: 200 sin redirección"done

Ejemplo: Migraciones acumuladas

Entrada

/blog /noticias
/noticias /actualidad
/actualidad /revista
/tienda /productos
---
/blog
/tienda
/contacto
/noticias

Salida esperada

/blog: CADENA de 3 saltos → /revista (redirige directamente)
/tienda: 301 → /productos
/contacto: 200 sin redirección
/noticias: CADENA de 2 saltos → /revista (redirige directamente)

Ejemplo: Un bucle

Entrada

/a /b
/b /c
/c /a
/x /x
/inicio /a
---
/inicio
/x
/b

Salida esperada

/inicio: BUCLE /inicio → /a → /b → /c → /a
/x: BUCLE /x → /x
/b: BUCLE /b → /c → /a → /b

8. ¿Qué location atiende la petición?

Muy difícil · Servidores Web: Apache y Nginx · apuntesdam.com/subject/despliegue/topic/servidores-web

Entender cómo elige Nginx el bloque location evita horas de depuración. La entrada tiene bloques location («= /ruta», «^~ /prefijo», «~ expresión», «~* expresión» o solo «/prefijo»), una línea «---» y URIs. Aplica el algoritmo de Nginx: 1) si una location exacta (=) coincide, gana; 2) si no, busca el prefijo (normal o ^~) más largo que coincida; si ese prefijo es ^~, gana sin mirar las expresiones; 3) si no, prueba las expresiones regulares (~ distingue mayúsculas, ~* no) en el orden en que aparecen y gana la primera que coincida; 4) si ninguna coincide, gana el prefijo más largo encontrado en el paso 2. Escribe «uri → location» tal como está escrita (el prefijo normal sin modificador), o «uri → 404 (ninguna)».

Código de partida (bash)
#!/bin/bashmods=(); pats=()while read -r a b && [ "$a" != "---" ]; do    if [ -n "$b" ]; then mods+=("$a"); pats+=("$b"); else mods+=(""); pats+=("$a"); fidone while read -r uri || [ -n "$uri" ]; do    [ -z "$uri" ] && continue    # TODO: aplica el algoritmo de Nginx    echo "$uri → 404 (ninguna)"done

Ejemplo: Configuración típica

Entrada

= /
/
/api/
^~ /static/
~ \.php$
~* \.(jpg|png)$
---
/
/contacto
/api/usuarios
/static/logo.png
/imagenes/FOTO.JPG
/admin/index.php
/api/export.php

Salida esperada

/ → = /
/contacto → /
/api/usuarios → /api/
/static/logo.png → ^~ /static/
/imagenes/FOTO.JPG → ~* \.(jpg|png)$
/admin/index.php → ~ \.php$
/api/export.php → ~ \.php$

Ejemplo: Prefijo más largo y regex

Entrada

/docs/
/docs/v2/
~ ^/docs/v[0-9]+/privado
---
/docs/intro
/docs/v2/guia
/docs/v2/privado/claves
/blog

Salida esperada

/docs/intro → /docs/
/docs/v2/guia → /docs/v2/
/docs/v2/privado/claves → ~ ^/docs/v[0-9]+/privado
/blog → 404 (ninguna)

Servidores de Aplicaciones

9. Estado de los servicios

Medio · Servidores de Aplicaciones · apuntesdam.com/subject/despliegue/topic/servidores-aplicaciones

Cada línea es el resultado de comprobar un servicio: «nombre código_http milisegundos». Clasifica cada uno como «OK» (200 y menos de 500 ms), «LENTO» (200 y 500 ms o más) o «CAÍDO» (cualquier otro código), con el formato «nombre: ESTADO». Al final, «Estado general: OK», «DEGRADADO» (si hay alguno lento y ninguno caído) o «CAÍDO» (si hay alguno caído).

Código de partida (bash)
#!/bin/bashwhile read -r nombre codigo ms || [ -n "$nombre" ]; do    # TODO    :done

Ejemplo: Todo bien

Entrada

web 200 120
api 200 340

Salida esperada

web: OK
api: OK
Estado general: OK

Ejemplo: Uno lento

Entrada

web 200 120
api 200 850
cola 200 499

Salida esperada

web: OK
api: LENTO
cola: OK
Estado general: DEGRADADO

Ejemplo: Uno caído

Entrada

web 200 120
api 503 30
pagos 200 900

Salida esperada

web: OK
api: CAÍDO
pagos: LENTO
Estado general: CAÍDO

10. Revisar el server.xml de Tomcat

Medio · Servidores de Aplicaciones · apuntesdam.com/subject/despliegue/topic/servidores-aplicaciones

La entrada es un fichero server.xml (cada elemento en una línea). Muestra, en el orden en que aparecen: «Apagado: puerto P, orden O» (elemento Server), «Conector P PROTOCOLO» con « (TLS)» si tiene SSLEnabled="true" (si no hay protocol, es HTTP/1.1) y «Host nombre → appBase (autoDeploy=valor)» (si no se indica, autoDeploy vale true). Ignora las líneas de comentario (<!-- … -->). Después escribe los avisos, en el orden en que se detectan: «AVISO: puerto de apagado P con la orden por defecto» si el puerto no es -1 y la orden es SHUTDOWN; «PELIGRO: conector AJP P accesible desde fuera (Ghostcat)» si un conector AJP no tiene address local (127.0.0.1, ::1 o localhost); «AVISO: autoDeploy activado en nombre» para cada host que lo tenga. Termina con «Avisos: N».

Código de partida (bash)
#!/bin/bash# TODO: inventario y avisoscat > /dev/nullecho "Avisos: 0"

Ejemplo: Instalación por defecto

Entrada

<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
  <Service name="Catalina">
    <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
    <!-- <Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true" /> -->
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
    <Engine name="Catalina" defaultHost="localhost">
      <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
      </Host>
    </Engine>
  </Service>
</Server>

Salida esperada

Apagado: puerto 8005, orden SHUTDOWN
Conector 8080 HTTP/1.1
Conector 8009 AJP/1.3
Host localhost → webapps (autoDeploy=true)
AVISO: puerto de apagado 8005 con la orden por defecto
PELIGRO: conector AJP 8009 accesible desde fuera (Ghostcat)
AVISO: autoDeploy activado en localhost
Avisos: 3

Ejemplo: Bien configurado

Entrada

<Server port="-1" shutdown="SHUTDOWN">
  <Service name="Catalina">
    <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" SSLEnabled="true" maxThreads="200" />
    <Connector port="8009" protocol="AJP/1.3" address="127.0.0.1" secret="s3cr3t" />
    <Engine name="Catalina" defaultHost="tienda.es">
      <Host name="tienda.es" appBase="/srv/tienda" autoDeploy="false" />
      <Host name="intranet.tienda.es" appBase="/srv/intranet" autoDeploy="false" />
    </Engine>
  </Service>
</Server>

Salida esperada

Apagado: puerto -1, orden SHUTDOWN
Conector 8443 org.apache.coyote.http11.Http11NioProtocol (TLS)
Conector 8009 AJP/1.3
Host tienda.es → /srv/tienda (autoDeploy=false)
Host intranet.tienda.es → /srv/intranet (autoDeploy=false)
Avisos: 0

11. Despliegue atómico con releases y vuelta atrás

Difícil · Servidores de Aplicaciones · apuntesdam.com/subject/despliegue/topic/servidores-aplicaciones

Cada versión se copia en su propia carpeta releases/VERSIÓN y la web apunta a un enlace simbólico current: cambiarlo es instantáneo y volver atrás es apuntarlo a la anterior. Usa carpetas y enlaces reales dentro de /tmp/app. Cada línea es una orden: «desplegar v» (crea releases/v, apunta current a ella y conserva solo las 3 releases más recientes, sin borrar nunca la actual; si v ya existe, «Error: v ya existe»), «rollback» (apunta current a la release desplegada justo antes de la actual; si no hay, «Error: no hay versión anterior») y «estado». Tras cada cambio y con «estado», escribe «current → v (releases: a b c)» con las releases de más antigua a más reciente (o «Sin desplegar»).

Código de partida (bash)
#!/bin/bashbase=/tmp/apprm -rf "$base" && mkdir -p "$base/releases"orden=() while read -r accion version || [ -n "$accion" ]; do    # TODO    echo "Sin desplegar"done

Ejemplo: Varias versiones

Entrada

estado
desplegar v1
desplegar v2
desplegar v3
desplegar v4
rollback
rollback
estado

Salida esperada

Sin desplegar
current → v1 (releases: v1)
current → v2 (releases: v1 v2)
current → v3 (releases: v1 v2 v3)
current → v4 (releases: v2 v3 v4)
current → v3 (releases: v2 v3 v4)
current → v2 (releases: v2 v3 v4)
current → v2 (releases: v2 v3 v4)

Ejemplo: Errores

Entrada

rollback
desplegar a
rollback
desplegar b
desplegar b
rollback
desplegar c
desplegar d
desplegar e

Salida esperada

Error: no hay versión anterior
current → a (releases: a)
Error: no hay versión anterior
current → b (releases: a b)
Error: b ya existe
current → a (releases: a b)
current → c (releases: a b c)
current → d (releases: b c d)
current → e (releases: c d e)

12. Despliegue canary: ¿promocionar o revertir?

Muy difícil · Servidores de Aplicaciones · apuntesdam.com/subject/despliegue/topic/servidores-aplicaciones

En un despliegue canary, la versión nueva recibe primero un 5 % del tráfico, luego un 25 %, un 50 % y por fin el 100 %, y solo avanza si se comporta al menos tan bien como la estable. Cada línea es una evaluación con las métricas de ambas versiones: «peticiones errores p95 | peticiones errores p95» (estable | canario; p95 en milisegundos). En cada evaluación, en este orden: si el canario tiene menos de 50 peticiones, «N: X % → datos insuficientes (P peticiones)» y se sigue igual; si su tasa de errores (en %) supera en más de 0,5 puntos a la estable, «N: X % → REVERTIDA (errores A % frente a B %)» con 2 decimales, y se termina; si su p95 supera en más de un 20 % al estable, «N: X % → REVERTIDA (latencia A ms frente a B ms)» y se termina; si todo va bien, «N: X % → ok, pasa al Y %» o, si ya estaba al 100 %, «N: 100 % → ok, PROMOCIONADA» y se termina. Si se acaban las evaluaciones sin decidir, «Sin decisión: se mantiene el X %».

Código de partida (bash)
#!/bin/bashexport LC_ALL=C# TODO: evalúa cada línea con awkcat > /dev/null

Ejemplo: Todo va bien

Entrada

10000 20 180 | 520 1 190
8000 16 175 | 2600 6 200
6000 12 170 | 6000 15 180
2000 5 160 | 10000 30 185

Salida esperada

1: 5 % → ok, pasa al 25 %
2: 25 % → ok, pasa al 50 %
3: 50 % → ok, pasa al 100 %
4: 100 % → ok, PROMOCIONADA

Ejemplo: Errores y datos insuficientes

Entrada

9000 45 200 | 30 0 150
9000 45 200 | 480 3 210
7000 30 190 | 2400 25 195

Salida esperada

1: 5 % → datos insuficientes (30 peticiones)
2: 5 % → ok, pasa al 25 %
3: 25 % → REVERTIDA (errores 1.04 % frente a 0.43 %)

Contenedores con Docker

13. Revisar un Dockerfile

Medio · Contenedores con Docker · apuntesdam.com/subject/despliegue/topic/docker

La entrada es un Dockerfile. Escribe un aviso por línea, en este orden, cuando corresponda: «Imagen base sin versión concreta» (algún FROM sin etiqueta o con :latest), «apt-get install sin limpiar la caché» (un RUN con apt-get install sin rm -rf /var/lib/apt/lists), «Se copia todo el código antes de instalar las dependencias» (un COPY . aparece antes del RUN que ejecuta npm install/npm ci, composer install o pip install) y «El contenedor se ejecuta como root» (no hay ninguna instrucción USER). Si no hay ningún aviso, escribe «Sin avisos».

Código de partida (bash)
#!/bin/bashdockerfile=$(cat)avisos=() # TODO

Ejemplo: Dockerfile mejorable

Entrada

FROM node
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

Salida esperada

Imagen base sin versión concreta
Se copia todo el código antes de instalar las dependencias
El contenedor se ejecuta como root

Ejemplo: Dockerfile correcto

Entrada

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "server.js"]

Salida esperada

Sin avisos

Ejemplo: apt-get y latest

Entrada

FROM ubuntu:latest
RUN apt-get update && apt-get install -y nginx
USER www-data

Salida esperada

Imagen base sin versión concreta
apt-get install sin limpiar la caché

14. Puertos de un docker compose

Medio · Contenedores con Docker · apuntesdam.com/subject/despliegue/topic/docker

Cada línea describe los puertos de un servicio: «servicio: p1 p2 …», donde cada puerto es «anfitrión:contenedor» (se publica) o solo «contenedor» (queda en la red interna). Para cada puerto, en orden: si es solo interno, «servicio: P solo interno (no se publica)»; si alguno de los números no es un entero entre 1 y 65535, «servicio: P ERROR puerto no válido»; si el puerto del anfitrión ya lo publica otro servicio, «servicio: P ERROR el puerto H ya lo publica otro»; si no, «servicio: H → C», con « (privilegiado)» si el del anfitrión es menor que 1024. Al final, «Publicados: N, errores: M».

Código de partida (bash)
#!/bin/bashdeclare -A usadoerrores=0while IFS= read -r linea || [ -n "$linea" ]; do    [ -z "$linea" ] && continue    servicio=${linea%%:*}    resto=${linea#*:}    for p in $resto; do        # TODO        echo "$servicio: $p"    donedoneecho "Publicados: ${#usado[@]}, errores: $errores"

Ejemplo: Proxy, aplicación y base de datos

Entrada

proxy: 80:80 443:443
app: 3000
db: 3306
adminer: 8080:8080

Salida esperada

proxy: 80 → 80 (privilegiado)
proxy: 443 → 443 (privilegiado)
app: 3000 solo interno (no se publica)
db: 3306 solo interno (no se publica)
adminer: 8080 → 8080
Publicados: 3, errores: 0

Ejemplo: Conflictos y errores

Entrada

web: 8080:80
api: 8080:3000 8081:3000
cola: 70000:5672 abc:15672
metricas: 9090:9090 443:9443

Salida esperada

web: 8080 → 80
api: 8080:3000 ERROR el puerto 8080 ya lo publica web
api: 8081 → 3000
cola: 70000:5672 ERROR puerto no válido
cola: abc:15672 ERROR puerto no válido
metricas: 9090 → 9090
metricas: 443 → 9443 (privilegiado)
Publicados: 4, errores: 3

15. Orden de arranque según depends_on

Difícil · Contenedores con Docker · apuntesdam.com/subject/despliegue/topic/docker

Docker Compose arranca primero los servicios de los que dependen otros. Cada línea es «servicio: dep1 dep2 …» (puede no tener dependencias). Calcula el arranque por rondas: en cada ronda arrancan a la vez, en orden alfabético, todos los servicios cuyas dependencias ya han arrancado («Ronda N: a b c»). Antes, comprueba dos errores: si un servicio depende de uno que no existe, escribe solo «Error: s depende de d, que no existe» (el primero que encuentres, en el orden de las líneas y de sus dependencias); si hay dependencias circulares, escribe solo «Ciclo: a → b → … → a», buscando con una búsqueda en profundidad que visita los servicios y sus dependencias en orden alfabético y muestra el ciclo desde el primer servicio repetido.

Código de partida (bash)
#!/bin/bashdeclare -A deporden=()while IFS=: read -r s d || [ -n "$s" ]; do    [ -z "$s" ] && continue    s=$(echo $s); dep[$s]=$(echo $d); orden+=("$s")done # TODO: errores, ciclos y rondasecho "Ronda 1: ${orden[*]}"

Ejemplo: Tienda completa

Entrada

proxy: app
app: db cache
cache:
db:
worker: db cache
migraciones: db

Salida esperada

Ronda 1: cache db
Ronda 2: app migraciones worker
Ronda 3: proxy

Ejemplo: Un ciclo

Entrada

api: auth
auth: usuarios
usuarios: api
web: api

Salida esperada

Ciclo: api → auth → usuarios → api

Ejemplo: Dependencia inexistente

Entrada

app: db redis
db:

Salida esperada

Error: app depende de redis, que no existe

16. ¿Qué capas se reconstruyen?

Muy difícil · Contenedores con Docker · apuntesdam.com/subject/despliegue/topic/docker

Docker reutiliza cada capa de la construcción anterior si la instrucción es idéntica, si en las instrucciones COPY o ADD no ha cambiado ninguno de los ficheros que copian y si ninguna capa anterior se ha reconstruido (en cuanto una se reconstruye, todas las siguientes también). La entrada tiene el Dockerfile de la construcción anterior, «---», el nuevo, «---» y la lista de ficheros modificados. Una fuente de COPY puede ser «.» (todo), un fichero, una carpeta (acabada o no en /: cubre lo que hay dentro) o un patrón con * (package*.json). El último argumento de COPY es el destino, y un COPY --from=… copia de otra etapa: no depende de los ficheros. Escribe por cada instrucción del nuevo Dockerfile «N instrucción → caché» o «N instrucción → RECONSTRUIR (motivo)», con motivo «instrucción cambiada», «cambió fichero» (el primer fichero modificado de la lista que la afecte) o «capa anterior reconstruida», y al final «Capas reutilizadas: X de N».

Código de partida (bash)
#!/bin/bashmapfile -t todobloque=0; v1=(); v2=(); cambiados=()for l in "${todo[@]}"; do    if [ "$l" = "---" ]; then ((bloque++)); continue; fi    [ -z "$l" ] && continue    case $bloque in 0) v1+=("$l") ;; 1) v2+=("$l") ;; 2) cambiados+=("$l") ;; esacdone # TODO: decide caché o reconstrucción para cada instrucción de v2for i in "${!v2[@]}"; do echo "$((i + 1)) ${v2[i]} → caché"; doneecho "Capas reutilizadas: ${#v2[@]} de ${#v2[@]}"

Ejemplo: Buen orden: solo cambia el código

Entrada

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
---
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
---
src/app.js

Salida esperada

1 FROM node:22-alpine → caché
2 WORKDIR /app → caché
3 COPY package*.json ./ → caché
4 RUN npm ci → caché
5 COPY . . → RECONSTRUIR (cambió src/app.js)
6 RUN npm run build → RECONSTRUIR (capa anterior reconstruida)
Capas reutilizadas: 4 de 6

Ejemplo: Mal orden: se reinstalan las dependencias

Entrada

FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
---
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
---
src/app.js

Salida esperada

1 FROM node:22-alpine → caché
2 WORKDIR /app → caché
3 COPY . . → RECONSTRUIR (cambió src/app.js)
4 RUN npm ci → RECONSTRUIR (capa anterior reconstruida)
5 RUN npm run build → RECONSTRUIR (capa anterior reconstruida)
Capas reutilizadas: 2 de 5

Ejemplo: Cambian las dependencias y una instrucción

Entrada

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
---
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build
---
package-lock.json
README.md

Salida esperada

1 FROM node:22-alpine → caché
2 WORKDIR /app → caché
3 COPY package*.json ./ → RECONSTRUIR (cambió package-lock.json)
4 RUN npm ci --omit=dev → RECONSTRUIR (capa anterior reconstruida)
5 COPY . . → RECONSTRUIR (capa anterior reconstruida)
6 RUN npm run build → RECONSTRUIR (capa anterior reconstruida)
Capas reutilizadas: 2 de 6

FTP, DNS y Servicios de Directorio

17. Leer una zona DNS

Medio · FTP, DNS y Servicios de Directorio · apuntesdam.com/subject/despliegue/topic/servicios-red

La entrada es un fichero de zona de BIND con una directiva $ORIGIN. Muestra los registros A, AAAA, CNAME y MX, en su orden, con el nombre completo: «www.tienda.com. CNAME tienda.com.». «@» es el propio origen; un nombre que no termina en punto es relativo y se le añade el origen (también en el destino de un CNAME). En los MX muestra la prioridad y el servidor: «tienda.com. MX 10 correo.tienda.com.». Ignora comentarios (;), directivas y otros tipos. Termina con «Registros: N».

Código de partida (bash)
#!/bin/bash# TODO: procesa la zona con awkcat > /dev/null

Ejemplo: Zona de la tienda

Entrada

$ORIGIN tienda.com.
$TTL 3600
@       IN  SOA ns1.tienda.com. admin.tienda.com. (2025093001 7200 3600 1209600 3600)
@       IN  NS    ns1.tienda.com.
@       IN  A     203.0.113.10
www     IN  CNAME @
api     IN  A     203.0.113.20
api     IN  AAAA  2001:db8::20
@       IN  MX 10 correo.tienda.com.
correo  IN  A     203.0.113.30
; comentario: servidor antiguo
ftp.otro.org. IN A 198.51.100.1

Salida esperada

tienda.com. A 203.0.113.10
www.tienda.com. CNAME tienda.com.
api.tienda.com. A 203.0.113.20
api.tienda.com. AAAA 2001:db8::20
tienda.com. MX 10 correo.tienda.com.
correo.tienda.com. A 203.0.113.30
ftp.otro.org. A 198.51.100.1
Registros: 7

18. Revisar una zona DNS antes de publicarla

Medio · FTP, DNS y Servicios de Directorio · apuntesdam.com/subject/despliegue/topic/servicios-red

Un error en la zona puede dejar sin web o sin correo a toda una empresa. La entrada es un fichero de zona con $ORIGIN (ignora comentarios, líneas vacías y otras directivas; cada registro es «nombre IN tipo datos…»; @ es el origen y los nombres sin punto final son relativos). Comprueba, escribiendo los mensajes en este orden: 1) para cada nombre, por orden de aparición, «ERROR: CNAME en el ápice (nombre)» si el propio dominio tiene un CNAME, o «ERROR: nombre tiene un CNAME y otros registros» si un nombre con CNAME tiene además otros registros; 2) para cada MX, «ERROR: el MX de nombre apunta a destino, que es un CNAME» (el destino de un MX debe tener registros A o AAAA); 3) para cada CNAME, MX o NS cuyo destino no acabe en punto pero termine en el dominio (o sea el dominio), «AVISO: falta el punto final en «destino» (se leería destino.origen)». Termina con «Errores: N, avisos: M».

Código de partida (bash)
#!/bin/bash# TODO: comprueba la zona con awkcat > /dev/nullecho "Errores: 0, avisos: 0"

Ejemplo: Zona con fallos

Entrada

$ORIGIN tienda.es.
$TTL 3600
@ IN SOA ns1.tienda.es. admin.tienda.es. (2026093001 7200 3600 1209600 3600)
@ IN NS ns1.tienda.es.
@ IN A 203.0.113.10
www IN CNAME @
correo IN CNAME www
@ IN MX 10 correo
blog IN CNAME tienda.es
blog IN TXT "verificacion"
ns1 IN A 203.0.113.2

Salida esperada

ERROR: blog.tienda.es. tiene un CNAME y otros registros
ERROR: el MX de tienda.es. apunta a correo.tienda.es., que es un CNAME
AVISO: falta el punto final en «tienda.es» (se leería tienda.es.tienda.es.)
Errores: 2, avisos: 1

Ejemplo: Zona correcta

Entrada

$ORIGIN ejemplo.org.
; zona correcta
@ IN NS ns1.proveedor.net.
@ IN A 192.0.2.1
www IN CNAME @
@ IN MX 10 mail
mail IN A 192.0.2.25

Salida esperada

Errores: 0, avisos: 0

19. Un resolvedor DNS iterativo con caché

Difícil · FTP, DNS y Servicios de Directorio · apuntesdam.com/subject/despliegue/topic/servicios-red

Así trabaja el DNS de tu operadora. La entrada tiene líneas «servidor nombre tipo valor ttl» con lo que sabe cada servidor (el servidor raíz se llama raiz; los demás, por su nombre), una línea «---» y consultas «segundo nombre». Para resolver un nombre: si su registro A está en caché y no ha caducado (caduca en segundo + ttl), responde «(caché)» sin preguntar a nadie. Si no, empieza por el servidor de la delegación (NS) en caché y vigente más específica cuyo dominio contenga el nombre (o por raiz si no hay ninguna) y pregunta: si ese servidor tiene el A del nombre, lo guarda en caché y responde; si tiene un NS de un dominio que contiene el nombre, guarda esa delegación en caché y pasa a preguntar al servidor indicado (si hay varios, el del dominio más largo); si no, el nombre no existe. Escribe «segundo nombre: IP (N consultas)», «… (caché)» o «… no existe (N consultas)» y, al final, «Consultas totales: N».

Código de partida (bash)
#!/bin/bashdeclare -A registro          # "servidor|nombre|tipo" -> "valor ttl"declare -A delegaciones      # servidor -> lista de "dominio|servidor_hijo|ttl"while read -r srv nom tipo val ttl && [ "$srv" != "---" ]; do    if [ "$tipo" = "NS" ]; then delegaciones[$srv]+="$nom|$val|$ttl "; else registro["$srv|$nom|$tipo"]="$val $ttl"; fidone total=0while read -r t nom || [ -n "$t" ]; do    [ -z "$t" ] && continue    # TODO    echo "$t $nom: no existe (0 consultas)"doneecho "Consultas totales: $total"

Ejemplo: Caché de respuestas y delegaciones

Entrada

raiz es. NS a.nic.es. 172800
raiz org. NS a0.org. 172800
a.nic.es. tienda.es. NS ns1.tienda.es. 3600
a.nic.es. academia.es. NS ns.academia.es. 3600
ns1.tienda.es. www.tienda.es. A 203.0.113.10 300
ns1.tienda.es. api.tienda.es. A 203.0.113.20 60
ns.academia.es. academia.es. A 198.51.100.7 600
---
0 www.tienda.es.
10 api.tienda.es.
20 www.tienda.es.
100 api.tienda.es.
400 www.tienda.es.

Salida esperada

0 www.tienda.es.: 203.0.113.10 (3 consultas)
10 api.tienda.es.: 203.0.113.20 (1 consultas)
20 www.tienda.es.: 203.0.113.10 (caché)
100 api.tienda.es.: 203.0.113.20 (1 consultas)
400 www.tienda.es.: 203.0.113.10 (1 consultas)
Consultas totales: 6

Ejemplo: Otros dominios y nombres inexistentes

Entrada

raiz es. NS a.nic.es. 172800
raiz org. NS a0.org. 172800
a.nic.es. tienda.es. NS ns1.tienda.es. 3600
a.nic.es. academia.es. NS ns.academia.es. 3600
ns1.tienda.es. www.tienda.es. A 203.0.113.10 300
ns1.tienda.es. api.tienda.es. A 203.0.113.20 60
ns.academia.es. academia.es. A 198.51.100.7 600
---
500 academia.es.
700 ftp.tienda.es.
800 www.ejemplo.org.
900 academia.es.

Salida esperada

500 academia.es.: 198.51.100.7 (3 consultas)
700 ftp.tienda.es.: no existe (2 consultas)
800 www.ejemplo.org.: no existe (2 consultas)
900 academia.es.: 198.51.100.7 (caché)
Consultas totales: 7

20. Evaluar filtros de búsqueda LDAP

Muy difícil · FTP, DNS y Servicios de Directorio · apuntesdam.com/subject/despliegue/topic/servicios-red

Las aplicaciones buscan en el directorio de la empresa con filtros LDAP en notación prefija: (uid=ana), (&(objectClass=person)(ou=ventas)), (|(ou=ventas)(ou=soporte)), (!(uid=admin)), (mail=*) (el atributo existe) y comodines como (cn=Ana*) o (mail=*@tienda.es). La entrada tiene entradas del directorio («atributo: valor» por línea, separadas por líneas en blanco; un atributo puede repetirse, como objectClass o memberOf), una línea «---» y filtros, uno por línea. Para cada filtro escribe «filtro → uid1, uid2» con los uid de las entradas que lo cumplen, en el orden del directorio, o «filtro → (ninguna)». Los nombres de atributo y las comparaciones de valores no distinguen mayúsculas; un atributo con varios valores cumple la condición si la cumple alguno.

Código de partida (bash)
#!/bin/bash# TODO: lee las entradas y evalúa cada filtro con awkcat > /dev/null

Ejemplo: Filtros básicos

Entrada

dn: uid=ana,ou=personas,dc=tienda,dc=es
objectClass: person
objectClass: inetOrgPerson
uid: ana
cn: Ana García
ou: ventas
mail: ana@tienda.es
memberOf: cn=empleados,ou=grupos,dc=tienda,dc=es

dn: uid=luis,ou=personas,dc=tienda,dc=es
objectClass: person
uid: luis
cn: Luis Pérez
ou: soporte
mail: luis@gmail.com
memberOf: cn=empleados,ou=grupos,dc=tienda,dc=es
memberOf: cn=admins,ou=grupos,dc=tienda,dc=es

dn: uid=admin,dc=tienda,dc=es
objectClass: account
uid: admin

dn: uid=eva,ou=personas,dc=tienda,dc=es
objectClass: person
uid: eva
cn: Eva Ruiz
ou: Ventas
mail: eva@tienda.es
---
(uid=ana)
(ou=ventas)
(&(objectClass=person)(mail=*))
(!(objectClass=person))

Salida esperada

(uid=ana) → ana
(ou=ventas) → ana, eva
(&(objectClass=person)(mail=*)) → ana, luis, eva
(!(objectClass=person)) → admin

Ejemplo: Comodines y combinaciones

Entrada

dn: uid=ana,ou=personas,dc=tienda,dc=es
objectClass: person
objectClass: inetOrgPerson
uid: ana
cn: Ana García
ou: ventas
mail: ana@tienda.es
memberOf: cn=empleados,ou=grupos,dc=tienda,dc=es

dn: uid=luis,ou=personas,dc=tienda,dc=es
objectClass: person
uid: luis
cn: Luis Pérez
ou: soporte
mail: luis@gmail.com
memberOf: cn=empleados,ou=grupos,dc=tienda,dc=es
memberOf: cn=admins,ou=grupos,dc=tienda,dc=es

dn: uid=admin,dc=tienda,dc=es
objectClass: account
uid: admin

dn: uid=eva,ou=personas,dc=tienda,dc=es
objectClass: person
uid: eva
cn: Eva Ruiz
ou: Ventas
mail: eva@tienda.es
---
(mail=*@tienda.es)
(|(ou=soporte)(memberOf=cn=admins,ou=grupos,dc=tienda,dc=es))
(&(objectclass=PERSON)(!(ou=ventas)))
(cn=*ga*)
(telefono=*)

Salida esperada

(mail=*@tienda.es) → ana, eva
(|(ou=soporte)(memberOf=cn=admins,ou=grupos,dc=tienda,dc=es)) → luis
(&(objectclass=PERSON)(!(ou=ventas))) → luis
(cn=*ga*) → ana
(telefono=*) → (ninguna)

Git, Documentación y CI/CD

21. La siguiente versión semántica

Fácil · Git, Documentación y CI/CD · apuntesdam.com/subject/despliegue/topic/ci-cd

Cada línea es «versión tipo», donde la versión es MAYOR.MENOR.PARCHE y el tipo es major, minor o patch. Escribe la siguiente versión: major sube el primero y pone los otros a 0; minor sube el segundo y pone a 0 el parche; patch sube el último. Si la versión no tiene el formato correcto o el tipo no es válido, escribe «Versión no válida».

Código de partida (bash)
#!/bin/bashwhile read -r version tipo || [ -n "$version" ]; do    # TODO    :done

Ejemplo: Los tres tipos

Entrada

1.4.2 patch
1.4.2 minor
1.4.2 major

Salida esperada

1.4.3
1.5.0
2.0.0

Ejemplo: Entradas no válidas

Entrada

1.4 patch
v2.0.0 minor
2.0.0 hotfix

Salida esperada

Versión no válida
Versión no válida
Versión no válida

22. Una fusión a tres bandas

Medio · Git, Documentación y CI/CD · apuntesdam.com/subject/despliegue/topic/ci-cd

Así fusiona Git dos ramas: compara cada línea de la versión común (base) con la nuestra y la suya. La entrada son tres versiones del mismo fichero con el mismo número de líneas, separadas por «---»: base, nuestra y suya. Para cada línea: si nuestra y suya coinciden, se queda esa; si solo cambió una de las dos respecto a la base, gana la que cambió; si cambiaron las dos de forma distinta, hay conflicto. Las líneas en conflicto consecutivas forman un único bloque: «<<<<<<< nuestra», nuestras líneas, «=======», sus líneas, «>>>>>>> suya». Escribe el fichero fusionado y, al final, «Conflictos: N» (número de bloques).

Código de partida (bash)
#!/bin/bashmapfile -t todobase=(); nuestra=(); suya=(); parte=0for l in "${todo[@]}"; do    if [ "$l" = "---" ]; then ((parte++)); continue; fi    case $parte in 0) base+=("$l") ;; 1) nuestra+=("$l") ;; 2) suya+=("$l") ;; esacdone # TODO: fusiona línea a líneaprintf '%s\n' "${nuestra[@]}"echo "Conflictos: 0"

Ejemplo: Cambios compatibles

Entrada

# Tienda
version=1.0
puerto=8080
debug=true
---
# Tienda
version=1.1
puerto=8080
debug=true
---
# Tienda
version=1.0
puerto=8080
debug=false

Salida esperada

# Tienda
version=1.1
puerto=8080
debug=false
Conflictos: 0

Ejemplo: Un conflicto de dos líneas

Entrada

a=1
b=2
c=3
d=4
---
a=1
b=20
c=30
d=4
---
a=1
b=200
c=300
d=40

Salida esperada

a=1
<<<<<<< nuestra
b=20
c=30
=======
b=200
c=300
>>>>>>> suya
d=40
Conflictos: 1

23. Changelog a partir de los commits

Difícil · Git, Documentación y CI/CD · apuntesdam.com/subject/despliegue/topic/ci-cd

La entrada son mensajes de commit con el formato de Conventional Commits («tipo(ámbito)!: descripción», con ámbito y ! opcionales). Genera un changelog con las secciones «## Cambios incompatibles» (commits con !), «## Novedades» (feat) y «## Correcciones» (fix), en ese orden, cada una con líneas «- descripción» en el orden original y omitiendo las secciones vacías. Los demás tipos (docs, chore, test…) se ignoran salvo que lleven !. Termina con una línea en blanco y «Siguiente versión: major», «minor», «patch» o «ninguna».

Código de partida (bash)
#!/bin/bashrotos=(); novedades=(); correcciones=()while IFS= read -r linea || [ -n "$linea" ]; do    # TODO: clasifica el commit    :done # TODO: escribe el changelog

Ejemplo: Versión con novedades

Entrada

feat(api): añadir login con token
fix: corregir el total del carrito
docs: actualizar el README
feat(web): filtro por categoría
chore: subir dependencias

Salida esperada

## Novedades
- añadir login con token
- filtro por categoría
## Correcciones
- corregir el total del carrito

Siguiente versión: minor

Ejemplo: Cambio incompatible

Entrada

fix(api): validar el email
refactor(api)!: renombrar /users a /usuarios
test: pruebas del carrito

Salida esperada

## Cambios incompatibles
- renombrar /users a /usuarios
## Correcciones
- validar el email

Siguiente versión: major

Ejemplo: Solo mantenimiento

Entrada

docs: arreglar erratas
chore(ci): cachear dependencias

Salida esperada

Siguiente versión: ninguna

24. Las métricas DORA de un equipo

Difícil · Git, Documentación y CI/CD · apuntesdam.com/subject/despliegue/topic/ci-cd

Las cuatro métricas DORA miden lo bien que un equipo entrega software. La entrada es un registro con líneas «despliegue FECHA HORA commit FECHA HORA resultado» (resultado ok o fallo; la fecha del commit es cuándo se escribió el cambio desplegado) y «restaurado FECHA HORA» (el servicio vuelve a funcionar tras un fallo), en orden cronológico. Calcula: «Despliegues: N en D días (X por semana)», con D los días naturales entre el primer y el último despliegue, ambos incluidos, y X con un decimal; «Tiempo de entrega (mediana): H h M min», la mediana en minutos de (despliegue − commit), y con un número par de valores, la media entera de los dos centrales; «Tasa de fallos: P %» con un decimal; y «Tiempo de recuperación (media): M min» (sin decimales), la media de los minutos entre cada despliegue fallido y el primer «restaurado» posterior, o «—» si no hubo fallos.

Código de partida (bash)
#!/bin/bashexport LC_ALL=C TZ=UTC# TODOcat > /dev/null

Ejemplo: Una semana de trabajo

Entrada

despliegue 2026-09-21 10:00 commit 2026-09-20 16:30 ok
despliegue 2026-09-22 12:00 commit 2026-09-22 09:00 ok
despliegue 2026-09-23 17:00 commit 2026-09-23 15:15 fallo
restaurado 2026-09-23 17:40
despliegue 2026-09-24 11:00 commit 2026-09-23 18:00 ok
despliegue 2026-09-27 09:30 commit 2026-09-25 09:30 ok

Salida esperada

Despliegues: 5 en 7 días (5.0 por semana)
Tiempo de entrega (mediana): 17 h 0 min
Tasa de fallos: 20.0 %
Tiempo de recuperación (media): 40 min

Ejemplo: Sin fallos

Entrada

despliegue 2026-01-10 08:00 commit 2026-01-09 08:00 ok
despliegue 2026-01-10 20:00 commit 2026-01-10 19:00 ok

Salida esperada

Despliegues: 2 en 1 días (14.0 por semana)
Tiempo de entrega (mediana): 12 h 30 min
Tasa de fallos: 0.0 %
Tiempo de recuperación (media): —

25. ¿Qué jobs ejecuta el pipeline?

Muy difícil · Git, Documentación y CI/CD · apuntesdam.com/subject/despliegue/topic/ci-cd

Las plataformas de CI deciden qué trabajos (jobs) lanzar según sus dependencias, la rama y los ficheros cambiados. La entrada tiene una línea por job, en un orden en el que las dependencias aparecen antes: «nombre [needs=a,b] [ramas=main,release/*] [rutas=src/*,Dockerfile]», una línea «---» y el contexto: «rama=…», «cambios=f1,f2…» y «fallan=j1,j2…» (jobs que fallarán si se ejecutan). Para cada job, en orden y comprobando en este orden: si alguna dependencia terminó en FALLO o cancelado → «cancelado (por dep)» (la primera en su lista); si alguna se omitió → «omitido (depende de dep)»; si tiene ramas y ninguna coincide con la rama (con comodines * como los de la shell) → «omitido (rama)»; si tiene rutas y ningún fichero cambiado coincide con ninguna → «omitido (sin cambios)»; si no, se ejecuta: «FALLO» si está en fallan u «ok». Termina con «Resultado: fallido» si algún job falló o «Resultado: éxito».

Código de partida (bash)
#!/bin/bashjobs=(); declare -A ctxenjobs=1while IFS= read -r l || [ -n "$l" ]; do    [ -z "$l" ] && continue    if [ "$l" = "---" ]; then enjobs=0; continue; fi    if (( enjobs )); then jobs+=("$l"); else ctx[${l%%=*}]=${l#*=}; fidone # TODO: decide cada jobfor l in "${jobs[@]}"; do echo "${l%% *}: ok"; doneecho "Resultado: éxito"

Ejemplo: Una rama de funcionalidad

Entrada

lint
pruebas needs=lint
imagen needs=pruebas ramas=main,release/* rutas=src/*,Dockerfile,package.json
docs rutas=docs/*
desplegar_staging needs=imagen ramas=main
desplegar_prod needs=desplegar_staging ramas=release/*
---
rama=feat/filtros
cambios=src/app.js,README.md
fallan=

Salida esperada

lint: ok
pruebas: ok
imagen: omitido (rama)
docs: omitido (sin cambios)
desplegar_staging: omitido (depende de imagen)
desplegar_prod: omitido (depende de desplegar_staging)
Resultado: éxito

Ejemplo: Main con un test roto

Entrada

lint
pruebas needs=lint
imagen needs=pruebas ramas=main,release/* rutas=src/*,Dockerfile,package.json
docs rutas=docs/*
desplegar_staging needs=imagen ramas=main
desplegar_prod needs=desplegar_staging ramas=release/*
---
rama=main
cambios=src/app.js,docs/guia.md
fallan=pruebas

Salida esperada

lint: ok
pruebas: FALLO
imagen: cancelado (por pruebas)
docs: ok
desplegar_staging: cancelado (por imagen)
desplegar_prod: cancelado (por desplegar_staging)
Resultado: fallido

Ejercicios de Despliegue de Aplicaciones Web

26. Conflictos de puertos en Docker Compose

Medio · Ejercicios de Despliegue de Aplicaciones Web · apuntesdam.com/subject/despliegue/topic/ejercicios-despliegue

Cada línea describe una publicación de puertos de un servicio: «servicio puerto_host:puerto_contenedor». Escribe «CONFLICTO puerto: servicio1, servicio2» por cada puerto del host usado por más de un servicio (en orden numérico de puerto y con los servicios en el orden en que aparecen), y «PRIVILEGIADO servicio: puerto» por cada puerto del host menor que 1024, en el orden de la entrada. Si no hay problemas, escribe «Sin problemas».

Código de partida (bash)
#!/bin/bashdeclare -A usosproblemas=0 # TODO

Ejemplo: Con conflictos

Entrada

web 8080:80
api 3000:3000
adminer 8080:8080
proxy 80:80
proxy 443:443
docs 3000:80

Salida esperada

CONFLICTO 3000: api, docs
CONFLICTO 8080: web, adminer
PRIVILEGIADO proxy: 80
PRIVILEGIADO proxy: 443

Ejemplo: Sin problemas

Entrada

web 8080:80
api 3000:3000

Salida esperada

Sin problemas

27. Informe de un pipeline de CI

Medio · Ejercicios de Despliegue de Aplicaciones Web · apuntesdam.com/subject/despliegue/topic/ejercicios-despliegue

Cada línea es un paso de un pipeline: «paso estado segundos», con estado ok, error o omitido. Escribe «Pasos: N (ok: X, error: Y, omitido: Z)», «Duración total: M min S s» (sumando solo los pasos no omitidos), «Paso más lento: nombre (N s)» entre los ejecutados y, si hay algún error, «Primer error: nombre»; si no, «Resultado: correcto».

Código de partida (bash)
#!/bin/bashwhile read -r paso estado segundos || [ -n "$paso" ]; do    # TODO    :done

Ejemplo: Pipeline con error

Entrada

checkout ok 3
instalar ok 48
lint ok 12
tests error 95
build omitido 0
desplegar omitido 0

Salida esperada

Pasos: 6 (ok: 3, error: 1, omitido: 2)
Duración total: 2 min 38 s
Paso más lento: tests (95 s)
Primer error: tests

Ejemplo: Pipeline correcto

Entrada

checkout ok 2
tests ok 61
build ok 130
desplegar ok 40

Salida esperada

Pasos: 4 (ok: 4, error: 0, omitido: 0)
Duración total: 3 min 53 s
Paso más lento: build (130 s)
Resultado: correcto

28. Rotación de copias de seguridad

Difícil · Ejercicios de Despliegue de Aplicaciones Web · apuntesdam.com/subject/despliegue/topic/ejercicios-despliegue

La primera línea es la fecha de hoy y las siguientes, nombres de copias «backup-AAAA-MM-DD.tar.gz» (desordenados). Aplica esta política: se conservan todas las copias de los últimos 7 días (hoy incluido, es decir, con 0 a 6 días de antigüedad); de las que tienen entre 7 y 30 días, solo las de domingo; y las de más de 30 días se borran. Escribe «CONSERVAR nombre» o «BORRAR nombre» ordenadas de la más reciente a la más antigua, y al final «Conservadas: N · Borradas: M». Los nombres que no siguen el formato se ignoran.

Código de partida (bash)
#!/bin/bashread -r hoy # TODO

Ejemplo: Un mes de copias

Entrada

2025-09-30
backup-2025-09-30.tar.gz
backup-2025-09-24.tar.gz
backup-2025-09-23.tar.gz
backup-2025-09-21.tar.gz
backup-2025-09-20.tar.gz
backup-2025-09-14.tar.gz
backup-2025-08-31.tar.gz
backup-2025-08-24.tar.gz
notas.txt
backup-2025-09-29.tar.gz

Salida esperada

CONSERVAR backup-2025-09-30.tar.gz
CONSERVAR backup-2025-09-29.tar.gz
CONSERVAR backup-2025-09-24.tar.gz
BORRAR backup-2025-09-23.tar.gz
CONSERVAR backup-2025-09-21.tar.gz
BORRAR backup-2025-09-20.tar.gz
CONSERVAR backup-2025-09-14.tar.gz
CONSERVAR backup-2025-08-31.tar.gz
BORRAR backup-2025-08-24.tar.gz
Conservadas: 6 · Borradas: 3

Ejercicios largos

29. Informe de un registro de accesos web

Difícil · Bash · 75 minutos · apuntesdam.com/ejercicios/bash/informe-registro-accesos-web

Los servidores web como Apache y Nginx anotan cada petición en un registro de accesos (access.log). Cada línea dice quién pidió qué, cuándo y cómo respondió el servidor, en un formato estándar llamado Common Log Format.

El administrador de una web pequeña quiere un resumen diario sin instalar herramientas de análisis: un script de Bash que lea el registro por la entrada estándar (./informe.sh < access.log) y escriba las cifras importantes.

Un registro real puede tener líneas cortadas (por ejemplo, si el disco se llenó a mitad de una escritura). El script no debe contarlas como peticiones, pero sí decir cuántas había.

Requisitos

  • Cada línea tiene la forma IP - - [fecha zona] "MÉTODO ruta PROTOCOLO" código bytes. Separada por espacios, la fecha ocupa dos campos y la petición tres; el código es el 9.º campo y los bytes el 10.º. Los bytes valen - cuando la respuesta no tiene cuerpo (por ejemplo, una redirección): cuentan como 0.

  • Las líneas vacías se ignoran. Una línea no es válida si no llega al campo de los bytes o si el código no es un número de tres cifras.

  • Si no hay ninguna petición válida, escribe solo No hay peticiones válidas.

  • Escribe Peticiones: N, Visitantes distintos: N (el número de IP distintas), Respuestas: 2xx A · 3xx B · 4xx C · 5xx D (cuántas respuestas hay de cada familia, según la primera cifra del código) y Datos enviados: N KB (la suma de los bytes dividida entre 1024, sin decimales).

  • Bajo IPs con más peticiones:, las 3 IP con más peticiones (o menos, si no hay tantas), de más a menos: el número en 3 caracteres alineado a la derecha, un espacio y la IP. A igual número, por orden de texto de la IP, carácter a carácter (por eso 10.0.0.10 va antes que 10.0.0.2).

  • Página más pedida: ruta (N veces), con «vez» si es 1. Si hay empate, la primera en orden de texto.

  • Errores 404: y debajo, con dos espacios delante, cada ruta distinta que haya respondido 404, en orden de texto. Si no hay ninguna, Errores 404: ninguno en una sola línea.

  • Si ha habido líneas no válidas, termina con Líneas no válidas: N.

Formato de la entrada

  • El registro de accesos por la entrada estándar, una petición por línea en Common Log Format.

Campos de una línea del registro
CampoEjemploSignificado
1192.168.1.10IP del cliente
2 y 3- -identidad y usuario (casi siempre vacíos)
4 y 5[03/Oct/2026:10:15:32 +0200]fecha, hora y zona horaria
6, 7 y 8"GET /index.html HTTP/1.1"método, ruta y protocolo
9200código de respuesta
105120bytes enviados (- si no hay cuerpo)

Ejemplo: Diez peticiones

Entrada

192.168.1.10 - - [03/Oct/2026:10:15:32 +0200] "GET /index.html HTTP/1.1" 200 5120
192.168.1.10 - - [03/Oct/2026:10:15:33 +0200] "GET /css/estilos.css HTTP/1.1" 200 2048
10.0.0.7 - - [03/Oct/2026:10:16:01 +0200] "GET /index.html HTTP/1.1" 200 5120
10.0.0.7 - - [03/Oct/2026:10:16:02 +0200] "GET /favicon.ico HTTP/1.1" 404 512
172.16.0.3 - - [03/Oct/2026:10:17:45 +0200] "POST /login HTTP/1.1" 302 -
172.16.0.3 - - [03/Oct/2026:10:17:46 +0200] "GET /panel HTTP/1.1" 200 8192
192.168.1.10 - - [03/Oct/2026:10:18:10 +0200] "GET /admin HTTP/1.1" 404 512
10.0.0.7 - - [03/Oct/2026:10:19:00 +0200] "GET /index.html HTTP/1.1" 200 5120
192.168.1.10 - - [03/Oct/2026:10:20:15 +0200] "GET /api/datos HTTP/1.1" 500 128
8.8.4.4 - - [03/Oct/2026:10:21:30 +0200] "GET /index.html HTTP/1.1" 200 5120

Salida esperada

Peticiones: 10
Visitantes distintos: 4
Respuestas: 2xx 6 · 3xx 1 · 4xx 2 · 5xx 1
Datos enviados: 31 KB
IPs con más peticiones:
  4 192.168.1.10
  3 10.0.0.7
  2 172.16.0.3
Página más pedida: /index.html (4 veces)
Errores 404:
  /admin
  /favicon.ico

Ejemplo: Empates y líneas rotas

Entrada

10.0.0.2 - - [04/Oct/2026:09:00:00 +0200] "GET /b HTTP/1.1" 200 1024
10.0.0.10 - - [04/Oct/2026:09:00:01 +0200] "GET /a HTTP/1.1" 200 1024
línea rota
10.0.0.2 - - [04/Oct/2026:09:00:02 +0200] "GET /a HTTP/1.1" 301 -
10.0.0.10 - - [04/Oct/2026:09:00:03 +0200] "GET /b HTTP/1.1" 200 2048

10.0.0.3 - - [04/Oct/2026:09:00:04 +0200] "GET /c HTTP/1.1" 200 1024

Salida esperada

Peticiones: 5
Visitantes distintos: 3
Respuestas: 2xx 4 · 3xx 1 · 4xx 0 · 5xx 0
Datos enviados: 5 KB
IPs con más peticiones:
  2 10.0.0.10
  2 10.0.0.2
  1 10.0.0.3
Página más pedida: /a (2 veces)
Errores 404: ninguno
Líneas no válidas: 1
Código de partida (bash)
#!/bin/bash# Orden de texto byte a byte: así sort y [[ < ]] ordenan igual en cualquier máquinaexport LC_ALL=C declare -A por_ip por_ruta rutas_404total=0bytes_total=0invalidas=0c2=0 c3=0 c4=0 c5=0 while read -r ip _ _ _ _ metodo ruta _ codigo bytes _; do  [ -z "$ip" ] && continue  # TODO: valida la línea y actualiza los contadoresdone # TODO: escribe el informe

30. Analizador de la configuración de Nginx

Muy difícil · Bash · 90 minutos · apuntesdam.com/ejercicios/bash/analizador-de-configuracion-nginx

En un servidor con varias webs, la configuración de Nginx crece con el tiempo: un server para el dominio, otro para la API, otro que nadie recuerda… y los errores aparecen: un sitio que escucha en el 443 sin certificado (Nginx no arranca), un autoindex on olvidado que enseña los ficheros a cualquiera, o un dominio con HTTPS cuyo puerto 80 sigue sirviendo la web sin cifrar.

nginx -t comprueba la sintaxis, pero no esas buenas prácticas. Vas a escribir un script que lea un fichero de sitios (el que estaría en /etc/nginx/sites-available/), resuma cada bloque server y avise de los problemas.

El formato de Nginx es sencillo de leer línea a línea: directivas terminadas en ; y bloques entre llaves (server { … }, y dentro bloques location … { … }). Para saber dónde termina cada server hay que llevar la cuenta de las llaves abiertas.

Requisitos

  • Se ignoran los comentarios (desde # hasta el final de la línea), los espacios de los extremos y las líneas vacías. Cada línea es server {, otra apertura de bloque (termina en {), un cierre } o una directiva terminada en ;. Las líneas se numeran desde 1 contando todas.

  • Errores, que detienen el script: Error: llave de cierre sin abrir en la línea N, Error: bloque fuera de un server en la línea N, Error: directiva fuera de un server en la línea N y, al final, Error: llaves sin cerrar. Si no hay ningún server, No hay ningún bloque server.

  • De cada server se guardan (también desde dentro de sus location): los puertos de listen (el número tras los dos puntos, si los hay, y ssl si la línea lo lleva; sin repetir; 80 si no hay ningún listen), los nombres de server_name, el primer root, el primer proxy_pass, la URL del último return (lo que va tras el código), si tiene ssl_certificate, si tiene ssl_certificate_key y si tiene autoindex on.

  • Por cada servidor: Servidor N (líneas A-B): puerto P · nombres · acción, con los puertos separados por y (443 ssl y 80), los nombres tal cual o (sin nombre), y como acción redirige a URL, proxy a URL, sirve RUTA o no sirve nada, por ese orden de preferencia.

  • Avisos, primero los de cada servidor en orden (Servidor N: usa ssl pero le falta ssl_certificate y ssl_certificate_key —o solo el que falte—, Servidor N: autoindex activado (muestra la lista de ficheros), Servidor N: no tiene root, return ni proxy_pass) y después, por orden alfabético, dominio: aparece en K servidores del puerto P (para cada dominio y puerto con más de un servidor) y dominio: el puerto 80 no redirige a HTTPS (para cada dominio que se sirve en el 443 sin un servidor en el 80 cuyo return vaya a https://).

  • Escribe Avisos: y cada aviso con - delante, o Sin avisos.

Formato de la entrada

  • Un fichero de configuración de sitios de Nginx.

Ejemplo: Un dominio con API

Entrada

# Sitio principal
server {
    listen 80;
    listen [::]:80;
    server_name ejemplo.com www.ejemplo.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name ejemplo.com www.ejemplo.com;
    ssl_certificate /etc/letsencrypt/live/ejemplo.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ejemplo.com/privkey.pem;
    root /var/www/ejemplo;

    location /descargas/ {
        autoindex on;   # se dejó activado tras una prueba
    }
}

server {
    listen 443 ssl;
    server_name api.ejemplo.com;
    ssl_certificate /etc/ssl/api.crt;
    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

Salida esperada

Servidor 1 (líneas 2-7): puerto 80 · ejemplo.com www.ejemplo.com · redirige a https://$host$request_uri
Servidor 2 (líneas 9-20): puerto 443 ssl · ejemplo.com www.ejemplo.com · sirve /var/www/ejemplo
Servidor 3 (líneas 22-29): puerto 443 ssl · api.ejemplo.com · proxy a http://127.0.0.1:3000
Avisos:
  - Servidor 2: autoindex activado (muestra la lista de ficheros)
  - Servidor 3: usa ssl pero le falta ssl_certificate_key
  - api.ejemplo.com: el puerto 80 no redirige a HTTPS

Ejemplo: Servidores repetidos y uno vacío

Entrada

server {
    server_name intranet.local;
}
server {
    listen 8080;
    server_name intranet.local;
    root /srv/intranet;
}
server {
    listen 8080;
    server_name intranet.local;
    return 302 /mantenimiento.html;
}

Salida esperada

Servidor 1 (líneas 1-3): puerto 80 · intranet.local · no sirve nada
Servidor 2 (líneas 4-8): puerto 8080 · intranet.local · sirve /srv/intranet
Servidor 3 (líneas 9-13): puerto 8080 · intranet.local · redirige a /mantenimiento.html
Avisos:
  - Servidor 1: no tiene root, return ni proxy_pass
  - intranet.local: aparece en 2 servidores del puerto 8080
Código de partida (bash)
#!/bin/bash# Revisa un fichero de sitios de Nginx (como /etc/nginx/sites-available/web) y avisa de los fallos típicosexport LC_ALL=C n=0 profundidad=0 numero=0declare -a inicio fin puertos nombres raiz retorno proxy cert clave autoindex while IFS= read -r linea || [ -n "$linea" ]; do  numero=$((numero + 1))  # TODO: quita comentarios y espacios, sigue las llaves y guarda las directivas de cada serverdone # TODO: resumen de cada servidor y avisos

31. Despliegue por releases con enlace simbólico y vuelta atrás

Difícil · Bash · 70 minutos · apuntesdam.com/ejercicios/bash/despliegue-por-releases-con-rollback

Copiar los ficheros nuevos encima de los de la web en producción tiene un problema: durante unos segundos conviven ficheros viejos y nuevos, y si algo sale mal no hay forma rápida de volver atrás. Las herramientas de despliegue (Capistrano, Deployer, Envoyer) lo resuelven con releases: cada versión se instala en su propia carpeta (releases/20261004-1000) y el servidor web apunta a un enlace simbólico current.

Desplegar es preparar la carpeta nueva por completo y, solo al final, cambiar el enlace. Ese cambio se hace de forma atómica (se crea un enlace temporal y se renombra encima del antiguo con mv -T), así que ninguna petición ve una web a medias. Volver atrás es cambiar el enlace a la release anterior: un segundo, sin reinstalar nada.

El script recibe las releases que ya hay y una serie de órdenes, y escribe las órdenes de shell que ejecutaría. Las líneas informativas empiezan por #, así la salida se puede revisar y, después, ejecutar tal cual.

Requisitos

  • La primera línea es RELEASES seguida de las carpetas que ya existen, de la más antigua a la más reciente (puede no haber ninguna); la más reciente es la actual. Si no empieza por RELEASES, escribe La primera línea debe ser RELEASES r1 r2… (puede estar vacía) y termina. Las órdenes pueden ir en minúsculas; las líneas vacías se ignoran; una orden desconocida escribe # Orden desconocida: orden.

  • DESPLIEGA AAAAMMDD-HHMM vX.Y.Z: si el formato no es ese, # Uso: DESPLIEGA AAAAMMDD-HHMM vX.Y.Z; si la release ya existe, # La release R ya existe; si es anterior a la más reciente, # La release R es anterior a ÚLTIMA. Si todo va bien, escribe # Desplegando vX.Y.Z en R y las cinco órdenes de la tabla, la release pasa a ser la actual y, si hay más de 3, se escribe rm -rf /var/www/tienda/releases/R para borrar las más antiguas hasta dejar 3.

  • ROLLBACK: si no hay actual o es la más antigua, # No hay una release anterior a la que volver; si no, # Volviendo de ACTUAL a ANTERIOR, la orden del enlace y la recarga de PHP-FPM, y la anterior pasa a ser la actual (se puede volver atrás varias veces seguidas).

  • ESTADO: # Releases (N): y una línea por release de la más antigua a la más reciente, # R y, en la actual, # R ← current; o # Sin releases.

  • Un despliegue después de un rollback despliega la versión nueva como más reciente (la release a la que se había vuelto deja de ser la actual).

Formato de la entrada

  • Línea 1: RELEASES y las releases existentes. Resto: DESPLIEGA fecha vX.Y.Z, ROLLBACK o ESTADO.

Órdenes de un despliegue (R es la release, base /var/www/tienda)
PasoOrden
Descargar la versióngit clone --depth 1 --branch vX.Y.Z https://git.ejemplo.com/tienda.git /var/www/tienda/releases/R
Configuración compartidaln -s /var/www/tienda/shared/.env /var/www/tienda/releases/R/.env
Dependenciascomposer install --no-dev --optimize-autoloader --working-dir=/var/www/tienda/releases/R
Cambio atómicoln -sfn /var/www/tienda/releases/R /var/www/tienda/current.tmp && mv -T /var/www/tienda/current.tmp /var/www/tienda/current
Recargar PHPsystemctl reload php8.3-fpm

Ejemplo: Dos despliegues y una vuelta atrás

Entrada

RELEASES 20260901-1200 20260915-0930
ESTADO
DESPLIEGA 20261001-1800 v1.4.0
DESPLIEGA 20261004-1000 v1.5.0
ROLLBACK
ESTADO

Salida esperada

# Releases (2):
#   20260901-1200
#   20260915-0930  ← current
# Desplegando v1.4.0 en 20261001-1800
git clone --depth 1 --branch v1.4.0 https://git.ejemplo.com/tienda.git /var/www/tienda/releases/20261001-1800
ln -s /var/www/tienda/shared/.env /var/www/tienda/releases/20261001-1800/.env
composer install --no-dev --optimize-autoloader --working-dir=/var/www/tienda/releases/20261001-1800
ln -sfn /var/www/tienda/releases/20261001-1800 /var/www/tienda/current.tmp && mv -T /var/www/tienda/current.tmp /var/www/tienda/current
systemctl reload php8.3-fpm
# Desplegando v1.5.0 en 20261004-1000
git clone --depth 1 --branch v1.5.0 https://git.ejemplo.com/tienda.git /var/www/tienda/releases/20261004-1000
ln -s /var/www/tienda/shared/.env /var/www/tienda/releases/20261004-1000/.env
composer install --no-dev --optimize-autoloader --working-dir=/var/www/tienda/releases/20261004-1000
ln -sfn /var/www/tienda/releases/20261004-1000 /var/www/tienda/current.tmp && mv -T /var/www/tienda/current.tmp /var/www/tienda/current
systemctl reload php8.3-fpm
rm -rf /var/www/tienda/releases/20260901-1200
# Volviendo de 20261004-1000 a 20261001-1800
ln -sfn /var/www/tienda/releases/20261001-1800 /var/www/tienda/current.tmp && mv -T /var/www/tienda/current.tmp /var/www/tienda/current
systemctl reload php8.3-fpm
# Releases (3):
#   20260915-0930
#   20261001-1800  ← current
#   20261004-1000

Ejemplo: Órdenes que no valen

Entrada

RELEASES 20261001-1800
DESPLIEGA 2026-10-04 v1.5
DESPLIEGA 20261001-1800 v1.5.0
DESPLIEGA 20260930-0800 v1.5.0
ROLLBACK
PUBLICA

Salida esperada

# Uso: DESPLIEGA AAAAMMDD-HHMM vX.Y.Z
# La release 20261001-1800 ya existe
# La release 20260930-0800 es anterior a 20261001-1800
# No hay una release anterior a la que volver
# Orden desconocida: PUBLICA
Código de partida (bash)
#!/bin/bash# Despliegue por releases (como Capistrano o Deployer): cada versión en su carpeta y un enlace «current»# que se cambia de golpe. Modo simulación: escribe las órdenes en lugar de ejecutarlas.export LC_ALL=CBASE=/var/www/tiendaMAX_RELEASES=3 releases=()          # carpetas de releases, de la más antigua a la más recienteactual="" read -r palabra resto# TODO: lee las releases existentes y atiende DESPLIEGA, ROLLBACK y ESTADO