kert 245ef1c3b6
Some checks failed
ci/woodpecker/push/infra-ci Pipeline failed
ci/woodpecker/push/deploy Pipeline failed
ci/woodpecker/push/ci Pipeline was successful
centralize all theme CSS into top-level css/ directory
Move scattered CSS files into css/ as canonical home for all styles:
- inject.css (universal injectable, was nginx/throwback.css)
- dashboard.css (extracted from nginx/index.html inline styles)
- gitea.css, woodpecker.css, marimo.css (moved from service dirs)
- throwback.css (reference palette, single source of truth)

Rewire compose.yml volume mounts to source from css/.
2026-02-28 18:08:01 -05:00
2026-02-09 23:39:08 -05:00
2026-02-07 16:18:44 -05:00
2026-02-08 22:45:09 -05:00
2026-02-08 17:41:28 -05:00
2026-02-07 16:18:44 -05:00
2026-02-08 17:41:28 -05:00

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.

Services

All services are accessible via Traefik reverse proxy on port 80.

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)

Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│                              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 ───────────────┴───────────────┘           │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Prerequisites

  • Docker with rootless mode
  • NVIDIA GPU with container toolkit
  • iptables for port 80 redirect (rootless Docker can't bind privileged ports)

Fix for rootless NVIDIA containers:

sudo sed -i 's/#no-cgroups = false/no-cgroups = true/' /etc/nvidia-container-runtime/config.toml

DNS Setup

Add to /etc/hosts (replace IP with your server's LAN IP):

# 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:

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'

Setup

1. Environment Variables

Create .env file:

POSTGRES_PASSWORD=<your_password>
RUSTFS_ACCESS_KEY=<your_access_key>
RUSTFS_SECRET_KEY=<your_secret_key>
GITEA_S3_ACCESS_KEY=<gitea_s3_user>
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_AGENT_SECRET=<generated_secret>
WOODPECKER_GITEA_CLIENT=<oauth_client_id>
WOODPECKER_GITEA_SECRET=<oauth_client_secret>

# Nessie Data Lake
NESSIE_DB_PASSWORD=<nessie_db_password>
NESSIE_S3_ACCESS_KEY=<nessie_s3_user>
NESSIE_S3_SECRET_KEY=<nessie_s3_password>

# Polaris Catalog
POLARIS_DB_PASSWORD=<polaris_db_password>
POLARIS_ROOT_SECRET=<polaris_root_secret>
POLARIS_S3_ACCESS_KEY=<polaris_s3_user>
POLARIS_S3_SECRET_KEY=<polaris_s3_password>

Generate agent secret:

openssl rand -hex 32

2. Start Core Services

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
  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
  5. Create bucket: lakehouse
  6. Apply IAM policies to users (see below)

Gitea IAM Policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetBucketLocation",
        "s3:ListBucket",
        "s3:ListBucketMultipartUploads"
      ],
      "Resource": [
        "arn:aws:s3:::gitea",
        "arn:aws:s3:::gitea-lfs",
        "arn:aws:s3:::gitea-packages"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListMultipartUploadParts",
        "s3:AbortMultipartUpload"
      ],
      "Resource": [
        "arn:aws:s3:::gitea/*",
        "arn:aws:s3:::gitea-lfs/*",
        "arn:aws:s3:::gitea-packages/*"
      ]
    }
  ]
}

Nessie IAM Policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetBucketLocation",
        "s3:ListBucket",
        "s3:ListBucketMultipartUploads"
      ],
      "Resource": ["arn:aws:s3:::lakehouse"]
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListMultipartUploadParts",
        "s3:AbortMultipartUpload"
      ],
      "Resource": ["arn:aws:s3:::lakehouse/*"]
    }
  ]
}

4. Configure PostgreSQL

Create databases and users:

# Gitea database
docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "CREATE DATABASE gitea;"
docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "CREATE USER git WITH PASSWORD '<gitea_db_password>'; ALTER DATABASE gitea OWNER TO git;"

# Woodpecker database
docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "CREATE DATABASE woodpecker;"
docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "CREATE USER woodpecker WITH PASSWORD '<woodpecker_db_password>'; ALTER DATABASE woodpecker OWNER TO woodpecker;"

# 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:

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

docker compose up -d

6. Configure Gitea

  1. Complete initial setup at http://gitea.home.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
    • 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

8. Verify Services

# Check Traefik routes
curl -s http://localhost:8081/api/http/routers | jq -r '.[].name'

# Check Nessie
curl http://nessie.home.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'

Observability

The stack includes a complete observability solution:

Components

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
Traefik Native OTLP export
Nessie Quarkus OTEL
Polaris Quarkus OTEL
Trino Native OTEL

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
prometheus localhost:9090
traefik traefik:8080/metrics
jaeger jaeger:14269
loki loki:3100
nessie nessie:19120/q/metrics
polaris polaris:8182/q/metrics
trino trino:8080/v1/status

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

Nessie vs Polaris

Feature Nessie Polaris
Git-like branching ✓ ✗
Time travel ✓ ✓
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

# 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}
    }
  }'

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

Example: Create a Table

-- 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,
    user_id VARCHAR,
    event_type VARCHAR,
    properties MAP(VARCHAR, VARCHAR)
)
WITH (
    format = 'PARQUET',
    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:

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
├── gitea/
│   └── custom/
│       └── public/assets/css/
│           └── theme-throwback.css
├── grafana/
│   ├── provisioning/
│   │   ├── dashboards/
│   │   │   └── dashboards.yml
│   │   └── datasources/
│   │       └── datasources.yml
│   └── dashboards/
│       ├── homelab-overview.json
│       └── traefik.json
├── 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
├── trino/
│   └── etc/
│       ├── config.properties
│       ├── jvm.config
│       ├── node.properties
│       └── catalog/
│           └── iceberg.properties
├── woodpecker/
│   └── custom.css
└── zotero/
    ├── Dockerfile
    └── data/

SSH Access

Configure Git SSH access in ~/.ssh/config:

Host gitea
    HostName gitea.home.fhirworx.io
    Port 2222
    User git
    IdentityFile ~/.ssh/gitea_ed25519
    IdentitiesOnly yes

Generate and add SSH key:

ssh-keygen -t ed25519 -C "user@homelab" -f ~/.ssh/gitea_ed25519 -N ""
ssh-keyscan -p 2222 gitea.home.fhirworx.io >> ~/.ssh/known_hosts

Add public key to Gitea: http://gitea.home.fhirworx.io/user/settings/keys

Container Registry

Login:

echo "<GITEA_TOKEN>" | docker login gitea.home.fhirworx.io -u <username> --password-stdin

Tag and push:

docker tag <image>:latest gitea.home.fhirworx.io/homelab/<image>:latest
docker push gitea.home.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
Description
No description provided
Readme 4.9 GiB
Languages
Python 94.2%
HTML 3%
CSS 1.4%
Go 0.5%
Shell 0.3%
Other 0.6%