Initial monorepo scaffold

Turborepo + pnpm monorepo for k3s homelab cluster on Intel NUCs.

- Apps: Next.js web frontend, Express API (TypeScript, Dockerfiles, k8s manifests)
- Packages: shared UI, ESLint config, TypeScript config, Drizzle DB schemas
- Infra/Ansible: bare-metal provisioning with roles for common, k3s-server, k3s-agent, hardening
- Infra/Kubernetes: ArgoCD GitOps (app-of-apps + ApplicationSets), platform components
  (cert-manager, Traefik, CloudNativePG, Valkey, Longhorn, Sealed Secrets), namespaces
- Observability: kube-prometheus-stack, Loki, Promtail as ArgoCD Applications
- CI/CD: GitHub Actions for PR builds, preview deploys, production deploys
- DX: Taskfile, utility scripts, copier templates, Ubiquiti network docs
This commit is contained in:
Julia McGhee
2026-03-19 22:24:56 +00:00
commit 96e3f32f28
118 changed files with 2681 additions and 0 deletions

26
apps/web/Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
FROM node:20-alpine AS base
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json pnpm-lock.yaml* ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN corepack enable pnpm && pnpm build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]

View File

@@ -0,0 +1,39 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
labels:
app: web
spec:
replicas: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: ghcr.io/OWNER/homelab-web:latest
ports:
- containerPort: 3000
resources:
requests:
memory: 128Mi
cpu: 100m
limits:
memory: 512Mi
readinessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 15
periodSeconds: 20

View File

@@ -0,0 +1,23 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
spec:
ingressClassName: traefik
rules:
- host: homelab.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
tls:
- hosts:
- homelab.local
secretName: web-tls

View File

@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- ingress.yaml

View File

@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 3000
protocol: TCP
selector:
app: web

View File

@@ -0,0 +1,25 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
- target:
kind: Deployment
name: web
patch: |
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 1
- target:
kind: Ingress
name: web
patch: |
- op: replace
path: /spec/rules/0/host
value: preview.homelab.local
- op: replace
path: /spec/tls/0/hosts/0
value: preview.homelab.local

View File

@@ -0,0 +1,15 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
- target:
kind: Deployment
name: web
patch: |
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 2

6
apps/web/next.config.js Normal file
View File

@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
};
module.exports = nextConfig;

23
apps/web/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "@homelab/web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --port 3000",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "echo \"no tests yet\""
},
"dependencies": {
"next": "^15.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "^22.10.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.7.0"
}
}

View File

@@ -0,0 +1,18 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Homelab",
description: "Self-hosted applications",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

View File

@@ -0,0 +1,8 @@
export default function Home() {
return (
<main style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
<h1>Homelab</h1>
<p>Self-hosted applications running on k3s.</p>
</main>
);
}

21
apps/web/tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./src/*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}