init
This commit is contained in:
commit
c021228161
24 changed files with 1509 additions and 0 deletions
26
.env.example
Normal file
26
.env.example
Normal file
|
@ -0,0 +1,26 @@
|
|||
#This is designed to be used with network ACLs.
|
||||
TRUSTED_IPS=123.123.123.123/32,123.123.123.123/32
|
||||
|
||||
#Jekyll Config file to Run in the bundler, values are Dev,Prod,Both
|
||||
BUILD_TARGET=prod
|
||||
#This contols the admin panel in jekyll-buidler. If you are happy doing dev and testing on your own machine or you dont want the markdown you can just remove the whole jekyll-builder container.
|
||||
ADMIN_MODE=true
|
||||
# Change the line below to your user name and password //
|
||||
# echo $(htpasswd -nB [your-user-name]) | sed -e s/\\$/\\$\\$/g
|
||||
JEKYLL_ADMIN_USER_PASS=stewart:$$2y$$05$$y1DN2vwS5cL48efnsUrnS.cuLy2ugNAdWWqIepiVSDKBmCBZqVk6e
|
||||
|
||||
#Certs
|
||||
#Linode is just who I used. Hetzner is another great cloud provider.
|
||||
LINODE_API_KEY=dfjkafjdklajfklsdajfkadj
|
||||
EMAIL=for-the-certs@pm.me
|
||||
DOMAIN=motherfuckingblog.com
|
||||
|
||||
# CrowdSec Configuration
|
||||
# See crowdsec setup md doc.
|
||||
CROWDSEC_BOUNCER_API_KEY=31974091270phid
|
||||
|
||||
|
||||
#Analytics
|
||||
UMAMI_PG_PWD=hfdanmvnu29phnmvda791bvak
|
||||
UMAMI_HASH_SALT=kdsAkfNlSwoiSfdQWRnvdksVxse22edsa
|
||||
|
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Old files that are no longer needed
|
||||
setup.sh
|
||||
deploy-page
|
||||
|
||||
# Forgejo data
|
||||
forgejo-data/
|
||||
|
||||
# Only need to sync Jekyll-source
|
||||
sites/jekyll-source/prod-site/*
|
||||
sites/jekyll-source/dev-site/*
|
||||
sites/jekyll-source/jekyll-cache/*
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.idea.env
|
115
CROWDSEC_SETUP.md
Normal file
115
CROWDSEC_SETUP.md
Normal file
|
@ -0,0 +1,115 @@
|
|||
# CrowdSec Setup Instructions
|
||||
|
||||
This document provides instructions for setting up CrowdSec with Traefik in your environment.
|
||||
|
||||
## Initial Setup
|
||||
|
||||
1. First, start the services with a temporary API key:
|
||||
|
||||
```bash
|
||||
# Set CROWDSEC_BOUNCER_API_KEY to a temporary value in .env
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
2. Generate a bouncer API key:
|
||||
|
||||
```bash
|
||||
docker exec -it crowdsec cscli bouncers add traefik-bouncer
|
||||
```
|
||||
|
||||
3. Copy the generated API key and add it to your `.env` file:
|
||||
|
||||
```
|
||||
CROWDSEC_BOUNCER_API_KEY=your_generated_key_here
|
||||
```
|
||||
|
||||
4. Restart the services to apply the API key:
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Verify CrowdSec Installation
|
||||
|
||||
1. Check if CrowdSec is running properly:
|
||||
|
||||
```bash
|
||||
docker exec -it crowdsec cscli metrics
|
||||
```
|
||||
|
||||
2. List installed collections:
|
||||
|
||||
```bash
|
||||
docker exec -it crowdsec cscli collections list
|
||||
```
|
||||
|
||||
3. Test the CrowdSec setup:
|
||||
|
||||
```bash
|
||||
# Check if CrowdSec is properly connected to Traefik
|
||||
docker logs traefik | grep -i crowdsec
|
||||
|
||||
# Check if there are any decisions (blocks) in CrowdSec
|
||||
docker exec -it crowdsec cscli decisions list
|
||||
```
|
||||
|
||||
## Additional Security Configurations
|
||||
|
||||
### Install Additional Collections
|
||||
|
||||
You can install additional security collections for better protection:
|
||||
|
||||
```bash
|
||||
docker exec -it crowdsec cscli collections install crowdsecurity/http-cve
|
||||
docker exec -it crowdsec cscli collections install crowdsecurity/nginx
|
||||
docker exec -it crowdsec cscli collections install crowdsecurity/wordpress
|
||||
```
|
||||
|
||||
### Configure Custom Rules - Untested and from LLM
|
||||
|
||||
If you need custom security rules, you can create them in the CrowdSec configuration:
|
||||
|
||||
1. Create a custom rule file:
|
||||
|
||||
```bash
|
||||
docker exec -it crowdsec touch /etc/crowdsec/parsers/s00-custom/custom-rules.yaml
|
||||
```
|
||||
|
||||
2. Edit the file with your custom rules.
|
||||
|
||||
3. Restart CrowdSec:
|
||||
|
||||
```bash
|
||||
docker restart crowdsec
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Check Logs
|
||||
|
||||
If you encounter issues, check the logs:
|
||||
|
||||
```bash
|
||||
# CrowdSec logs
|
||||
docker logs crowdsec
|
||||
|
||||
# Traefik logs (includes bouncer plugin logs)
|
||||
docker logs traefik
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **API Key Issues**: If the bouncer can't connect to CrowdSec, verify the API key is correct.
|
||||
|
||||
2. **No Decisions**: If CrowdSec isn't blocking anything, check if it's receiving logs:
|
||||
|
||||
```bash
|
||||
docker exec -it crowdsec cscli metrics
|
||||
```
|
||||
|
||||
3. **False Positives**: If legitimate traffic is being blocked, you can add exceptions:
|
||||
|
||||
```bash
|
||||
docker exec -it crowdsec cscli decisions delete --ip 192.168.1.100
|
||||
```
|
219
README.md
Normal file
219
README.md
Normal file
|
@ -0,0 +1,219 @@
|
|||
# Self-Hosted Blog Platform
|
||||
|
||||
A secure, privacy-focused blogging starter kit that keeps your data away from big tech. Deploy a complete blog platform with version control, analytics, and security in minutes.
|
||||
|
||||
## What This Is
|
||||
|
||||
A **self-hosted starter kit** for creating blogs that are safer and respect end-users without dependencies on big tech platforms. Entirely free and open source. Do What the Fuck You Want To Public License (Standard WTFPLv2), plus the licenses of the underlying software.
|
||||
|
||||
**For 99% of users**: A fast, secure blog with no ads, no tracking, and complete data ownership.
|
||||
|
||||
**For developers**: A Jekyll-based static site generator with modern DevOps practices baked in.
|
||||
|
||||
**For privacy advocates**: 1st party analytics, no cookies, and complete control over your data.
|
||||
|
||||
## Features
|
||||
|
||||
- **🚀 Jekyll**: Static site generator with automated builds. Less-style Theme fork included.
|
||||
- **🔒 Traefik**: Reverse proxy for segmentation with automatic SSL, security headers, and IP allowlisting
|
||||
- **📊 Umami**: Privacy-focused analytics (no cookies, 1st party data)
|
||||
- **🛡️ CrowdSec**: Crowdsourced WAF with real-time threat intelligence
|
||||
- **📝 Jekyll Admin**: WYSIWYG markdown editor so your boss's nephew can ~~justify his existence~~ write blog posts.
|
||||
- **🔧 Forgejo**: Self-hosted Git platform for version control
|
||||
- **⚡ Nginx**: High-performance static file serving
|
||||
- **🔐 Security**: IP allowlisting, TLS certificates, and redundant access controls
|
||||
|
||||
## Who This Is For
|
||||
|
||||
This platform satisfies the needs of four key audiences:
|
||||
|
||||
### 🧑💻 Developers
|
||||
- Trusted Jekyll ecosystem with static deployment
|
||||
- Runs efficiently on small cloud instances
|
||||
- Version control through Forgejo (optional but included)
|
||||
- Docker-based deployment for consistency
|
||||
|
||||
### ✍️ Content Creators
|
||||
- WYSIWYG Markdown editor with live preview
|
||||
- SEO optimizations built-in
|
||||
- Automated site building on content changes
|
||||
- Separate dev/prod environments
|
||||
|
||||
### 📈 Analytics & Data Teams
|
||||
- 1st party analytics without cookies
|
||||
- Complete data ownership
|
||||
- Privacy-compliant user tracking
|
||||
- No dependency on Google Analytics
|
||||
|
||||
### 🔒 Security Teams
|
||||
- Crowdsourced WAF protection
|
||||
- IP allowlisting for admin interfaces
|
||||
- Automatic TLS certificate management
|
||||
- Redundant access controls
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Clone and configure**:
|
||||
```bash
|
||||
git clone <this-repo>
|
||||
cd self-hosted-blog
|
||||
cp .env.example .env
|
||||
vim .env # Configure your domain and settings
|
||||
```
|
||||
|
||||
2. **Deploy**:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
3. **Access your services**:
|
||||
- **Blog**: `https://yourdomain.com` (public)
|
||||
- **Admin**: `https://admin.yourdomain.com:333` (IP restricted)
|
||||
- **Analytics**: `https://analytics.yourdomain.com:333` (IP restricted)
|
||||
- **Git**: `https://git.yourdomain.com:333` (IP restricted)
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Minimum**: 1 shared CPU, 2GB RAM
|
||||
|
||||
Tested successfully on Linode shared instances.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Key variables to configure in your `.env` file:
|
||||
|
||||
```bash
|
||||
# Domain Configuration for Routes and LetsEncrypt
|
||||
DOMAIN=yourdomain.com
|
||||
EMAIL=admin@yourdomain.com
|
||||
|
||||
# Security
|
||||
TRUSTED_IPS=192.168.1.0/24,10.0.0.0/8 # Your allowed IP ranges
|
||||
JEKYLL_ADMIN_USER_PASS=admin:$2y$10$... # Generated with htpasswd
|
||||
|
||||
# SSL Configuration (Linode DNS example)
|
||||
LINODE_API_KEY=your_linode_api_key
|
||||
|
||||
# Analytics
|
||||
UMAMI_PG_PWD=secure_password_here
|
||||
UMAMI_HASH_SALT=random_salt_here
|
||||
|
||||
# CrowdSec
|
||||
CROWDSEC_BOUNCER_API_KEY=your_crowdsec_key
|
||||
```
|
||||
|
||||
### DNS & Firewall Setup
|
||||
|
||||
1. **DNS Records**: Point your domain and subdomains to your server IP
|
||||
2. **Firewall**:
|
||||
- Open ports 80, 443 (public)
|
||||
- Restrict port 333 to trusted IPs only
|
||||
3. **SSL**: Configured automatically via Let's Encrypt
|
||||
|
||||
## Content Management
|
||||
|
||||
### Creating Content
|
||||
|
||||
**Option 1: Web Interface** (Recommended)
|
||||
- Access Jekyll Admin at `https://admin.yourdomain.com:333/admin`
|
||||
- Create/edit posts with live preview
|
||||
- Automatic site rebuilding
|
||||
|
||||
**Option 2: Direct File Editing**
|
||||
- Edit files in `sites/jekyll-source/_posts/`
|
||||
- Follow naming: `YYYY-MM-DD-title.md`
|
||||
- Add front matter for metadata
|
||||
|
||||
### Jekyll Build Environments
|
||||
|
||||
Control build targets with the `BUILD_TARGET` environment variable:
|
||||
|
||||
- `BUILD_TARGET=prod` - Production site only
|
||||
- `BUILD_TARGET=dev` - Development site only
|
||||
- `BUILD_TARGET=both` - Both environments
|
||||
|
||||
## Security Features
|
||||
|
||||
### Multi-Layer Protection
|
||||
|
||||
1. **Network Level**: Cloud provider firewall + IP allowlisting
|
||||
2. **Application Level**: Traefik IP allowlisting + basic auth
|
||||
3. **WAF Level**: CrowdSec real-time threat detection
|
||||
4. **Transport Level**: Automatic TLS with security headers
|
||||
|
||||
### Admin Interface Security
|
||||
|
||||
- **IP Allowlisting**: Only trusted IPs can access admin interfaces
|
||||
- **Custom Port**: Non-standard port 333 for admin access
|
||||
- **Basic Auth**: HTTP authentication for Jekyll Admin
|
||||
- **TLS**: All traffic encrypted with Let's Encrypt certificates
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Use a VPN for remote administration
|
||||
- Regularly update the trusted IP list
|
||||
- Monitor CrowdSec logs for blocked attempts
|
||||
- Keep Docker images updated
|
||||
|
||||
## Customization
|
||||
|
||||
### Changing Jekyll Theme
|
||||
|
||||
1. Edit `sites/jekyll-source/Gemfile`
|
||||
2. Update `_config.yml` theme setting
|
||||
3. Run `bundle install` in the Jekyll container
|
||||
|
||||
### Adding Custom CSS/JS
|
||||
|
||||
- CSS: Edit `assets/css/style.scss`
|
||||
- JavaScript: Edit `assets/js/custom.js`
|
||||
|
||||
### Modifying Services
|
||||
|
||||
The Docker Compose stack is modular. Remove or modify services as needed:
|
||||
|
||||
- Don't need Git? Remove the `forgejo` service
|
||||
- Want different analytics? Replace `umami` with your preferred solution
|
||||
- Need additional security? Add more CrowdSec collections
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Can't access admin interfaces**:
|
||||
- Check your IP is in `TRUSTED_IPS`
|
||||
- Verify firewall rules on your cloud provider
|
||||
- Ensure port 333 is accessible from your location
|
||||
|
||||
**SSL certificate errors**:
|
||||
- Verify DNS records are pointing to your server
|
||||
- Check API credentials for your DNS provider
|
||||
- Review Traefik logs: `docker logs mf-traefik`
|
||||
|
||||
**Site not building**:
|
||||
- Check Jekyll container logs: `docker logs jekyll-builder`
|
||||
- Verify file permissions in `sites/jekyll-source/`
|
||||
- Ensure `BUILD_TARGET` is set correctly
|
||||
- Ensure your nginx volume is mounted at the correct site directory
|
||||
|
||||
### Getting Help
|
||||
|
||||
1. Check container logs: `docker logs <container-name>`
|
||||
2. Verify service status: `docker ps`
|
||||
3. Review configuration files for syntax errors
|
||||
4. Check DNS resolution: `nslookup yourdomain.com`
|
||||
|
||||
## Contributing
|
||||
|
||||
This is a starter kit meant to be forked and customized. Take what you need, modify or remove everything else.
|
||||
|
||||
**Known Issues**:
|
||||
- Jekyll Admin interface may show harmless error messages
|
||||
- Some themes may require additional configuration
|
||||
- Mobile responsiveness varies by theme choice
|
||||
|
||||
## License
|
||||
|
||||
Open source - take it, modify it, make it yours.
|
326
docker-compose.yml
Normal file
326
docker-compose.yml
Normal file
|
@ -0,0 +1,326 @@
|
|||
services:
|
||||
traefik:
|
||||
image: traefik:v3
|
||||
container_name: mf-traefik
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "333:333"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./docker/traefik/:/letsencrypt
|
||||
- traefik-config:/etc/traefik
|
||||
- traefik-logs:/var/log/traefik
|
||||
command:
|
||||
- --api.dashboard=false
|
||||
- --api.insecure=false
|
||||
- --providers.docker=true
|
||||
- --providers.docker.exposedbydefault=false
|
||||
- --entrypoints.web.address=:80
|
||||
- --entrypoints.websecure.address=:443
|
||||
- --entrypoints.custom.address=:333
|
||||
# Enable Traefik logs for CrowdSec
|
||||
- --accesslog=true
|
||||
- --accesslog.filepath=/var/log/traefik/access.log
|
||||
# CrowdSec Bouncer Plugin configuration
|
||||
- --experimental.plugins.crowdsec-bouncer-traefik-plugin.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||
- --experimental.plugins.crowdsec-bouncer-traefik-plugin.version=v1.4.2
|
||||
|
||||
# HTTP to HTTPS redirect
|
||||
- --entrypoints.web.http.redirections.entryPoint.to=websecure
|
||||
- --entrypoints.web.http.redirections.entryPoint.scheme=https
|
||||
|
||||
# Let's Encrypt configuration
|
||||
- --certificatesresolvers.linodedns.acme.email=${EMAIL}
|
||||
- --certificatesresolvers.linodedns.acme.storage=/letsencrypt/acme.json
|
||||
- --certificatesresolvers.linodedns.acme.dnschallenge=true
|
||||
- --certificatesresolvers.linodedns.acme.dnschallenge.provider=linode
|
||||
|
||||
environment:
|
||||
- LINODE_TOKEN=${LINODE_API_KEY}
|
||||
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.traefik.rule=Host(`traefik.${DOMAIN}`)"
|
||||
- "traefik.http.routers.traefik.entrypoints=custom"
|
||||
- "traefik.http.routers.traefik.service=api@internal"
|
||||
- "traefik.http.services.traefik.loadbalancer.server.port=8080"
|
||||
- "traefik.http.routers.traefik.tls=true"
|
||||
- "traefik.http.routers.traefik.tls.certresolver=linodedns"
|
||||
# CrowdSec Bouncer Plugin middleware configuration
|
||||
- "traefik.http.middlewares.crowdsec-bouncer.plugin.crowdsec-bouncer-traefik-plugin.crowdsecLapiKey=${CROWDSEC_BOUNCER_API_KEY}"
|
||||
- "traefik.http.middlewares.crowdsec-bouncer.plugin.crowdsec-bouncer-traefik-plugin.crowdsecLapiHost=crowdsec:8080"
|
||||
- "traefik.http.middlewares.crowdsec-bouncer.plugin.crowdsec-bouncer-traefik-plugin.crowdsecLapiScheme=http"
|
||||
- "traefik.http.middlewares.crowdsec-bouncer.plugin.crowdsec-bouncer-traefik-plugin.logLevel=INFO"
|
||||
# Security Headers Middleware
|
||||
- "traefik.http.middlewares.sec-headers.headers.customResponseHeaders.Strict-Transport-Security=max-age=31536000; includeSubDomains; preload"
|
||||
- "traefik.http.middlewares.sec-headers.headers.customResponseHeaders.X-Content-Type-Options=nosniff"
|
||||
- "traefik.http.middlewares.sec-headers.headers.customResponseHeaders.X-Frame-Options=DENY"
|
||||
- "traefik.http.middlewares.sec-headers.headers.customResponseHeaders.X-XSS-Protection=1; mode=block"
|
||||
- "traefik.http.middlewares.sec-headers.headers.customResponseHeaders.Referrer-Policy=strict-origin-when-cross-origin"
|
||||
forgejo:
|
||||
image: codeberg.org/forgejo/forgejo:11
|
||||
container_name: forgejo
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
- FORGEJO__server__DOMAIN=git.${DOMAIN}
|
||||
- FORGEJO__server__ROOT_URL=https://git.${DOMAIN}:333
|
||||
- FORGEJO__server__HTTP_PORT=3000
|
||||
- FORGEJO__server__STATIC_ROOT_PATH=/data/gitea/public
|
||||
- FORGEJO__service__ENABLE_BASIC_AUTHENTICATION=true
|
||||
- FORGEJO__ui__DEFAULT_THEME=gitea
|
||||
- FORGEJO__repository__ENABLE_PUSH_CREATE_USER=true
|
||||
- FORGEJO__repository__ENABLE_PUSH_CREATE_ORG=true
|
||||
- FORGEJO__repository__PREFERRED_LICENSES=MIT,Apache-2.0,GPL-3.0
|
||||
- FORGEJO__service__DISABLE_REGISTRATION=false
|
||||
- FORGEJO__service__REQUIRE_SIGNIN_VIEW=false
|
||||
- FORGEJO__picture__DISABLE_GRAVATAR=false
|
||||
- FORGEJO__repository__ENABLE_PUSH_CREATE=true
|
||||
- FORGEJO__repository__ACCESS_CONTROL_ALLOW_ORIGIN=*
|
||||
volumes:
|
||||
- forgejo-data:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.forgejo.rule=Host(`git.${DOMAIN}`)"
|
||||
- "traefik.http.routers.forgejo.tls=true"
|
||||
- "traefik.http.routers.forgejo.tls.certresolver=linodedns"
|
||||
- "traefik.http.routers.forgejo.entrypoints=custom"
|
||||
- "traefik.http.services.forgejo.loadbalancer.server.port=3000"
|
||||
- "traefik.http.middlewares.forgejo-allowlist.ipallowlist.sourcerange=${TRUSTED_IPS}"
|
||||
- "traefik.http.routers.forgejo.middlewares=forgejo-allowlist,sec-headers,crowdsec-bouncer"
|
||||
|
||||
jekyll:
|
||||
image: ruby:3.1
|
||||
container_name: jekyll-builder
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./sites/jekyll-source:/srv/jekyll
|
||||
- ./sites/jekyll-source/prod-site:/srv/jekyll/prod-site
|
||||
- ./sites/jekyll-source/dev-site:/srv/jekyll/dev-site
|
||||
environment:
|
||||
- JEKYLL_ENV=${JEKYLL_ENV:-development}
|
||||
- BUILD_TARGET=${BUILD_TARGET:-prod}
|
||||
- ADMIN_MODE=${ADMIN_MODE:-false}
|
||||
command: >
|
||||
/bin/sh -c '
|
||||
apt-get update && apt-get install -y build-essential libffi-dev zlib1g-dev &&
|
||||
cd /srv/jekyll &&
|
||||
gem install bundler jekyll jekyll-admin &&
|
||||
bundle install &&
|
||||
set -e # Exit on any error
|
||||
|
||||
echo "Build target: $BUILD_TARGET"
|
||||
echo "Admin mode: ${ADMIN_MODE:-false}"
|
||||
|
||||
if [ "$BUILD_TARGET" = "prod" ]; then
|
||||
echo "Building production site..."
|
||||
if [ "$ADMIN_MODE" = "true" ]; then
|
||||
echo "Starting Jekyll server in admin mode (prod)..."
|
||||
bundle exec jekyll serve \
|
||||
--host 0.0.0.0 \
|
||||
--port 4000 \
|
||||
--config _config.yml,_config_prod.yml,_config_admin.yml \
|
||||
--force_polling
|
||||
--destination ./prod-site
|
||||
else
|
||||
bundle exec jekyll build \
|
||||
--config _config.yml,_config_prod.yml \
|
||||
--destination /srv/jekyll/prod-site \
|
||||
--force_polling
|
||||
fi
|
||||
|
||||
elif [ "$BUILD_TARGET" = "dev" ]; then
|
||||
if [ "$ADMIN_MODE" = "true" ]; then
|
||||
echo "Starting Jekyll server in admin mode (dev)..."
|
||||
bundle exec jekyll serve \
|
||||
--host 0.0.0.0 \
|
||||
--port 4000 \
|
||||
--config _config.yml,_config_dev.yml,_config_admin.yml \
|
||||
--force_polling
|
||||
--destination ./dev-site
|
||||
else
|
||||
echo "Building development site..."
|
||||
bundle exec jekyll build \
|
||||
--watch \
|
||||
--force_polling \
|
||||
--config _config.yml,_config_dev.yml \
|
||||
--destination /srv/jekyll/dev-site
|
||||
fi
|
||||
|
||||
elif [ "$BUILD_TARGET" = "both" ]; then
|
||||
echo "Building both dev and prod sites..."
|
||||
|
||||
# PROD (non-watching)
|
||||
bundle exec jekyll build \
|
||||
--config _config.yml,_config_prod.yml \
|
||||
--destination /srv/jekyll/prod-site &
|
||||
|
||||
# DEV (non-serving, non-watching)
|
||||
bundle exec jekyll build \
|
||||
--config _config.yml,_config_dev.yml \
|
||||
--destination /srv/jekyll/dev-site &
|
||||
|
||||
wait
|
||||
|
||||
else
|
||||
echo "❌ Unknown or missing BUILD_TARGET: $BUILD_TARGET"
|
||||
echo "Please set BUILD_TARGET to dev, prod, or both"
|
||||
exit 1
|
||||
fi
|
||||
'
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.jekyll-admin.rule=Host(`admin.${DOMAIN}`)"
|
||||
- "traefik.http.routers.jekyll-admin.tls=true"
|
||||
- "traefik.http.routers.jekyll-admin.tls.certresolver=linodedns"
|
||||
- "traefik.http.routers.jekyll-admin.entrypoints=custom"
|
||||
- "traefik.http.services.jekyll-admin.loadbalancer.server.port=4000"
|
||||
- "traefik.http.middlewares.jekyll-allowlist.ipallowlist.sourcerange=${TRUSTED_IPS}"
|
||||
- "traefik.http.middlewares.jekyll-admin-auth.basicauth.users=${JEKYLL_ADMIN_USER_PASS}"
|
||||
- "traefik.http.routers.jekyll-admin.middlewares=jekyll-allowlist,jekyll-admin-auth,crowdsec-bouncer"
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: site-server
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./sites/jekyll-source/prod-site:/usr/share/nginx/html/prod:ro
|
||||
- ./sites/jekyll-source/dev-site:/usr/share/nginx/html/dev:ro
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# Main site configuration
|
||||
- "traefik.http.routers.site.rule=Host(`${DOMAIN}`)"
|
||||
- "traefik.http.routers.site.entrypoints=web,websecure"
|
||||
- "traefik.http.services.site.loadbalancer.server.port=80"
|
||||
- "traefik.http.routers.site.middlewares=sec-headers,crowdsec-bouncer,umami-cors,custom-server-header"
|
||||
- "traefik.http.routers.site.tls=true"
|
||||
- "traefik.http.routers.site.tls.certresolver=linodedns"
|
||||
# Main site CORS
|
||||
- "traefik.http.middlewares.prod-site-header.headers.accesscontrolallowmethods=GET,OPTIONS,PUT"
|
||||
- "traefik.http.middlewares.prod-site-header.headers.accesscontrolallowheaders=*"
|
||||
- "traefik.http.middlewares.prod-site-header.headers.accesscontrolalloworiginlist=https://fuckbigbro.${DOMAIN},https://${DOMAIN}"
|
||||
- "traefik.http.middlewares.prod-site-header.headers.accesscontrolmaxage=100"
|
||||
- "traefik.http.middlewares.prod-site-header.headers.addvaryheader=true"
|
||||
# Wildcard certificate definition
|
||||
- "traefik.http.routers.site.tls.domains[0].main=${DOMAIN}"
|
||||
- "traefik.http.routers.site.tls.domains[0].sans=*.${DOMAIN}"
|
||||
# Don't advertise your server stack unless you intend to:
|
||||
- "traefik.http.middlewares.custom-server-header.headers.customresponseheaders.Server=FuckOff"
|
||||
# Dev site configuration
|
||||
- "traefik.http.routers.site-dev.rule=Host(`dev.${DOMAIN}`)"
|
||||
- "traefik.http.routers.site-dev.entrypoints=custom"
|
||||
- "traefik.http.middlewares.site-allowlist.ipallowlist.sourcerange=${TRUSTED_IPS}"
|
||||
- "traefik.http.routers.site-dev.middlewares=site-allowlist,sec-headers,crowdsec-bouncer"
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:latest
|
||||
container_name: crowdsec
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- GID=1000
|
||||
- UID=1000
|
||||
- COLLECTIONS=crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/nginx
|
||||
volumes:
|
||||
- crowdsec-data:/var/lib/crowdsec/data
|
||||
- crowdsec-config:/etc/crowdsec
|
||||
- ./docker/crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
||||
- traefik-logs:/var/log/traefik:ro
|
||||
depends_on:
|
||||
- traefik
|
||||
|
||||
#########################################
|
||||
### BEGIN UMAMI ANALYTICS SECTION ###
|
||||
#########################################
|
||||
umami_db:
|
||||
image: postgres:16-alpine
|
||||
restart: always
|
||||
volumes:
|
||||
- umami-db-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_DB=umami
|
||||
- POSTGRES_USER=umami
|
||||
- POSTGRES_PASSWORD=${UMAMI_PG_PWD} # Set this in your .env
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U umami"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 1m
|
||||
|
||||
umami:
|
||||
image: ghcr.io/umami-software/umami:postgresql-latest
|
||||
restart: always
|
||||
depends_on:
|
||||
umami_db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://umami:${UMAMI_PG_PWD}@umami_db:5432/umami
|
||||
- DATABASE_TYPE=postgresql
|
||||
- HASH_SALT=${UMAMI_HASH_SALT}
|
||||
- TRACKER_SCRIPT_NAME=script.js
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
|
||||
# Main Umami interface (admin dashboard)
|
||||
- "traefik.http.routers.umami.rule=Host(`fuckbigbro.${DOMAIN}`)"
|
||||
- "traefik.http.routers.umami.tls=true"
|
||||
- "traefik.http.routers.umami.tls.certresolver=linodedns"
|
||||
- "traefik.http.services.umami.loadbalancer.server.port=3000"
|
||||
- "traefik.http.routers.umami.entrypoints=custom"
|
||||
- "traefik.http.middlewares.umami-allowlist.ipallowlist.sourcerange=${TRUSTED_IPS}"
|
||||
- "traefik.http.routers.umami.middlewares=umami-allowlist,sec-headers,crowdsec-bouncer"
|
||||
|
||||
# Public script endpoint - accessible via websecure (443) or web (80)
|
||||
- "traefik.http.routers.umami-script.rule=Host(`fuckbigbro.${DOMAIN}`) && Path(`/script.js`) && Method(`GET`)"
|
||||
- "traefik.http.routers.umami-script.entrypoints=websecure"
|
||||
- "traefik.http.routers.umami-script.service=umami"
|
||||
- "traefik.http.routers.umami-script.middlewares=crowdsec-bouncer"
|
||||
- "traefik.http.routers.umami-script.tls=true"
|
||||
- "traefik.http.routers.umami-script.tls.certresolver=linodedns"
|
||||
# Public API endpoint for collecting data
|
||||
- "traefik.http.routers.umami-api.rule=Host(`fuckbigbro.${DOMAIN}`) && PathPrefix(`/api/send`)"
|
||||
- "traefik.http.routers.umami-api.entrypoints=websecure"
|
||||
- "traefik.http.routers.umami-api.service=umami"
|
||||
- "traefik.http.routers.umami-api.middlewares=crowdsec-bouncer,sec-headers,umami-cors"
|
||||
- "traefik.http.routers.umami-api.tls=true"
|
||||
- "traefik.http.routers.umami-api.tls.certresolver=linodedns"
|
||||
# CORS middleware for API endpoints
|
||||
- "traefik.http.middlewares.umami-cors.headers.accesscontrolallowmethods=GET,POST,OPTIONS"
|
||||
- "traefik.http.middlewares.umami-cors.headers.accesscontrolalloworiginlist=https://fuckbigbro.${DOMAIN},https://${DOMAIN},https://www.${DOMAIN}"
|
||||
- "traefik.http.middlewares.umami-cors.headers.accesscontrolallowheaders=Content-Type,Authorization,X-Requested-With,X-Umami-Cache"
|
||||
- "traefik.http.middlewares.umami-cors.headers.accesscontrolmaxage=86400"
|
||||
- "traefik.http.middlewares.umami-cors.headers.addvaryheader=true"
|
||||
|
||||
# CORS Preflight OPTIONS handler
|
||||
- "traefik.http.routers.umami-options.rule=Host(`fuckbigbro.${DOMAIN}`) && PathPrefix(`/api/send`) && Method(`OPTIONS`)"
|
||||
- "traefik.http.routers.umami-options.entrypoints=websecure"
|
||||
- "traefik.http.routers.umami-options.middlewares=umami-cors"
|
||||
- "traefik.http.routers.umami-options.tls=true"
|
||||
- "traefik.http.routers.umami-options.tls.certresolver=linodedns"
|
||||
- "traefik.http.routers.umami-options.service=noop@internal"
|
||||
|
||||
volumes:
|
||||
umami-db-data:
|
||||
driver: local
|
||||
forgejo-data:
|
||||
driver: local
|
||||
traefik-config:
|
||||
driver: local
|
||||
crowdsec-data:
|
||||
driver: local
|
||||
crowdsec-config:
|
||||
driver: local
|
||||
traefik-logs:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: traefik
|
4
docker/crowdsec/acquis.yaml
Normal file
4
docker/crowdsec/acquis.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
filenames:
|
||||
- /var/log/traefik/access.log
|
||||
labels:
|
||||
type: traefik
|
33
docker/nginx/nginx.conf
Normal file
33
docker/nginx/nginx.conf
Normal file
|
@ -0,0 +1,33 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name motherfuckingblog.com;
|
||||
root /usr/share/nginx/html/prod;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Handle Hugo's(Not necessary with Jekyll) pretty URLs
|
||||
#location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
# expires 1y;
|
||||
# add_header Cache-Control "public, immutable";
|
||||
#}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name dev.motherfuckingblog.com;
|
||||
root /usr/share/nginx/html/dev;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Handle pretty URLs
|
||||
#location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
# expires 1y;
|
||||
# add_header Cache-Control "public, immutable";
|
||||
#}
|
||||
}
|
28
docker/traefik/acme.json
Normal file
28
docker/traefik/acme.json
Normal file
File diff suppressed because one or more lines are too long
30
sites/jekyll-source/Gemfile
Normal file
30
sites/jekyll-source/Gemfile
Normal file
|
@ -0,0 +1,30 @@
|
|||
source "https://rubygems.org"
|
||||
|
||||
# Jekyll and plugins
|
||||
gem "jekyll", "~> 4.3.2"
|
||||
|
||||
# Plugins
|
||||
group :jekyll_plugins do
|
||||
gem "jekyll-paginate"
|
||||
gem "jekyll-sitemap"
|
||||
gem "jekyll-feed"
|
||||
gem "jekyll-seo-tag"
|
||||
gem "jekyll-admin"
|
||||
end
|
||||
|
||||
# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
|
||||
# and associated library.
|
||||
platforms :mingw, :x64_mingw, :mswin, :jruby do
|
||||
gem "tzinfo", ">= 1", "< 3"
|
||||
gem "tzinfo-data"
|
||||
end
|
||||
|
||||
# Performance-booster for watching directories on Windows
|
||||
gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin]
|
||||
|
||||
# Lock gem to on JRuby builds since newer versions of the gem
|
||||
# do not have a Java counterpart.
|
||||
gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby]
|
||||
|
||||
# When running Jekyll locally
|
||||
gem "webrick", "~> 1.8"
|
126
sites/jekyll-source/Gemfile.lock
Normal file
126
sites/jekyll-source/Gemfile.lock
Normal file
|
@ -0,0 +1,126 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
base64 (0.3.0)
|
||||
colorator (1.1.0)
|
||||
concurrent-ruby (1.3.5)
|
||||
em-websocket (0.5.3)
|
||||
eventmachine (>= 0.12.9)
|
||||
http_parser.rb (~> 0)
|
||||
eventmachine (1.2.7)
|
||||
ffi (1.17.2-aarch64-linux-gnu)
|
||||
ffi (1.17.2-arm64-darwin)
|
||||
ffi (1.17.2-x86_64-darwin)
|
||||
ffi (1.17.2-x86_64-linux-gnu)
|
||||
forwardable-extended (2.6.0)
|
||||
http_parser.rb (0.8.0)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jekyll (4.3.4)
|
||||
addressable (~> 2.4)
|
||||
colorator (~> 1.0)
|
||||
em-websocket (~> 0.5)
|
||||
i18n (~> 1.0)
|
||||
jekyll-sass-converter (>= 2.0, < 4.0)
|
||||
jekyll-watch (~> 2.0)
|
||||
kramdown (~> 2.3, >= 2.3.1)
|
||||
kramdown-parser-gfm (~> 1.0)
|
||||
liquid (~> 4.0)
|
||||
mercenary (>= 0.3.6, < 0.5)
|
||||
pathutil (~> 0.9)
|
||||
rouge (>= 3.0, < 5.0)
|
||||
safe_yaml (~> 1.0)
|
||||
terminal-table (>= 1.8, < 4.0)
|
||||
webrick (~> 1.7)
|
||||
jekyll-admin (0.12.0)
|
||||
jekyll (>= 3.7, < 5.0)
|
||||
rackup (~> 2.0)
|
||||
sinatra (~> 4.0)
|
||||
sinatra-contrib (~> 4.0)
|
||||
jekyll-feed (0.17.0)
|
||||
jekyll (>= 3.7, < 5.0)
|
||||
jekyll-paginate (1.1.0)
|
||||
jekyll-sass-converter (2.2.0)
|
||||
sassc (> 2.0.1, < 3.0)
|
||||
jekyll-seo-tag (2.8.0)
|
||||
jekyll (>= 3.8, < 5.0)
|
||||
jekyll-sitemap (1.4.0)
|
||||
jekyll (>= 3.7, < 5.0)
|
||||
jekyll-watch (2.2.1)
|
||||
listen (~> 3.0)
|
||||
kramdown (2.5.1)
|
||||
rexml (>= 3.3.9)
|
||||
kramdown-parser-gfm (1.1.0)
|
||||
kramdown (~> 2.0)
|
||||
liquid (4.0.4)
|
||||
listen (3.9.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
logger (1.7.0)
|
||||
mercenary (0.4.0)
|
||||
multi_json (1.15.0)
|
||||
mustermann (3.0.3)
|
||||
ruby2_keywords (~> 0.0.1)
|
||||
pathutil (0.16.2)
|
||||
forwardable-extended (~> 2.6)
|
||||
public_suffix (5.1.1)
|
||||
rack (3.1.16)
|
||||
rack-protection (4.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
logger (>= 1.6.0)
|
||||
rack (>= 3.0.0, < 4)
|
||||
rack-session (2.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rackup (2.2.1)
|
||||
rack (>= 3)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.11.1)
|
||||
ffi (~> 1.0)
|
||||
rexml (3.4.1)
|
||||
rouge (3.30.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
safe_yaml (1.0.5)
|
||||
sassc (2.4.0)
|
||||
ffi (~> 1.9)
|
||||
sinatra (4.1.1)
|
||||
logger (>= 1.6.0)
|
||||
mustermann (~> 3.0)
|
||||
rack (>= 3.0.0, < 4)
|
||||
rack-protection (= 4.1.1)
|
||||
rack-session (>= 2.0.0, < 3)
|
||||
tilt (~> 2.0)
|
||||
sinatra-contrib (4.1.1)
|
||||
multi_json (>= 0.0.2)
|
||||
mustermann (~> 3.0)
|
||||
rack-protection (= 4.1.1)
|
||||
sinatra (= 4.1.1)
|
||||
tilt (~> 2.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
tilt (2.6.0)
|
||||
unicode-display_width (2.6.0)
|
||||
webrick (1.9.1)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
universal-darwin-24
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
http_parser.rb (~> 0.6.0)
|
||||
jekyll (~> 4.3.2)
|
||||
jekyll-admin
|
||||
jekyll-feed
|
||||
jekyll-paginate
|
||||
jekyll-seo-tag
|
||||
jekyll-sitemap
|
||||
tzinfo (>= 1, < 3)
|
||||
tzinfo-data
|
||||
wdm (~> 0.1.1)
|
||||
webrick (~> 1.8)
|
||||
|
||||
BUNDLED WITH
|
||||
2.3.25
|
76
sites/jekyll-source/_config.yml
Normal file
76
sites/jekyll-source/_config.yml
Normal file
|
@ -0,0 +1,76 @@
|
|||
# Jekyll configuration with less-style-please theme
|
||||
title: Motherfucking Blog
|
||||
tagline: Sick of All the Shit
|
||||
description: >-
|
||||
Turning a joke into a self-hosted starter kit for motherfucking blogs.
|
||||
url: ''
|
||||
baseurl: ''
|
||||
lang: en-US
|
||||
timezone: UTC
|
||||
|
||||
# Author information
|
||||
author:
|
||||
name: Stewart Pidasso
|
||||
email: stu[at][this-site].com
|
||||
links:
|
||||
- https://github.com/Steward-Pidasso
|
||||
- https://twitter.com/username
|
||||
|
||||
# GitHub repository
|
||||
repository: username/repo-name
|
||||
|
||||
# Jekyll settings
|
||||
markdown: kramdown
|
||||
highlighter: rouge
|
||||
permalink: /:year/:month/:day/:title/
|
||||
paginate: 10
|
||||
|
||||
# Plugins
|
||||
plugins:
|
||||
- jekyll-paginate
|
||||
- jekyll-sitemap
|
||||
- jekyll-feed
|
||||
- jekyll-seo-tag
|
||||
|
||||
# Exclude files/folders
|
||||
exclude:
|
||||
- Gemfile
|
||||
- Gemfile.lock
|
||||
- node_modules
|
||||
- vendor
|
||||
- _site
|
||||
- .sass-cache
|
||||
- .jekyll-cache
|
||||
- gemfiles
|
||||
- vendor/bundle/
|
||||
- vendor/cache/
|
||||
- vendor/gems/
|
||||
- vendor/ruby/
|
||||
|
||||
jekyll_admin:
|
||||
#You will likely need to change this depending on your template.
|
||||
metadata:
|
||||
_posts:
|
||||
- name: "layout"
|
||||
field:
|
||||
element: "hidden" # Change from "text" to "hidden"
|
||||
value: "post"
|
||||
- name: "title"
|
||||
field:
|
||||
element: "text"
|
||||
label: "Post title"
|
||||
- name: "date" # Add this field
|
||||
field:
|
||||
element: "text"
|
||||
label: "Date"
|
||||
value: "CURRENT_DATETIME" # This will use the current date/time
|
||||
- name: "categories"
|
||||
field:
|
||||
element: "text"
|
||||
label: "Categories (comma-separated)"
|
||||
placeholder: "category1, category2"
|
||||
- name: "tags"
|
||||
field:
|
||||
element: "text"
|
||||
label: "Tags (comma-separated)"
|
||||
placeholder: "tag1, tag2"
|
3
sites/jekyll-source/_config_admin.yml
Normal file
3
sites/jekyll-source/_config_admin.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Jekyll Admin specific configuration
|
||||
url: 'http://admin.localhost:333'
|
||||
baseurl: ''
|
9
sites/jekyll-source/_config_dev.yml
Normal file
9
sites/jekyll-source/_config_dev.yml
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Development environment configuration
|
||||
url: 'http://dev.localhost'
|
||||
baseurl: ''
|
||||
environment: development
|
||||
destination: dev-site
|
||||
# Additional development-specific settings
|
||||
safe: false
|
||||
future: true
|
||||
unpublished: true
|
14
sites/jekyll-source/_config_prod.yml
Normal file
14
sites/jekyll-source/_config_prod.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Production environment configuration
|
||||
url: 'http://localhost'
|
||||
baseurl: 'https://motherfuckingblog.com'
|
||||
environment: production
|
||||
|
||||
# Additional production-specific settings
|
||||
safe: true
|
||||
future: false
|
||||
unpublished: false
|
||||
destination: prod-site
|
||||
|
||||
|
||||
# Analytics and SEO settings for production
|
||||
google_analytics: # Don't Add your Google Analytics ID here. They are a bad company.
|
1
sites/jekyll-source/_includes/analytics/umami.html
Normal file
1
sites/jekyll-source/_includes/analytics/umami.html
Normal file
|
@ -0,0 +1 @@
|
|||
<script defer src="https://fuckbigbro.motherfuckingblog.com/script.js" data-website-id="7a8d87bd-5232-42fb-8aad-5f05283b7115"></script>
|
27
sites/jekyll-source/_layouts/default.html
Normal file
27
sites/jekyll-source/_layouts/default.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ site.lang | default: "en-US" }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% if page.title %}{{ page.title | escape }} - {{ site.title | escape }}{% else %}{{ site.title | escape }}{% endif %}</title>
|
||||
<meta name="description" content="{{ page.description | default: site.description | strip_html | normalize_whitespace | truncate: 160 | escape }}">
|
||||
<link rel="stylesheet" href="{{ "/assets/css/style.css" | relative_url }}">
|
||||
{% feed_meta %}
|
||||
{% seo %}
|
||||
{% include analytics/umami.html %}
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1><a href="{{ "/" | relative_url }}">{{ site.title | escape }}</a></h1>
|
||||
<p>{{ site.tagline | escape }}</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{{ content }}
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© {{ site.time | date: '%Y' }} {{ site.author.name | default: site.title }}</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
24
sites/jekyll-source/_layouts/home.html
Normal file
24
sites/jekyll-source/_layouts/home.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
layout: default
|
||||
---
|
||||
<div class="home">
|
||||
{{ content }}
|
||||
|
||||
<h2>Recent Posts</h2>
|
||||
|
||||
<ul class="post-list">
|
||||
{% for post in site.posts limit:10 %}
|
||||
<li>
|
||||
<h3>
|
||||
<a class="post-link" href="{{ post.url | relative_url }}">{{ post.title | escape }}</a>
|
||||
</h3>
|
||||
<p class="post-meta">{{ post.date | date: "%b %-d, %Y" }}</p>
|
||||
{% if post.description %}
|
||||
<p>{{ post.description }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<p><a href="{{ "/archive" | relative_url }}">View all posts</a></p>
|
||||
</div>
|
46
sites/jekyll-source/_layouts/post.html
Normal file
46
sites/jekyll-source/_layouts/post.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
layout: default
|
||||
---
|
||||
<article class="post">
|
||||
<header class="post-header">
|
||||
<h1 class="post-title">{{ page.title | escape }}</h1>
|
||||
<p class="post-meta">
|
||||
<time datetime="{{ page.date | date_to_xmlschema }}">{{ page.date | date: "%b %-d, %Y" }}</time>
|
||||
{% if page.author %}
|
||||
• <span>{{ page.author }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="post-content">
|
||||
{{ content }}
|
||||
</div>
|
||||
|
||||
{% if page.tags.size > 0 %}
|
||||
<div class="post-tags">
|
||||
<p>Tags:
|
||||
{% for tag in page.tags %}
|
||||
<a href="{{ site.baseurl }}/tags/#{{ tag | slugify }}">{{ tag }}</a>{% unless forloop.last %}, {% endunless %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
<br>
|
||||
<h2>Recent Posts</h2>
|
||||
|
||||
<ul class="post-list">
|
||||
{% for post in site.posts limit:10 %}
|
||||
<li>
|
||||
<h3>
|
||||
<a class="post-link" href="{{ post.url | relative_url }}">{{ post.title | escape }}</a>
|
||||
</h3>
|
||||
<p class="post-meta">{{ post.date | date: "%b %-d, %Y" }}</p>
|
||||
{% if post.description %}
|
||||
<p>{{ post.description }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<p><a href="{{ "/archive" | relative_url }}">View all posts</a></p>
|
88
sites/jekyll-source/_posts/2023-05-01-welcome-to-jekyll.md
Normal file
88
sites/jekyll-source/_posts/2023-05-01-welcome-to-jekyll.md
Normal file
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
title: Boilerplate Jekyll How-to
|
||||
layout: post
|
||||
date: '2025-05-01 12:00:00 +0000'
|
||||
categories:
|
||||
- jekyll
|
||||
tags:
|
||||
- getting-started
|
||||
---
|
||||
|
||||
# Welcome to Jekyll!
|
||||
|
||||
This is your first post using Jekyll with the less-style theme. Jekyll is a static site generator that transforms your plain text into static websites and blogs.
|
||||
|
||||
## How to Create Posts
|
||||
|
||||
To create a new post, simply add a file in the `_posts` directory that follows the naming convention `YYYY-MM-DD-title.md`, where `YYYY-MM-DD` is the date of your post and `title` is the title of your post.
|
||||
|
||||
At the top of each post, you need to include what's called "front matter" - this is YAML that tells Jekyll how to process the file. Here's an example:
|
||||
|
||||
```yaml
|
||||
---
|
||||
layout: post
|
||||
title: "Your Post Title"
|
||||
date: 2023-05-01 12:00:00 -0000
|
||||
categories: [category1, category2]
|
||||
tags: [tag1, tag2]
|
||||
---
|
||||
```
|
||||
|
||||
## Markdown Formatting
|
||||
|
||||
Jekyll uses Markdown for formatting. Here are some examples:
|
||||
|
||||
### Headers
|
||||
|
||||
```markdown
|
||||
# H1
|
||||
## H2
|
||||
### H3
|
||||
```
|
||||
|
||||
### Emphasis
|
||||
|
||||
```markdown
|
||||
*italic* or _italic_
|
||||
**bold** or __bold__
|
||||
```
|
||||
|
||||
### Lists
|
||||
|
||||
```markdown
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Subitem 2.1
|
||||
- Subitem 2.2
|
||||
|
||||
1. First item
|
||||
2. Second item
|
||||
```
|
||||
|
||||
### Links and Images
|
||||
|
||||
```markdown
|
||||
[Link text](URL)
|
||||

|
||||
```
|
||||
|
||||
### Code
|
||||
|
||||
```markdown
|
||||
`inline code`
|
||||
|
||||
```python
|
||||
# Code block with syntax highlighting
|
||||
def hello_world():
|
||||
print("Hello, world!")
|
||||
```
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Customize your site by editing `_config.yml`
|
||||
2. Create new posts in the `_posts` directory
|
||||
3. Add pages by creating new markdown files in the root directory
|
||||
4. Customize the theme by overriding CSS in `assets/css/style.scss`
|
||||
|
||||
Happy blogging with Jekyll!
|
83
sites/jekyll-source/_posts/2025-06-22-wtf-is-this.md
Normal file
83
sites/jekyll-source/_posts/2025-06-22-wtf-is-this.md
Normal file
|
@ -0,0 +1,83 @@
|
|||
---
|
||||
title: WTF Is this? Gimme the deets.
|
||||
layout: post
|
||||
date: '2025-07-06 15:11:26'
|
||||
---
|
||||
|
||||
[TL;DR Github Repo](http://git.motherfuckingblog.com)
|
||||
|
||||
For **99% of users**:
|
||||
- a <20kb blog about web infrastructure that they don't understand
|
||||
- they found high due to its 99% SEO Scores
|
||||
- seems different, but they don't quite know why?
|
||||
- "yeah its whatever. no ads was nice." - Average Chad
|
||||
|
||||
For **OG [MotherfuckingWebsite.com](https://motherfuckingwebsite.com)** enjoyers:
|
||||
- A way to learn about static site generators like **Jekyll** (Check out Hugo or 11ty too if this concept is new to you)
|
||||
-- A way to systemize and organize if you plan on having more than a handful of posts.
|
||||
-- Extensible, but not in the gross Wordpress way. For example, add search, table of contents, and more.
|
||||
- Some cool other shit in docker compose that makes it easier to own your data and your infrastructure.
|
||||
|
||||
|
||||
For **fucks sake whats all this other shit**:
|
||||
|
||||
- Isn't it weird products don't ship with **security** natively included? It's opinionated, but its safer than default in the othr shit.
|
||||
- *Traefik* - Reverse proxy your management interfaces and IP Allowlist them in an easy way. Secure yourself against any dumb idea you are bound to add later.
|
||||
- *Crowdsec WAF* - Crowdsec is legit innovating and deserves more attention. Crowdsourced blacklisting and malicious signature detection.
|
||||
- Data Privacy:
|
||||
- *Forgejo* - FOSS Github. Better Post Revision History than Wordpress. But
|
||||
- *Umami* - FOSS Google Analytics Alternative. Privacy forward, 1st party.
|
||||
|
||||
|
||||
## How TF Do I Make A Blog
|
||||
Configure your DNS and Firewall on your cloud provider. You should restrict port `333` to trust IP addresses only.
|
||||
|
||||
In the example I have used Linode API to get a wildcard cert. If you don't care about TLS, you are wrong, but I get it. Traefik will create self signed certs for you, but you will likely need to remove or modify the TLS settings in the compose file.
|
||||
|
||||
Once you have set up your cloud provider:
|
||||
|
||||
```
|
||||
git clone thisRepo
|
||||
cd thisRepo
|
||||
cp .env.example .env
|
||||
vim .env # make it match your environment
|
||||
docker compose up
|
||||
```
|
||||
### Deployment
|
||||
It is capable of running on a 1 shared CPU, with 2GB of RAM as tested on Linode.
|
||||
|
||||
Running on 2 shared CPUs with 4 GB of ram is sufficient for the full stack to run as smooth as butter for 99% of all users. On my personal fork unrelated to motherfuckingblog.com, I keep my Forgejo instance on another server with a Nextcloud instance and additional hardening. This isn't a bible, its a starter kit to make it easier for normal people to iterate from. Take what you need and throw the rest out.
|
||||
|
||||
|
||||
|
||||
### Security
|
||||
Products should ship secure by default or at the least have a small handful of options that are easy to configure and harden.
|
||||
|
||||
This comes with Crowdsec WAF which will share limited data with crowdsec. If strict data isolation is essential. You need to remove this and consider a different WAF. There are alternatives, but at least be aware of what you are losing. Crowdsec with AppSec protects against human laziness. Essentially it acts as a real-time updated block list that matches against known malicious signatures in outdated software that you probably haven't patched yet, ya filthy animal.
|
||||
|
||||
All Admin Interfaces are restricted to an IP Allowlist. Almost every Org has VPNs, this is what they are meant for.
|
||||
**If you are a noob** and you just `curl icanhazip.com ` then paste in your IP. *Prepare to temporarily lose access*. Your ISP will rotate your IP and you will think you are SOL. You are not. You need your own private VPN (which is actually easy to do), or you need to bind these to the local interface then perform ssh port forwarding anytime you want to perform maintenance. That sounds scary, but I promise it is like two simple commands. I'm pretty sure you can just change the port binding at the top of the compose file from `333:333` to `127.0.0.1:333:333`. Then you simply `ssh user@remote -L 333:127.0.0.1:333`
|
||||
|
||||
The Jekyll Admin interface had no authentication by default so basic http authentication was added via Traefik.
|
||||
|
||||
Additionally there is a strong benefit to using a non-standard port / custom entry point in Traefik (port 333 in this example) in the docker-compose file for this project. This allows for redudant whitelisting. On my cloud provider I also whitelist my IP address to those ports and deny all others. This means even if traefik IPAllowlisting is bypassed via some hacker black magic, I have a secondary defense.
|
||||
|
||||
|
||||
|
||||
#### Random Aside
|
||||
|
||||
It's just insane to me we just leave admin login portals open in the wild. WTF are we doing?
|
||||
|
||||
> Please guess employee passwords on my public website an unlimited number of times.
|
||||
> \- **Every Chief Information Security Officer**
|
||||
|
||||
|
||||
### Issues
|
||||
|
||||
##### It's ugly!
|
||||
While, I find this beautiful. You may perfer one of the thousands of free themes for Jekyll which will make your site look exactly like the corporate garbage flooding the internet.
|
||||
|
||||
##### X Feature is Broken
|
||||
Yep! Personally I know of the error that appears, but is incorrect in jekyll admin interface. But honestly that is at least half the point. If you properly restrict your broken tech behind network level access controls it mitigates the risk for a large portion of people.
|
||||
|
||||
Also this isn't a pick it up and make an identical blog. It is a starter kit. I know half of you are going to rip Jekyll admin out. If you plan on using it you are welcome to open issues with them or figure out the way to use. It works for my purposes and allows me to create, edit, and tag posts. I need nothing else.
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
title: Obligatory Rant/Background
|
||||
layout: post
|
||||
---
|
||||
|
||||
All I wanted was a simple small af static blog where I owned the stack and the data. But the **problems**:
|
||||
* Thinking about CSS between paragraphs ruins my ability to write.
|
||||
* TinyMCE's minified and compressed core is [100x larger](https://github.com/tinymce/tinymce/issues/4028) than my entire front end codebase.
|
||||
* Managing more than a dozen posts is a nightmare.
|
||||
* Wordpress is somehow even worse.
|
||||
* Need to add search, tags, or any other features? Almost everything requires Node, PHP, or something insane that is probably millions of times larger than my entire codebase.
|
||||
* Hosting? Every Jekyll post is how to host on Microsoft's Github. (Surely Microsoft would [never](https://arstechnica.com/information-technology/2025/02/copilot-exposes-private-github-pages-some-removed-by-microsoft/) [misuse](https://www.wired.com/story/github-commercial-ai-tool-built-open-source-code/) your data. )
|
||||
* Want to know if anyone even looked at your site? Good thing Google hasn't been using your data to literally facilitate a genocide or anything... [oh wait](https://web.archive.org/web/20240720084622/https://www.wired.com/story/amazon-google-project-nimbus-israel-idf/).
|
||||
|
||||
|
||||
So this is my opinionated attempt to move the needle the other direction.
|
||||
|
||||
**If you didn't understand anything I just said** throw it in an LLM/"AI" and ask it to explain. You can understanding anything you set your mind to. Don't doubt yourself.
|
||||
|
||||
**For the Nerds who are still with me**: This is a ***self-hosted*** **starter kit** for people sick of big bro. It is at the point where it could be taken several directions.
|
||||
- Dev and Prod versions of the site for development? Check.
|
||||
- 10kb static site? Check
|
||||
- Layers of security? Check
|
||||
- First Party data? Check
|
||||
|
||||
Check out other posts for some ideas or to learn more. In short, this is an opinionated **Docker Compose** project that uses:
|
||||
- **Traefik** for reverse proxying all admin interfaces.
|
||||
- **Jekyll** the same tech behind Github Pages, to build static sites.
|
||||
- **Nginx** as the lightweight webserver.
|
||||
- **Forgejo** for version control.
|
||||
- **Umami Analytics** we are all a little vain.
|
||||
- **Crowdsec WAF** for additional security.
|
||||
|
||||
This gives me a clean, cloud-based markdown writing environment with proper version control and monitoring.
|
||||
|
||||
|
||||
### Deployment
|
||||
It is capable of running on a 1 shared CPU, with 2GB of RAM. RAM is the limiting factor.
|
||||
Running on 2 shared CPUs with 4 GB of ram is sufficient for the full stack to run as smooth as butter for 99.9% of all users.
|
||||
|
||||
On my personal fork unrelated to motherfuckingblog.com, I keep my Forgejo instance on another server with a Nextcloud instance and additional hardening. This isn't a bible, its a starter kit to make it easier for normal people to iterate from. Take what you need and throw the rest out.
|
||||
|
||||
|
||||
|
||||
### Security
|
||||
Products should ship secure by default or at least have a small handful of options that are easy to configure and harden the product.
|
||||
|
||||
This comes with Crowdsec WAF which will share limited data with crowdsec. If strict data isolation is essential. You need to remove this and consider a different WAF. There are alternatives, but at least be aware of what you are losing. Crowdsec with AppSec protects against human laziness. Essentially **it acts as a real-time updated block list** that matches against known malicious signatures in outdated software that you probably haven't patched yet, ya filthy animal.
|
||||
|
||||
***All Admin Interfaces are restricted to an IP Allowlist.*** Almost every Org has VPNs, this is what they are meant for.
|
||||
*If you are a complete noob*. It is okay. You will need to learn a bit to use this project, but it is within grasp. Don't discount yourself. **There is a big gotcha here.** If you just `curl icanhazip.com ` then paste in your IP on the allowlist. It will work. YAY! Party time! But *prepare to temporarily lose access*. Your ISP will rotate your IP and you will think you are shit outta luck. You are not. Ideally you would use your own private VPN (which they literally have scripts to deploy, it's easy to do), or you need to bind these to the local interface then perform ssh port forwarding anytime you want to perform maintenance/ view your dashboards. That sounds scary, but I promise it is like two simple commands. Though some additional config is probable.
|
||||
|
||||
The **Jekyll Admin interface had no authentication by default so basic http authentication was added** via Traefik. For God's sake, keep your admin interfaces from being externally accessible to anyone but trusted individuals.
|
||||
|
||||
|
||||
Lastly, there is a strong benefit to **using a non-standard port / custom entry point in Traefik** (port 333 in this example) in the docker-compose file for this project. This allows for redudant whitelisting. On my cloud provider I also whitelist my IP address to those ports and deny all others. This means even if Traefik `IPAllowlist` is bypassed via some hacker black magic, I have a secondary defense.
|
32
sites/jekyll-source/archive.md
Normal file
32
sites/jekyll-source/archive.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
layout: default
|
||||
title: Archive
|
||||
---
|
||||
|
||||
# Archive
|
||||
{::nomarkdown}
|
||||
{% assign current_year = "" %}
|
||||
{% for post in site.posts %}
|
||||
{% assign post_year = post.date | date: "%Y" %}
|
||||
|
||||
{% if post_year != current_year %}
|
||||
{% unless forloop.first %}
|
||||
</ul>
|
||||
{% endunless %}
|
||||
|
||||
<h2 id="y{{ post_year }}">{{ post_year }}</h2>
|
||||
<ul class="post-list">
|
||||
|
||||
{% assign current_year = post_year %}
|
||||
{% endif %}
|
||||
|
||||
<li>
|
||||
<span class="post-meta">{{ post.date | date: "%b %-d" }}</span>
|
||||
<a href="{{ post.url | relative_url }}">{{ post.title }}</a>
|
||||
</li>
|
||||
|
||||
{% if forloop.last %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{:/}
|
102
sites/jekyll-source/assets/css/style.scss
Normal file
102
sites/jekyll-source/assets/css/style.scss
Normal file
|
@ -0,0 +1,102 @@
|
|||
---
|
||||
---
|
||||
|
||||
// less-style-please theme styling
|
||||
// Inspired by https://feeshy.github.io/less-style-please/
|
||||
|
||||
html {
|
||||
font-size: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: 650px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
header, footer {
|
||||
text-align: center;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0366d6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
font-size: 0.9em;
|
||||
background-color: #f6f8fa;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 1rem;
|
||||
border-left: 4px solid #ddd;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
.post-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.post-list li {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 2rem 0;
|
||||
}
|
26
sites/jekyll-source/index.md
Normal file
26
sites/jekyll-source/index.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
title: Motherfucking Blog
|
||||
layout: home
|
||||
---
|
||||
|
||||
## What if it wasn't satire?
|
||||
The OG motherfuckingwebsite.com (and the spinoffs) all insisted it was satire, but what if it wasn't? Seriously, look at all this unvarnished negative space. It is beautiful. Does your brain even know what to do when it isn't being inundated with ads or upsells?
|
||||
|
||||
<div markdown="0"><br><br></div>
|
||||
|
||||
##### Just stop and dwell in the empty space.
|
||||
|
||||
<div markdown="0"><br><br><br></div>
|
||||
|
||||
|
||||
This isn't meditation. This is respect for you as a living being. You have probably never seen it on the internet.
|
||||
|
||||
You came here to see one madman's ravings and that is all you are getting. No insane nagging for a login or subscription. No selling you some shit you don't need. No cookies or their pain in the ass banners. No giving your data to other people. ***It's just you and an idea. It's fucking magical.*** I want it to happen more often so I put it in an easy to replicate package.
|
||||
|
||||
At a high-level, this is **self-hosted starter-kit of a blog platform** which makes it easier for tech and tech-curious people to create a diversity of blogs which are safer and respect end-users without any explicit dependencies on big tech. Oh and of course it is entirely free.
|
||||
|
||||
It satisfies the basic needs of four teams:
|
||||
- **Devs**: Trusted Jekyll ecosystem with statically deployed pages that can run on a very small instance. Version control through Forgejo optional, but included.
|
||||
- **Content Creators**: WYSIWYG Markdown editor with SEO optimizations.
|
||||
- **Analytics and Data**: 1st party data without cookies.
|
||||
- **Security**: Crowdsourced WAF. IP Allowlisting. TLS. Reverse Proxy. Redudant controls.
|
Loading…
Add table
Add a link
Reference in a new issue