293 lines
8.2 KiB
Markdown
293 lines
8.2 KiB
Markdown
# ridgwaysystems.org
|
|
|
|
Personal homelab site. Landing page, build log / blog, infrastructure overview, service status
|
|
page, and an admin panel for managing content.
|
|
|
|
## Why Go?
|
|
|
|
- Compiles to a single static binary. Easy to deploy on FreeBSD with no runtime dependencies.
|
|
- Standard library handles HTTP, HTML templates, and file serving without pulling in a framework.
|
|
- Fast startup, low memory footprint. Fits comfortably behind relayd on the SuperMicro 1U.
|
|
- The tool I know best for this kind of thing.
|
|
|
|
A static site generator (Hugo, etc.) was considered but ruled out because the admin panel
|
|
requirement means a server-side process is needed anyway. Combining the site server and admin
|
|
into one binary keeps the deployment simple.
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
.
|
|
├── cmd/server/main.go # Entry point
|
|
├── internal/
|
|
│ ├── blog/ # Post parsing and store (markdown + YAML frontmatter)
|
|
│ ├── feed/ # RSS 2.0 feed generation
|
|
│ ├── handler/ # HTTP handlers (public, admin, auth)
|
|
│ └── status/ # Service status JSON loading/saving
|
|
├── templates/
|
|
│ ├── base.html # Shared layout
|
|
│ ├── index.html # Landing page
|
|
│ ├── blog.html # Post list
|
|
│ ├── post.html # Single post
|
|
│ ├── infrastructure.html # Hardware and network diagram
|
|
│ ├── status.html # Service status page
|
|
│ ├── about.html # About / contact
|
|
│ └── admin/ # Admin panel templates
|
|
├── static/css/style.css # Stylesheet (dark mode, responsive)
|
|
├── content/posts/ # Blog posts as Markdown files with YAML frontmatter
|
|
├── data/status.json # Service status data (updated by cron or manually)
|
|
├── .env.example # Configuration reference
|
|
└── README.md
|
|
```
|
|
|
|
## Building
|
|
|
|
```sh
|
|
go build -o website ./cmd/server
|
|
```
|
|
|
|
Requires Go 1.22+. All dependencies are managed with Go modules.
|
|
|
|
To download dependencies:
|
|
```sh
|
|
go mod tidy
|
|
```
|
|
|
|
## Running
|
|
|
|
```sh
|
|
# Development — reloads templates on each request
|
|
DEV=1 PORT=8080 go run ./cmd/server
|
|
|
|
# With a .env file (requires a shell that sources it, or use envdir/dotenv)
|
|
export $(cat .env | grep -v '#' | xargs)
|
|
./website
|
|
```
|
|
|
|
## Configuration
|
|
|
|
All configuration is via environment variables. See `.env.example` for the full list.
|
|
|
|
| Variable | Default | Description |
|
|
|-----------------------|----------------------|---------------------------------------|
|
|
| `PORT` | `8080` | Listen port |
|
|
| `SITE_URL` | `https://ridgwaysystems.org` | Public URL for RSS feed links |
|
|
| `CONTENT_DIR` | `content` | Path to content directory |
|
|
| `DATA_DIR` | `data` | Path to data directory (status.json) |
|
|
| `DEV` | `0` | Set `1` for template hot-reloading |
|
|
| `ADMIN_PASSWORD_HASH` | — | bcrypt hash of admin password |
|
|
| `SESSION_SECRET` | — | HMAC signing key for session cookies |
|
|
|
|
### Generating an admin password hash
|
|
|
|
```sh
|
|
# Using htpasswd (from apache2-utils / httpd-tools)
|
|
htpasswd -bnBC 12 "" yourpassword | tr -d ':\n'
|
|
|
|
# Or write a small Go snippet:
|
|
# import "golang.org/x/crypto/bcrypt"
|
|
# hash, _ := bcrypt.GenerateFromPassword([]byte("yourpassword"), 12)
|
|
# fmt.Println(string(hash))
|
|
```
|
|
|
|
## Blog Post Format
|
|
|
|
Posts are Markdown files in `content/posts/`. Filenames become slugs unless overridden in
|
|
frontmatter.
|
|
|
|
```markdown
|
|
---
|
|
title: "Post Title"
|
|
date: 2025-01-15
|
|
tags: [openbsd, homelab]
|
|
slug: optional-custom-slug
|
|
description: "Short description for meta tags and post listings."
|
|
draft: false
|
|
---
|
|
|
|
Post content here. Standard Markdown with GFM extensions (tables, strikethrough, etc.)
|
|
Syntax highlighting for fenced code blocks is automatic.
|
|
```
|
|
|
|
## Status Page JSON Schema
|
|
|
|
`data/status.json` is read on each request to the `/status` page. A cron job or monitoring
|
|
script can write to this file to update service statuses.
|
|
|
|
```json
|
|
{
|
|
"last_checked": "2025-02-10T12:00:00Z",
|
|
"services": [
|
|
{
|
|
"name": "Web (httpd)",
|
|
"description": "ridgwaysystems.org",
|
|
"url": "https://ridgwaysystems.org",
|
|
"status": "up",
|
|
"note": "Optional note shown on status page"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Valid `status` values: `up`, `degraded`, `down`, `unknown`.
|
|
|
|
### Example cron update script
|
|
|
|
```sh
|
|
#!/bin/sh
|
|
# /usr/local/bin/check-services.sh
|
|
# Run every 5 minutes via cron. Updates data/status.json.
|
|
|
|
SITE_DIR=/var/www/ridgwaysystems
|
|
STATUS_FILE=$SITE_DIR/data/status.json
|
|
TMPFILE=$(mktemp)
|
|
|
|
check() {
|
|
name="$1"
|
|
url="$2"
|
|
if curl -sf --max-time 5 "$url" > /dev/null 2>&1; then
|
|
echo "up"
|
|
else
|
|
echo "down"
|
|
fi
|
|
}
|
|
|
|
WEB_STATUS=$(check "web" "https://ridgwaysystems.org")
|
|
GITEA_STATUS=$(check "gitea" "https://git.ridgwaysystems.org")
|
|
|
|
cat > "$TMPFILE" << EOF
|
|
{
|
|
"last_checked": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
"services": [
|
|
{"name": "Web (httpd)", "description": "ridgwaysystems.org", "status": "$WEB_STATUS"},
|
|
{"name": "Gitea", "description": "git.ridgwaysystems.org", "status": "$GITEA_STATUS"}
|
|
]
|
|
}
|
|
EOF
|
|
|
|
mv "$TMPFILE" "$STATUS_FILE"
|
|
```
|
|
|
|
## Deploying on FreeBSD
|
|
|
|
### 1. Build
|
|
|
|
On your control machine (or directly on the server):
|
|
|
|
```sh
|
|
GOOS=openbsd GOARCH=amd64 go build -o website ./cmd/server
|
|
```
|
|
|
|
Copy the binary and required files to the server:
|
|
|
|
```sh
|
|
scp website srv01:/usr/local/bin/website
|
|
rsync -av templates/ srv01:/var/www/ridgwaysystems/templates/
|
|
rsync -av static/ srv01:/var/www/ridgwaysystems/static/
|
|
rsync -av content/ srv01:/var/www/ridgwaysystems/content/
|
|
rsync -av data/ srv01:/var/www/ridgwaysystems/data/
|
|
```
|
|
|
|
### 2. rc.d service
|
|
|
|
Create `/etc/rc.d/website`:
|
|
|
|
```sh
|
|
#!/bin/ksh
|
|
|
|
daemon="/usr/local/bin/website"
|
|
daemon_user="www"
|
|
daemon_flags="-f"
|
|
|
|
. /etc/rc.d/rc.subr
|
|
|
|
rc_cmd $1
|
|
```
|
|
|
|
```sh
|
|
chmod +x /etc/rc.d/website
|
|
rcctl enable website
|
|
rcctl set website env "PORT=8080 SITE_URL=https://ridgwaysystems.org \
|
|
CONTENT_DIR=/var/www/ridgwaysystems/content \
|
|
DATA_DIR=/var/www/ridgwaysystems/data \
|
|
ADMIN_PASSWORD_HASH=<hash> SESSION_SECRET=<secret>"
|
|
rcctl start website
|
|
```
|
|
|
|
### 3. relayd configuration
|
|
|
|
Add to `/etc/relayd.conf` on fw01:
|
|
|
|
```
|
|
# ridgwaysystems.org
|
|
http protocol "ridgway-https" {
|
|
tls { keypair ridgwaysystems.org }
|
|
match request header set "X-Forwarded-For" value "$REMOTE_ADDR"
|
|
match request header set "X-Forwarded-Proto" value "https"
|
|
pass
|
|
}
|
|
|
|
relay "ridgway-web" {
|
|
listen on egress port 443 tls
|
|
protocol "ridgway-https"
|
|
forward to 10.0.10.10 port 8080 check http "/" code 200
|
|
}
|
|
```
|
|
|
|
### 4. httpd for Let's Encrypt ACME challenges (optional)
|
|
|
|
If using acme-client on fw01 for TLS certificates:
|
|
|
|
```
|
|
# /etc/httpd.conf on fw01
|
|
server "ridgwaysystems.org" {
|
|
listen on * port 80
|
|
location "/.well-known/acme-challenge/*" {
|
|
root "/acme"
|
|
request strip 2
|
|
}
|
|
location "*" {
|
|
block return 301 "https://ridgwaysystems.org$REQUEST_URI"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5. TLS certificates with acme-client
|
|
|
|
```
|
|
# /etc/acme-client.conf
|
|
authority letsencrypt {
|
|
api url "https://acme-v02.api.letsencrypt.org/directory"
|
|
account key "/etc/acme/letsencrypt-privkey.pem"
|
|
}
|
|
|
|
domain ridgwaysystems.org {
|
|
domain key "/etc/ssl/private/ridgwaysystems.org.key"
|
|
domain full chain certificate "/etc/ssl/ridgwaysystems.org.fullchain.pem"
|
|
sign with letsencrypt
|
|
}
|
|
```
|
|
|
|
Daily cron renewal in `/etc/daily.local`:
|
|
```sh
|
|
acme-client ridgwaysystems.org && rcctl reload relayd
|
|
```
|
|
|
|
## Admin Panel
|
|
|
|
The admin panel is available at `/admin`. Log in with the password configured in
|
|
`ADMIN_PASSWORD_HASH`.
|
|
|
|
Features:
|
|
- Create, edit, and delete blog posts with Markdown editor and live preview
|
|
- Edit the service status JSON directly
|
|
- Session expires after 24 hours
|
|
|
|
The admin panel is intentionally minimal — one user, no roles, no audit log. It's for
|
|
personal use on a self-hosted site.
|
|
|
|
## No External Dependencies
|
|
|
|
The site loads nothing from external servers. No CDN fonts, no analytics, no third-party
|
|
scripts. Everything is self-contained in `static/`. This is intentional.
|