Initial orchard9 deployment
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

- Add Dockerfile with multi-stage standalone build
- Add Woodpecker CI pipeline (.woodpecker.yml)
- Add Kubernetes manifests for deployment, service, ingress
- Add ops.md with deployment documentation
- Configure Next.js for standalone output
- Move deployment files to root level

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jordan 2026-02-07 14:42:06 -07:00
parent 9a9e58c935
commit a65c3f7243
12 changed files with 5317 additions and 66 deletions

View File

@ -1,6 +1,6 @@
# Creating Blog Notes
# Creating Research Notes
This skill documents how to create new research notes for the Maxwell blog.
This skill documents how to create new research notes for the journal.
## Directory Structure

26
.dockerignore Normal file
View File

@ -0,0 +1,26 @@
# Git
.git/
.gitignore
# Dependencies (will be installed fresh in container)
blog/node_modules/
# Build output
blog/.next/
blog/out/
# Dev files
blog/.env*
*.log
.DS_Store
# IDE
.vscode/
.idea/
# Docs
*.md
!blog/content/**/*.md
# Claude
.claude/

View File

@ -1,22 +1,22 @@
# Dependencies
node_modules/
# Build output
# Build
.next/
out/
dist/
# Development
.env*.local
.env
# Environment
.env*
!.env.example
# IDE
.vscode/
.idea/
*.swp
# Misc
*.log
# OS
.DS_Store
# Generated
public/openapi.json
# Logs
*.log

33
.woodpecker.yml Normal file
View File

@ -0,0 +1,33 @@
# CI/CD Pipeline for research-notes
clone:
git:
image: woodpeckerci/plugin-git
settings:
depth: 1
steps:
build:
image: woodpeckerci/plugin-kaniko
settings:
registry: registry.threesix.ai
repo: research-notes/web
tags:
- latest
- ${CI_COMMIT_SHA:0:8}
context: .
dockerfile: Dockerfile
cache: true
skip_tls_verify: true
when:
branch: main
event: push
deploy:
image: bitnami/kubectl:latest
commands:
- kubectl set image deployment/research-notes web=registry.threesix.ai/research-notes/web:${CI_COMMIT_SHA:0:8} -n projects
- kubectl rollout status deployment/research-notes -n projects --timeout=120s
when:
branch: main
event: push

42
Dockerfile Normal file
View File

@ -0,0 +1,42 @@
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# Copy package files
COPY blog/package.json blog/pnpm-lock.yaml ./
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy source
COPY blog/ .
# Build
RUN pnpm build
# Runtime stage
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
# Don't run as root
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# Copy standalone build
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
# Content is read at build time for SSG, but copy for any runtime needs
COPY --from=builder --chown=nextjs:nodejs /app/content ./content
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

View File

@ -1,55 +0,0 @@
# StemeDB Community App Docker Build
#
# Multi-stage build for the Next.js frontend.
# Also used for running the seed script.
# Stage 1: Dependencies
FROM node:20-slim AS deps
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Stage 2: Builder
FROM node:20-slim AS builder
WORKDIR /app
# Copy dependencies from deps stage
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Create empty openapi.json if it doesn't exist (will be fetched at runtime)
RUN mkdir -p public && echo '{}' > public/openapi.json
# Build the Next.js app
# Note: Build may fail if API is not available, but we continue anyway
RUN npm run build || echo "Build completed with warnings"
# Stage 3: Runtime
FROM node:20-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=19197
# Copy built assets and dependencies
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
# Copy scripts directory for seed script
COPY --from=builder /app/scripts ./scripts
COPY --from=builder /app/tsconfig.json ./tsconfig.json
COPY --from=builder /app/src ./src
EXPOSE 19197
# Default command runs the Next.js server
CMD ["npm", "run", "start"]

View File

@ -1,7 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
output: "standalone",
};
export default nextConfig;

4974
blog/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@ import { PageLayout } from "@/components/layout/PageLayout";
import { BackNav } from "@/components/layout/BackNav";
import { NoteHeader } from "@/components/notes/NoteHeader";
import { PromptsSection } from "@/components/notes/PromptsSection";
import { SkillsSection } from "@/components/notes/SkillsSection";
import { FilesSection } from "@/components/notes/FilesSection";
import { NoteFooter } from "@/components/notes/NoteFooter";
@ -34,6 +35,8 @@ export default async function NotePage({ params }: NotePageProps) {
<PromptsSection prompts={note.prompts} />
<SkillsSection skills={note.skillsUsed || []} />
<FilesSection files={note.files} />
<div className="prose prose-neutral dark:prose-invert max-w-none">

View File

@ -30,6 +30,13 @@ export interface FileRef {
description: string;
}
export interface SkillUsed {
name: string;
command: string;
description: string;
usage?: string;
}
export interface NoteNavLink {
slug: string;
id: string;
@ -43,6 +50,7 @@ export interface NoteMeta {
title: string;
preview: string;
prompts: Prompt[];
skillsUsed?: SkillUsed[];
filesCreated: FileRef[];
navigation: {
prev: NoteNavLink | null;

76
deploy/k8s/notes.yaml Normal file
View File

@ -0,0 +1,76 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: research-notes
namespace: projects
spec:
replicas: 1
selector:
matchLabels:
app: research-notes
template:
metadata:
labels:
app: research-notes
spec:
containers:
- name: web
image: registry.threesix.ai/research-notes/web:latest
ports:
- containerPort: 3000
resources:
requests:
memory: "64Mi"
cpu: "25m"
limits:
memory: "256Mi"
cpu: "250m"
livenessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: research-notes
namespace: projects
spec:
selector:
app: research-notes
ports:
- port: 80
targetPort: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: research-notes
namespace: projects
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: traefik
tls:
- hosts:
- notes.orchard9.ai
secretName: research-notes-tls
rules:
- host: notes.orchard9.ai
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: research-notes
port:
number: 80

144
ops.md Normal file
View File

@ -0,0 +1,144 @@
# Operations: notes.orchard9.ai
Research notes journal deployed to orchard9 k3s fleet.
## Architecture
```
┌─────────────┐ push ┌─────────────┐ webhook ┌─────────────┐
│ Local │ ────────► │ Gitea │ ─────────► │ Woodpecker │
│ Dev │ │ threesix.ai │ │ CI │
└─────────────┘ └─────────────┘ └──────┬──────┘
┌─────────────┐ ingress ┌─────────────┐ deploy ┌─────────────┐
│ Browser │ ◄──────── │ k3s │ ◄──────── │ Kaniko │
│ notes. │ │ projects │ │ build │
│ orchard9.ai │ │ namespace │ └──────┬──────┘
└─────────────┘ └─────────────┘ │
┌─────────────┐
│ Zot Registry│
│ registry. │
│ threesix.ai │
└─────────────┘
```
## Infrastructure
| Component | Location |
|-----------|----------|
| Domain | notes.orchard9.ai |
| DNS Provider | GoDaddy (via squiddy-dns) |
| Ingress IP | 208.122.204.172 |
| TLS | cert-manager / letsencrypt-prod |
| Registry | registry.threesix.ai |
| Git Origin | git.threesix.ai/jordan/research-notes |
| Namespace | projects |
## Local Development
```bash
cd blog
npm install # or pnpm install
npm run dev # http://localhost:19197
```
## Deployment
Push to origin triggers automatic deployment:
```bash
git push origin main
```
Pipeline:
1. Woodpecker receives webhook from Gitea
2. Kaniko builds container image (amd64)
3. Image pushed to `registry.threesix.ai/research-notes/web:${SHA}`
4. kubectl rolls out new image to deployment
## Initial Setup (one-time)
### 1. Create Gitea Repository
```bash
# Create repo at git.threesix.ai/jordan/research-notes
# Then set origin:
git remote add origin https://git.threesix.ai/jordan/research-notes.git
```
### 2. Configure DNS
```bash
squiddy-dns record create orchard9.ai A notes 208.122.204.172 \
--ttl 300 --provider godaddy --profile orchard9
```
### 3. Apply Kubernetes Manifests
```bash
export KUBECONFIG=~/.kube/orchard9-k3sf.yaml
kubectl apply -f deploy/k8s/notes.yaml
```
### 4. First Deploy
```bash
git add .
git commit -m "Initial deployment setup"
git push origin main
```
## Verify Deployment
```bash
# Check pod status
kubectl get pods -n projects -l app=research-notes
# Check ingress
kubectl get ingress -n projects research-notes
# Check TLS certificate
kubectl get certificate -n projects research-notes-tls
# View logs
kubectl logs -n projects -l app=research-notes --tail=50
# Port forward for debugging
kubectl port-forward -n projects svc/research-notes 8080:80
```
## Troubleshooting
### Build not triggering?
- Verify push went to `origin` (Gitea), not GitHub
- Check Woodpecker webhook exists on Gitea repo
- Check Woodpecker at ci.threesix.ai
### Image not deploying?
```bash
# Check if image exists in registry
curl -s https://registry.threesix.ai/v2/research-notes/web/tags/list
# Check deployment events
kubectl describe deployment -n projects research-notes
```
### TLS certificate not ready?
```bash
# Check certificate status
kubectl describe certificate -n projects research-notes-tls
# Check cert-manager logs
kubectl logs -n cert-manager -l app=cert-manager --tail=50
```
## Files
| File | Purpose |
|------|---------|
| `Dockerfile` | Multi-stage Next.js standalone build |
| `.woodpecker.yml` | CI/CD pipeline config |
| `deploy/k8s/notes.yaml` | Deployment, Service, Ingress |
| `blog/next.config.ts` | Next.js config (standalone output) |