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.


