Compare commits
5 Commits
b0d760c3ab
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd9351599e | ||
|
|
2992c17fd7 | ||
|
|
5465665c26 | ||
|
|
95dece016f | ||
|
|
6c2f77d5b7 |
@@ -4,7 +4,11 @@ set -euo pipefail
|
|||||||
# Paths — adjust if your layout changes
|
# Paths — adjust if your layout changes
|
||||||
GITEA_DIR="/gitea/gitea"
|
GITEA_DIR="/gitea/gitea"
|
||||||
BACKUP_DIR="/gitea/backups"
|
BACKUP_DIR="/gitea/backups"
|
||||||
CONTAINER="gitea"
|
CONTAINER="Gitea"
|
||||||
|
|
||||||
|
# Retention
|
||||||
|
KEEP_COUNT=7
|
||||||
|
B2_REMOTE="B2:SoloForge-backup"
|
||||||
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
@@ -22,9 +26,68 @@ mv "$GITEA_DIR/$dump_file" "$BACKUP_DIR/$dump_file"
|
|||||||
|
|
||||||
echo "[backup] Dump created at $BACKUP_DIR/$dump_file"
|
echo "[backup] Dump created at $BACKUP_DIR/$dump_file"
|
||||||
|
|
||||||
# Optional: Upload to Backblaze B2 via rclone
|
# Upload to Backblaze B2 via rclone
|
||||||
# Make sure you configured a remote named 'B2'
|
rclone copy "$BACKUP_DIR/$dump_file" "$B2_REMOTE"
|
||||||
rclone copy "$BACKUP_DIR/$dump_file" B2:soloforge-backups
|
|
||||||
|
|
||||||
echo "[backup] Uploaded $dump_file to Backblaze B2"
|
echo "[backup] Uploaded $dump_file to Backblaze B2"
|
||||||
|
|
||||||
|
# -----------------------
|
||||||
|
# Cleanup: keep latest N
|
||||||
|
# -----------------------
|
||||||
|
|
||||||
|
echo "[cleanup] Keeping newest $KEEP_COUNT local backups in $BACKUP_DIR"
|
||||||
|
|
||||||
|
mapfile -t localBackups < <(ls -1t "$BACKUP_DIR"/gitea-dump-*.zip 2>/dev/null || true)
|
||||||
|
|
||||||
|
if (( ${#localBackups[@]} > KEEP_COUNT )); then
|
||||||
|
for oldPath in "${localBackups[@]:KEEP_COUNT}"; do
|
||||||
|
oldFile="$(basename "$oldPath")"
|
||||||
|
|
||||||
|
# Extra safety: only delete files matching our exact dump pattern
|
||||||
|
if [[ "$oldFile" =~ ^gitea-dump-[0-9]{8}-[0-9]{6}\.zip$ ]]; then
|
||||||
|
echo "[cleanup] Deleting local: $oldPath"
|
||||||
|
rm -f -- "$oldPath"
|
||||||
|
else
|
||||||
|
echo "[cleanup] Skipping unexpected local filename (won't delete): $oldPath" >&2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "[cleanup] Local backups <= $KEEP_COUNT, nothing to delete."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[cleanup] Keeping newest $KEEP_COUNT backups in B2 ($B2_REMOTE)"
|
||||||
|
|
||||||
|
# Safety guard: require remote to be in the form "REMOTE:bucket"
|
||||||
|
# (bucket root only, no trailing slash/path)
|
||||||
|
if ! [[ "$B2_REMOTE" =~ ^[^:]+:[^/]+$ ]]; then
|
||||||
|
echo "[cleanup] Refusing remote cleanup: B2_REMOTE must be bucket-root like 'B2:SoloForge-backup' (got: $B2_REMOTE)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remote prune: list only root-level filenames, newest-first (timestamped name makes this safe).
|
||||||
|
# rclone lsf on a bucket root returns immediate entries only (non-recursive by default).
|
||||||
|
mapfile -t remoteBackups < <(
|
||||||
|
rclone lsf "$B2_REMOTE" \
|
||||||
|
--files-only \
|
||||||
|
--max-depth 1 \
|
||||||
|
--include "gitea-dump-*.zip" 2>/dev/null \
|
||||||
|
| sed 's:/$::' \
|
||||||
|
| sort -r
|
||||||
|
)
|
||||||
|
|
||||||
|
if (( ${#remoteBackups[@]} > KEEP_COUNT )); then
|
||||||
|
for old in "${remoteBackups[@]:KEEP_COUNT}"; do
|
||||||
|
# Extra safety: ensure we only ever delete matching root-level dump zips
|
||||||
|
if [[ "$old" =~ ^gitea-dump-[0-9]{8}-[0-9]{6}\.zip$ ]]; then
|
||||||
|
echo "[cleanup] Deleting remote: $old"
|
||||||
|
rclone deletefile "$B2_REMOTE/$old"
|
||||||
|
else
|
||||||
|
echo "[cleanup] Skipping unexpected remote filename (won't delete): $old" >&2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "[cleanup] Remote backups <= $KEEP_COUNT, nothing to delete."
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[backup] All done."
|
echo "[backup] All done."
|
||||||
|
Update backup sctip
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ providers:
|
|||||||
certificatesResolvers:
|
certificatesResolvers:
|
||||||
cloudflare:
|
cloudflare:
|
||||||
acme:
|
acme:
|
||||||
email: ksolomon@gmail.com
|
email: 'your-email@yourdomain.com'
|
||||||
storage: /traefik_cert/acme.json
|
storage: /traefik_cert/acme.json
|
||||||
|
|
||||||
dnsChallenge:
|
dnsChallenge:
|
||||||
|
|||||||
53
bin/build.js
53
bin/build.js
@@ -9,6 +9,7 @@ const CONTENT_DIR = path.join(ROOT, 'content');
|
|||||||
const DIST_DIR = path.join(ROOT, 'dist');
|
const DIST_DIR = path.join(ROOT, 'dist');
|
||||||
const TEMPLATE_DIR = path.join(ROOT, 'templates');
|
const TEMPLATE_DIR = path.join(ROOT, 'templates');
|
||||||
const ASSETS_DIR = path.join(ROOT, 'assets');
|
const ASSETS_DIR = path.join(ROOT, 'assets');
|
||||||
|
const SECTIONS_FILE = path.join(TEMPLATE_DIR, 'sections.json');
|
||||||
|
|
||||||
const md = new MarkdownIt({
|
const md = new MarkdownIt({
|
||||||
html: true,
|
html: true,
|
||||||
@@ -57,6 +58,25 @@ const cleanDir = (dir) => {
|
|||||||
|
|
||||||
const stripMarkdown = (text) => text.replace(/[`*_>#\-]/g, '').replace(/\s+/g, ' ').trim();
|
const stripMarkdown = (text) => text.replace(/[`*_>#\-]/g, '').replace(/\s+/g, ' ').trim();
|
||||||
|
|
||||||
|
const loadSectionMeta = () => {
|
||||||
|
if (!fs.existsSync(SECTIONS_FILE)) return [];
|
||||||
|
const raw = fs.readFileSync(SECTIONS_FILE, 'utf8');
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
if (Array.isArray(data)) return data;
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
return Object.entries(data).map(([slug, meta]) => ({
|
||||||
|
slug,
|
||||||
|
name: meta?.name || slug,
|
||||||
|
description: meta?.description || '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Invalid sections.json, skipping section descriptions.', err);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
const loadNotes = () => {
|
const loadNotes = () => {
|
||||||
const files = walkMarkdownFiles(CONTENT_DIR);
|
const files = walkMarkdownFiles(CONTENT_DIR);
|
||||||
return files.map((filePath) => {
|
return files.map((filePath) => {
|
||||||
@@ -131,19 +151,39 @@ const buildPages = () => {
|
|||||||
cleanDir(DIST_DIR);
|
cleanDir(DIST_DIR);
|
||||||
|
|
||||||
const notes = loadNotes();
|
const notes = loadNotes();
|
||||||
|
const sectionMeta = loadSectionMeta();
|
||||||
|
const sectionMetaMap = new Map(
|
||||||
|
sectionMeta.map((item) => {
|
||||||
|
const slug = slugify(item.slug || item.name || '');
|
||||||
|
return [slug, { ...item, slug }];
|
||||||
|
})
|
||||||
|
);
|
||||||
const sectionsMap = new Map();
|
const sectionsMap = new Map();
|
||||||
|
|
||||||
notes.forEach((note) => {
|
notes.forEach((note) => {
|
||||||
if (!sectionsMap.has(note.sectionSlug)) {
|
if (!sectionsMap.has(note.sectionSlug)) {
|
||||||
|
const meta = sectionMetaMap.get(note.sectionSlug);
|
||||||
sectionsMap.set(note.sectionSlug, {
|
sectionsMap.set(note.sectionSlug, {
|
||||||
name: note.section,
|
name: meta?.name || note.section,
|
||||||
slug: note.sectionSlug,
|
slug: note.sectionSlug,
|
||||||
|
description: meta?.description || '',
|
||||||
notes: [],
|
notes: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
sectionsMap.get(note.sectionSlug).notes.push(note);
|
sectionsMap.get(note.sectionSlug).notes.push(note);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
sectionMetaMap.forEach((meta) => {
|
||||||
|
if (!sectionsMap.has(meta.slug)) {
|
||||||
|
sectionsMap.set(meta.slug, {
|
||||||
|
name: meta.name || meta.slug,
|
||||||
|
slug: meta.slug,
|
||||||
|
description: meta.description || '',
|
||||||
|
notes: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const sections = Array.from(sectionsMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
const sections = Array.from(sectionsMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
||||||
sections.forEach((section) => section.notes.sort((a, b) => (a.nav - b.nav) || a.title.localeCompare(b.title)));
|
sections.forEach((section) => section.notes.sort((a, b) => (a.nav - b.nav) || a.title.localeCompare(b.title)));
|
||||||
|
|
||||||
@@ -165,17 +205,17 @@ const buildPages = () => {
|
|||||||
<a class="card" href="/${note.sectionSlug}/${note.slug}/">
|
<a class="card" href="/${note.sectionSlug}/${note.slug}/">
|
||||||
<div class="badge">${section.name}</div>
|
<div class="badge">${section.name}</div>
|
||||||
<h3>${note.title}</h3>
|
<h3>${note.title}</h3>
|
||||||
<p class="muted">${note.summary}</p>
|
|
||||||
<div class="tag-list">${note.tags.map((tag) => `<span class="tag">#${tag}</span>`).join('')}</div>
|
<div class="tag-list">${note.tags.map((tag) => `<span class="tag">#${tag}</span>`).join('')}</div>
|
||||||
</a>
|
</a>
|
||||||
`)
|
`)
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
|
const sectionDescription = section.description || `Notes grouped by ${section.name}.`;
|
||||||
const sectionContent = `
|
const sectionContent = `
|
||||||
<div class="hero">
|
<div class="hero">
|
||||||
<p class="eyebrow">Section</p>
|
<p class="eyebrow">Section</p>
|
||||||
<h1>${section.name}</h1>
|
<h1>${section.name}</h1>
|
||||||
<p class="muted">Notes grouped by ${section.name}. Use the sidebar or search to jump in.</p>
|
<p class="muted">${sectionDescription}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-grid">${sectionList}</div>
|
<div class="card-grid">${sectionList}</div>
|
||||||
`;
|
`;
|
||||||
@@ -227,8 +267,7 @@ const buildPages = () => {
|
|||||||
<a class="card" href="/${section.slug}/">
|
<a class="card" href="/${section.slug}/">
|
||||||
<div class="badge">${section.notes.length} note${section.notes.length === 1 ? '' : 's'}</div>
|
<div class="badge">${section.notes.length} note${section.notes.length === 1 ? '' : 's'}</div>
|
||||||
<h3>${section.name}</h3>
|
<h3>${section.name}</h3>
|
||||||
<p class="muted">${section.notes[0]?.summary || 'Section overview'}
|
<p class="muted">${section.description || 'Section overview'}</p>
|
||||||
</p>
|
|
||||||
</a>
|
</a>
|
||||||
`)
|
`)
|
||||||
.join('');
|
.join('');
|
||||||
@@ -236,8 +275,8 @@ const buildPages = () => {
|
|||||||
const homeContent = `
|
const homeContent = `
|
||||||
<div class="hero">
|
<div class="hero">
|
||||||
<p class="eyebrow">Workspace</p>
|
<p class="eyebrow">Workspace</p>
|
||||||
<h1>Developer Notes Hub</h1>
|
<h1>Development Notes Hub</h1>
|
||||||
<p class="muted">Markdown-first notes rendered into a fast static site. Use search or browse by section.</p>
|
<p class="muted">Notes related to various aspects of my projects and homelab.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-grid">${summaryCards}</div>
|
<div class="card-grid">${summaryCards}</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: Cloudflare Pages Workflow
|
title: Cloudflare Pages Workflow
|
||||||
section: infra
|
section: dev-notes
|
||||||
summary: Steps to publish the static notes site to Cloudflare Pages using the provided workflow and build output.
|
summary: Steps to publish the static notes site to Cloudflare Pages using the provided workflow and build output
|
||||||
tags: [cloudflare, ci, deploy]
|
tags: [cloudflare, ci, deploy]
|
||||||
nav: 1
|
nav: 3
|
||||||
---
|
---
|
||||||
|
|
||||||
# Cloudflare Pages Workflow
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This note captures the build and deploy flow for the static notes site. The site compiles Markdown into static HTML under the `dist/` directory.
|
This note captures the build and deploy flow for the static notes site. The site compiles Markdown into static HTML under the `dist/` directory.
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
---
|
---
|
||||||
title: Markdown Authoring Guide
|
title: Markdown Authoring Guide
|
||||||
section: docs
|
section: dev-notes
|
||||||
summary: Conventions for writing notes, front matter fields, and embedding code or media in the site.
|
summary: Conventions for writing notes, front matter fields, and embedding code or media in the site
|
||||||
tags: [markdown, style, notes]
|
tags: [markdown, style, notes]
|
||||||
nav: 1
|
nav: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
# Markdown Authoring Guide
|
|
||||||
|
|
||||||
## Front matter
|
## Front matter
|
||||||
|
|
||||||
Provide metadata at the top of every note:
|
Provide metadata at the top of every note:
|
||||||
|
|
||||||
```
|
```markdown
|
||||||
---
|
---
|
||||||
title: Example Title
|
title: Example Title
|
||||||
section: docs
|
section: docs
|
||||||
@@ -25,13 +24,15 @@ nav: 1
|
|||||||
- `nav`: optional integer to influence ordering within a section; lower numbers show first.
|
- `nav`: optional integer to influence ordering within a section; lower numbers show first.
|
||||||
|
|
||||||
## Writing tips
|
## Writing tips
|
||||||
- Start with an `#` heading matching the title.
|
|
||||||
- Keep paragraphs short; use bullet lists for tasks.
|
- Keep paragraphs short; use bullet lists for tasks.
|
||||||
- Use fenced code blocks with language hints (` ```bash `, ` ```js `) for highlighting.
|
- Use fenced code blocks with language hints (` ```bash `, ` ```js `) for highlighting.
|
||||||
- Link to related notes with absolute paths, e.g., `/docs/markdown-authoring-guide/`.
|
- Link to related notes with absolute paths, e.g., `/docs/markdown-authoring-guide/`.
|
||||||
|
|
||||||
## Media
|
## Media
|
||||||
|
|
||||||
Place images next to the note or in an `/assets/media` folder and reference relatively.
|
Place images next to the note or in an `/assets/media` folder and reference relatively.
|
||||||
|
|
||||||
## Testing locally
|
## Testing locally
|
||||||
|
|
||||||
Run `npm run build` to regenerate HTML. Open `dist/index.html` in a browser to review layout and syntax highlighting.
|
Run `npm run build` to regenerate HTML. Open `dist/index.html` in a browser to review layout and syntax highlighting.
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
---
|
---
|
||||||
title: Developer Onboarding
|
title: Developer Onboarding
|
||||||
section: ops
|
section: dev-notes
|
||||||
summary: Quick start steps to clone, install dependencies, and generate the site locally.
|
summary: Quick start steps to clone, install dependencies, and generate the site locally
|
||||||
tags: [onboarding, setup]
|
tags: [onboarding, setup]
|
||||||
nav: 1
|
nav: 1
|
||||||
---
|
---
|
||||||
|
|
||||||
# Developer Onboarding
|
## Getting started
|
||||||
|
|
||||||
1. Clone the repository and install Node 20+.
|
1. Clone the repository and install Node 20+.
|
||||||
2. Run `npm install` to pull dependencies.
|
2. Run `npm install` to pull dependencies.
|
||||||
@@ -14,11 +14,14 @@ nav: 1
|
|||||||
4. Serve `dist/` via any static server (e.g., `npx serve dist`).
|
4. Serve `dist/` via any static server (e.g., `npx serve dist`).
|
||||||
|
|
||||||
## Directory overview
|
## Directory overview
|
||||||
|
|
||||||
- `content/`: Markdown notes with front matter.
|
- `content/`: Markdown notes with front matter.
|
||||||
- `templates/`: HTML shells for header, footer, and layout.
|
- `templates/`: HTML shells for header, footer, and layout.
|
||||||
- `assets/`: CSS and JavaScript shared across pages.
|
- `assets/`: CSS and JavaScript shared across pages.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
- Use kebab-case filenames.
|
- Use kebab-case filenames.
|
||||||
|
- For organization, use sections defined in `sections.json` and include section in filename.
|
||||||
- Keep summaries short; they populate listings and the search index.
|
- Keep summaries short; they populate listings and the search index.
|
||||||
- Add tags to improve search results.
|
- Add tags to improve search results.
|
||||||
309
content/docs-gateway-stack.md
Normal file
309
content/docs-gateway-stack.md
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
---
|
||||||
|
title: Gateway Documentation
|
||||||
|
section: docs
|
||||||
|
summary: Homelab Gateway — Traefik Reverse Proxy, Authelia SSO, LAN Routing, macvlan IP assignment, and Docker-managed services
|
||||||
|
tags: [networking, traefik, authelia, infrastructure, docker, home-lab]
|
||||||
|
nav: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
## Homelab Gateway Stack (Traefik + Authelia)
|
||||||
|
|
||||||
|
This document describes the architecture and configuration of the homelab gateway stack — the system responsible for HTTPS termination, reverse proxying, SSO-protected internal services, LAN-to-LAN routing, and stable external exposure.
|
||||||
|
|
||||||
|
This documentation intentionally mirrors the SoloForge Gitea documentation format for consistency across the infrastructure stack.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
The Gateway Stack provides:
|
||||||
|
|
||||||
|
- **Reverse proxying via Traefik v3**
|
||||||
|
- **Authentication & SSO via Authelia v4**
|
||||||
|
- **Per-service routing with Docker labels or file-provider YAML**
|
||||||
|
- **macvlan-based static IP assignment (Traefik appears as its own LAN host)**
|
||||||
|
- **Secure exposure of internal services to the outside world**
|
||||||
|
- **ForwardAuth protection for otherwise unauthenticated apps**
|
||||||
|
- **Ability to proxy both Docker-based and remote LAN-based services**
|
||||||
|
|
||||||
|
This system replaces the previous Traefik + YAML editor stack and centralizes the “front door” of the homelab under a configuration that is fully self-documented, reproducible, and Git-tracked.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. System Layout
|
||||||
|
|
||||||
|
### 2.1 Host Machine
|
||||||
|
|
||||||
|
- Bare-metal machine (temporarily — will migrate back into Proxmox)
|
||||||
|
- Debian/Ubuntu-based environment
|
||||||
|
- Docker + Docker Compose v2+
|
||||||
|
- macvlan network configured so Traefik has a **dedicated LAN IP** (`192.168.2.253`)
|
||||||
|
|
||||||
|
### 2.2 Static Network Assignment (macvlan)
|
||||||
|
|
||||||
|
Traefik receives its own IP on the LAN:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network create -d macvlan \
|
||||||
|
--subnet=192.168.2.0/24 \
|
||||||
|
--gateway=192.168.2.1 \
|
||||||
|
-o parent=eth0 \
|
||||||
|
traefik_macvlan
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
|
||||||
|
- Makes Traefik behave like a true router on the network
|
||||||
|
- Allows the router to port-forward directly to Traefik instead of the host
|
||||||
|
- Eliminates host→Traefik conflicts
|
||||||
|
- Clean separation once this stack eventually lives in a VM or LXC again
|
||||||
|
|
||||||
|
### 2.3 Directory Structure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/gateway/
|
||||||
|
├── traefik/
|
||||||
|
│ ├── traefik.yml # Static config
|
||||||
|
│ ├── config/ # File provider configs (GUI writes here)
|
||||||
|
│ ├── cert/ # ACME cert store
|
||||||
|
│ └── logs/ # Traefik logs
|
||||||
|
├── authelia/
|
||||||
|
│ ├── config/ # Authelia configuration.yml + users file
|
||||||
|
│ ├── secrets/ # Env-based secrets, if used
|
||||||
|
│ └── logs/ # Authelia logs
|
||||||
|
├── traefik-gui/
|
||||||
|
│ └── db/ # GUI internal sqlite db
|
||||||
|
└── traefik-stack.yml # Main stack
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Traefik Deployment
|
||||||
|
|
||||||
|
### 3.1 Traefik Service
|
||||||
|
|
||||||
|
Launched via Compose with two network attachments:
|
||||||
|
|
||||||
|
- `traefik_macvlan` → gives Traefik its LAN IP
|
||||||
|
- `proxy` → internal Docker network for app connectivity
|
||||||
|
|
||||||
|
Traefik is responsible for:
|
||||||
|
|
||||||
|
- HTTPS certificates via Let’s Encrypt (DNS-01 with Cloudflare)
|
||||||
|
- Routing per-service via hostnames
|
||||||
|
- Applying Authelia ForwardAuth checks
|
||||||
|
- Proxying internal Docker services and remote LAN machines
|
||||||
|
|
||||||
|
### 3.2 File Provider
|
||||||
|
|
||||||
|
The file provider watches:
|
||||||
|
|
||||||
|
`/traefik/config/`
|
||||||
|
|
||||||
|
Traefik GUI writes dynamic route definitions into this folder.
|
||||||
|
|
||||||
|
### 3.3 Docker Provider
|
||||||
|
|
||||||
|
Configured with:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
providers:
|
||||||
|
docker:
|
||||||
|
endpoint: "unix:///var/run/docker.sock"
|
||||||
|
exposedByDefault: false
|
||||||
|
network: proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
Allows Traefik to auto-discover containers with labels on the `proxy` network.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Authelia (SSO / Authentication)
|
||||||
|
|
||||||
|
### 4.1 Purpose
|
||||||
|
|
||||||
|
Authelia handles:
|
||||||
|
|
||||||
|
- Single-sign-on for internal apps
|
||||||
|
- Multi-factor auth (optional)
|
||||||
|
- Protecting services with ForwardAuth (even those without native authentication)
|
||||||
|
- Login portal at `auth.keithsolomon.net`
|
||||||
|
|
||||||
|
### 4.2 Configuration Notes
|
||||||
|
|
||||||
|
- Config lives at `/authelia/config/configuration.yml`
|
||||||
|
- Secrets (session, storage encryption, reset-password JWT) supplied via environment variables
|
||||||
|
- SMTP notifier is required — missing credentials cause Authelia to crash-loop
|
||||||
|
- Logging set to stdout during debugging, file logging available once stable
|
||||||
|
|
||||||
|
### 4.3 ForwardAuth Middleware
|
||||||
|
|
||||||
|
Exposed to Traefik via labels:
|
||||||
|
|
||||||
|
`traefik.http.middlewares.authelia.forwardAuth.address=http://authelia:9091/api/authz/forward-auth`
|
||||||
|
|
||||||
|
Any protected service adds:
|
||||||
|
|
||||||
|
`traefik.http.routers.<service>.middlewares=authelia@docker`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. LAN Services & Routing
|
||||||
|
|
||||||
|
### 5.1 Dockerized Services (local to gateway)
|
||||||
|
|
||||||
|
Containers running on the same host as Traefik should:
|
||||||
|
|
||||||
|
1. Join the `proxy` network
|
||||||
|
2. Use Traefik Docker labels
|
||||||
|
3. NOT be accessed via the host IP (192.168.2.9) when Traefik is on macvlan
|
||||||
|
- macvlan cannot reliably reach the host’s bridge interfaces
|
||||||
|
|
||||||
|
Example (Sonarr):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=proxy"
|
||||||
|
- "traefik.http.routers.sonarr.rule=Host(`sonarr.keithsolomon.net`)"
|
||||||
|
- "traefik.http.services.sonarr.loadbalancer.server.port=8989"
|
||||||
|
```
|
||||||
|
|
||||||
|
Traefik will connect to `http://sonarr:8989` over the internal Docker network.
|
||||||
|
|
||||||
|
### 5.2 Remote LAN Services (other machines)
|
||||||
|
|
||||||
|
These should be added via the Traefik GUI (file provider), using the machine’s actual LAN IP:
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
docs:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://192.168.2.51:8083"
|
||||||
|
```
|
||||||
|
|
||||||
|
These are not affected by the macvlan limitation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Gateway Compose Stack
|
||||||
|
|
||||||
|
**Services included:**
|
||||||
|
|
||||||
|
- traefik
|
||||||
|
- authelia
|
||||||
|
- traefik-gui
|
||||||
|
- eventually all internal homelab UIs
|
||||||
|
|
||||||
|
**Networks:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
networks:
|
||||||
|
traefik_macvlan:
|
||||||
|
external: true
|
||||||
|
proxy:
|
||||||
|
name: proxy
|
||||||
|
driver: bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important: .env File**
|
||||||
|
|
||||||
|
Stores secrets such as Authelia session keys and SMTP passwords. Generate strong random hex values for the secrets using `openssl rand -hex 64`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AUTHELIA_SESSION_SECRET=<hex>
|
||||||
|
AUTHELIA_STORAGE_ENCRYPTION_KEY=<hex>
|
||||||
|
AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET=<hex>
|
||||||
|
AUTHELIA_NOTIFIER_SMTP_PASSWORD=<app-password>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Operational Notes
|
||||||
|
|
||||||
|
### 7.1 Authelia Startup Failures
|
||||||
|
|
||||||
|
Authelia will crash-loop if:
|
||||||
|
|
||||||
|
- SMTP notifier missing password
|
||||||
|
- storage encryption key missing
|
||||||
|
- identity_validation.reset_password.jwt_secret missing
|
||||||
|
- configuration.yml malformed
|
||||||
|
|
||||||
|
Always check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs Authelia
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 macvlan Gotcha (Critical)
|
||||||
|
|
||||||
|
Traefik on macvlan **cannot reach the host’s own IP (192.168.2.9)**.
|
||||||
|
Use container names on the `proxy` network for same-host services.
|
||||||
|
|
||||||
|
### 7.3 Debugging Backend Connectivity
|
||||||
|
|
||||||
|
To simulate Traefik’s point of view:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -it --network=proxy alpine sh
|
||||||
|
apk add curl
|
||||||
|
curl -v http://sonarr:8989
|
||||||
|
curl -v http://192.168.2.51:8083
|
||||||
|
```
|
||||||
|
|
||||||
|
If container → hostIP fails, but container → container works, it’s macvlan isolation.
|
||||||
|
|
||||||
|
### 7.4 Traefik Dashboard
|
||||||
|
|
||||||
|
Available at `https://tfk.keithsolomon.net`
|
||||||
|
|
||||||
|
Protected by Authelia.
|
||||||
|
|
||||||
|
Check:
|
||||||
|
|
||||||
|
- Routers → status, errors, middlewares
|
||||||
|
- Services → backend URLs
|
||||||
|
- Middlewares → ensure authelia@docker exists
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Future Improvements
|
||||||
|
|
||||||
|
- Migrate gateway stack into Proxmox VM/LXC
|
||||||
|
- Replace Traefik-GUI with a cleaner UI (or maintain YAML by hand)
|
||||||
|
- Add Prometheus metrics for request/latency monitoring
|
||||||
|
- Add fail2ban or rate-limiting middleware
|
||||||
|
- Add OIDC provider configuration to Authelia for full single sign-on
|
||||||
|
- Automate propagation of routes from remote hosts (pull or push model)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TL;DR Cheat Sheet
|
||||||
|
|
||||||
|
**Traefik not routing?**
|
||||||
|
|
||||||
|
- → Check router → check service → check backend URL
|
||||||
|
- → If backend is on the same machine: use container name, not host IP
|
||||||
|
|
||||||
|
**Authelia crash-loop?**
|
||||||
|
|
||||||
|
- → Missing SMTP password
|
||||||
|
- → Missing storage/session/jwt secrets
|
||||||
|
- → Look inside /authelia/logs/authelia.log
|
||||||
|
|
||||||
|
**Adding a remote LAN service?**
|
||||||
|
|
||||||
|
- → Use Traefik GUI
|
||||||
|
- → Backend = `http://192.168.2.X:PORT`
|
||||||
|
- → Protect via Authelia middleware as needed
|
||||||
|
|
||||||
|
**Adding a local Docker service?**
|
||||||
|
|
||||||
|
- → Put it on the `proxy` network
|
||||||
|
- → Add Traefik labels
|
||||||
|
- → Use `loadbalancer.server.port`, not host IP
|
||||||
|
- → Do NOT use `192.168.2.9` when Traefik is on macvlan
|
||||||
@@ -3,7 +3,7 @@ title: SoloForge Documentation
|
|||||||
section: docs
|
section: docs
|
||||||
summary: SoloForge Infrastructure Documentation for Self-Hosted Gitea with Actions Runner on Hetzner
|
summary: SoloForge Infrastructure Documentation for Self-Hosted Gitea with Actions Runner on Hetzner
|
||||||
tags: [servers, infrasctructure, gitea, ci/cd, hetzner, docker]
|
tags: [servers, infrasctructure, gitea, ci/cd, hetzner, docker]
|
||||||
nav: 1
|
nav: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
## Self-Hosted Gitea + Actions Runner (Hetzner Deployment)
|
## Self-Hosted Gitea + Actions Runner (Hetzner Deployment)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: My Servers
|
title: My Servers
|
||||||
section: network
|
section: infra
|
||||||
summary: My Servers.
|
summary: Documentation relating to the various servers I use and maintain
|
||||||
tags: [hardware, servers, infrasctructure]
|
tags: [hardware, servers, infrasctructure]
|
||||||
nav: 1
|
nav: 1
|
||||||
---
|
---
|
||||||
28
content/other-repos-of-note.md
Normal file
28
content/other-repos-of-note.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
title: Repos of Note
|
||||||
|
section: other
|
||||||
|
summary: My personal collection of repositories, both mine and by others, that I find useful
|
||||||
|
tags: [repos, projects, tools]
|
||||||
|
nav: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
## My Repositories
|
||||||
|
|
||||||
|
### Public Repositories
|
||||||
|
|
||||||
|
- **[Dev Notes](https://git.keithsolomon.net/keith/dev-notes)** (This repo)
|
||||||
|
- Lightweight static notes site built from Markdown with front matter.
|
||||||
|
- **[Dotfiles](https://github.com/ksolomon/dotfiles)**
|
||||||
|
- My personal configuration files for various tools and environments.
|
||||||
|
- **[Random Scripts](https://github.com/ksolomon/Random-Scripts)**
|
||||||
|
- A collection of miscellaneous scripts I've written for various tasks and platforms.
|
||||||
|
- **[TODO-Sync](https://github.com/Solo-Web-Works/TODO-Sync)**
|
||||||
|
- Sync TODO comments in code to GitHub Issues.
|
||||||
|
|
||||||
|
### Private Repositories
|
||||||
|
|
||||||
|
- **[Project Notes](https://github.com/ksolomon/notes)**
|
||||||
|
- Notes and documentation for various projects I've worked on.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
18
templates/sections.json
Normal file
18
templates/sections.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Dev Notes",
|
||||||
|
"description": "Documentation relating to the Dev Notes project."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Docs",
|
||||||
|
"description": "Documentation relating to various aspects of my projects."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Infra",
|
||||||
|
"description": "Network and server documentation."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Other",
|
||||||
|
"description": "Miscellaneous stuff that doesn't fit in other categories."
|
||||||
|
}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user