update README for current stack state
All checks were successful
ci/woodpecker/push/infra-ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful

Reflects domain change (homelab.fhirworx.io), network segmentation,
centralized CSS theming, Go template routing, and favicon injection.
This commit is contained in:
kert
2026-02-28 19:34:00 -05:00
parent d269695ad1
commit 89f8dec187

495
README.md
View File

@@ -1,88 +1,117 @@
# Homelab Stack
Self-hosted infrastructure stack with GPU support, S3-compatible storage, Git server with container registry, CI/CD, data lakehouse, observability, and research tools.
Self-hosted infrastructure stack with GPU support, S3-compatible storage, Git server with container registry, CI/CD, data lakehouse, observability, and research tools. All services share a unified retro "throwback" theme injected via Traefik middleware.
## Services
All services are accessible via Traefik reverse proxy on port 80.
All services are accessible via Traefik reverse proxy at `*.homelab.fhirworx.io`.
| Service | URL | Description |
|---------|-----|-------------|
| Dashboard | http://home.fhirworx.io | Homelab command center |
| Traefik | http://traefik.home.fhirworx.io | Reverse proxy dashboard |
| Gitea | http://gitea.home.fhirworx.io | Git server with LFS + container registry |
| Woodpecker CI | http://ci.home.fhirworx.io | CI/CD pipelines |
| RustFS Console | http://minio.home.fhirworx.io | S3 storage management UI |
| RustFS S3 API | http://s3.home.fhirworx.io | S3-compatible object storage |
| Notebooks | http://notebooks.home.fhirworx.io | GPU-accelerated Marimo notebooks |
| Zotero | http://zotero.home.fhirworx.io | Reference manager with VNC |
| Nessie | http://nessie.home.fhirworx.io | Git-like data catalog for Iceberg |
| Polaris | http://polaris.home.fhirworx.io | Apache Iceberg catalog with governance |
| Trino | http://trino.home.fhirworx.io | Distributed SQL query engine |
| Grafana | http://grafana.home.fhirworx.io | Observability dashboards |
| Prometheus | http://prometheus.home.fhirworx.io | Metrics collection |
| Jaeger | http://jaeger.home.fhirworx.io | Distributed tracing |
| Loki | http://loki.home.fhirworx.io | Log aggregation |
| PostgreSQL | localhost:5432 | Database (Bitnami hardened) |
| Dashboard | `homelab.fhirworx.io` | Homelab command center |
| Traefik | `traefik.homelab.fhirworx.io` | Reverse proxy dashboard |
| Gitea | `gitea.homelab.fhirworx.io` | Git server with LFS + container registry |
| Woodpecker CI | `ci.homelab.fhirworx.io` | CI/CD pipelines |
| RustFS Console | `s3console.homelab.fhirworx.io` | S3 storage management UI |
| RustFS S3 API | `s3.homelab.fhirworx.io` | S3-compatible object storage |
| Notebooks | `notebooks.homelab.fhirworx.io` | GPU-accelerated Marimo notebooks |
| Zotero | `zotero.homelab.fhirworx.io` | Reference manager (KasmVNC) |
| Nessie | `nessie.homelab.fhirworx.io` | Git-like data catalog for Iceberg |
| Polaris | `polaris.homelab.fhirworx.io` | Apache Iceberg catalog with governance |
| Trino | `trino.homelab.fhirworx.io` | Distributed SQL query engine |
| Grafana | `grafana.homelab.fhirworx.io` | Observability dashboards |
| Prometheus | `prometheus.homelab.fhirworx.io` | Metrics collection |
| Jaeger | `jaeger.homelab.fhirworx.io` | Distributed tracing |
| Loki | `loki.homelab.fhirworx.io` | Log aggregation |
## Architecture
### Network Segmentation
The stack uses five isolated Docker networks. Services only join the networks they need.
| Network | Type | Purpose |
|---------|------|---------|
| `gateway` | external | Traefik + public-facing services |
| `storage` | internal | PostgreSQL + RustFS (S3) |
| `data` | internal | Lakehouse (Nessie, Trino, Polaris) |
| `observability` | internal | Grafana, Prometheus, Jaeger, Loki |
| `ci` | external | Woodpecker + Gitea + RustFS |
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ intrastack network │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Traefik :80/:443 │ │
│ │ Reverse Proxy + Load Balancer + OTEL Tracing │ │
│ └───────────────────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────┼─────────────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Gitea │ │Woodpecker│ │ Notebooks│ │ Zotero │ │ Dashboard│ │
│ │ :3000 │ │ :8000 │ │ :2718 │ │ :8080 │ │ :80 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
│ └─────────────┼─────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL :5432 │ │
│ │ (gitea, woodpecker, nessie) │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ Data Lakehouse │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────────────────┐ │ │
│ │ │ Trino │────▶│ Nessie │────▶│ RustFS │ │ │
│ │ │ :8080 │ │ :19120 │ │ S3 :9000 | Console :9001 │ │ │
│ │ └──────────┘ └──────────┘ │ Buckets: gitea, lakehouse │ │ │
│ │ │ └──────────────────────────────┘ │ │
│ │ │ ┌──────────┐ ▲ │ │
│ │ └──────────▶│ Polaris │──────────────┘ │ │
│ │ │ :8181 │ Governance + RBAC │ │
│ │ └──────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ Observability Stack │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Grafana │────▶│Prometheus│ │ Jaeger │◀────│ Loki │ │ │
│ │ │ :3000 │ │ :9090 │ │ :16686 │ │ :3100 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ ▲ ▲ │ │
│ │ └──────── Dashboards ───────────────┴───────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────┐
│ Traefik :80/:443 │
│ Reverse Proxy + Tracing │
└────────────┬─────────────┘
gateway network │
┌───────────┬──────────┬────────┼────────┬───────────┐
▼ ▼ ▼ ▼ ▼ ▼
┌────────┐ ┌─────────┐ ┌────────┐ ┌──────┐ ┌──────┐ ┌────────┐
│ Gitea │ │Woodpeck.│ │Notebook│ │Zotero│ │Dash- │ │Grafana │
│ :3000 │ │ :8000 │ │ :2718 │ │:8080 │ │board │ │ :3000 │
└───┬────┘ └────┬────┘ └────────┘ └──────┘ └──────┘ └───┬────┘
│ │ │
│ ci network observability network
│ │ ┌───────┬───────┬───┘
│ │ ▼ ▼ ▼
│ │ ┌──────┐ ┌───────┐ ┌──────┐
│ │ │Jaeger│ │Promet.│ │ Loki │
│ │ │:16686│ │ :9090 │ │:3100 │
│ │ └──────┘ └───────┘ └──────┘
│ │
└─────┬─────┘ data network
│ ┌────────┬──────────┐
storage network ▼ ▼ ▼
┌─────┼────┐ ┌──────┐ ┌──────┐ ┌───────┐
▼ ▼ │ │Nessie│ │Trino │ │Polaris│
┌────────┐ ┌────┴──┐ │:19120│ │:8080 │ │ :8181 │
│Postgres│ │RustFS │ └──────┘ └──────┘ └───────┘
│ :5432 │ │:9000/1│
└────────┘ └───────┘
```
### Traefik Routing
Routes are defined in `traefik/dynamic/services.yml` using Go templates with Sprig functions. The `$reef` dict is the single source of truth — each entry generates a router, service, and middleware chain:
```
"gitea" (dict "port" "3000" "theme" true "mw" "secure-headers")
```
- **`theme: true`** applies the `inject-throwback` middleware (injects favicon + CSS via HTML rewrite)
- **`theme: false`** skips injection (API-only services like Nessie, Polaris, Loki)
- **`mw`** is a comma-separated list of additional middlewares (`secure-headers`, `local-only`, `infra-headers`)
- **`subdomain`** overrides the FQDN prefix (e.g. `woodpecker-server` → `ci.homelab.fhirworx.io`)
To add a new service, add one line to the `$reef` dict and a service block to `compose.yml`.
### Throwback Theme
All HTML-serving services get a unified retro theme via the `inject-throwback` Traefik middleware, which uses the [rewrite-body](https://github.com/packruler/rewrite-body) plugin to inject `<link>` tags before `</head>`:
- **`favicon.svg`** — Pixel-art "H" favicon served from the dashboard container
- **`throwback.css`** — Global CSS overrides (colors, fonts, borders)
- **`inject.css`** — Served as `throwback.css`, hides service-specific logos and applies the palette
Per-service CSS files handle service-specific overrides:
| File | Purpose |
|------|---------|
| `css/inject.css` | Global throwback palette + logo hiding (served as `throwback.css`) |
| `css/dashboard.css` | Dashboard tile grid layout |
| `css/gitea.css` | Gitea theme (registered as `throwback` in Gitea UI settings) |
| `css/woodpecker.css` | Woodpecker custom CSS (via `WOODPECKER_CUSTOM_CSS_FILE`) |
| `css/marimo.css` | Marimo notebook theme |
| `css/favicon.svg` | Canonical SVG favicon |
| `css/logo.svg` | Navbar logo for Gitea |
Grafana favicons are replaced via volume mounts over the default Grafana icons.
## Prerequisites
- Docker with rootless mode
- NVIDIA GPU with container toolkit
- iptables for port 80 redirect (rootless Docker can't bind privileged ports)
- Domain pointing to your server (or `/etc/hosts` entries)
Fix for rootless NVIDIA containers:
```bash
@@ -93,36 +122,23 @@ sudo sed -i 's/#no-cgroups = false/no-cgroups = true/' /etc/nvidia-container-run
Add to `/etc/hosts` (replace IP with your server's LAN IP):
```bash
# Homelab Stack
192.168.1.192 home.fhirworx.io
192.168.1.192 dashboard.home.fhirworx.io
192.168.1.192 traefik.home.fhirworx.io
192.168.1.192 gitea.home.fhirworx.io
192.168.1.192 ci.home.fhirworx.io
192.168.1.192 notebooks.home.fhirworx.io
192.168.1.192 zotero.home.fhirworx.io
192.168.1.192 nessie.home.fhirworx.io
192.168.1.192 trino.home.fhirworx.io
192.168.1.192 minio.home.fhirworx.io
192.168.1.192 s3.home.fhirworx.io
192.168.1.192 grafana.home.fhirworx.io
192.168.1.192 prometheus.home.fhirworx.io
192.168.1.192 jaeger.home.fhirworx.io
192.168.1.192 loki.home.fhirworx.io
192.168.1.192 polaris.home.fhirworx.io
```
## Port 80 Redirect
Rootless Docker cannot bind to privileged ports. Use iptables to redirect port 80 to Traefik's port 8880:
```bash
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8880
sudo iptables -t nat -A OUTPUT -p tcp --dport 80 -o lo -j REDIRECT --to-port 8880
# Persist across reboots
sudo sh -c 'iptables-save > /etc/iptables/rules.v4'
192.168.1.192 homelab.fhirworx.io
192.168.1.192 dashboard.homelab.fhirworx.io
192.168.1.192 traefik.homelab.fhirworx.io
192.168.1.192 gitea.homelab.fhirworx.io
192.168.1.192 ci.homelab.fhirworx.io
192.168.1.192 notebooks.homelab.fhirworx.io
192.168.1.192 zotero.homelab.fhirworx.io
192.168.1.192 nessie.homelab.fhirworx.io
192.168.1.192 trino.homelab.fhirworx.io
192.168.1.192 s3console.homelab.fhirworx.io
192.168.1.192 s3.homelab.fhirworx.io
192.168.1.192 grafana.homelab.fhirworx.io
192.168.1.192 prometheus.homelab.fhirworx.io
192.168.1.192 jaeger.homelab.fhirworx.io
192.168.1.192 loki.homelab.fhirworx.io
192.168.1.192 polaris.homelab.fhirworx.io
```
## Setup
@@ -132,6 +148,9 @@ sudo sh -c 'iptables-save > /etc/iptables/rules.v4'
Create `.env` file:
```bash
DOMAIN=homelab.fhirworx.io
HOST_IP=192.168.1.192
POSTGRES_PASSWORD=<your_password>
RUSTFS_ACCESS_KEY=<your_access_key>
RUSTFS_SECRET_KEY=<your_secret_key>
@@ -140,9 +159,11 @@ GITEA_S3_SECRET_KEY=<gitea_s3_password>
GITEA_DB_PASSWORD=<gitea_db_password>
GITEA_TOKEN=<gitea_api_token>
WOODPECKER_DB_PASSWORD=<woodpecker_db_password>
WOODPECKER_ADMIN=<admin_username>
WOODPECKER_AGENT_SECRET=<generated_secret>
WOODPECKER_GITEA_CLIENT=<oauth_client_id>
WOODPECKER_GITEA_SECRET=<oauth_client_secret>
GF_ADMIN_PASSWORD=<grafana_password>
# Nessie Data Lake
NESSIE_DB_PASSWORD=<nessie_db_password>
@@ -169,10 +190,10 @@ docker compose up -d postgres rustfs traefik
### 3. Configure RustFS
1. Access console at http://minio.home.fhirworx.io
2. Create user `git` with S3 credentials matching `GITEA_S3_ACCESS_KEY` and `GITEA_S3_SECRET_KEY`
1. Access console at `s3console.homelab.fhirworx.io`
2. Create user `git` with S3 credentials matching `.env`
3. Create buckets: `gitea`, `gitea-lfs`, `gitea-packages`
4. Create user `nessie` with S3 credentials matching `NESSIE_S3_ACCESS_KEY` and `NESSIE_S3_SECRET_KEY`
4. Create user `nessie` with S3 credentials matching `.env`
5. Create bucket: `lakehouse`
6. Apply IAM policies to users (see below)
@@ -260,27 +281,10 @@ docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "CREA
# Nessie database
docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "CREATE USER nessie WITH PASSWORD '<nessie_db_password>';"
docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "CREATE DATABASE nessie OWNER nessie;"
docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE nessie TO nessie;"
# Polaris database
docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "CREATE USER polaris WITH PASSWORD '<polaris_db_password>';"
docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "CREATE DATABASE polaris OWNER polaris;"
docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE polaris TO polaris;"
```
### Bootstrap Polaris
After starting Polaris, bootstrap the realm:
```bash
docker run --rm --network stack_intrastack \
-e POLARIS_PERSISTENCE_TYPE=relational-jdbc \
-e QUARKUS_DATASOURCE_DB_KIND=postgresql \
-e QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgres:5432/polaris \
-e QUARKUS_DATASOURCE_USERNAME=polaris \
-e QUARKUS_DATASOURCE_PASSWORD=<polaris_db_password> \
apache/polaris-admin-tool:latest \
bootstrap -r POLARIS -c POLARIS,root,<polaris_root_secret> -p
```
### 5. Start All Services
@@ -291,20 +295,20 @@ docker compose up -d
### 6. Configure Gitea
1. Complete initial setup at http://gitea.home.fhirworx.io
1. Complete initial setup at `gitea.homelab.fhirworx.io`
2. Create organization `homelab`
3. Create repository `stack`
4. Generate API token for container registry access
5. Create OAuth2 application for Woodpecker:
- Name: `Woodpecker CI`
- Redirect URI: `http://ci.home.fhirworx.io/authorize`
- Redirect URI: `http://ci.homelab.fhirworx.io/authorize`
- Copy Client ID and Secret to `.env`
### 7. Configure Woodpecker Secrets
Add secrets in Woodpecker UI (http://ci.home.fhirworx.io):
- `registry_user` - Gitea username
- `registry_pass` - Gitea token
Add secrets in Woodpecker UI (`ci.homelab.fhirworx.io`):
- `registry_user` — Gitea username
- `registry_pass` — Gitea token
### 8. Verify Services
@@ -313,35 +317,29 @@ Add secrets in Woodpecker UI (http://ci.home.fhirworx.io):
curl -s http://localhost:8081/api/http/routers | jq -r '.[].name'
# Check Nessie
curl http://nessie.home.fhirworx.io/api/v2/config
curl http://nessie.homelab.fhirworx.io/api/v2/config
# Check Trino
docker exec -it trino trino --execute "SHOW CATALOGS"
# Check Prometheus targets
curl -s http://prometheus.home.fhirworx.io/api/v1/targets | jq '.data.activeTargets[].labels.job'
curl -s http://prometheus.homelab.fhirworx.io/api/v1/targets | jq '.data.activeTargets[].labels.job'
```
## Observability
The stack includes a complete observability solution:
| Component | Purpose |
|-----------|---------|
| **Prometheus** | Metrics collection and storage |
| **Grafana** | Visualization and dashboards |
| **Jaeger** | Distributed tracing (OTLP collector) |
| **Loki** | Log aggregation |
| **Promtail** | Log shipper (Docker container logs) |
### Components
Services instrumented with OpenTelemetry tracing:
| Component | Purpose | Metrics |
|-----------|---------|---------|
| **Prometheus** | Metrics collection and storage | Scrapes all services |
| **Grafana** | Visualization and dashboards | Homelab Overview, Traefik |
| **Jaeger** | Distributed tracing | OTLP collector |
| **Loki** | Log aggregation | Container logs via Promtail |
| **Promtail** | Log shipper | Scrapes Docker container logs |
### Tracing
Services instrumented with OpenTelemetry:
| Service | Tracing |
|---------|---------|
| Service | Method |
|---------|--------|
| Traefik | Native OTLP export |
| Nessie | Quarkus OTEL |
| Polaris | Quarkus OTEL |
@@ -349,12 +347,6 @@ Services instrumented with OpenTelemetry:
All traces flow to Jaeger via OTLP gRPC (port 4317).
### Dashboards
Pre-provisioned Grafana dashboards:
- **Homelab Overview** - Service health, logs, traces
- **Traefik** - Request rates, latencies, errors by router/service
### Prometheus Targets
| Job | Endpoint |
@@ -369,81 +361,40 @@ Pre-provisioned Grafana dashboards:
## Data Lakehouse
The stack includes a complete data lakehouse built on Apache Iceberg with two catalog options:
### Components
| Component | Purpose |
|-----------|---------|
| **Nessie** | Git-like version control for data (branches, merges, time travel) |
| **Polaris** | Apache Iceberg catalog with governance, RBAC, and multi-tenant support |
| **Apache Iceberg** | Open table format with ACID transactions, schema evolution |
| **Trino** | Distributed SQL query engine with JDBC/ODBC support |
| **RustFS** | S3 storage backend (lakehouse, polaris buckets) |
| **PyIceberg** | Python client for Iceberg tables in notebooks |
| **RustFS** | S3 storage backend (lakehouse bucket) |
### Nessie vs Polaris
| Feature | Nessie | Polaris |
|---------|--------|---------|
| Git-like branching | ✓ | ✗ |
| Time travel | ✓ | ✓ |
| Git-like branching | Yes | No |
| Time travel | Yes | Yes |
| RBAC/governance | Basic | Full |
| Multi-catalog management | ✗ | ✓ |
| Iceberg REST API | ✓ | ✓ |
| Unity Catalog-like | ✗ | ✓ |
**Use Nessie** for data versioning, experimentation branches, and development workflows.
**Use Polaris** for production governance, access control, and multi-tenant catalog management.
### Polaris API
```bash
# Get access token
TOKEN=$(curl -s -X POST http://polaris.home.fhirworx.io/api/catalog/v1/oauth/tokens \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=root&client_secret=<polaris_root_secret>" | jq -r '.access_token')
# List catalogs
curl -s http://polaris.home.fhirworx.io/api/management/v1/catalogs \
-H "Authorization: Bearer $TOKEN"
# Create a catalog
curl -s -X POST http://polaris.home.fhirworx.io/api/management/v1/catalogs \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "analytics",
"type": "INTERNAL",
"properties": {"default-base-location": "s3://polaris/analytics/"},
"storageConfigInfo": {
"storageType": "S3",
"allowedLocations": ["s3://polaris/"],
"s3": {"region": "us-east-1", "endpoint": "http://rustfs:9000", "pathStyleAccess": true}
}
}'
```
| Multi-catalog management | No | Yes |
| Iceberg REST API | Yes | Yes |
### Query Access
| Method | Use Case | Connection |
|--------|----------|------------|
| **Trino CLI** | Ad-hoc SQL queries | `docker exec -it trino trino` |
| **Trino JDBC** | External tools (DBeaver, etc.) | `jdbc:trino://trino.home.fhirworx.io/iceberg` |
| **Trino Web UI** | Query monitoring | http://trino.home.fhirworx.io |
| **PyIceberg** | Python notebooks | Direct to Nessie REST API |
| **DuckDB** | Fast local queries | Read Iceberg via PyArrow |
| Method | Connection |
|--------|------------|
| **Trino CLI** | `docker exec -it trino trino` |
| **Trino JDBC** | `jdbc:trino://trino.homelab.fhirworx.io/iceberg` |
| **Trino Web UI** | `trino.homelab.fhirworx.io` |
| **PyIceberg** | Direct to Nessie REST API |
| **DuckDB** | Read Iceberg via PyArrow |
### Example: Create a Table
```sql
-- Connect to Trino
docker exec -it trino trino
-- Create a schema
CREATE SCHEMA iceberg.analytics;
-- Create a partitioned table
CREATE TABLE iceberg.analytics.events (
event_id VARCHAR,
event_time TIMESTAMP(6) WITH TIME ZONE,
@@ -456,101 +407,62 @@ WITH (
partitioning = ARRAY['day(event_time)']
);
-- Insert data
INSERT INTO iceberg.analytics.events VALUES
('evt-001', CURRENT_TIMESTAMP, 'user-1', 'page_view', MAP(ARRAY['page'], ARRAY['/home']));
-- Query data
SELECT * FROM iceberg.analytics.events;
```
## Traefik Reverse Proxy
All HTTP services are routed through Traefik using file-based configuration (not Docker labels).
### Configuration
Routes are defined in `traefik/dynamic/services.yml`. To add a new service:
```yaml
http:
routers:
myservice:
rule: "Host(`myservice.home.fhirworx.io`)"
service: myservice
entryPoints:
- web
services:
myservice:
loadBalancer:
servers:
- url: "http://myservice:8080"
```
### Features
- **OTEL Tracing** - All requests traced to Jaeger
- **Prometheus Metrics** - Request rates, latencies by router/service
- **File Provider** - Routes defined in YAML, hot-reloaded
- **Dashboard** - http://traefik.home.fhirworx.io
### TCP/UDP Routing
Traefik can also route TCP and UDP traffic. See `traefik/dynamic/` for examples.
## Directory Structure
```
stack/
├── compose.yml # Docker Compose configuration
├── .env # Environment variables (not in git)
├── .woodpecker.yml # CI/CD pipeline
├── README.md # This file
├── data/ # Shared data directory
├── compose.yml # Docker Compose (all services)
├── .env # Environment variables (not in git)
├── README.md
├── css/ # Centralized theme assets
│ ├── inject.css # Global throwback CSS (served as throwback.css)
│ ├── dashboard.css # Dashboard tile grid
│ ├── gitea.css # Gitea throwback theme
│ ├── woodpecker.css # Woodpecker custom CSS
│ ├── marimo.css # Marimo notebook theme
│ ├── favicon.svg # Pixel-art "H" favicon (canonical)
│ ├── favicon.png # 64x64 PNG favicon
│ ├── fav32.png # 32x32 PNG favicon
│ ├── fav16.png # 16x16 PNG favicon
│ ├── apple-touch-icon.png # 180x180 Apple touch icon
│ ├── logo.svg # Navbar logo (Gitea)
│ └── logo.png # PNG logo (256x256)
├── nginx/
│ ├── nginx.conf # Dashboard nginx config (CORS for theme assets)
│ └── index.html # Dashboard HTML
├── traefik/
│ ├── traefik.yml # Static config (entrypoints, plugins, OTEL)
│ ├── dynamic/
│ │ └── services.yml # All routes, middlewares, services (Go template)
│ └── plugins/ # Local rewrite-body plugin
├── gitea/
│ └── custom/
│ └── public/assets/css/
│ └── theme-throwback.css
│ └── custom/ # Gitea custom branding
├── grafana/
│ ├── provisioning/
│ │ ├── dashboards/
│ │ │ └── dashboards.yml
│ │ └── datasources/
│ │ └── datasources.yml
│ └── dashboards/
│ ├── homelab-overview.json
│ └── traefik.json
│ └── provisioning/ # Datasources + dashboard config
├── loki/
│ ├── loki-config.yml
│ └── promtail-config.yml
├── nginx/
│ ├── nginx.conf # Dashboard nginx config
│ └── index.html # Dashboard HTML
├── notebooks/
│ ├── Dockerfile
│ ├── pyproject.toml
│ ├── .marimo.toml
│ └── retro-arcade.css
├── prometheus/
│ └── prometheus.yml
├── traefik/
│ ├── traefik.yml # Static configuration
│ └── dynamic/
│ ├── middlewares.yml # Rate limiting, security headers
│ └── services.yml # All HTTP routes
│ ├── prometheus.yml
│ └── targets/services.yml
├── trino/
│ └── etc/
│ ├── config.properties
│ ├── jvm.config
│ ├── node.properties
│ └── catalog/
│ └── iceberg.properties
├── woodpecker/
│ └── custom.css
└── zotero/
├── Dockerfile
└── data/
│ └── etc/ # Trino config + catalog properties
├── notebooks/ # Marimo notebook files
├── zotero/ # Zotero data + profiles
├── src/ # Python source (narwhals expressions, pipes)
├── tests/ # Test suite
├── .woodpecker/ # CI/CD pipeline definitions
│ ├── ci.yml
│ ├── deploy.yml
│ ├── infra-ci.yml
│ └── rebuild-all.yml
└── data/ # Shared data directory
```
## SSH Access
@@ -559,7 +471,7 @@ Configure Git SSH access in `~/.ssh/config`:
```
Host gitea
HostName gitea.home.fhirworx.io
HostName homelab.fhirworx.io
Port 2222
User git
IdentityFile ~/.ssh/gitea_ed25519
@@ -569,33 +481,28 @@ Host gitea
Generate and add SSH key:
```bash
ssh-keygen -t ed25519 -C "user@homelab" -f ~/.ssh/gitea_ed25519 -N ""
ssh-keyscan -p 2222 gitea.home.fhirworx.io >> ~/.ssh/known_hosts
ssh-keyscan -p 2222 homelab.fhirworx.io >> ~/.ssh/known_hosts
```
Add public key to Gitea: http://gitea.home.fhirworx.io/user/settings/keys
Add public key to Gitea: `gitea.homelab.fhirworx.io/user/settings/keys`
## Container Registry
Login:
```bash
echo "<GITEA_TOKEN>" | docker login gitea.home.fhirworx.io -u <username> --password-stdin
```
# Login
echo "<GITEA_TOKEN>" | docker login gitea.homelab.fhirworx.io -u <username> --password-stdin
Tag and push:
```bash
docker tag <image>:latest gitea.home.fhirworx.io/homelab/<image>:latest
docker push gitea.home.fhirworx.io/homelab/<image>:latest
# Tag and push
docker tag <image>:latest gitea.homelab.fhirworx.io/homelab/<image>:latest
docker push gitea.homelab.fhirworx.io/homelab/<image>:latest
```
## Ports Reference
| Port | Service | Notes |
|------|---------|-------|
| 80 | Traefik HTTP | iptables redirect to 8880 |
| 443 | Traefik HTTPS | iptables redirect to 8443 |
| 2222 | Gitea SSH | Direct access |
| 4317 | Jaeger OTLP gRPC | Direct access for external traces |
| 4318 | Jaeger OTLP HTTP | Direct access for external traces |
| 5432 | PostgreSQL | Direct access |
| 9000 | RustFS S3 API | Also via s3.home.fhirworx.io |
| 9001 | RustFS Console | Also via minio.home.fhirworx.io |
| 80 | Traefik HTTP | All subdomain routing |
| 443 | Traefik HTTPS | TLS termination |
| 8081 | Traefik API | Dashboard + API (localhost only) |
| 2222 | Gitea SSH | Git over SSH |
| 3478 | Zotero TURN | WebRTC for KasmVNC |