feat: Add Caddy static sites and implement domain strategy

Domain Strategy:
- fig.systems: Homelab services only (removed edfig.dev fallback from all services)
- edfig.dev: Professional/public sites (personal site, blog)
- figgy.foo: Experimental/private content (SSO protected)

Removed edfig.dev Fallbacks:
- Updated 22 compose files to remove || Host(...edfig.dev) pattern
- All homelab services now use fig.systems only
- Traefik email remains admin@edfig.dev

Added Caddy Static Sites Service:
- compose/services/static-sites/ with Caddy 2
- Serves three domains with different configurations:
  * edfig.dev (personal/professional) - Public, no SSO
  * blog.edfig.dev (blog) - Public, Markdown rendering, templates
  * figgy.foo (experimental) - SSO protected, directory browsing
- Example sites with modern, responsive designs
- Comprehensive README with usage examples
- Auto-reload on config changes (no restarts needed)

Features:
- Markdown rendering (write .md, serves as HTML)
- Go templates for dynamic content
- Directory browsing (figgy.foo)
- Automatic gzip compression
- Static asset caching
- Zero-downtime config reloads

Updated Documentation:
- README.md: Added domain strategy section, static sites in directory structure
- README.md: Added static sites to service URLs table
- README.md: Updated deployment instructions
This commit is contained in:
Claude 2025-11-10 12:11:03 +00:00
parent 07a8154fea
commit 953a9d52af
No known key found for this signature in database
31 changed files with 1324 additions and 25 deletions

View file

@ -60,6 +60,7 @@ compose/
└── services/ # Utility services └── services/ # Utility services
├── homarr/ # Dashboard (home.fig.systems) ├── homarr/ # Dashboard (home.fig.systems)
├── backrest/ # Backup manager (backup.fig.systems) ├── backrest/ # Backup manager (backup.fig.systems)
├── static-sites/ # Static websites (Caddy)
├── karakeep/ # Bookmark manager with AI (links.fig.systems) ├── karakeep/ # Bookmark manager with AI (links.fig.systems)
├── ollama/ # Local LLM server (ollama.fig.systems) ├── ollama/ # Local LLM server (ollama.fig.systems)
├── vikunja/ # Task management (tasks.fig.systems) ├── vikunja/ # Task management (tasks.fig.systems)
@ -74,9 +75,21 @@ compose/
## 🌐 Domains ## 🌐 Domains
All services are accessible via: Three domains are used with different purposes:
- Primary: `*.fig.systems`
- Secondary: `*.edfig.dev` ### fig.systems (Homelab Services)
Primary domain for all self-hosted homelab services:
- `*.fig.systems` - All services listed below
### edfig.dev (Professional/Public)
Professional and public-facing sites:
- `edfig.dev` / `www.edfig.dev` - Personal website/portfolio
- `blog.edfig.dev` - Technical blog
### figgy.foo (Experimental/Private)
Testing and experimental services:
- `figgy.foo` - Experimental lab (SSO protected)
- `*.figgy.foo` - Test instances of services
### Service URLs ### Service URLs
@ -85,6 +98,10 @@ All services are accessible via:
| Traefik Dashboard | traefik.fig.systems | ✅ | | Traefik Dashboard | traefik.fig.systems | ✅ |
| LLDAP | lldap.fig.systems | ✅ | | LLDAP | lldap.fig.systems | ✅ |
| Tinyauth | auth.fig.systems | ❌ | | Tinyauth | auth.fig.systems | ❌ |
| **Static Sites** | | |
| Personal Site | edfig.dev | ❌ |
| Blog | blog.edfig.dev | ❌ |
| Experimental Lab | figgy.foo | ✅ |
| **Monitoring** | | | | **Monitoring** | | |
| Grafana (Logs) | logs.fig.systems | ❌* | | Grafana (Logs) | logs.fig.systems | ❌* |
| Loki (API) | loki.fig.systems | ✅ | | Loki (API) | loki.fig.systems | ✅ |
@ -183,6 +200,7 @@ cd compose/media/automation/recyclarr && docker compose up -d
cd compose/media/automation/profilarr && docker compose up -d cd compose/media/automation/profilarr && docker compose up -d
# Utility services # Utility services
cd compose/services/static-sites && docker compose up -d # Static websites (edfig.dev, blog, figgy.foo)
cd compose/services/karakeep && docker compose up -d cd compose/services/karakeep && docker compose up -d
cd compose/services/ollama && docker compose up -d cd compose/services/ollama && docker compose up -d
cd compose/services/vikunja && docker compose up -d cd compose/services/vikunja && docker compose up -d

View file

@ -13,7 +13,7 @@ services:
- homelab - homelab
labels: labels:
traefik.enable: true traefik.enable: true
traefik.http.routers.lldap.rule: Host(`lldap.fig.systems`) || Host(`lldap.edfig.dev`) traefik.http.routers.lldap.rule: Host(`lldap.fig.systems`)
traefik.http.routers.lldap.entrypoints: websecure traefik.http.routers.lldap.entrypoints: websecure
traefik.http.routers.lldap.tls.certresolver: letsencrypt traefik.http.routers.lldap.tls.certresolver: letsencrypt
traefik.http.services.lldap.loadbalancer.server.port: 17170 traefik.http.services.lldap.loadbalancer.server.port: 17170

View file

@ -11,7 +11,7 @@ services:
labels: labels:
traefik.enable: true traefik.enable: true
# Web UI routing # Web UI routing
traefik.http.routers.tinyauth.rule: Host(`auth.fig.systems`) || Host(`auth.edfig.dev`) traefik.http.routers.tinyauth.rule: Host(`auth.fig.systems`)
traefik.http.routers.tinyauth.entrypoints: websecure traefik.http.routers.tinyauth.entrypoints: websecure
traefik.http.routers.tinyauth.tls.certresolver: letsencrypt traefik.http.routers.tinyauth.tls.certresolver: letsencrypt
traefik.http.routers.tinyauth.service: tinyauth-ui traefik.http.routers.tinyauth.service: tinyauth-ui

View file

@ -34,7 +34,7 @@ services:
labels: labels:
traefik.enable: true traefik.enable: true
# Dashboard routing # Dashboard routing
traefik.http.routers.traefik.rule: Host(`traefik.fig.systems`) || Host(`traefik.edfig.dev`) traefik.http.routers.traefik.rule: Host(`traefik.fig.systems`)
traefik.http.routers.traefik.entrypoints: websecure traefik.http.routers.traefik.entrypoints: websecure
traefik.http.routers.traefik.tls.certresolver: letsencrypt traefik.http.routers.traefik.tls.certresolver: letsencrypt
traefik.http.routers.traefik.service: api@internal traefik.http.routers.traefik.service: api@internal

View file

@ -22,7 +22,7 @@ services:
traefik.docker.network: homelab traefik.docker.network: homelab
# Web UI # Web UI
traefik.http.routers.profilarr.rule: Host(`profilarr.fig.systems`) || Host(`profilarr.edfig.dev`) traefik.http.routers.profilarr.rule: Host(`profilarr.fig.systems`)
traefik.http.routers.profilarr.entrypoints: websecure traefik.http.routers.profilarr.entrypoints: websecure
traefik.http.routers.profilarr.tls.certresolver: letsencrypt traefik.http.routers.profilarr.tls.certresolver: letsencrypt
traefik.http.services.profilarr.loadbalancer.server.port: 6868 traefik.http.services.profilarr.loadbalancer.server.port: 6868

View file

@ -20,7 +20,7 @@ services:
- homelab - homelab
labels: labels:
traefik.enable: true traefik.enable: true
traefik.http.routers.qbittorrent.rule: Host(`qbt.fig.systems`) || Host(`qbt.edfig.dev`) traefik.http.routers.qbittorrent.rule: Host(`qbt.fig.systems`)
traefik.http.routers.qbittorrent.entrypoints: websecure traefik.http.routers.qbittorrent.entrypoints: websecure
traefik.http.routers.qbittorrent.tls.certresolver: letsencrypt traefik.http.routers.qbittorrent.tls.certresolver: letsencrypt
traefik.http.services.qbittorrent.loadbalancer.server.port: 8080 traefik.http.services.qbittorrent.loadbalancer.server.port: 8080

View file

@ -20,7 +20,7 @@ services:
- homelab - homelab
labels: labels:
traefik.enable: true traefik.enable: true
traefik.http.routers.radarr.rule: Host(`radarr.fig.systems`) || Host(`radarr.edfig.dev`) traefik.http.routers.radarr.rule: Host(`radarr.fig.systems`)
traefik.http.routers.radarr.entrypoints: websecure traefik.http.routers.radarr.entrypoints: websecure
traefik.http.routers.radarr.tls.certresolver: letsencrypt traefik.http.routers.radarr.tls.certresolver: letsencrypt
traefik.http.services.radarr.loadbalancer.server.port: 7878 traefik.http.services.radarr.loadbalancer.server.port: 7878

View file

@ -17,7 +17,7 @@ services:
- homelab - homelab
labels: labels:
traefik.enable: true traefik.enable: true
traefik.http.routers.sabnzbd.rule: Host(`sabnzbd.fig.systems`) || Host(`sabnzbd.edfig.dev`) traefik.http.routers.sabnzbd.rule: Host(`sabnzbd.fig.systems`)
traefik.http.routers.sabnzbd.entrypoints: websecure traefik.http.routers.sabnzbd.entrypoints: websecure
traefik.http.routers.sabnzbd.tls.certresolver: letsencrypt traefik.http.routers.sabnzbd.tls.certresolver: letsencrypt
traefik.http.services.sabnzbd.loadbalancer.server.port: 8080 traefik.http.services.sabnzbd.loadbalancer.server.port: 8080

View file

@ -20,7 +20,7 @@ services:
- homelab - homelab
labels: labels:
traefik.enable: true traefik.enable: true
traefik.http.routers.sonarr.rule: Host(`sonarr.fig.systems`) || Host(`sonarr.edfig.dev`) traefik.http.routers.sonarr.rule: Host(`sonarr.fig.systems`)
traefik.http.routers.sonarr.entrypoints: websecure traefik.http.routers.sonarr.entrypoints: websecure
traefik.http.routers.sonarr.tls.certresolver: letsencrypt traefik.http.routers.sonarr.tls.certresolver: letsencrypt
traefik.http.services.sonarr.loadbalancer.server.port: 8989 traefik.http.services.sonarr.loadbalancer.server.port: 8989

View file

@ -40,7 +40,7 @@ services:
labels: labels:
traefik.enable: true traefik.enable: true
traefik.docker.network: homelab traefik.docker.network: homelab
traefik.http.routers.immich.rule: Host(`photos.fig.systems`) || Host(`photos.edfig.dev`) traefik.http.routers.immich.rule: Host(`photos.fig.systems`)
traefik.http.routers.immich.entrypoints: websecure traefik.http.routers.immich.entrypoints: websecure
traefik.http.routers.immich.tls.certresolver: letsencrypt traefik.http.routers.immich.tls.certresolver: letsencrypt
traefik.http.services.immich.loadbalancer.server.port: 2283 traefik.http.services.immich.loadbalancer.server.port: 2283

View file

@ -25,7 +25,7 @@ services:
- homelab - homelab
labels: labels:
traefik.enable: true traefik.enable: true
traefik.http.routers.jellyfin.rule: Host(`flix.fig.systems`) || Host(`flix.edfig.dev`) traefik.http.routers.jellyfin.rule: Host(`flix.fig.systems`)
traefik.http.routers.jellyfin.entrypoints: websecure traefik.http.routers.jellyfin.entrypoints: websecure
traefik.http.routers.jellyfin.tls.certresolver: letsencrypt traefik.http.routers.jellyfin.tls.certresolver: letsencrypt
traefik.http.services.jellyfin.loadbalancer.server.port: 8096 traefik.http.services.jellyfin.loadbalancer.server.port: 8096

View file

@ -14,7 +14,7 @@ services:
- homelab - homelab
labels: labels:
traefik.enable: true traefik.enable: true
traefik.http.routers.jellyseerr.rule: Host(`requests.fig.systems`) || Host(`requests.edfig.dev`) traefik.http.routers.jellyseerr.rule: Host(`requests.fig.systems`)
traefik.http.routers.jellyseerr.entrypoints: websecure traefik.http.routers.jellyseerr.entrypoints: websecure
traefik.http.routers.jellyseerr.tls.certresolver: letsencrypt traefik.http.routers.jellyseerr.tls.certresolver: letsencrypt
traefik.http.services.jellyseerr.loadbalancer.server.port: 5055 traefik.http.services.jellyseerr.loadbalancer.server.port: 5055

View file

@ -26,7 +26,7 @@ services:
traefik.docker.network: homelab traefik.docker.network: homelab
# Loki API # Loki API
traefik.http.routers.loki.rule: Host(`loki.fig.systems`) || Host(`loki.edfig.dev`) traefik.http.routers.loki.rule: Host(`loki.fig.systems`)
traefik.http.routers.loki.entrypoints: websecure traefik.http.routers.loki.entrypoints: websecure
traefik.http.routers.loki.tls.certresolver: letsencrypt traefik.http.routers.loki.tls.certresolver: letsencrypt
traefik.http.services.loki.loadbalancer.server.port: 3100 traefik.http.services.loki.loadbalancer.server.port: 3100
@ -95,7 +95,7 @@ services:
traefik.docker.network: homelab traefik.docker.network: homelab
# Grafana Web UI # Grafana Web UI
traefik.http.routers.grafana.rule: Host(`logs.fig.systems`) || Host(`logs.edfig.dev`) traefik.http.routers.grafana.rule: Host(`logs.fig.systems`)
traefik.http.routers.grafana.entrypoints: websecure traefik.http.routers.grafana.entrypoints: websecure
traefik.http.routers.grafana.tls.certresolver: letsencrypt traefik.http.routers.grafana.tls.certresolver: letsencrypt
traefik.http.services.grafana.loadbalancer.server.port: 3000 traefik.http.services.grafana.loadbalancer.server.port: 3000

View file

@ -22,7 +22,7 @@ services:
traefik.docker.network: homelab traefik.docker.network: homelab
# Web UI # Web UI
traefik.http.routers.uptime-kuma.rule: Host(`status.fig.systems`) || Host(`status.edfig.dev`) traefik.http.routers.uptime-kuma.rule: Host(`status.fig.systems`)
traefik.http.routers.uptime-kuma.entrypoints: websecure traefik.http.routers.uptime-kuma.entrypoints: websecure
traefik.http.routers.uptime-kuma.tls.certresolver: letsencrypt traefik.http.routers.uptime-kuma.tls.certresolver: letsencrypt
traefik.http.services.uptime-kuma.loadbalancer.server.port: 3001 traefik.http.services.uptime-kuma.loadbalancer.server.port: 3001

View file

@ -22,7 +22,7 @@ services:
traefik.docker.network: homelab traefik.docker.network: homelab
# Web UI # Web UI
traefik.http.routers.freshrss.rule: Host(`rss.fig.systems`) || Host(`rss.edfig.dev`) traefik.http.routers.freshrss.rule: Host(`rss.fig.systems`)
traefik.http.routers.freshrss.entrypoints: websecure traefik.http.routers.freshrss.entrypoints: websecure
traefik.http.routers.freshrss.tls.certresolver: letsencrypt traefik.http.routers.freshrss.tls.certresolver: letsencrypt
traefik.http.services.freshrss.loadbalancer.server.port: 80 traefik.http.services.freshrss.loadbalancer.server.port: 80

View file

@ -21,7 +21,7 @@ services:
labels: labels:
# Traefik # Traefik
traefik.enable: true traefik.enable: true
traefik.http.routers.backrest.rule: Host(`backup.fig.systems`) || Host(`backup.edfig.dev`) traefik.http.routers.backrest.rule: Host(`backup.fig.systems`)
traefik.http.routers.backrest.entrypoints: websecure traefik.http.routers.backrest.entrypoints: websecure
traefik.http.routers.backrest.tls.certresolver: letsencrypt traefik.http.routers.backrest.tls.certresolver: letsencrypt
traefik.http.services.backrest.loadbalancer.server.port: 9898 traefik.http.services.backrest.loadbalancer.server.port: 9898

View file

@ -22,7 +22,7 @@ services:
traefik.docker.network: homelab traefik.docker.network: homelab
# Web UI # Web UI
traefik.http.routers.booklore.rule: Host(`booklore.fig.systems`) || Host(`booklore.edfig.dev`) traefik.http.routers.booklore.rule: Host(`booklore.fig.systems`)
traefik.http.routers.booklore.entrypoints: websecure traefik.http.routers.booklore.entrypoints: websecure
traefik.http.routers.booklore.tls.certresolver: letsencrypt traefik.http.routers.booklore.tls.certresolver: letsencrypt
traefik.http.services.booklore.loadbalancer.server.port: 3000 traefik.http.services.booklore.loadbalancer.server.port: 3000

View file

@ -28,7 +28,7 @@ services:
traefik.docker.network: homelab traefik.docker.network: homelab
# Web UI # Web UI
traefik.http.routers.karakeep.rule: Host(`links.fig.systems`) || Host(`links.edfig.dev`) traefik.http.routers.karakeep.rule: Host(`links.fig.systems`)
traefik.http.routers.karakeep.entrypoints: websecure traefik.http.routers.karakeep.entrypoints: websecure
traefik.http.routers.karakeep.tls.certresolver: letsencrypt traefik.http.routers.karakeep.tls.certresolver: letsencrypt
traefik.http.services.karakeep.loadbalancer.server.port: 3000 traefik.http.services.karakeep.loadbalancer.server.port: 3000

View file

@ -22,7 +22,7 @@ services:
traefik.docker.network: homelab traefik.docker.network: homelab
# Web UI # Web UI
traefik.http.routers.microbin.rule: Host(`paste.fig.systems`) || Host(`paste.edfig.dev`) traefik.http.routers.microbin.rule: Host(`paste.fig.systems`)
traefik.http.routers.microbin.entrypoints: websecure traefik.http.routers.microbin.entrypoints: websecure
traefik.http.routers.microbin.tls.certresolver: letsencrypt traefik.http.routers.microbin.tls.certresolver: letsencrypt
traefik.http.services.microbin.loadbalancer.server.port: 8080 traefik.http.services.microbin.loadbalancer.server.port: 8080

View file

@ -35,7 +35,7 @@ services:
traefik.docker.network: homelab traefik.docker.network: homelab
# API endpoint # API endpoint
traefik.http.routers.ollama.rule: Host(`ollama.fig.systems`) || Host(`ollama.edfig.dev`) traefik.http.routers.ollama.rule: Host(`ollama.fig.systems`)
traefik.http.routers.ollama.entrypoints: websecure traefik.http.routers.ollama.entrypoints: websecure
traefik.http.routers.ollama.tls.certresolver: letsencrypt traefik.http.routers.ollama.tls.certresolver: letsencrypt
traefik.http.services.ollama.loadbalancer.server.port: 11434 traefik.http.services.ollama.loadbalancer.server.port: 11434

View file

@ -23,7 +23,7 @@ services:
traefik.docker.network: homelab traefik.docker.network: homelab
# Web UI # Web UI
traefik.http.routers.rsshub.rule: Host(`rsshub.fig.systems`) || Host(`rsshub.edfig.dev`) traefik.http.routers.rsshub.rule: Host(`rsshub.fig.systems`)
traefik.http.routers.rsshub.entrypoints: websecure traefik.http.routers.rsshub.entrypoints: websecure
traefik.http.routers.rsshub.tls.certresolver: letsencrypt traefik.http.routers.rsshub.tls.certresolver: letsencrypt
traefik.http.services.rsshub.loadbalancer.server.port: 1200 traefik.http.services.rsshub.loadbalancer.server.port: 1200

View file

@ -0,0 +1,7 @@
# Caddy Static Sites Configuration
# Timezone
TZ=America/Los_Angeles
# Optional: Caddy admin API (disabled by default in Caddyfile)
# CADDY_ADMIN=localhost:2019

View file

@ -0,0 +1,10 @@
# Caddy data and config
caddy_data/
caddy_config/
# Keep example sites but ignore actual site content
# (uncomment if you want to version control your sites)
# sites/
# Keep .env.example if created
!.env.example

View file

@ -0,0 +1,57 @@
# Caddyfile - Static Sites Configuration
# Docs: https://caddyserver.com/docs/caddyfile
# Global options
{
# Listen on port 80 (Traefik handles SSL)
auto_https off
admin off
}
# Personal/Professional Site (edfig.dev)
www.edfig.dev, edfig.dev {
root * /srv/edfig.dev
file_server
encode gzip
# Try files, then index, then 404
try_files {path} {path}/index.html index.html
# Cache static assets
@static {
path *.css *.js *.jpg *.jpeg *.png *.gif *.ico *.svg *.woff *.woff2 *.ttf *.eot
}
header @static Cache-Control "public, max-age=604800, immutable"
}
# Blog (blog.edfig.dev)
blog.edfig.dev {
root * /srv/blog.edfig.dev
file_server
encode gzip
# Enable templates for dynamic content
templates
# Markdown files automatically render as HTML
try_files {path} {path}/index.html {path}.md {path}/index.md index.html
# Cache static assets
@static {
path *.css *.js *.jpg *.jpeg *.png *.gif *.ico *.svg *.woff *.woff2 *.ttf *.eot
}
header @static Cache-Control "public, max-age=604800, immutable"
}
# Experimental/Private (figgy.foo)
figgy.foo, www.figgy.foo {
root * /srv/figgy.foo
# Enable directory browsing for experiments
file_server browse
encode gzip
# Templates enabled for dynamic pages
templates
}

View file

@ -0,0 +1,604 @@
# Caddy Static Sites Server
Serves static websites for edfig.dev (professional), blog.edfig.dev (blog), and figgy.foo (experimental).
## Overview
**Caddy** is a modern web server with automatic HTTPS and simple configuration:
- ✅ **Static file serving** - HTML, CSS, JavaScript, images
- ✅ **Markdown rendering** - Write `.md` files, served as HTML automatically
- ✅ **Templates** - Dynamic content with Go templates
- ✅ **Directory browsing** - Beautiful file listing (figgy.foo)
- ✅ **Auto-compression** - Gzip for all responses
- ✅ **Zero-downtime reloads** - Config changes apply instantly
## Domain Strategy
### edfig.dev (Professional/Public)
- **Purpose**: Personal website, portfolio
- **URL**: https://edfig.dev or https://www.edfig.dev
- **SSO**: No (public site)
- **Content**: `/sites/edfig.dev/`
### blog.edfig.dev (Blog/Public)
- **Purpose**: Technical blog, articles
- **URL**: https://blog.edfig.dev
- **SSO**: No (public blog)
- **Content**: `/sites/blog.edfig.dev/`
- **Features**: Markdown auto-rendering, templates
### figgy.foo (Experimental/Private)
- **Purpose**: Testing, development, experiments
- **URL**: https://figgy.foo or https://www.figgy.foo
- **SSO**: Yes (protected by Tinyauth)
- **Content**: `/sites/figgy.foo/`
- **Features**: Directory browsing, templates
## Quick Start
### 1. Deploy
```bash
cd ~/homelab/compose/services/static-sites
docker compose up -d
```
### 2. Access Sites
- **edfig.dev**: https://edfig.dev
- **Blog**: https://blog.edfig.dev
- **Experimental**: https://figgy.foo (requires SSO login)
### 3. Verify
```bash
# Check container is running
docker ps | grep caddy-static
# Check logs
docker logs caddy-static
# Test sites
curl -I https://edfig.dev
curl -I https://blog.edfig.dev
```
## Directory Structure
```
static-sites/
├── compose.yaml # Docker Compose + Traefik labels
├── Caddyfile # Caddy configuration
├── .env # Environment variables
├── .gitignore # Ignored files
├── README.md # This file
└── sites/ # Site content (can be version controlled)
├── edfig.dev/
│ ├── index.html
│ ├── assets/
│ │ ├── css/
│ │ ├── js/
│ │ └── images/
│ └── ...
├── blog.edfig.dev/
│ ├── index.html
│ └── posts/
│ ├── example-post.md # Markdown posts
│ └── ...
└── figgy.foo/
├── index.html
└── experiments/
└── ...
```
## Managing Content
### Adding/Editing HTML
Simply edit files in the `sites/` directory:
```bash
# Edit main site
vim sites/edfig.dev/index.html
# Add new page
echo "<h1>About Me</h1>" > sites/edfig.dev/about.html
# Changes are live immediately (no restart needed!)
```
### Writing Blog Posts (Markdown)
Create `.md` files in `sites/blog.edfig.dev/posts/`:
```bash
# Create new post
cat > sites/blog.edfig.dev/posts/my-post.md << 'EOF'
# My New Blog Post
**Published:** January 10, 2025
This is my blog post content...
## Code Example
```bash
docker compose up -d
```
[Back to Blog](/)
EOF
# Access at: https://blog.edfig.dev/posts/my-post.md
# (renders as HTML automatically!)
```
**Markdown features:**
- Headers (`#`, `##`, `###`)
- **Bold**, *italic*, `code`
- Links, images
- Lists (ordered/unordered)
- Code blocks with syntax highlighting
- Tables
- Blockquotes
### Using Templates
Caddy supports Go templates for dynamic content:
**Example - Current time:**
```html
<!-- In any .html file under blog.edfig.dev -->
<p>Page generated at: {{.Now.Format "2006-01-02 15:04:05"}}</p>
```
**Example - Include header:**
```html
{{include "header.html"}}
<main>
<h1>My Page</h1>
</main>
{{include "footer.html"}}
```
**Template variables:**
- `{{.Now}}` - Current time
- `{{.Req.URL}}` - Request URL
- `{{.Req.Host}}` - Request hostname
- `{{.Req.Method}}` - HTTP method
- `{{env "VARIABLE"}}` - Environment variable
See [Caddy Templates Docs](https://caddyserver.com/docs/caddyfile/directives/templates)
### Directory Browsing (figgy.foo)
figgy.foo has directory browsing enabled:
```bash
# Add files to browse
cp some-file.txt sites/figgy.foo/experiments/
# Access: https://figgy.foo/experiments/
# Shows beautiful file listing with search!
```
## Adding New Sites
### Option 1: New Subdomain (same domain)
**Add to Caddyfile:**
```caddy
test.figgy.foo {
root * /srv/test.figgy.foo
file_server
encode gzip
}
```
**Add Traefik labels to compose.yaml:**
```yaml
# test.figgy.foo
traefik.http.routers.figgy-test.rule: Host(`test.figgy.foo`)
traefik.http.routers.figgy-test.entrypoints: websecure
traefik.http.routers.figgy-test.tls.certresolver: letsencrypt
traefik.http.routers.figgy-test.service: caddy-static
traefik.http.routers.figgy-test.middlewares: tinyauth # If SSO needed
```
**Create site directory:**
```bash
mkdir -p sites/test.figgy.foo
echo "<h1>Test Site</h1>" > sites/test.figgy.foo/index.html
```
**Reload (instant, no restart):**
```bash
# Caddy auto-reloads when Caddyfile changes!
# Just wait 1-2 seconds, then access https://test.figgy.foo
```
### Option 2: New Domain
Follow same process but use new domain name. Make sure DNS points to your server.
## Caddyfile Features
### Basic Site
```caddy
example.com {
root * /srv/example
file_server
}
```
### With Compression
```caddy
example.com {
root * /srv/example
file_server
encode gzip zstd brotli
}
```
### With Caching
```caddy
example.com {
root * /srv/example
file_server
@static {
path *.css *.js *.jpg *.png *.gif *.ico
}
header @static Cache-Control "public, max-age=604800"
}
```
### With Redirects
```caddy
www.example.com {
redir https://example.com{uri} permanent
}
example.com {
root * /srv/example
file_server
}
```
### With Custom 404
```caddy
example.com {
root * /srv/example
file_server
handle_errors {
rewrite * /404.html
file_server
}
}
```
### With Basic Auth (alternative to SSO)
```caddy
example.com {
root * /srv/example
basicauth {
user $2a$14$hashedpassword
}
file_server
}
```
Generate hashed password:
```bash
docker exec caddy-static caddy hash-password --plaintext "mypassword"
```
## Traefik Integration
All sites route through Traefik:
```
Internet → DNS (*.edfig.dev, *.figgy.foo)
Traefik (SSL termination)
Tinyauth (SSO check for figgy.foo only)
Caddy (static file serving)
```
**SSL certificates:**
- Traefik handles Let's Encrypt
- Caddy receives plain HTTP on port 80
- Users see HTTPS
**SSO protection:**
- `edfig.dev` & `blog.edfig.dev`: No SSO (public)
- `figgy.foo`: SSO protected (private)
## Performance
### Caching
Static assets automatically cached:
```caddy
@static {
path *.css *.js *.jpg *.jpeg *.png *.gif *.ico *.svg
}
header @static Cache-Control "public, max-age=604800, immutable"
```
- 7 days cache for images, CSS, JS
- Browsers won't re-request until expired
### Compression
All responses auto-compressed with gzip:
```caddy
encode gzip
```
- 70-90% size reduction for HTML/CSS/JS
- Faster page loads
- Lower bandwidth usage
### Performance Tips
1. **Optimize images**: Use WebP format, compress before uploading
2. **Minify CSS/JS**: Use build tools (optional)
3. **Use CDN**: For high-traffic sites (optional)
4. **Enable HTTP/2**: Traefik handles this automatically
## Monitoring
### Check Service Status
```bash
# Container status
docker ps | grep caddy-static
# Logs
docker logs caddy-static -f
# Resource usage
docker stats caddy-static
```
### Check Specific Site
```bash
# Test site is reachable
curl -I https://edfig.dev
# Test with timing
curl -w "@curl-format.txt" -o /dev/null -s https://edfig.dev
# Check SSL certificate
echo | openssl s_client -connect edfig.dev:443 -servername edfig.dev 2>/dev/null | openssl x509 -noout -dates
```
### Access Logs
Caddy logs to stdout (captured by Docker):
```bash
# View logs
docker logs caddy-static
# Follow logs
docker logs caddy-static -f
# Last 100 lines
docker logs caddy-static --tail 100
```
### Grafana Logs
All logs forwarded to Loki automatically:
**Query in Grafana** (https://logs.fig.systems):
```logql
{container="caddy-static"}
```
Filter by status code:
```logql
{container="caddy-static"} |= "404"
```
## Troubleshooting
### Site not loading
**Check container:**
```bash
docker ps | grep caddy-static
# If not running:
docker compose up -d
```
**Check logs:**
```bash
docker logs caddy-static
# Look for errors in Caddyfile or file not found
```
**Check DNS:**
```bash
dig +short edfig.dev
# Should point to your server IP
```
**Check Traefik:**
```bash
# See if Traefik sees the route
docker logs traefik | grep edfig
```
### 404 Not Found
**Check file exists:**
```bash
ls -la sites/edfig.dev/index.html
```
**Check path in Caddyfile:**
```bash
grep "root" Caddyfile
# Should show: root * /srv/edfig.dev
```
**Check permissions:**
```bash
# Files should be readable
chmod -R 755 sites/
```
### Changes not appearing
**Caddy auto-reloads**, but double-check:
```bash
# Check file modification time
ls -lh sites/edfig.dev/index.html
# Force reload (shouldn't be needed)
docker exec caddy-static caddy reload --config /etc/caddy/Caddyfile
```
**Browser cache:**
```bash
# Force refresh in browser: Ctrl+Shift+R (Linux/Win) or Cmd+Shift+R (Mac)
# Or open in incognito/private window
```
### Markdown not rendering
**Check templates enabled:**
```caddy
# In Caddyfile for blog.edfig.dev
blog.edfig.dev {
templates # <-- This must be present!
# ...
}
```
**Check file extension:**
```bash
# Must be .md
mv post.txt post.md
```
**Test rendering:**
```bash
curl https://blog.edfig.dev/posts/example-post.md
# Should return HTML, not raw markdown
```
### SSO not working on figgy.foo
**Check middleware:**
```yaml
# In compose.yaml
traefik.http.routers.figgy-main.middlewares: tinyauth
```
**Check Tinyauth is running:**
```bash
docker ps | grep tinyauth
```
**Test without SSO:**
```bash
# Temporarily remove SSO to isolate issue
# Comment out middleware line in compose.yaml
# docker compose up -d
```
## Backup
### Backup Site Content
```bash
# Backup all sites
cd ~/homelab/compose/services/static-sites
tar czf sites-backup-$(date +%Y%m%d).tar.gz sites/
# Backup to external storage
scp sites-backup-*.tar.gz user@backup-server:/backups/
```
### Version Control (Optional)
Consider using Git for your sites:
```bash
cd sites/
git init
git add .
git commit -m "Initial site content"
# Add remote
git remote add origin git@github.com:efigueroa/sites.git
git push -u origin main
```
## Security
### Public vs Private
**Public sites** (`edfig.dev`, `blog.edfig.dev`):
- No SSO middleware
- Accessible to everyone
- Use for portfolio, blog, public content
**Private sites** (`figgy.foo`):
- SSO middleware enabled
- Requires LLDAP authentication
- Use for experiments, private content
### Content Security
**Don't commit:**
- API keys
- Passwords
- Private information
- Sensitive data
**Do commit:**
- HTML, CSS, JS
- Images, assets
- Markdown blog posts
- Public content
### File Permissions
```bash
# Sites should be read-only to Caddy
chmod -R 755 sites/
chown -R $USER:$USER sites/
```
## Resources
- [Caddy Documentation](https://caddyserver.com/docs/)
- [Caddyfile Tutorial](https://caddyserver.com/docs/caddyfile-tutorial)
- [Templates Documentation](https://caddyserver.com/docs/caddyfile/directives/templates)
- [Markdown Rendering](https://caddyserver.com/docs/caddyfile/directives/templates#markdown)
## Next Steps
1. ✅ Deploy Caddy static sites
2. ✅ Access edfig.dev, blog.edfig.dev, figgy.foo
3. ⬜ Customize edfig.dev with your content
4. ⬜ Write first blog post in Markdown
5. ⬜ Add experiments to figgy.foo
6. ⬜ Set up Git version control for sites
7. ⬜ Configure automated backups
---
**Serve static content, simply and securely!** 🌐

View file

@ -0,0 +1,63 @@
# Caddy - Static Sites Server
# Docs: https://caddyserver.com/docs/
services:
caddy:
container_name: caddy-static
image: caddy:2-alpine
restart: unless-stopped
env_file:
- .env
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./sites:/srv:ro
- caddy_data:/data
- caddy_config:/config
networks:
- homelab
labels:
# Traefik
traefik.enable: true
traefik.docker.network: homelab
# edfig.dev (personal/professional site)
traefik.http.routers.edfig-www.rule: Host(`www.edfig.dev`) || Host(`edfig.dev`)
traefik.http.routers.edfig-www.entrypoints: websecure
traefik.http.routers.edfig-www.tls.certresolver: letsencrypt
traefik.http.routers.edfig-www.service: caddy-static
# No SSO - public personal site
# blog.edfig.dev (blog)
traefik.http.routers.edfig-blog.rule: Host(`blog.edfig.dev`)
traefik.http.routers.edfig-blog.entrypoints: websecure
traefik.http.routers.edfig-blog.tls.certresolver: letsencrypt
traefik.http.routers.edfig-blog.service: caddy-static
# No SSO - public blog
# figgy.foo (experimental/private)
traefik.http.routers.figgy-main.rule: Host(`figgy.foo`) || Host(`www.figgy.foo`)
traefik.http.routers.figgy-main.entrypoints: websecure
traefik.http.routers.figgy-main.tls.certresolver: letsencrypt
traefik.http.routers.figgy-main.service: caddy-static
traefik.http.routers.figgy-main.middlewares: tinyauth
# SSO protected - experimental/private content
# Service definition (single backend for all routes)
traefik.http.services.caddy-static.loadbalancer.server.port: 80
# Homarr Discovery
homarr.name: Static Sites (Caddy)
homarr.group: Services
homarr.icon: mdi:web
volumes:
caddy_data:
caddy_config:
networks:
homelab:
external: true

View file

@ -0,0 +1,160 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog | Eduardo Figueroa</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #333;
background: #f5f5f5;
}
header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2em 0;
text-align: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
header h1 {
font-size: 2.5em;
margin-bottom: 0.3em;
}
header p {
font-size: 1.1em;
opacity: 0.9;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
}
.post {
background: white;
border-radius: 10px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.post:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.post h2 {
color: #667eea;
margin-bottom: 0.5em;
font-size: 1.8em;
}
.post-meta {
color: #999;
font-size: 0.9em;
margin-bottom: 1em;
}
.post p {
color: #555;
line-height: 1.8;
margin-bottom: 1em;
}
.read-more {
color: #667eea;
text-decoration: none;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 5px;
}
.read-more:hover {
text-decoration: underline;
}
.no-posts {
text-align: center;
padding: 60px 20px;
color: #999;
}
.no-posts h2 {
font-size: 2em;
margin-bottom: 0.5em;
color: #667eea;
}
nav {
text-align: center;
margin-top: 2em;
}
nav a {
color: #667eea;
text-decoration: none;
font-weight: 500;
}
nav a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<header>
<h1>Blog</h1>
<p>Thoughts on technology, systems, and automation</p>
</header>
<div class="container">
<!-- Example post structure - replace with real posts -->
<div class="no-posts">
<h2>Coming Soon</h2>
<p>Blog posts will appear here. Stay tuned!</p>
<p style="margin-top: 2em;">
In the meantime, you can write posts as:<br>
<code style="background: #f5f5f5; padding: 5px 10px; border-radius: 5px;">
/srv/blog.edfig.dev/posts/my-post.md
</code>
</p>
<p style="margin-top: 1em; font-size: 0.9em; color: #666;">
Markdown files (.md) will automatically render as HTML!
</p>
</div>
<!-- Example of how posts would look -->
<!--
<article class="post">
<h2>Setting Up a Homelab with Docker and Traefik</h2>
<div class="post-meta">January 10, 2025 • 5 min read</div>
<p>
Learn how to set up a complete homelab infrastructure using Docker Compose,
Traefik for reverse proxy, and automated SSL certificates...
</p>
<a href="/posts/homelab-setup.html" class="read-more">
Read more →
</a>
</article>
-->
<nav>
<a href="https://edfig.dev">← Back to Home</a>
</nav>
</div>
</body>
</html>

View file

@ -0,0 +1,68 @@
# Example Blog Post
**Published:** January 10, 2025
**Tags:** #homelab #docker #traefik
---
## Introduction
This is an example blog post written in Markdown. Caddy automatically renders `.md` files as HTML!
## Why Markdown?
Markdown is perfect for writing blog posts because:
1. **Simple syntax** - Easy to write and read
2. **Fast** - No build step required
3. **Portable** - Works everywhere
4. **Clean** - Focus on content, not formatting
## Code Examples
Here's some example code:
```bash
# Deploy a service
cd ~/homelab/compose/services/example
docker compose up -d
# Check logs
docker logs example-service -f
```
## Features
### Supported Elements
- **Bold text**
- *Italic text*
- `Code snippets`
- [Links](https://edfig.dev)
- Lists (ordered and unordered)
- Code blocks with syntax highlighting
- Blockquotes
- Tables
### Example Table
| Service | URL | Purpose |
|---------|-----|---------|
| Traefik | traefik.fig.systems | Reverse Proxy |
| Sonarr | sonarr.fig.systems | TV Automation |
| Radarr | radarr.fig.systems | Movie Automation |
## Blockquote Example
> "The best way to predict the future is to invent it."
> — Alan Kay
## Conclusion
This is just an example post. Delete this file and create your own posts in the `posts/` directory!
Each `.md` file will be automatically rendered when accessed via the browser.
---
[← Back to Blog](/)

View file

@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Eduardo Figueroa</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
max-width: 800px;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 60px 40px;
text-align: center;
}
h1 {
font-size: 3em;
margin-bottom: 0.5em;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
font-size: 1.5em;
color: #666;
margin-bottom: 2em;
}
.description {
font-size: 1.1em;
color: #555;
margin-bottom: 2em;
line-height: 1.8;
}
.links {
display: flex;
gap: 20px;
justify-content: center;
flex-wrap: wrap;
margin-top: 2em;
}
.link {
display: inline-block;
padding: 12px 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 50px;
transition: transform 0.2s, box-shadow 0.2s;
font-weight: 500;
}
.link:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
}
.link.secondary {
background: white;
color: #667eea;
border: 2px solid #667eea;
}
.link.secondary:hover {
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.2);
}
footer {
margin-top: 3em;
padding-top: 2em;
border-top: 1px solid #eee;
color: #999;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="container">
<h1>Eduardo Figueroa</h1>
<p class="subtitle">Software Engineer & DevOps Enthusiast</p>
<p class="description">
Welcome to my personal site. I build scalable systems, automate infrastructure,
and explore the intersection of technology and efficiency.
</p>
<div class="links">
<a href="https://blog.edfig.dev" class="link">Blog</a>
<a href="https://github.com/efigueroa" class="link secondary" target="_blank">GitHub</a>
<a href="https://home.fig.systems" class="link secondary">Homelab Dashboard</a>
</div>
<footer>
<p>&copy; 2025 Eduardo Figueroa | <a href="mailto:admin@edfig.dev" style="color: #667eea; text-decoration: none;">admin@edfig.dev</a></p>
</footer>
</div>
</body>
</html>

View file

@ -0,0 +1,191 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>figgy.foo | Experimental Lab</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Courier New', monospace;
line-height: 1.6;
color: #0f0;
background: #000;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.terminal {
max-width: 800px;
width: 100%;
background: #1a1a1a;
border: 2px solid #0f0;
border-radius: 5px;
box-shadow: 0 0 20px rgba(0, 255, 0, 0.3);
}
.terminal-header {
background: #0f0;
color: #000;
padding: 10px 15px;
font-weight: bold;
display: flex;
align-items: center;
gap: 10px;
}
.terminal-buttons {
display: flex;
gap: 8px;
}
.terminal-button {
width: 12px;
height: 12px;
border-radius: 50%;
background: #000;
}
.terminal-body {
padding: 20px;
font-size: 14px;
}
.prompt {
color: #0ff;
}
.command {
color: #ff0;
}
.output {
color: #0f0;
margin-left: 20px;
}
.line {
margin-bottom: 10px;
}
.cursor {
display: inline-block;
width: 8px;
height: 16px;
background: #0f0;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
.warning {
color: #ff0;
text-decoration: underline;
}
a {
color: #0ff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.ascii-art {
color: #0f0;
font-size: 10px;
line-height: 1.2;
white-space: pre;
margin: 20px 0;
}
</style>
</head>
<body>
<div class="terminal">
<div class="terminal-header">
<div class="terminal-buttons">
<div class="terminal-button"></div>
<div class="terminal-button"></div>
<div class="terminal-button"></div>
</div>
<span>figgy.foo — Terminal</span>
</div>
<div class="terminal-body">
<div class="ascii-art">
______ _ ______
| ____(_) | ____|
| |__ _ __ _ __ _ _ _| |__ ___ ___
| __| | |/ _` |/ _` | | | | __| / _ \ / _ \
| | | | (_| | (_| | |_| | | | (_) | (_) |
|_| |_|\__, |\__, |\__, |_| \___/ \___/
__/ | __/ | __/ |
|___/ |___/ |___/
</div>
<div class="line">
<span class="prompt">root@figgy:~$</span> <span class="command">whoami</span>
</div>
<div class="output line">experimental lab user</div>
<div class="line">
<span class="prompt">root@figgy:~$</span> <span class="command">ls -la</span>
</div>
<div class="output line">
drwxr-xr-x 3 root root 4096 Jan 10 2025 .
drwxr-xr-x 24 root root 4096 Jan 10 2025 ..
drwxr-xr-x 2 root root 4096 Jan 10 2025 experiments
-rw-r--r-- 1 root root 420 Jan 10 2025 README.txt
</div>
<div class="line">
<span class="prompt">root@figgy:~$</span> <span class="command">cat README.txt</span>
</div>
<div class="output line">
╔════════════════════════════════════════════╗
║ FIGGY.FOO EXPERIMENTAL LAB ║
╚════════════════════════════════════════════╝
<span class="warning">WARNING:</span> This is a private experimental environment.
Access is restricted to authorized users only.
Purpose:
• Test new services before production
• Develop and debug configurations
• Experiment with new technologies
• Break things safely
Directory Browsing: ENABLED
Authentication: REQUIRED (SSO)
Access Dashboard: <a href="https://home.fig.systems">home.fig.systems</a>
Status Monitor: <a href="https://status.fig.systems">status.fig.systems</a>
</div>
<div class="line">
<span class="prompt">root@figgy:~$</span> <span class="command">./list-services.sh</span>
</div>
<div class="output line">
Production Homelab: <a href="https://home.fig.systems">home.fig.systems</a>
Professional Site: <a href="https://edfig.dev">edfig.dev</a>
Blog: <a href="https://blog.edfig.dev">blog.edfig.dev</a>
</div>
<div class="line">
<span class="prompt">root@figgy:~$</span> <span class="cursor"></span>
</div>
</div>
</div>
</body>
</html>

View file

@ -19,7 +19,7 @@ services:
labels: labels:
traefik.enable: true traefik.enable: true
traefik.docker.network: homelab traefik.docker.network: homelab
traefik.http.routers.vikunja.rule: Host(`tasks.fig.systems`) || Host(`tasks.edfig.dev`) traefik.http.routers.vikunja.rule: Host(`tasks.fig.systems`)
traefik.http.routers.vikunja.entrypoints: websecure traefik.http.routers.vikunja.entrypoints: websecure
traefik.http.routers.vikunja.tls.certresolver: letsencrypt traefik.http.routers.vikunja.tls.certresolver: letsencrypt
traefik.http.services.vikunja.loadbalancer.server.port: 3456 traefik.http.services.vikunja.loadbalancer.server.port: 3456