aRustyDev / aRustyDev/mdbook-htmx

docs(mdbook-htmx): Docker Compose Local Development

Open
#40 0 comments 0 reactions 1 assignee Claimed by @aRustyDev View on GitHub
documentation
Dominant language
Rust
Stars
0
Forks
1
PR merge metrics
No merged PRs in 30d

Description

# Docker Compose Local Development

Run mdbook-htmx locally with Docker Compose for development and testing.

## Implementation

The Docker Compose configuration lives in the mdbook-htmx repository:

```
mdbook-htmx/docker/
├── compose.yml # Base configuration
├── compose.auth.yml # +OAuth2 proxy (Phase 3)
├── compose.search.yml # +Meilisearch (Phase 4)
├── compose.prod.yml # +Caching, metrics (Phase 5)
├── nginx.conf # NGINX with HTMX routing
├── Dockerfile.build # Multi-stage build image
└── .env.example # Environment template
```

See [ADR-0016](../adr/0016-implementation-phasing-strategy.md) for phase-by-phase additions.

## Overview

This setup includes:
- **NGINX**: Serving documentation with HTMX fragment routing
- **Meilisearch**: Full-text search (optional, Phase 4+)
- **OAuth2 Proxy**: Authentication (optional, Phase 3+)
- **Live reload**: Watch for changes

## Basic Setup

### docker-compose.yml

```yaml
version: "3.8"

services:
docs:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./book:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/nginx.conf:ro
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/healthz"]
interval: 10s
timeout: 5s
retries: 3

# Live rebuild (optional)
builder:
image: rust:latest
volumes:
- .:/docs
- cargo-cache:/usr/local/cargo/registry
working_dir: /docs
command: >
bash -c "
cargo install mdbook mdbook-htmx &&
mdbook watch --open
"
profiles:
- dev

volumes:
cargo-cache:
```

### nginx.conf

```nginx
events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

sendfile on;
keepalive_timeout 65;
gzip on;
gzip_types text/plain text/css application/json application/javascript;

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

# HTMX requests
location / {
try_files $uri $uri/ /index.html;

# CORS for local development
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type, HX-Request";
}

# Health check
location /healthz {
return 200 "OK\n";
add_header Content-Type text/plain;
}
}
}
```

## With Meilisearch

### docker-compose.yml

```yaml
version: "3.8"

services:
docs:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./book:/usr/share/nginx/html:ro
- ./nginx-with-search.conf:/etc/nginx/nginx.conf:ro
depends_on:
meilisearch:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/healthz"]
interval: 10s
timeout: 5s
retries: 3

meilisearch:
image: getmeili/meilisearch:v1.6
ports:
- "7700:7700"
environment:
MEILI_ENV: development
MEILI_MASTER_KEY: devkey123
MEILI_NO_ANALYTICS: "true"
volumes:
- meili-data:/meili_data
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:7700/health"]
interval: 10s
timeout: 5s
retries: 3

# Index documentation on startup
indexer:
image: python:3.11-slim
volumes:
- ./book:/docs:ro
- ./scripts:/scripts:ro
environment:
MEILI_HOST: http://meilisearch:7700
MEILI_MASTER_KEY: devkey123
command: >
bash -c "
pip install meilisearch &&
python /scripts/index.py
"
depends_on:
meilisearch:
condition: service_healthy
profiles:
- index

volumes:
meili-data:
```

### nginx-with-search.conf

```nginx
events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

upstream meilisearch {
server meilisearch:7700;
}

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

# Static files
location / {
try_files $uri $uri/ /index.html;
}

# Proxy search to Meilisearch
location /search {
proxy_pass http://meilisearch/indexes/docs/search;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Authorization "Bearer devkey123";

# CORS
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;

if ($request_method = OPTIONS) {
return 204;
}
}

location /healthz {
return 200 "OK\n";
}
}
}
```

### scripts/index.py

```python
#!/usr/bin/env python3
"""Index documentation to Meilisearch."""
import json
import os
import hashlib
from pathlib import Path
import meilisearch
import time

MEILI_HOST = os.environ.get('MEILI_HOST', 'http://localhost:7700')
MEILI_KEY = os.environ.get('MEILI_MASTER_KEY', 'devkey123')
DOCS_PATH = os.environ.get('DOCS_PATH', '/docs')

def main():
# Wait for Meilisearch
client = meilisearch.Client(MEILI_HOST, MEILI_KEY)
for _ in range(30):
try:
client.health()
break
except:
time.sleep(1)
else:
raise Exception("Meilisearch not ready")

# Load search index
search_index = Path(DOCS_PATH) / 'search-index.json'
if not search_index.exists():
print("No search-index.json found, skipping indexing")
return

with open(search_index) as f:
data = json.load(f)

# Transform documents
documents = []
for page in data.get('pages', []):
documents.append({
'id': hashlib.md5(page['path'].encode()).hexdigest(),
'path': page['path'],
'title': page['title'],
'content': page.get('content', ''),
'headings': page.get('headings', []),
})

# Create/update index
index = client.index('docs')
index.update_settings({
'searchableAttributes': ['title', 'headings', 'content'],
'displayedAttributes': ['path', 'title', 'headings'],
})

task = index.add_documents(documents)
client.wait_for_task(task.task_uid)
print(f"Indexed {len(documents)} documents")

if __name__ == '__main__':
main()
```

## With Live Reload

### docker-compose.yml

```yaml
version: "3.8"

services:
docs:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./book:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/nginx.conf:ro

# Watch and rebuild
watcher:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- .:/docs
- cargo-cache:/usr/local/cargo/registry
working_dir: /docs
command: mdbook watch
environment:
RUST_LOG: info

# Browser sync (optional)
browsersync:
image: ustwo/browser-sync
ports:
- "3000:3000"
- "3001:3001"
command: start --proxy "docs:80" --files "book/**/*"
depends_on:
- docs

volumes:
cargo-cache:
```

### Dockerfile.dev

```dockerfile
FROM rust:latest

# Install mdbook and plugins
RUN cargo install mdbook mdbook-htmx

# Install watchexec for better file watching
RUN cargo install watchexec-cli

WORKDIR /docs
```

## With Authentication (OAuth2 Proxy)

### docker-compose.yml

```yaml
version: "3.8"

services:
docs:
image: nginx:alpine
volumes:
- ./book:/usr/share/nginx/html:ro
- ./nginx-auth.conf:/etc/nginx/nginx.conf:ro

oauth2-proxy:
image: quay.io/oauth2-proxy/oauth2-proxy:v7.5.1
ports:
- "4180:4180"
environment:
OAUTH2_PROXY_PROVIDER: github
OAUTH2_PROXY_CLIENT_ID: ${GITHUB_CLIENT_ID}
OAUTH2_PROXY_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
OAUTH2_PROXY_COOKIE_SECRET: ${COOKIE_SECRET}
OAUTH2_PROXY_EMAIL_DOMAINS: "*"
OAUTH2_PROXY_UPSTREAMS: http://docs:80
OAUTH2_PROXY_HTTP_ADDRESS: 0.0.0.0:4180
OAUTH2_PROXY_REDIRECT_URL: http://localhost:4180/oauth2/callback
```

### .env

```bash
GITHUB_CLIENT_ID=your-client-id
GITHUB_CLIENT_SECRET=your-client-secret
COOKIE_SECRET=$(openssl rand -base64 32 | head -c 32)
```

## Make Commands

### Makefile

```makefile
.PHONY: build serve dev clean index

# Build documentation
build:
mdbook build

# Start docs server
serve:
docker-compose up -d docs
@echo "Docs available at http://localhost:8080"

# Development mode with live reload
dev:
docker-compose up docs watcher browsersync

# Start with search
search:
docker-compose up -d docs meilisearch
docker-compose run --rm indexer

# Rebuild search index
index:
docker-compose run --rm indexer

# Stop all services
stop:
docker-compose down

# Clean up
clean:
docker-compose down -v
rm -rf book/

# View logs
logs:
docker-compose logs -f

# Shell into docs container
shell:
docker-compose exec docs sh
```

## Environment Variables

### .env.example

```bash
# Meilisearch
MEILI_MASTER_KEY=devkey123

# OAuth (for authenticated docs)
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
COOKIE_SECRET=

# Build options
MDBOOK_BUILD=release
```

## Usage

```bash
# Start basic docs server
docker-compose up -d

# Build and serve with search
make search

# Development with live reload
make dev

# View logs
docker-compose logs -f docs

# Stop all
docker-compose down
```

## Port Reference

| Service | Port | Description |
|---------|------|-------------|
| docs (nginx) | 8080 | Documentation server |
| meilisearch | 7700 | Search API |
| browsersync | 3000 | Live reload proxy |
| browsersync | 3001 | BrowserSync UI |
| oauth2-proxy | 4180 | Auth proxy |

## Troubleshooting

### Container won't start

```bash
# Check logs
docker-compose logs docs

# Verify volumes
docker-compose config

# Rebuild
docker-compose build --no-cache
```

### Search not working

```bash
# Check Meilisearch health
curl http://localhost:7700/health

# Check index
curl http://localhost:7700/indexes/docs -H "Authorization: Bearer devkey123"

# Re-index
docker-compose run --rm indexer
```

### Permission issues

```bash
# Fix permissions on macOS/Linux
chmod -R 755 book/
```

## Next Steps

- Deploy to [Cloudflare Pages](./cf-pages-static.md)
- Set up [Kubernetes](./k8s-deployment.md)
- Configure [GitHub Pages](./github-pages.md)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.