Serving Static Content with nginx

Web Servers vs Application Code

When you visit a website, the server returns different types of content:

Static Content - Files that never change:

  • HTML pages: index.html
  • CSS stylesheets: styles.css
  • JavaScript files: app.js
  • Images: logo.png, photo.jpg
  • Documents: report.pdf, data.json

Dynamic Content - Generated by running code:

  • User profiles (different for each user)
  • Search results (different for each query)
  • Current weather data
  • Database query results

Web servers like nginx are optimized for serving static files directly from disk. Application servers run your Python/Java/etc. code to generate dynamic content.

For static files, nginx is:

  • Faster - No code execution, just file system reads
  • More efficient - Uses less memory and CPU
  • More reliable - Fewer moving parts to break

What is nginx?

nginx (pronounced “engine-x”) is a web server that excels at serving static files and acting as a reverse proxy. It handles thousands of concurrent connections efficiently.

Common nginx uses:

  • Serve static websites (HTML, CSS, images)
  • Serve API documentation and data files
  • Load balance between application servers
  • Handle SSL/HTTPS certificates
  • Cache frequently requested content

For your assignments, nginx provides an easy way to serve JSON data files or HTML reports over HTTP.

MIME Types

Web browsers need to know what type of file they’re receiving. The server tells them using MIME types in HTTP headers:

Content-Type: text/html        # HTML pages
Content-Type: text/css         # CSS stylesheets  
Content-Type: application/json # JSON data
Content-Type: image/png        # PNG images
Content-Type: application/pdf  # PDF documents

nginx automatically sets the correct MIME type based on file extensions:

  • .htmltext/html
  • .jsonapplication/json
  • .csstext/css
  • .jsapplication/javascript
  • .pngimage/png
  • .pdfapplication/pdf

This is why file extensions matter for web content.

Basic nginx Container

The simplest way to serve files with nginx:

# Create some content to serve
mkdir website
echo '<h1>Hello World</h1>' > website/index.html
echo '{"message": "Hello from JSON"}' > website/data.json

# Serve with nginx container
docker run --rm \
    --name static-server \
    -p 8080:80 \
    -v $(pwd)/website:/usr/share/nginx/html:ro \
    nginx:alpine

# Access at http://localhost:8080

This mounts your website/ directory to nginx’s default document root (/usr/share/nginx/html).

Directory Structure

nginx serves files based on URL paths:

website/
├── index.html          # http://localhost:8080/
├── about.html          # http://localhost:8080/about.html
├── data.json          # http://localhost:8080/data.json
├── css/
│   └── style.css      # http://localhost:8080/css/style.css
├── images/
│   └── logo.png       # http://localhost:8080/images/logo.png
└── api/
    └── papers.json    # http://localhost:8080/api/papers.json

URL path maps directly to file system path:

  • /index.html (default)
  • /about.htmlabout.html
  • /api/papers.jsonapi/papers.json

Serving JSON Data

Perfect for serving API data files or results from data processing:

# Create API data structure
mkdir -p api-data/api/v1
mkdir -p api-data/docs

# Sample JSON files
cat > api-data/api/v1/papers.json << 'EOF'
[
  {
    "id": "2301.12345",
    "title": "Deep Learning Paper",
    "authors": ["Smith", "Jones"],
    "abstract": "This paper discusses..."
  }
]
EOF

cat > api-data/api/v1/stats.json << 'EOF'
{
  "total_papers": 150,
  "categories": {
    "cs.LG": 45,
    "cs.AI": 32
  }
}
EOF

# Serve the data
docker run --rm \
    --name json-server \
    -p 8080:80 \
    -v $(pwd)/api-data:/usr/share/nginx/html:ro \
    nginx:alpine

Now you can access:

  • http://localhost:8080/api/v1/papers.json
  • http://localhost:8080/api/v1/stats.json

Test with curl:

curl http://localhost:8080/api/v1/papers.json | python -m json.tool

HTML Interface for Data

Create a simple web interface to view your data:

# Create HTML page that loads JSON via JavaScript
cat > api-data/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
    <title>Papers API</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 40px; }
        .paper { border: 1px solid #ddd; padding: 10px; margin: 10px 0; }
        pre { background: #f5f5f5; padding: 10px; overflow-x: auto; }
    </style>
</head>
<body>
    <h1>Papers Data</h1>
    <div id="papers"></div>
    
    <h2>Stats</h2>
    <div id="stats"></div>
    
    <script>
        // Load papers
        fetch('/api/v1/papers.json')
            .then(response => response.json())
            .then(papers => {
                const container = document.getElementById('papers');
                papers.forEach(paper => {
                    container.innerHTML += `
                        <div class="paper">
                            <h3>${paper.title}</h3>
                            <p>Authors: ${paper.authors.join(', ')}</p>
                            <p>${paper.abstract.substring(0, 200)}...</p>
                        </div>
                    `;
                });
            });
        
        // Load stats
        fetch('/api/v1/stats.json')
            .then(response => response.json())
            .then(stats => {
                document.getElementById('stats').innerHTML = 
                    '<pre>' + JSON.stringify(stats, null, 2) + '</pre>';
            });
    </script>
</body>
</html>
EOF

Visit http://localhost:8080/ to see your data in a web interface.

Configuration File

For more control, create custom nginx configuration:

# Create nginx config
cat > nginx.conf << 'EOF'
server {
    listen 80;
    server_name localhost;
    
    # Document root
    root /usr/share/nginx/html;
    
    # Default file
    index index.html;
    
    # Enable gzip compression
    gzip on;
    gzip_types application/json text/css text/javascript;
    
    # CORS headers for API endpoints
    location /api/ {
        add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Methods "GET, OPTIONS";
        add_header Access-Control-Allow-Headers "Content-Type";
    }
    
    # Cache static assets
    location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg)$ {
        expires 1h;
        add_header Cache-Control "public, immutable";
    }
    
    # Security headers
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
}
EOF

# Run with custom config
docker run --rm \
    --name custom-server \
    -p 8080:80 \
    -v $(pwd)/api-data:/usr/share/nginx/html:ro \
    -v $(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf:ro \
    nginx:alpine

Docker Compose Setup

For more complex setups, use docker-compose:

# docker-compose.yml
version: '3.8'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "8080:80"
    volumes:
      - ./website:/usr/share/nginx/html:ro
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    restart: unless-stopped

  # Example: serve different content on different ports
  api-server:
    image: nginx:alpine
    ports:
      - "8081:80"
    volumes:
      - ./api-data:/usr/share/nginx/html:ro

Start with:

docker-compose up -d

Serving Assignment Results

Common pattern for homework assignments:

# Process data and generate results
python process_arxiv.py data/papers.json output/

# Results directory structure
output/
├── index.html          # Summary report
├── results.json        # Raw data
├── analysis.json       # Analysis results
├── charts/
   ├── categories.png  # Generated charts
   └── timeline.png
└── css/
    └── report.css      # Styling

# Serve results
docker run --rm \
    --name results-server \
    -p 8080:80 \
    -v $(pwd)/output:/usr/share/nginx/html:ro \
    nginx:alpine

Now you can share results with: “Visit http://localhost:8080 to see the analysis”

Common Patterns

API-like Structure

data/
├── index.html          # Documentation
├── v1/
   ├── papers.json
   ├── authors.json
   └── stats.json
├── v2/
   └── papers.json     # Updated format
└── docs/
    └── api.html        # API documentation

Single Page Application

app/
├── index.html          # Main application
├── js/
   ├── app.js         # Application code
   └── data.js        # Data loading
├── css/
   └── style.css      # Styling
└── data/
    └── papers.json    # Static data

Development vs Production

# Development - auto-reload on file changes
docker run --rm \
    -p 8080:80 \
    -v $(pwd)/src:/usr/share/nginx/html \
    nginx:alpine

# Production - read-only, specific tag
docker run -d \
    --name prod-server \
    -p 80:80 \
    -v /var/www/html:/usr/share/nginx/html:ro \
    nginx:1.21-alpine

Testing Static Server

Verify your static server works correctly:

# Test basic connectivity
curl -v http://localhost:8080/

# Test JSON endpoint
curl -H "Accept: application/json" http://localhost:8080/api/data.json

# Test MIME types
curl -I http://localhost:8080/style.css  # Should be text/css
curl -I http://localhost:8080/data.json  # Should be application/json

# Test 404 handling
curl -I http://localhost:8080/nonexistent.html  # Should be 404

# Load test
for i in {1..100}; do
    curl -s http://localhost:8080/ > /dev/null &
done
wait

Security Considerations

Basic security for static content:

server {
    listen 80;
    
    # Prevent access to hidden files
    location ~ /\. {
        deny all;
        return 404;
    }
    
    # Prevent access to sensitive files
    location ~* \.(env|log|backup|tmp)$ {
        deny all;
        return 404;
    }
    
    # Rate limiting
    location / {
        limit_req zone=basic burst=10;
        try_files $uri $uri/ =404;
    }
}

Debugging nginx

Common issues and solutions:

# Check nginx is running
docker ps | grep nginx

# View nginx logs
docker logs container_name

# Test nginx config syntax
docker run --rm \
    -v $(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf:ro \
    nginx:alpine nginx -t

# Access container to debug
docker exec -it container_name sh

# Inside container:
ls -la /usr/share/nginx/html/  # Check files are mounted
cat /etc/nginx/conf.d/default.conf  # Check config

Common errors:

  • 403 Forbidden: Check file permissions and volume mounts
  • 404 Not Found: Verify file paths and directory structure
  • Connection refused: Check port mapping and firewall

Quick Reference

Basic nginx Container

# Serve current directory
docker run --rm -p 8080:80 -v $(pwd):/usr/share/nginx/html:ro nginx:alpine

# With custom config
docker run --rm -p 8080:80 \
    -v $(pwd)/content:/usr/share/nginx/html:ro \
    -v $(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf:ro \
    nginx:alpine

URL Mapping

URL                           File
/                        →    index.html
/about.html             →    about.html  
/api/data.json          →    api/data.json
/css/style.css          →    css/style.css

Common MIME Types

.html    →    text/html
.css     →    text/css
.js      →    application/javascript
.json    →    application/json
.png     →    image/png
.jpg     →    image/jpeg
.pdf     →    application/pdf
.txt     →    text/plain

Docker Commands

docker run --rm -p 8080:80 -v $(pwd):/usr/share/nginx/html:ro nginx:alpine
docker logs nginx-container
docker exec -it nginx-container sh