Initial commit: Resume site with Flux CD automation

- Add HTML comment for hiring pipeline
- Configure Helm chart for Kubernetes deployment
- Set up ingress for resume.caffeinetux.com
- Configure Harbor registry at images.caffeinetux.com
- Add Flux CD manifests for GitOps deployment
- Update CI workflow for Harbor integration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Neon Vortex
2025-11-25 13:34:27 -05:00
commit b803ba5468
18 changed files with 1631 additions and 0 deletions

20
.dockerignore Normal file
View File

@@ -0,0 +1,20 @@
# Git
.git
.gitignore
# Documentation
README.md
*.md
# Helm chart (not needed in Docker image)
helm/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db

106
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,106 @@
name: Build and Deploy Resume
on:
push:
branches: [main]
paths:
- 'index.html'
- 'Dockerfile'
- 'helm/**'
- '.github/workflows/**'
pull_request:
branches: [main]
env:
REGISTRY: images.caffeinetux.com
IMAGE_NAME: production/resume-site
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Harbor Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix=
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
helm-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Helm
uses: azure/setup-helm@v3
with:
version: v3.13.0
- name: Lint Helm chart
run: helm lint ./helm
- name: Template Helm chart
run: |
helm template resume ./helm \
--set image.repository=test \
--set ingress.hosts[0].host=test.example.com \
--set ingress.hosts[0].paths[0].path=/ \
--set ingress.hosts[0].paths[0].pathType=Prefix
# Optional: Deploy to cluster (uncomment and configure)
# deploy:
# needs: [build, helm-lint]
# if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
# runs-on: ubuntu-latest
# steps:
# - name: Checkout
# uses: actions/checkout@v4
#
# - name: Set up Helm
# uses: azure/setup-helm@v3
#
# - name: Configure kubectl
# uses: azure/k8s-set-context@v3
# with:
# kubeconfig: ${{ secrets.KUBECONFIG }}
#
# - name: Deploy to Kubernetes
# run: |
# helm upgrade --install resume ./helm \
# --namespace resume \
# --create-namespace \
# --set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \
# --set image.tag=${{ github.sha }} \
# --wait

25
Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
FROM nginx:alpine
# Copy the resume site
COPY index.html /usr/share/nginx/html/index.html
# Custom nginx config for SPA routing
RUN echo 'server { \
listen 80; \
server_name _; \
root /usr/share/nginx/html; \
index index.html; \
location / { \
try_files $uri $uri/ /index.html; \
} \
location ~* \.(html|css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { \
expires 1d; \
add_header Cache-Control "public, immutable"; \
} \
gzip on; \
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript; \
}' > /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

165
README.md Normal file
View File

@@ -0,0 +1,165 @@
# Nicholas Haven - Resume Website
A professional, responsive resume website designed for deployment on Kubernetes.
## Features
- **Modern Design**: Dark theme with cyan accents, animated backgrounds, and smooth transitions
- **Responsive**: Works on all device sizes
- **Production-Ready**: Includes Dockerfile and Helm chart
- **Kubernetes-Native**: Configured with health checks, PDB, HPA, and proper security contexts
- **TLS Ready**: Pre-configured for cert-manager integration
## Quick Start
### 1. Build the Docker Image
```bash
# Build locally
docker build -t resume-site:latest .
# Or build and push to your registry
docker build -t your-registry.com/resume-site:latest .
docker push your-registry.com/resume-site:latest
```
### 2. Deploy with Helm
```bash
# Create namespace (optional)
kubectl create namespace resume
# Install with custom values
helm upgrade --install resume ./helm \
--namespace resume \
--set image.repository=your-registry.com/resume-site \
--set image.tag=latest \
--set ingress.hosts[0].host=resume.yourdomain.com \
--set ingress.hosts[0].paths[0].path=/ \
--set ingress.hosts[0].paths[0].pathType=Prefix \
--set ingress.tls[0].secretName=resume-tls \
--set ingress.tls[0].hosts[0]=resume.yourdomain.com
```
### 3. Using a Values File (Recommended)
Create a `values-production.yaml`:
```yaml
image:
repository: your-registry.com/resume-site
tag: "1.0.0"
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- host: resume.yourdomain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: resume-tls
hosts:
- resume.yourdomain.com
resources:
limits:
cpu: 100m
memory: 64Mi
requests:
cpu: 10m
memory: 32Mi
```
Then deploy:
```bash
helm upgrade --install resume ./helm \
--namespace resume \
-f values-production.yaml
```
## Configuration Options
| Parameter | Description | Default |
|-----------|-------------|---------|
| `replicaCount` | Number of pods | `2` |
| `image.repository` | Docker image repository | `resume-site` |
| `image.tag` | Docker image tag | `latest` |
| `ingress.enabled` | Enable ingress | `true` |
| `ingress.className` | Ingress class name | `nginx` |
| `ingress.hosts` | Ingress hosts configuration | `[]` |
| `autoscaling.enabled` | Enable HPA | `false` |
| `podDisruptionBudget.enabled` | Enable PDB | `true` |
## Local Development
```bash
# Run with Docker
docker build -t resume-site:dev .
docker run -p 8080:80 resume-site:dev
# Open http://localhost:8080
```
## Directory Structure
```
.
├── index.html # Main resume page
├── Dockerfile # Multi-stage Docker build
├── README.md # This file
└── helm/
├── Chart.yaml # Helm chart metadata
├── values.yaml # Default values
└── templates/
├── _helpers.tpl
├── deployment.yaml
├── service.yaml
├── ingress.yaml
├── serviceaccount.yaml
├── pdb.yaml
└── hpa.yaml
```
## Customization
### Updating Content
Edit `index.html` directly. The file is self-contained with embedded CSS.
### Changing Colors
CSS variables are defined at the top of the `<style>` block:
```css
:root {
--accent-cyan: #06b6d4;
--accent-emerald: #10b981;
--accent-violet: #8b5cf6;
/* ... */
}
```
### Adding New Sections
Follow the existing pattern using `section` elements with appropriate class names.
## Health Checks
The nginx configuration includes a `/health` endpoint that returns `200 OK` for Kubernetes probes.
## Security
- Runs as non-root user (nginx:101)
- Drops all capabilities
- Configured with proper security contexts
- TLS termination at ingress level
---
*Built with clean code and deployed on Kubernetes* ☸️

86
flux/README.md Normal file
View File

@@ -0,0 +1,86 @@
# Flux Deployment for Resume Site
This directory contains Flux CD manifests for automated deployment of the resume site to Kubernetes.
## Prerequisites
1. Flux CD installed in your cluster
2. Gitea repository created and pushed
3. Harbor credentials configured
4. Docker image built and pushed to Harbor
## Setup Instructions
### 1. Update GitRepository URL
Edit `gitrepository.yaml` and replace the placeholder URL with your actual Gitea repository URL:
```yaml
url: https://your-gitea-url/username/resume-site.git
```
### 2. Build and Push Docker Image
```bash
# Login to Harbor
docker login images.caffeinetux.com
# Build the image
docker build -t images.caffeinetux.com/production/resume-site:latest .
# Push to Harbor
docker push images.caffeinetux.com/production/resume-site:latest
```
### 3. Deploy with Flux
Apply the Flux manifests to your cluster:
```bash
kubectl apply -k flux/
```
Flux will:
- Clone the Git repository
- Deploy the Helm chart from `./helm`
- Create an Ingress at https://resume.caffeinetux.com
- Automatically sync changes from Git
### 4. Verify Deployment
```bash
# Check Flux GitRepository
kubectl get gitrepository -n flux-system resume-site
# Check Flux HelmRelease
kubectl get helmrelease -n default resume-site
# Check pods
kubectl get pods -n default -l app.kubernetes.io/name=resume-site
# Check ingress
kubectl get ingress -n default
```
## Automatic Updates
Flux checks the Git repository every minute. Any changes to the `helm/` directory will trigger an automatic update of the deployment.
## Secrets
If you need to configure Harbor image pull secrets:
```bash
kubectl create secret docker-registry harbor-creds \
--docker-server=images.caffeinetux.com \
--docker-username=YOUR_USERNAME \
--docker-password=YOUR_PASSWORD \
--namespace=default
```
Then update `helm/values.yaml`:
```yaml
imagePullSecrets:
- name: harbor-creds
```

15
flux/gitrepository.yaml Normal file
View File

@@ -0,0 +1,15 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: resume-site
namespace: flux-system
spec:
interval: 1m
url: https://GITEA_URL/GITEA_USER/resume-site.git # Update with actual Gitea URL
ref:
branch: main
ignore: |
# exclude all
/*
# include helm chart
!/helm/

36
flux/helmrelease.yaml Normal file
View File

@@ -0,0 +1,36 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: resume-site
namespace: default
spec:
interval: 5m
chart:
spec:
chart: ./helm
sourceRef:
kind: GitRepository
name: resume-site
namespace: flux-system
interval: 1m
values:
replicaCount: 2
image:
repository: images.caffeinetux.com/production/resume-site
pullPolicy: IfNotPresent
tag: "latest"
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- host: resume.caffeinetux.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: resume-tls
hosts:
- resume.caffeinetux.com

5
flux/kustomization.yaml Normal file
View File

@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- gitrepository.yaml
- helmrelease.yaml

13
helm/Chart.yaml Normal file
View File

@@ -0,0 +1,13 @@
apiVersion: v2
name: nicholas-haven-resume
description: Personal resume website for Nicholas Haven
type: application
version: 1.0.0
appVersion: "1.0.0"
keywords:
- resume
- portfolio
- personal-site
maintainers:
- name: Nicholas Haven
email: NickH@libem.one

View File

@@ -0,0 +1,42 @@
{{- define "resume.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "resume.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{- define "resume.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "resume.labels" -}}
helm.sh/chart: {{ include "resume.chart" . }}
{{ include "resume.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "resume.selectorLabels" -}}
app.kubernetes.io/name: {{ include "resume.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{- define "resume.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "resume.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,69 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "resume.fullname" . }}
labels:
{{- include "resume.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "resume.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "resume.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "resume.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: {{ .Values.healthCheck.path }}
port: http
initialDelaySeconds: {{ .Values.healthCheck.initialDelaySeconds }}
periodSeconds: {{ .Values.healthCheck.periodSeconds }}
timeoutSeconds: {{ .Values.healthCheck.timeoutSeconds }}
failureThreshold: {{ .Values.healthCheck.failureThreshold }}
readinessProbe:
httpGet:
path: {{ .Values.healthCheck.path }}
port: http
initialDelaySeconds: {{ .Values.healthCheck.initialDelaySeconds }}
periodSeconds: {{ .Values.healthCheck.periodSeconds }}
timeoutSeconds: {{ .Values.healthCheck.timeoutSeconds }}
failureThreshold: {{ .Values.healthCheck.failureThreshold }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

24
helm/templates/hpa.yaml Normal file
View File

@@ -0,0 +1,24 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "resume.fullname" . }}
labels:
{{- include "resume.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "resume.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,41 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "resume.fullname" . }}
labels:
{{- include "resume.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "resume.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

13
helm/templates/pdb.yaml Normal file
View File

@@ -0,0 +1,13 @@
{{- if .Values.podDisruptionBudget.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "resume.fullname" . }}
labels:
{{- include "resume.labels" . | nindent 4 }}
spec:
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
selector:
matchLabels:
{{- include "resume.selectorLabels" . | nindent 6 }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "resume.fullname" . }}
labels:
{{- include "resume.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "resume.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "resume.serviceAccountName" . }}
labels:
{{- include "resume.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

91
helm/values.yaml Normal file
View File

@@ -0,0 +1,91 @@
replicaCount: 2
image:
repository: images.caffeinetux.com/production/resume-site
pullPolicy: IfNotPresent
tag: "latest"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 101
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
service:
type: ClusterIP
port: 80
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- host: resume.caffeinetux.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: resume-tls
hosts:
- resume.caffeinetux.com
resources:
limits:
cpu: 100m
memory: 64Mi
requests:
cpu: 10m
memory: 32Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 5
targetCPUUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- nicholas-haven-resume
topologyKey: kubernetes.io/hostname
healthCheck:
path: /health
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
podDisruptionBudget:
enabled: true
minAvailable: 1

853
index.html Normal file
View File

@@ -0,0 +1,853 @@
<!-- If you're reading this, the hiring pipeline has reached manual approval. -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nicholas Haven | DevOps Engineer</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-primary: #0a0e14;
--bg-secondary: #11151c;
--bg-card: #151a23;
--bg-hover: #1a2029;
--text-primary: #e6e6e6;
--text-secondary: #8b949e;
--text-muted: #5c6370;
--accent-cyan: #39c5cf;
--accent-green: #7ee787;
--accent-orange: #f0883e;
--accent-purple: #a371f7;
--accent-blue: #58a6ff;
--accent-red: #ff7b72;
--border-color: #21262d;
--glow-cyan: rgba(57, 197, 207, 0.15);
--glow-green: rgba(126, 231, 135, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: 'Outfit', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.7;
min-height: 100vh;
overflow-x: hidden;
}
/* Animated background grid */
.bg-grid {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
linear-gradient(rgba(57, 197, 207, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(57, 197, 207, 0.03) 1px, transparent 1px);
background-size: 50px 50px;
pointer-events: none;
z-index: 0;
}
.bg-gradient {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 100vh;
background: radial-gradient(ellipse at 50% 0%, var(--glow-cyan) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
/* Main container */
.container {
max-width: 1100px;
margin: 0 auto;
padding: 0 24px;
position: relative;
z-index: 1;
}
/* Navigation */
nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
padding: 16px 24px;
background: rgba(10, 14, 20, 0.85);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border-color);
}
nav .container {
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-logo {
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
font-size: 1.1rem;
color: var(--accent-cyan);
text-decoration: none;
display: flex;
align-items: center;
gap: 8px;
}
.nav-logo::before {
content: '>';
opacity: 0.5;
}
.nav-links {
display: flex;
gap: 32px;
list-style: none;
}
.nav-links a {
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
color: var(--text-secondary);
text-decoration: none;
transition: color 0.2s;
position: relative;
}
.nav-links a:hover {
color: var(--accent-cyan);
}
.nav-links a::before {
content: '#';
color: var(--text-muted);
margin-right: 4px;
}
/* Hero Section */
.hero {
min-height: 100vh;
display: flex;
align-items: center;
padding: 120px 0 80px;
}
.hero-content {
width: 100%;
}
.hero-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 100px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
color: var(--accent-green);
margin-bottom: 24px;
animation: fadeInUp 0.6s ease-out;
}
.hero-badge::before {
content: '';
width: 8px;
height: 8px;
background: var(--accent-green);
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.hero h1 {
font-size: clamp(2.5rem, 6vw, 4.5rem);
font-weight: 700;
line-height: 1.1;
margin-bottom: 24px;
animation: fadeInUp 0.6s ease-out 0.1s backwards;
}
.hero h1 .name {
display: block;
background: linear-gradient(135deg, var(--accent-cyan) 0%, var(--accent-blue) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-subtitle {
font-size: clamp(1.1rem, 2.5vw, 1.4rem);
color: var(--text-secondary);
max-width: 600px;
margin-bottom: 40px;
animation: fadeInUp 0.6s ease-out 0.2s backwards;
}
.hero-contact {
display: flex;
flex-wrap: wrap;
gap: 16px;
animation: fadeInUp 0.6s ease-out 0.3s backwards;
}
.contact-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 20px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 8px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem;
color: var(--text-primary);
text-decoration: none;
transition: all 0.2s;
}
.contact-item:hover {
border-color: var(--accent-cyan);
background: var(--bg-hover);
transform: translateY(-2px);
}
.contact-item svg {
width: 18px;
height: 18px;
color: var(--accent-cyan);
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Section Styles */
section {
padding: 80px 0;
}
.section-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 48px;
}
.section-header h2 {
font-family: 'JetBrains Mono', monospace;
font-size: 1.5rem;
font-weight: 600;
color: var(--text-primary);
}
.section-header::after {
content: '';
flex: 1;
height: 1px;
background: linear-gradient(90deg, var(--border-color) 0%, transparent 100%);
}
.section-number {
font-family: 'JetBrains Mono', monospace;
font-size: 1rem;
color: var(--accent-cyan);
}
/* Skills Section */
.skills-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 20px;
}
.skill-category {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 24px;
transition: all 0.3s;
}
.skill-category:hover {
border-color: var(--accent-cyan);
transform: translateY(-4px);
box-shadow: 0 8px 30px var(--glow-cyan);
}
.skill-category h3 {
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
font-weight: 600;
color: var(--accent-cyan);
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.skill-category h3 svg {
width: 18px;
height: 18px;
}
.skill-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.skill-tag {
padding: 6px 12px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
color: var(--text-secondary);
transition: all 0.2s;
}
.skill-tag:hover {
color: var(--text-primary);
border-color: var(--text-muted);
}
/* Experience Section */
.experience-timeline {
position: relative;
padding-left: 32px;
}
.experience-timeline::before {
content: '';
position: absolute;
left: 0;
top: 8px;
bottom: 8px;
width: 2px;
background: linear-gradient(180deg, var(--accent-cyan) 0%, var(--accent-purple) 50%, var(--accent-green) 100%);
border-radius: 2px;
}
.experience-item {
position: relative;
margin-bottom: 48px;
padding: 28px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
transition: all 0.3s;
}
.experience-item:hover {
border-color: var(--accent-cyan);
transform: translateX(8px);
}
.experience-item::before {
content: '';
position: absolute;
left: -38px;
top: 34px;
width: 12px;
height: 12px;
background: var(--bg-primary);
border: 3px solid var(--accent-cyan);
border-radius: 50%;
}
.experience-item:nth-child(2)::before {
border-color: var(--accent-purple);
}
.experience-item:nth-child(3)::before {
border-color: var(--accent-orange);
}
.experience-item:nth-child(4)::before {
border-color: var(--accent-green);
}
.experience-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
}
.experience-title h3 {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
}
.experience-title .company {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem;
color: var(--accent-cyan);
}
.experience-date {
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
color: var(--text-muted);
padding: 6px 12px;
background: var(--bg-secondary);
border-radius: 6px;
}
.experience-description {
color: var(--text-secondary);
margin-bottom: 20px;
font-size: 0.95rem;
}
.experience-highlights {
list-style: none;
}
.experience-highlights li {
position: relative;
padding-left: 24px;
margin-bottom: 12px;
font-size: 0.9rem;
color: var(--text-secondary);
}
.experience-highlights li::before {
content: '▹';
position: absolute;
left: 0;
color: var(--accent-cyan);
font-size: 1rem;
}
.experience-award {
display: inline-flex;
align-items: center;
gap: 8px;
margin-top: 16px;
padding: 10px 16px;
background: linear-gradient(135deg, rgba(240, 136, 62, 0.1) 0%, rgba(163, 113, 247, 0.1) 100%);
border: 1px solid rgba(240, 136, 62, 0.3);
border-radius: 8px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
color: var(--accent-orange);
}
.experience-award svg {
width: 16px;
height: 16px;
}
/* Certifications / Clearance */
.clearance-card {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 16px 24px;
background: linear-gradient(135deg, rgba(126, 231, 135, 0.1) 0%, rgba(57, 197, 207, 0.1) 100%);
border: 1px solid rgba(126, 231, 135, 0.3);
border-radius: 12px;
margin-bottom: 48px;
}
.clearance-card svg {
width: 24px;
height: 24px;
color: var(--accent-green);
}
.clearance-card span {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem;
color: var(--accent-green);
}
/* Education */
.education-card {
padding: 28px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
max-width: 500px;
}
.education-card h3 {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
}
.education-card .school {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem;
color: var(--accent-purple);
margin-bottom: 8px;
}
.education-card .date {
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
color: var(--text-muted);
}
/* Footer */
footer {
padding: 48px 0;
border-top: 1px solid var(--border-color);
text-align: center;
}
.footer-text {
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
color: var(--text-muted);
}
.footer-text span {
color: var(--accent-cyan);
}
/* Mobile Responsive */
@media (max-width: 768px) {
.nav-links {
display: none;
}
.hero {
padding: 100px 0 60px;
}
.experience-timeline {
padding-left: 24px;
}
.experience-item::before {
left: -30px;
width: 10px;
height: 10px;
}
.experience-header {
flex-direction: column;
}
.skills-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="bg-grid"></div>
<div class="bg-gradient"></div>
<nav>
<div class="container">
<a href="#" class="nav-logo">nicholas_haven</a>
<ul class="nav-links">
<li><a href="#skills">skills</a></li>
<li><a href="#experience">experience</a></li>
<li><a href="#education">education</a></li>
</ul>
</div>
</nav>
<main>
<section class="hero">
<div class="container">
<div class="hero-content">
<div class="hero-badge">Available for Consulting</div>
<h1>
<span class="name">Nicholas Haven</span>
DevOps / Platform Engineer
</h1>
<p class="hero-subtitle">
6+ years designing and operating production Kubernetes infrastructure across multi-cloud environments. Expert in GitOps, infrastructure automation, and security-first DevOps practices.
</p>
<div class="hero-contact">
<a href="mailto:NickH@libem.one" class="contact-item">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
NickH@libem.one
</a>
<a href="tel:+15136800062" class="contact-item">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
+1.513.680.0062
</a>
</div>
</div>
</div>
</section>
<section id="skills">
<div class="container">
<div class="section-header">
<span class="section-number">01.</span>
<h2>Technical Skills</h2>
</div>
<div class="clearance-card">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path><polyline points="9 12 11 14 15 10"></polyline></svg>
<span>Active Security Clearance (July 2021)</span>
</div>
<div class="skills-grid">
<div class="skill-category">
<h3>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path></svg>
Multi-Cloud Platforms
</h3>
<div class="skill-tags">
<span class="skill-tag">AWS (EKS, EC2, S3, RDS)</span>
<span class="skill-tag">GCP (GKE)</span>
<span class="skill-tag">Azure (AKS)</span>
<span class="skill-tag">Kops</span>
</div>
</div>
<div class="skill-category">
<h3>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path></svg>
Container Orchestration
</h3>
<div class="skill-tags">
<span class="skill-tag">Kubernetes</span>
<span class="skill-tag">Docker</span>
<span class="skill-tag">Helm</span>
<span class="skill-tag">Kustomize</span>
<span class="skill-tag">Karpenter</span>
</div>
</div>
<div class="skill-category">
<h3>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>
Infrastructure as Code & GitOps
</h3>
<div class="skill-tags">
<span class="skill-tag">Terraform</span>
<span class="skill-tag">Crossplane</span>
<span class="skill-tag">ArgoCD</span>
<span class="skill-tag">Atlantis</span>
<span class="skill-tag">Ansible</span>
<span class="skill-tag">Helmfile</span>
</div>
</div>
<div class="skill-category">
<h3>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
CI/CD Pipelines
</h3>
<div class="skill-tags">
<span class="skill-tag">GitLab-CI</span>
<span class="skill-tag">CircleCI</span>
<span class="skill-tag">GitHub Actions</span>
<span class="skill-tag">Tekton</span>
<span class="skill-tag">Jenkins</span>
</div>
</div>
<div class="skill-category">
<h3>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
Observability & Monitoring
</h3>
<div class="skill-tags">
<span class="skill-tag">Prometheus</span>
<span class="skill-tag">Grafana</span>
<span class="skill-tag">DataDog</span>
<span class="skill-tag">Custom Dashboards</span>
</div>
</div>
<div class="skill-category">
<h3>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
Security & Compliance
</h3>
<div class="skill-tags">
<span class="skill-tag">Vault</span>
<span class="skill-tag">Kyverno</span>
<span class="skill-tag">OPA</span>
<span class="skill-tag">Istio</span>
<span class="skill-tag">ModSecurity</span>
<span class="skill-tag">cert-manager</span>
</div>
</div>
<div class="skill-category">
<h3>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>
Languages & Scripting
</h3>
<div class="skill-tags">
<span class="skill-tag">Python</span>
<span class="skill-tag">Go</span>
<span class="skill-tag">Bash</span>
<span class="skill-tag">Node.js</span>
<span class="skill-tag">SQL</span>
</div>
</div>
</div>
</div>
</section>
<section id="experience">
<div class="container">
<div class="section-header">
<span class="section-number">02.</span>
<h2>Experience</h2>
</div>
<div class="experience-timeline">
<div class="experience-item">
<div class="experience-header">
<div class="experience-title">
<h3>Site Reliability Engineer</h3>
<span class="company">Fairwinds Ops Inc.</span>
</div>
<span class="experience-date">July 2022 Present</span>
</div>
<p class="experience-description">
Primary DevOps consultant for 20+ enterprise clients, architecting production Kubernetes infrastructure across multi-cloud environments.
</p>
<ul class="experience-highlights">
<li>Architect and maintain production clusters across AWS (EKS), GCP (GKE), Azure (AKS), and Kops serving millions of requests with 99.95% uptime</li>
<li>Lead direct client engagements through regular 1:1 meetings and technical consultations, delivering tailored infrastructure solutions</li>
<li>Automate multi-cloud deployments using Atlantis for Terraform and ArgoCD for GitOps-based application delivery</li>
<li>Design zero-trust security architectures using Vault, Kyverno, OPA, and Istio service mesh</li>
<li>Pioneer Terraform templating strategies reducing deployment time by 60% across client projects</li>
<li>Maintain and contribute to open-source Kubernetes tooling including custom operators and security tools</li>
</ul>
</div>
<div class="experience-item">
<div class="experience-header">
<div class="experience-title">
<h3>Senior DevOps Engineer</h3>
<span class="company">Mile Two LLC</span>
</div>
<span class="experience-date">Aug 2020 May 2022</span>
</div>
<p class="experience-description">
Delivered technical solutions for government and enterprise clients in secure and classified environments.
</p>
<ul class="experience-highlights">
<li>Collaborated with government teams to deliver solutions into classified environments with full compliance</li>
<li>Upgraded Infrastructure as Code from Terraform to Crossplane with Helmfile templating</li>
<li>Designed CI/CD pipelines with CVE scanning, secret detection, and compliance validation</li>
<li>Created Kubernetes dashboard for resource tracking, observability, and cost analysis</li>
<li>Enabled autonomous deployment capabilities through self-service automation</li>
</ul>
<div class="experience-award">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></svg>
Humble Expertise Award Q3 2021
</div>
</div>
<div class="experience-item">
<div class="experience-header">
<div class="experience-title">
<h3>DevOps Engineer</h3>
<span class="company">Hobsons</span>
</div>
<span class="experience-date">Mar 2019 Jul 2020</span>
</div>
<p class="experience-description">
Modernized legacy cloud infrastructure and established infrastructure-as-code practices.
</p>
<ul class="experience-highlights">
<li>Modernized legacy cloud infrastructure to current-generation solutions</li>
<li>Implemented comprehensive metrics and alerting for improved observability</li>
<li>Established version-controlled infrastructure management with Terraform</li>
</ul>
<div class="experience-award">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></svg>
Modernization Engineer Award Q4 2019
</div>
</div>
<div class="experience-item">
<div class="experience-header">
<div class="experience-title">
<h3>Senior DevOps Engineer</h3>
<span class="company">SC E-Learning</span>
</div>
<span class="experience-date">Aug 2016 Mar 2019</span>
</div>
<p class="experience-description">
Managed all client-facing services with 99.99% uptime and led major cloud migration initiative.
</p>
<ul class="experience-highlights">
<li>Managed 300+ client services with 99.99% uptime including maintenance windows</li>
<li>Orchestrated AWS migration achieving $250,000+ annual cost savings</li>
<li>Implemented CI/CD with GitHub, Jenkins, S3, Lambda, and auto-scaling across multiple AZs</li>
</ul>
</div>
</div>
</div>
</section>
<section id="education">
<div class="container">
<div class="section-header">
<span class="section-number">03.</span>
<h2>Education</h2>
</div>
<div class="education-card">
<h3>Advanced Networking & Computer Science</h3>
<p class="school">Cincinnati State</p>
<p class="date">September 2006 December 2008</p>
</div>
</div>
</section>
</main>
<footer>
<div class="container">
<p class="footer-text">
<span>&lt;</span> Built with passion for infrastructure <span>/&gt;</span>
</p>
</div>
</footer>
</body>
</html>