Servidor de Documentación Swagger y OpenAPI en Linux

OpenAPI (anteriormente Swagger) es el estándar de facto para describir APIs REST, y herramientas como Swagger UI y ReDoc generan documentación interactiva a partir de una especificación en formato JSON o YAML. Alojar tu documentación de API en un servidor Linux dedicado—con control de versiones, autenticación y pipeline CI/CD—es fundamental para equipos de desarrollo que trabajan con APIs internas y externas. Esta guía cubre la escritura de especificaciones OpenAPI, el despliegue de Swagger UI y ReDoc con Nginx, el versionado y la automatización del pipeline de documentación.

Requisitos Previos

  • Ubuntu 20.04+ o Rocky Linux 8+
  • Nginx instalado
  • Docker (para despliegue en contenedor)
  • Node.js 18+ (para herramientas de validación)
# Instalar herramientas de validación OpenAPI
npm install -g @stoplight/spectral-cli
npm install -g swagger-cli

# Verificar instalación
spectral --version
swagger-cli --version

Estructura de la Especificación OpenAPI

# api-spec/openapi.yaml - Especificación OpenAPI 3.1 completa
openapi: 3.1.0

info:
  title: Mi API
  description: |
    # Documentación de Mi API
    
    Esta API proporciona acceso a los recursos de nuestra plataforma.
    
    ## Autenticación
    Todas las rutas requieren token JWT en el header `Authorization: Bearer <token>`.
    
    ## Rate Limiting
    Las solicitudes están limitadas a 100 por minuto por IP.
  version: 1.2.0
  contact:
    name: Equipo de API
    email: [email protected]
    url: https://docs.miempresa.com
  license:
    name: Apache 2.0
    url: https://www.apache.org/licenses/LICENSE-2.0

servers:
  - url: https://api.miempresa.com/v1
    description: Servidor de producción
  - url: https://api-staging.miempresa.com/v1
    description: Servidor de staging
  - url: http://localhost:3000/v1
    description: Desarrollo local

# Componentes reutilizables
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key

  schemas:
    Usuario:
      type: object
      required: [id, email, nombre]
      properties:
        id:
          type: string
          format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
        email:
          type: string
          format: email
          example: "[email protected]"
        nombre:
          type: string
          minLength: 2
          maxLength: 100
          example: "Juan García"
        createdAt:
          type: string
          format: date-time
          readOnly: true

    Error:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
          example: "NOT_FOUND"
        message:
          type: string
          example: "El recurso solicitado no existe"
        details:
          type: object

    Paginacion:
      type: object
      properties:
        page:
          type: integer
          minimum: 1
          default: 1
        limit:
          type: integer
          minimum: 1
          maximum: 100
          default: 20
        total:
          type: integer
        hasNextPage:
          type: boolean

  responses:
    NoAutorizado:
      description: Token inválido o expirado
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NoEncontrado:
      description: Recurso no encontrado
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

# Seguridad global (aplica a todos los endpoints)
security:
  - BearerAuth: []

paths:
  /usuarios:
    get:
      summary: Listar usuarios
      description: Obtiene la lista paginada de usuarios del sistema
      operationId: listarUsuarios
      tags: [Usuarios]
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: search
          in: query
          description: Filtrar por nombre o email
          schema:
            type: string
      responses:
        '200':
          description: Lista de usuarios
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Paginacion'
                  - type: object
                    properties:
                      items:
                        type: array
                        items:
                          $ref: '#/components/schemas/Usuario'
        '401':
          $ref: '#/components/responses/NoAutorizado'

    post:
      summary: Crear usuario
      operationId: crearUsuario
      tags: [Usuarios]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, nombre, password]
              properties:
                email:
                  type: string
                  format: email
                nombre:
                  type: string
                password:
                  type: string
                  minLength: 8
            examples:
              ejemplo_basico:
                summary: Usuario básico
                value:
                  email: "[email protected]"
                  nombre: "María López"
                  password: "contraseña123"
      responses:
        '201':
          description: Usuario creado exitosamente
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Usuario'

Desplegar Swagger UI con Docker

# Crear directorio para la documentación
mkdir -p /opt/api-docs/{specs,nginx}

# Copiar la especificación OpenAPI
cp api-spec/openapi.yaml /opt/api-docs/specs/

# Docker Compose para Swagger UI
cat > /opt/api-docs/docker-compose.yml << 'EOF'
version: '3.8'

services:
  swagger-ui:
    image: swaggerapi/swagger-ui:latest
    container_name: swagger-ui
    environment:
      - SWAGGER_JSON=/api-docs/openapi.yaml
      - BASE_URL=/docs
      - SWAGGER_JSON_URL=/specs/openapi.yaml
      # Múltiples URLs de specs (para varias versiones)
      - URLS=[{"url":"/specs/openapi.yaml","name":"v1 - API Principal"},{"url":"/specs/openapi-v2.yaml","name":"v2 - Nuevo API"}]
    volumes:
      - ./specs:/api-docs
    ports:
      - "8080:8080"
    restart: unless-stopped

  redoc:
    image: redocly/redoc:latest
    container_name: redoc
    environment:
      - SPEC_URL=/specs/openapi.yaml
    volumes:
      - ./specs:/usr/share/nginx/html/specs
    ports:
      - "8081:80"
    restart: unless-stopped

EOF

# Iniciar los servicios de documentación
docker compose -f /opt/api-docs/docker-compose.yml up -d

# Verificar que funcionan
curl -s http://localhost:8080 | head -5
curl -s http://localhost:8081 | head -5

Desplegar ReDoc

# Alternativa: desplegar ReDoc como archivo HTML estático
mkdir -p /var/www/api-docs/specs

cat > /var/www/api-docs/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
    <title>API Documentación</title>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
    <style>body { margin: 0; padding: 0; }</style>
</head>
<body>
    <redoc spec-url='/specs/openapi.yaml'
           expand-responses="200,201"
           show-extensions="true"
           scroll-y-offset="0"
           hide-hostname="false"
    ></redoc>
    <script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"></script>
</body>
</html>
EOF

# Copiar especificaciones al servidor web
cp /opt/api-docs/specs/*.yaml /var/www/api-docs/specs/
chown -R www-data:www-data /var/www/api-docs

Nginx para Servir la Documentación

cat > /etc/nginx/sites-available/api-docs << 'EOF'
server {
    listen 443 ssl http2;
    server_name docs.midominio.com;

    ssl_certificate /etc/letsencrypt/live/docs.midominio.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/docs.midominio.com/privkey.pem;

    root /var/www/api-docs;
    index index.html;

    # Servir archivos estáticos (ReDoc HTML estático)
    location / {
        try_files $uri $uri/ =404;
    }

    # Servir las especificaciones OpenAPI
    location /specs/ {
        alias /var/www/api-docs/specs/;
        
        # Headers CORS para que Swagger UI pueda cargar desde otros dominios
        add_header Access-Control-Allow-Origin *;
        add_header Content-Type "application/yaml";
        
        # Caché de 1 hora para los archivos de spec
        expires 1h;
        add_header Cache-Control "public, max-age=3600";
    }

    # Swagger UI (proxy al contenedor Docker)
    location /swagger/ {
        proxy_pass http://127.0.0.1:8080/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # Proteger acceso a la documentación interna
    location /internal/ {
        auth_basic "Documentación Interna";
        auth_basic_user_file /etc/nginx/.htpasswd-docs;
        
        try_files $uri $uri/ =404;
    }
}
EOF

# Crear contraseña para documentación interna
apt install apache2-utils
htpasswd -c /etc/nginx/.htpasswd-docs desarrollador

ln -s /etc/nginx/sites-available/api-docs /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

Versionado de la Especificación API

# Estructura de directorios para múltiples versiones
mkdir -p /var/www/api-docs/specs/{v1,v2}
cp openapi-v1.yaml /var/www/api-docs/specs/v1/openapi.yaml
cp openapi-v2.yaml /var/www/api-docs/specs/v2/openapi.yaml

# Script para validar y publicar una nueva versión
cat > /usr/local/bin/publish-api-docs.sh << 'EOF'
#!/bin/bash
# Publicar nueva versión de la especificación API
SPEC_FILE=$1
VERSION=$2
DESTINO="/var/www/api-docs/specs/$VERSION"

if [ -z "$SPEC_FILE" ] || [ -z "$VERSION" ]; then
    echo "Uso: $0 <archivo-spec> <versión>"
    echo "Ejemplo: $0 openapi.yaml v2"
    exit 1
fi

# Validar la especificación con Spectral
echo "Validando especificación OpenAPI..."
spectral lint "$SPEC_FILE" --ruleset .spectral.yaml
if [ $? -ne 0 ]; then
    echo "ERROR: La especificación tiene errores de validación"
    exit 1
fi

# Validar con swagger-cli
swagger-cli validate "$SPEC_FILE"
if [ $? -ne 0 ]; then
    echo "ERROR: La especificación es inválida"
    exit 1
fi

# Publicar
mkdir -p "$DESTINO"
cp "$SPEC_FILE" "$DESTINO/openapi.yaml"
chown -R www-data:www-data "$DESTINO"

echo "Especificación publicada en $DESTINO"
echo "Accesible en: https://docs.midominio.com/specs/$VERSION/openapi.yaml"
EOF
chmod +x /usr/local/bin/publish-api-docs.sh

Automatización CI/CD

# .github/workflows/docs.yml - Publicar docs automáticamente al hacer push
# Para un pipeline equivalente en un servidor Linux con scripts:

cat > /usr/local/bin/ci-publish-docs.sh << 'EOF'
#!/bin/bash
# CI/CD para documentación API - ejecutar en el servidor tras git pull
set -e

REPO_DIR="/opt/api-repo"
DOCS_DIR="/var/www/api-docs"

echo "=== Pipeline de Documentación API ==="

# 1. Actualizar el repositorio
cd "$REPO_DIR"
git pull origin main

# 2. Validar todas las especificaciones
echo "Validando especificaciones..."
for spec in api-spec/*.yaml; do
    echo "  Validando: $spec"
    spectral lint "$spec" 2>&1
    if [ $? -ne 0 ]; then
        echo "ERROR en $spec"
        exit 1
    fi
done

# 3. Publicar especificaciones
echo "Publicando especificaciones..."
cp api-spec/*.yaml "$DOCS_DIR/specs/"

# 4. Recargar Nginx para servir la nueva versión
nginx -s reload

echo "=== Documentación publicada exitosamente ==="
EOF
chmod +x /usr/local/bin/ci-publish-docs.sh

# Configurar webhook para ejecutar al hacer git push
# (usando un servidor de webhook simple)
apt install webhook

cat > /etc/webhook/hooks.json << 'EOF'
[
    {
        "id": "deploy-docs",
        "execute-command": "/usr/local/bin/ci-publish-docs.sh",
        "command-working-directory": "/opt/api-repo",
        "trigger-rule": {
            "match": {
                "type": "payload-hash-sha256",
                "secret": "webhook-secret-seguro",
                "parameter": {
                    "source": "header",
                    "name": "X-Hub-Signature-256"
                }
            }
        }
    }
]
EOF

systemctl enable --now webhook

Seguridad y Control de Acceso

# Configurar acceso restringido para documentación interna
# Método 1: Autenticación básica con Nginx
apt install apache2-utils
htpasswd -c /etc/nginx/.htpasswd-api admin

# Método 2: Restricción por IP para documentación sensible
cat >> /etc/nginx/sites-available/api-docs << 'EOF'
    # Solo permitir acceso a la documentación desde la red interna
    location /internal-api/ {
        allow 10.0.0.0/8;
        allow 192.168.0.0/16;
        deny all;
        
        try_files $uri $uri/ =404;
    }
EOF

nginx -t && systemctl reload nginx

# Método 3: Ocultar información sensible en la spec de producción
# Usar bundles diferentes para interno vs público
cat > /usr/local/bin/strip-internal-docs.sh << 'EOF'
#!/bin/bash
# Eliminar endpoints internos de la spec pública
# Requiere yq o python con pyyaml

python3 << 'PYTHON'
import yaml, sys

with open('openapi-full.yaml') as f:
    spec = yaml.safe_load(f)

# Eliminar paths marcados con x-internal: true
spec['paths'] = {
    path: details
    for path, details in spec['paths'].items()
    if not details.get('x-internal', False)
}

with open('openapi-public.yaml', 'w') as f:
    yaml.dump(spec, f, default_flow_style=False, allow_unicode=True)

print("Spec pública generada: openapi-public.yaml")
PYTHON
EOF
chmod +x /usr/local/bin/strip-internal-docs.sh

Solución de Problemas

Swagger UI muestra error "Failed to fetch":

# Problema de CORS al cargar la spec desde otro dominio
# Añadir header CORS en Nginx para los archivos .yaml
location /specs/ {
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods "GET, OPTIONS";
}
# Verificar que la URL de la spec es correcta en la config de Swagger UI

La spec YAML no se valida:

# Verificar sintaxis YAML
python3 -c "import yaml; yaml.safe_load(open('openapi.yaml'))"
# Validar contra el estándar OpenAPI
swagger-cli validate openapi.yaml
npx @redocly/cli lint openapi.yaml

ReDoc no muestra la documentación actualizada:

# Limpiar caché del navegador
# Verificar que el archivo fue actualizado en el servidor
ls -la /var/www/api-docs/specs/
# Ver si Nginx tiene caché configurado
grep -i expires /etc/nginx/sites-available/api-docs
# Forzar recarga sin caché en Nginx (si hay proxy_cache)
nginx -s reload

Conclusión

Una documentación de API bien mantenida es tan importante como el código de la API misma: facilita la adopción por parte de desarrolladores, reduce el soporte necesario y sirve como contrato formal entre productores y consumidores de la API. El pipeline de publicación automatizado—validar la especificación con Spectral, generar la documentación con Swagger UI o ReDoc y publicarla con Nginx—garantiza que la documentación siempre está sincronizada con el código. Versionar las especificaciones OpenAPI junto al código en el mismo repositorio es la mejor práctica para mantener la consistencia a lo largo del tiempo.