Lesson 3: Deploy Static Sites 📄¶
Deploy static content efficiently: MkDocs documentation, Next.js static exports, and single-page applications.
Progress Indicator¶
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):
Static (This lesson):
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¶
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¶
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¶
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¶
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export', // Enable static export
trailingSlash: true,
};
module.exports = nextConfig;
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¶
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¶
# 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¶
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:
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:
Prevents unnecessary files from being copied into image.
Caching Optimization¶
Set cache headers for fast delivery:
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:
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:
Step 2: Build Docker Image¶
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:
SSH to server:
Deploy:
Part 6: Verification Commands¶
Check Container is Healthy¶
View Access Logs¶
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:
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:
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:
Common output directories: - Next.js: out/ - MkDocs: site/ - npm build: dist/ or build/
⚠️ Mistake 5: SPA Not Handling Routing¶
Wrong:
Problem: Single-page apps need all routes to serve index.html.
Correct (Python):
Correct (Nginx):
Part 8: MkDocs Specific Tips¶
Building Offline Docs¶
Custom Theme Styling¶
Search Configuration¶
Deployment in CI/CD¶
- 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¶
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¶
// Static export means env vars must be available at build time
const config = {
env: {
API_URL: process.env.API_URL,
},
};
Set before building:
AI Prompts for This Lesson¶
MkDocs Material Configuration¶
Customize MkDocs Theme
Next.js Static Export Issues¶
Next.js Build Failing?
Multi-Stage Build Optimization¶
Optimize Static Site Docker Image
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:
Check health check:
Static files not being served¶
Verify content directory:
Check server configuration:
Slow initial load¶
Check caching headers:
What's Next¶
After completing this lesson:
- ✅ You've deployed static sites with Docker
- ✅ You understand build processes and multi-stage builds
- ✅ 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¶
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/JS → Docker 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 →