Skip to content

Lesson 3: Deploy Static Sites 📄

Deploy static content efficiently: MkDocs documentation, Next.js static exports, and single-page applications.

Progress Indicator

Module 2 of 4: Deploying Apps
└─ Lesson 3 of 3: Static Sites [Current]

Difficulty: Beginner | Time: 25-35 minutes

Learning Objectives

By the end of this lesson, you'll be able to:

  • Build and containerize static sites
  • Deploy MkDocs documentation sites
  • Deploy Next.js and other static exports
  • Optimize Docker images for static content
  • Understand caching and performance best practices
  • Deploy without needing a backend

Prerequisites Checklist

  • Completed Lesson 1: Simple Web App
  • Basic understanding of static sites
  • A static site ready to deploy (or use our examples)
  • SSH access to server
  • Either MkDocs, Next.js, or another static site generator

No database needed

This lesson is simpler than Lesson 2. No database, no complex setup!


Part 1: Understanding Static Sites

What is a Static Site?

Static Site:
- HTML, CSS, JavaScript files
- No database needed
- No server-side code
- Files generated at build time
- Fast and secure

Example:
  index.html
  about.html
  contact.html
  styles.css
  script.js

When to Use Static Sites

Great for: - Documentation (MkDocs, Sphinx, Jekyll) - Blogs and content sites - Landing pages - Portfolio sites - Single-page applications (React, Vue) - Company websites

Not suitable for: - Real-time applications - Highly interactive web apps with backend - Applications with user authentication - Dynamic content that changes frequently

Static vs Dynamic Deployment

Dynamic (Lessons 1-2):

Code → Build at runtime → App listens on port → Traefik routes

Static (This lesson):

Code → Build at build time → HTML files served → Traefik routes

Build Process

Source Code (Markdown, React, etc.)
   Build Command
  Output Directory (dist/, site/, build/)
    Docker Image
   Container (HTTP Server)
    Traefik Routes
    Internet

Part 2: Choose Your Static Site Type

What is MkDocs?

MkDocs generates beautiful documentation from Markdown files.

Examples: - Python documentation - API guides - Project wikis - Knowledge bases

Example MkDocs Project

my-docs/
├── docs/
   ├── index.md
   ├── getting-started.md
   └── api/
       └── overview.md
├── mkdocs.yml
├── Dockerfile
└── docker-compose.yml

mkdocs.yml

mkdocs.yml
site_name: My Documentation
site_description: Complete project documentation
theme:
  name: material
  palette:
    scheme: slate
nav:
  - Home: index.md
  - Getting Started: getting-started.md
  - API:
      - Overview: api/overview.md

requirements.txt

requirements.txt
mkdocs==1.5.3
mkdocs-material==9.4.10

Dockerfile

Dockerfile
# Build stage
FROM python:3.11-alpine AS builder

WORKDIR /app

# Install MkDocs
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy docs
COPY docs ./docs
COPY mkdocs.yml .

# Build static site
RUN mkdocs build

# Runtime stage
FROM python:3.11-alpine

WORKDIR /app

# Copy built site from builder
COPY --from=builder /app/site ./site

# Install simple HTTP server
RUN pip install --no-cache-dir http-server

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget --quiet --tries=1 --spider http://127.0.0.1:8000/ || exit 1

# Serve static files
CMD ["python", "-m", "http.server", "8000", "--directory", "site"]

Key Points: - Multi-stage build: first stage builds MkDocs, second serves it - Final image only contains built HTML (no source Markdown) - Much smaller and faster - Python's http.server is lightweight for static content

docker-compose.yml

docker-compose.yml
version: '3.8'

services:
  docs:
    build: .
    container_name: my-docs
    restart: unless-stopped
    labels:
      - traefik.enable=true
      - traefik.http.routers.my-docs.rule=Host(`docs.egygeeks.com`)
      - traefik.http.routers.my-docs.entrypoints=websecure
      - traefik.http.routers.my-docs.tls.certresolver=letsencrypt
      - traefik.http.services.my-docs.loadbalancer.server.port=8000
    networks:
      - traefik_public

networks:
  traefik_public:
    external: true

What is Next.js?

Next.js is a React framework that can export to static HTML.

When to use: - React single-page applications - Hybrid apps (some pages static, some dynamic) - Modern web applications - Better performance than client-side React

Example Next.js Project

my-nextjs-app/
├── pages/
   ├── index.tsx
   └── about.tsx
├── public/
   └── logo.png
├── package.json
├── next.config.js
├── Dockerfile
└── docker-compose.yml

next.config.js

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',  // Enable static export
  trailingSlash: true,
};

module.exports = nextConfig;

Dockerfile

Dockerfile
# Build stage
FROM node:18-alpine AS builder

WORKDIR /app

# Copy dependencies
COPY package*.json ./
RUN npm install

# Copy source
COPY . .

# Build static site
RUN npm run build

# Runtime stage
FROM node:18-alpine

WORKDIR /app

# Copy built output from builder
COPY --from=builder /app/out ./out

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget --quiet --tries=1 --spider http://127.0.0.1:3000/ || exit 1

# Serve static files with next
CMD ["npx", "serve", "-s", "out", "-l", "3000"]

Key Points: - output: 'export' in next.config.js generates static files - Built files in out/ directory - Multi-stage: build stage has full Node, runtime is lightweight - Use serve package to serve static files

docker-compose.yml

docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    container_name: nextjs-app
    restart: unless-stopped
    labels:
      - traefik.enable=true
      - traefik.http.routers.nextjs-app.rule=Host(`app.egygeeks.com`)
      - traefik.http.routers.nextjs-app.entrypoints=websecure
      - traefik.http.routers.nextjs-app.tls.certresolver=letsencrypt
      - traefik.http.services.nextjs-app.loadbalancer.server.port=3000
    networks:
      - traefik_public

networks:
  traefik_public:
    external: true

Plain HTML or Single-Page App

For any static content: HTML, CSS, JavaScript, Vue, etc.

Examples: - Hand-written HTML - Webpack/Parcel bundled apps - Vue or React SPAs - Static site generators (Hugo, Jekyll)

Dockerfile

Dockerfile
# Build stage (optional - only if you need to compile)
FROM node:18-alpine AS builder

WORKDIR /app
COPY . .
RUN npm install && npm run build

# Runtime stage
FROM alpine:latest

# Install lightweight HTTP server
RUN apk add --no-cache python3

WORKDIR /app

# Copy static files
COPY --from=builder /app/dist ./dist

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget --quiet --tries=1 --spider http://127.0.0.1:8000/ || exit 1

# Serve static files
CMD ["python3", "-m", "http.server", "8000", "--directory", "dist"]

docker-compose.yml

docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    container_name: static-app
    restart: unless-stopped
    labels:
      - traefik.enable=true
      - traefik.http.routers.static-app.rule=Host(`app.egygeeks.com`)
      - traefik.http.routers.static-app.entrypoints=websecure
      - traefik.http.routers.static-app.tls.certresolver=letsencrypt
      - traefik.http.services.static-app.loadbalancer.server.port=8000
    networks:
      - traefik_public

networks:
  traefik_public:
    external: true

Part 3: Multi-Stage Builds Explained

Multi-stage builds are key to small, fast static site containers.

Stage 1: Build

FROM node:18-alpine AS builder

WORKDIR /app
COPY . .
RUN npm install
RUN npm run build  # Creates dist/ or out/

Size: ~500MB (includes npm, Node compiler, build tools)

Stage 2: Runtime

FROM alpine:latest  # Tiny base image

WORKDIR /app
COPY --from=builder /app/dist ./dist  # Only copy built files

CMD ["python3", "-m", "http.server", "8000", "--directory", "dist"]

Size: ~50MB (only built HTML/CSS/JS)

Result

Large (500MB) → Used for building → Discarded
             dist/ folder (1-10MB)
Small (50MB) → Used for running ← Final image

Benefits: - Final image 10x smaller - Faster to download and start - Faster deployments - Lower resource usage


Part 4: Optimization Strategies

Image Size Optimization

Use Alpine base images:

# ✅ Small (~5MB)
FROM alpine:latest

# ❌ Large (~300MB)
FROM ubuntu:latest

Remove build dependencies:

# ✅ Optimized
RUN apk add --no-cache python3
RUN python3 -m pip install mkdocs
RUN rm -rf /root/.cache  # Remove pip cache

# ❌ Bloated
RUN apt-get install build-essential gcc
RUN npm install
# Build artifacts left in image

Use .dockerignore:

.dockerignore
node_modules/
.git/
.env
dist/
build/
*.md

Prevents unnecessary files from being copied into image.

Caching Optimization

Set cache headers for fast delivery:

CMD ["python3", "-m", "http.server", "8000", "--directory", "site"]

Better option with caching:

# Install lightweight web server with cache control
RUN apk add --no-cache nginx

COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /app/site /usr/share/nginx/html

CMD ["nginx", "-g", "daemon off;"]

With nginx.conf:

nginx.conf
events {}

http {
  server {
    listen 8000;
    root /usr/share/nginx/html;
    index index.html;

    # Cache static files for 1 year
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
      expires 1y;
      add_header Cache-Control "public, immutable";
    }

    # Don't cache HTML
    location ~* \.html$ {
      expires 1h;
      add_header Cache-Control "public, must-revalidate";
    }

    # SPA fallback
    error_page 404 =200 /index.html;
  }
}

Performance Metrics

Check image sizes:

# View image size
docker images | grep my-docs

# Break down layers
docker history my-docs

# Get size of running container
docker ps --size

Part 5: Building and Deploying

Step 1: Build Locally

Test your static site build locally:

# For MkDocs
mkdocs build

# For Next.js
npm run build

# For custom script
npm run build  # or your build command

Verify output directory exists with content:

# Check what was built
ls -la site/  # or dist/ or out/
ls -la site/index.html

Step 2: Build Docker Image

# Build the image
docker compose build

# Check size
docker images | grep my-docs

Step 3: Test Container

# Start container
docker compose up -d

# Wait a moment
sleep 3

# Test it works
curl http://localhost:8000

# View logs
docker compose logs

# Check health
docker compose ps

# Stop when done
docker compose down

Step 4: Deploy to Server

Push to GitHub:

git add .
git commit -m "Add static site deployment"
git push origin main

SSH to server:

ssh user@your-server
cd /opt/apps/my-docs
git pull origin main

Deploy:

docker compose up -d

# Verify it's healthy
docker compose ps
docker compose logs

Part 6: Verification Commands

Check Container is Healthy

docker compose ps

# Status should show "Up" and "healthy"

View Access Logs

docker compose logs -f

# Should show HTTP requests being served

Test Different URLs

# Home page
curl http://localhost:8000/

# Index with trailing slash
curl http://localhost:8000/index.html

# About page (if exists)
curl http://localhost:8000/about/

# Check response time
curl -w "@curl-format.txt" http://localhost:8000/

Check File Serving

# Check specific file types are served
curl -I http://localhost:8000/assets/style.css
curl -I http://localhost:8000/image.png

# View all served content
docker exec my-docs find /app/site -type f | head -20

Performance Testing

# Check response headers (should include cache headers)
curl -I http://localhost:8000/assets/app.js

# Expected: Cache-Control headers showing expiration

Part 7: Common Mistakes & Solutions

⚠️ Mistake 1: Including Build Tools in Runtime Image

Wrong:

FROM node:18  # ~900MB

WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
RUN npm serve -s build

Problem: Final image is huge (includes npm, compiler, all dependencies).

Correct:

FROM node:18 AS builder

WORKDIR /app
COPY . .
RUN npm install && npm run build

FROM node:18-alpine  # ~170MB for building, ~50MB final

COPY --from=builder /app/build ./build
CMD ["npx", "serve", "-s", "build"]

⚠️ Mistake 2: Rebuilding From Source Every Deploy

Wrong:

docker compose pull  # Gets new image
docker compose up -d

Problem: If you commit Dockerfile changes, full rebuild happens (slow).

Correct: - Commit built docker-compose.yml to git - Use docker-compose.yml from github - Push new commits triggers rebuild only when needed

⚠️ Mistake 3: No Health Check

Wrong:

FROM alpine:latest
# ... no HEALTHCHECK

Problem: Traefik thinks container is failing.

Correct:

HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget --quiet --tries=1 --spider http://127.0.0.1:8000/ || exit 1

⚠️ Mistake 4: Wrong Output Directory

Wrong:

COPY --from=builder /app/dist ./site
CMD ["python", "-m", "http.server", "8000", "--directory", "site"]

# But build outputs to 'out' directory

Correct:

# Check where build actually outputs
COPY --from=builder /app/out ./site

Common output directories: - Next.js: out/ - MkDocs: site/ - npm build: dist/ or build/

⚠️ Mistake 5: SPA Not Handling Routing

Wrong:

# User visits /about
# Server looks for /about.html
# Not found -> 404 error

Problem: Single-page apps need all routes to serve index.html.

Correct (Python):

# Use a simple fallback script

Correct (Nginx):

error_page 404 =200 /index.html;


Part 8: MkDocs Specific Tips

Building Offline Docs

# Build docs without external resources
mkdocs build --strict

# Check output
ls -la site/

Custom Theme Styling

mkdocs.yml
theme:
  name: material
  palette:
    scheme: dark
  custom_dir: overrides/

Search Configuration

plugins:
  search:
    lang: en

Deployment in CI/CD

.github/workflows/deploy.yml
- name: Build MkDocs
  run: |
    pip install -r requirements.txt
    mkdocs build

- name: Deploy
  run: |
    docker compose up -d

Part 9: Next.js Specific Tips

Static Export Configuration

next.config.js
const nextConfig = {
  output: 'export',
  trailingSlash: true,  // Add trailing slashes to URLs
  basePath: '/app',     // Deploy in subdirectory
  assetPrefix: '/app',
};

module.exports = nextConfig;

Image Optimization

const nextConfig = {
  output: 'export',
  images: {
    unoptimized: true,  // Required for static export
  },
};

Environment Variables

next.config.js
// Static export means env vars must be available at build time
const config = {
  env: {
    API_URL: process.env.API_URL,
  },
};

Set before building:

export API_URL=https://api.example.com
npm run build

AI Prompts for This Lesson

MkDocs Material Configuration

Customize MkDocs Theme
I'm deploying a MkDocs Material site and need help customizing:

What I want:
- Custom color scheme: [describe colors]
- Add search functionality
- Enable dark mode toggle
- Add social links

My current mkdocs.yml:
[paste relevant sections]

Generate the updated configuration with explanations.

Next.js Static Export Issues

Next.js Build Failing?
My Next.js static site build fails during deployment.

Error from `docker build`:
[paste build error]

My next.config.js:
[paste config]

My Dockerfile:
[paste Dockerfile]

What's causing the build failure and how do I fix it?

Multi-Stage Build Optimization

Optimize Static Site Docker Image
My static site Docker image is [X] MB. Help me optimize it.

Site generator: [MkDocs/Next.js/Hugo/Gatsby]

Current Dockerfile:
[paste Dockerfile]

Show me:
1. Multi-stage build approach
2. Minimal nginx configuration
3. How to reduce image size to <50MB

Nginx Cache Configuration

Configure Static Asset Caching
I need to set up proper caching for my static site.

Static site type: [MkDocs/Next.js/Plain HTML]
Server: [nginx/Python http.server/serve]

Requirements:
- Cache CSS/JS/images for 1 year
- Don't cache HTML files
- Add proper Cache-Control headers

Current nginx.conf (if applicable):
[paste config or specify "using Python http.server"]

Provide optimized configuration with explanations.

SPA Routing Issues

404 Errors on Direct Routes?
My single-page app shows 404 when accessing routes directly.

Framework: [React/Vue/Plain JS]
Server: [nginx/serve/http.server]

Problem:
- Homepage works: example.com/
- Direct route fails: example.com/about

Current Dockerfile:
[paste relevant sections]

How do I configure the server to handle SPA routing?

Build Output Directory Mismatch

Container Starts But Shows Empty Page?
My static site container starts but serves nothing.

Build tool: [Next.js/MkDocs/Webpack/Vite]

My Dockerfile COPY command:
COPY --from=builder /app/[directory] ./site

My serve command:
CMD ["serve", "-s", "[directory]"]

Error in browser: [404/blank page/directory listing]

Help me identify the correct build output directory and fix the paths.

Real Examples

See complete examples in egygeeks-docs repository:

  • MkDocs: /examples/mkdocs-docs/
  • Next.js Static: /examples/nextjs-static/
  • Plain HTML SPA: /examples/html-spa/

Troubleshooting

Build fails

Diagnosis:

docker compose build --no-cache
docker compose logs

# Also try locally:
mkdocs build  # or npm run build

Common causes: - Missing dependencies in requirements.txt or package.json - Wrong build command - Output directory doesn't exist

Container starts but shows 404

Check files were copied:

docker exec my-docs ls -la /app/site/

Check health check:

docker compose ps  # Status should be "healthy"

Static files not being served

Verify content directory:

docker compose exec my-docs find /app -name "index.html"

Check server configuration:

docker compose exec my-docs cat /etc/nginx/nginx.conf

Slow initial load

Check caching headers:

curl -I http://localhost:8000/assets/app.js

# Should show Cache-Control headers


What's Next

After completing this lesson:

  1. ✅ You've deployed static sites with Docker
  2. ✅ You understand build processes and multi-stage builds
  3. ✅ You've completed all lessons in Module 2

Your Next Steps:

Option 1: Try All Three Types

  • Deploy a simple web app
  • Deploy an app with a database
  • Deploy a static site

Option 2: Deep Dive Into One

  • Master one technology deeply
  • Build a real project
  • Deploy it to production

Option 3: Move to Module 3

Module 3: Going Live →

Learn about production configuration, SSL, monitoring, and making your apps accessible globally.


Need Help?

Quick Reference

Docker for Static Sites:

# Build
docker compose build

# Test
docker compose up -d
curl http://localhost:8000

# Deploy
docker compose down
docker compose up -d

# Logs
docker compose logs -f

File Checklist: - [ ] Dockerfile with multi-stage build - [ ] docker-compose.yml with Traefik labels - [ ] Build output directory exists - [ ] .gitignore excludes build artifacts - [ ] HEALTHCHECK in Dockerfile

Key Concepts

Static Build: - Source files → Build command → HTML/CSS/JSDocker image → Container

Multi-Stage Build: - Stage 1: Large (build tools) → Creates output - Stage 2: Small (runtime) → Serves output

Traefik Integration: - Same labels as dynamic apps - No port binding needed (Traefik handles routing) - Health checks still required


← Back to Module Overview Back to Lesson 2 Proceed to Module 3: Going Live →