Swagger and OpenAPI Documentation Server

OpenAPI (formerly Swagger) is the standard specification for describing REST APIs, and hosting interactive documentation with Swagger UI or ReDoc gives developers a self-service portal to explore and test your API. This guide covers writing OpenAPI specs, deploying Swagger UI and ReDoc, hosting documentation with Nginx, managing API versioning, and automating documentation updates in CI/CD pipelines.

Prerequisites

  • Ubuntu 20.04+ or CentOS/Rocky 8+ with root access
  • Nginx installed
  • Node.js (optional, for spec linting tools)
  • Docker (optional, for containerized deployment)

Writing an OpenAPI Specification

OpenAPI 3.x specifications are written in YAML or JSON:

mkdir -p /opt/api-docs/specs

cat > /opt/api-docs/specs/api-v1.yaml << 'EOF'
openapi: "3.0.3"
info:
  title: My API
  description: |
    This API provides access to user and order management.
    
    ## Authentication
    Use API key in the `X-API-Key` header or Bearer token.
  version: "1.0.0"
  contact:
    name: API Support
    email: [email protected]
    url: https://example.com/support
  license:
    name: Apache 2.0
    url: https://www.apache.org/licenses/LICENSE-2.0

servers:
  - url: https://api.example.com/v1
    description: Production
  - url: https://staging.api.example.com/v1
    description: Staging
  - url: http://localhost:3000/v1
    description: Local development

security:
  - ApiKeyAuth: []
  - BearerAuth: []

tags:
  - name: Users
    description: User management operations
  - name: Orders
    description: Order management operations

paths:
  /users:
    get:
      summary: List all users
      description: Returns a paginated list of users.
      operationId: listUsers
      tags: [Users]
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          description: Number of results per page
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
          description: Pagination offset
      responses:
        "200":
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/User"
                  total:
                    type: integer
              example:
                data:
                  - id: "usr_123"
                    name: "Alice Smith"
                    email: "[email protected]"
                total: 42
        "401":
          $ref: "#/components/responses/Unauthorized"

    post:
      summary: Create a user
      operationId: createUser
      tags: [Users]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateUserRequest"
      responses:
        "201":
          description: User created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "400":
          $ref: "#/components/responses/BadRequest"

  /users/{userId}:
    get:
      summary: Get a user
      operationId: getUser
      tags: [Users]
      parameters:
        - $ref: "#/components/parameters/UserId"
      responses:
        "200":
          description: User found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "404":
          $ref: "#/components/responses/NotFound"

components:
  schemas:
    User:
      type: object
      required: [id, name, email]
      properties:
        id:
          type: string
          example: "usr_123"
        name:
          type: string
          example: "Alice Smith"
        email:
          type: string
          format: email
          example: "[email protected]"
        createdAt:
          type: string
          format: date-time

    CreateUserRequest:
      type: object
      required: [name, email, password]
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 100
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 8
          writeOnly: true

    Error:
      type: object
      properties:
        error:
          type: string
        message:
          type: string

  parameters:
    UserId:
      name: userId
      in: path
      required: true
      schema:
        type: string
      description: The user's ID

  responses:
    Unauthorized:
      description: Authentication required
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
EOF

Validate the spec:

# Install swagger-cli for validation
npm install -g @apidevtools/swagger-cli

swagger-cli validate /opt/api-docs/specs/api-v1.yaml

Deploying Swagger UI

# Download Swagger UI
SWAGGER_VERSION="5.17.14"
wget https://github.com/swagger-api/swagger-ui/archive/refs/tags/v${SWAGGER_VERSION}.tar.gz
tar -xzf v${SWAGGER_VERSION}.tar.gz

# Copy the dist folder
sudo mkdir -p /opt/api-docs/swagger-ui
sudo cp -r swagger-ui-${SWAGGER_VERSION}/dist/* /opt/api-docs/swagger-ui/

# Configure Swagger UI to point to your spec
sudo tee /opt/api-docs/swagger-ui/swagger-initializer.js << 'EOF'
window.onload = function() {
  window.ui = SwaggerUIBundle({
    urls: [
      { url: "/specs/api-v1.yaml", name: "API v1" },
      { url: "/specs/api-v2.yaml", name: "API v2" },
    ],
    dom_id: '#swagger-ui',
    deepLinking: true,
    presets: [
      SwaggerUIBundle.presets.apis,
      SwaggerUIStandalonePreset
    ],
    plugins: [
      SwaggerUIBundle.plugins.DownloadUrl
    ],
    layout: "StandaloneLayout",
    persistAuthorization: true,
    tryItOutEnabled: true,
    requestInterceptor: function(request) {
      // Add custom headers to all try-it-out requests
      request.headers['X-Client'] = 'SwaggerUI';
      return request;
    },
  });
};
EOF

Deploying ReDoc

ReDoc provides a more polished, three-panel documentation layout:

sudo mkdir -p /opt/api-docs/redoc

sudo tee /opt/api-docs/redoc/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
    <title>My API Documentation</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/api-v1.yaml'
      expand-responses="200,201"
      required-props-first="true"
      sort-props-alphabetically="false"
      hide-download-button="false"
      theme='{"colors": {"primary": {"main": "#2196F3"}}}'
    ></redoc>
    <script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"></script>
</body>
</html>
EOF

For a self-hosted version (no CDN dependency):

# Download ReDoc bundle
wget https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js \
  -O /opt/api-docs/redoc/redoc.standalone.js

# Update the script tag in index.html
sed -i 's|https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js|/redoc/redoc.standalone.js|' \
  /opt/api-docs/redoc/index.html

Hosting Documentation with Nginx

sudo tee /etc/nginx/sites-available/api-docs << 'EOF'
server {
    listen 80;
    server_name docs.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name docs.example.com;

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

    root /opt/api-docs;

    # Security headers
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options SAMEORIGIN;

    # Default: redirect to Swagger UI
    location = / {
        return 301 /swagger-ui/;
    }

    # Swagger UI
    location /swagger-ui/ {
        alias /opt/api-docs/swagger-ui/;
        index index.html;
        try_files $uri $uri/ /swagger-ui/index.html;
    }

    # ReDoc
    location /redoc/ {
        alias /opt/api-docs/redoc/;
        index index.html;
    }

    # OpenAPI spec files
    location /specs/ {
        alias /opt/api-docs/specs/;
        
        # Allow CORS for spec files (needed if API is on different domain)
        add_header Access-Control-Allow-Origin "*";
        add_header Content-Type "application/yaml; charset=utf-8";
        
        # Cache specs (update via CI/CD)
        expires 1h;
        add_header Cache-Control "public, must-revalidate";
    }

    # Basic auth for staging docs (optional)
    # auth_basic "API Documentation";
    # auth_basic_user_file /etc/nginx/.htpasswd;
}
EOF

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

Docker Deployment

sudo tee /opt/api-docs/Dockerfile << 'EOF'
FROM nginx:alpine

# Copy documentation files
COPY swagger-ui/ /usr/share/nginx/html/swagger-ui/
COPY redoc/ /usr/share/nginx/html/redoc/
COPY specs/ /usr/share/nginx/html/specs/

# Copy Nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80
EOF
sudo tee /opt/api-docs/nginx.conf << 'EOF'
server {
    listen 80;
    root /usr/share/nginx/html;
    
    location = / {
        return 301 /swagger-ui/;
    }
    
    location /swagger-ui/ {
        try_files $uri $uri/ /swagger-ui/index.html;
    }
    
    location /redoc/ {
        try_files $uri $uri/ /redoc/index.html;
    }
    
    location /specs/ {
        add_header Access-Control-Allow-Origin "*";
    }
}
EOF
docker build -t api-docs:latest /opt/api-docs/
docker run -d \
  --name api-docs \
  --restart unless-stopped \
  -p 127.0.0.1:8080:80 \
  api-docs:latest

API Versioning Strategy

# Organize specs by version
mkdir -p /opt/api-docs/specs/{v1,v2}

# Maintain a versions index file
sudo tee /opt/api-docs/specs/versions.json << 'EOF'
{
  "versions": [
    {
      "name": "API v2 (Current)",
      "url": "/specs/v2/openapi.yaml",
      "status": "current"
    },
    {
      "name": "API v1 (Deprecated)",
      "url": "/specs/v1/openapi.yaml",
      "status": "deprecated",
      "deprecation_date": "2026-12-31"
    }
  ]
}
EOF

Add deprecation notices in your spec:

# In OpenAPI spec for deprecated endpoints
/api/v1/users/{userId}:
  get:
    deprecated: true
    summary: "Get user (deprecated - use /v2/users/{userId})"
    description: |
      **Deprecated**: This endpoint will be removed on 2026-12-31.
      Please migrate to `/v2/users/{userId}`.

CI/CD Automation

Automate spec generation and documentation deployment:

# GitHub Actions workflow or equivalent CI script
sudo tee /opt/api-docs/deploy-docs.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail

DOCS_DIR="/opt/api-docs"
SPECS_DIR="$DOCS_DIR/specs"

echo "Deploying API documentation..."

# 1. Validate spec
swagger-cli validate "$SPECS_DIR/v2/openapi.yaml"
echo "Spec validation passed"

# 2. Bundle multi-file spec into single file
swagger-cli bundle "$SPECS_DIR/v2/openapi.yaml" \
  --outfile "$SPECS_DIR/v2/openapi.bundled.yaml"

# 3. Generate static HTML (optional)
npx redoc-cli build "$SPECS_DIR/v2/openapi.bundled.yaml" \
  --output "$DOCS_DIR/redoc/index.html"

# 4. Reload Nginx to pick up new files
sudo nginx -s reload

echo "Documentation deployed successfully"
SCRIPT

chmod +x /opt/api-docs/deploy-docs.sh

Troubleshooting

CORS errors loading spec file:

# Check Nginx CORS headers for /specs/
curl -I https://docs.example.com/specs/api-v1.yaml | grep Access-Control
# Should show: Access-Control-Allow-Origin: *

Swagger UI shows "Failed to fetch" on Try It Out:

# Ensure API server has CORS headers for swagger.example.com origin
# Or proxy the API through the docs server:
# location /api/ { proxy_pass http://api.example.com; }

Spec validation errors:

# More detailed validation
npm install -g @stoplight/spectral-cli
spectral lint /opt/api-docs/specs/api-v1.yaml

# Common issues:
# - Missing required: [] on object schemas
# - $ref paths not matching actual file structure
# - Missing operationId (required for code generation)

Stale docs after deployment:

# Check cache headers
curl -I https://docs.example.com/specs/api-v1.yaml | grep Cache
# Purge with a version query parameter:
# url: "/specs/api-v1.yaml?v=1.2.0"

Conclusion

Hosting API documentation with Swagger UI and ReDoc on Linux requires minimal infrastructure: an OpenAPI spec file, static files served by Nginx, and an automated pipeline to keep the docs in sync with your API. ReDoc produces beautiful, consumer-friendly reference documentation, while Swagger UI's "Try It Out" feature makes the docs interactive for developers integrating your API. Validate your spec in CI/CD with swagger-cli or Spectral before deploying to catch specification errors before they reach production.