Initial commit: 23 docker stacks for GitOps deployment

Stacks included:
- Infrastructure: traefik, authentik, gitea, registry, watchtower, dockge
- Monitoring: smokeping, changedetection
- Apps: ghost, gollum, wallabag, radicale, invidious, xbackbone, filebrowser, syncthing, zerotier
- Custom: obsidian-tools, memento, perilous, ramz, bookclub, brain

🤖 Generated with Claude Code
This commit is contained in:
knight 2025-12-31 13:29:43 -05:00
commit 4dbb0b9180
56 changed files with 1390 additions and 0 deletions

114
.gitea/workflows/deploy.yml Normal file
View File

@ -0,0 +1,114 @@
name: Deploy Stacks
on:
push:
branches: [main]
paths:
- 'stacks/**'
workflow_dispatch:
inputs:
stack:
description: 'Stack to deploy (or "all")'
required: true
default: 'all'
env:
STACKS_DIR: /var/core
jobs:
detect-changes:
runs-on: ubuntu-prod
outputs:
stacks: ${{ steps.changes.outputs.stacks }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Find changed stacks
id: changes
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
if [ "${{ github.event.inputs.stack }}" = "all" ]; then
STACKS=$(ls stacks/ | tr '\n' ' ')
else
STACKS="${{ github.event.inputs.stack }}"
fi
else
STACKS=$(git diff --name-only HEAD~1 HEAD | grep '^stacks/' | cut -d'/' -f2 | sort -u | tr '\n' ' ')
fi
echo "stacks=$STACKS" >> $GITHUB_OUTPUT
echo "Detected stacks to deploy: $STACKS"
deploy:
needs: detect-changes
if: needs.detect-changes.outputs.stacks != ''
runs-on: ubuntu-prod
strategy:
matrix:
stack: ${{ fromJson(format('["{0}"]', join(fromJson(format('["{0}"]', replace(needs.detect-changes.outputs.stacks, ' ', '","'))), '","'))) }}
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Create .env file
run: |
cd stacks/${{ matrix.stack }}
if [ -f ".env.template" ]; then
envsubst < .env.template > .env
fi
env:
# Global
DOMAIN: ${{ secrets.DOMAIN }}
VOLUMES_ROOT: ${{ secrets.VOLUMES_ROOT }}
# Traefik
ACME_EMAIL: ${{ secrets.ACME_EMAIL }}
# Authentik
AUTHENTIK_SECRET_KEY: ${{ secrets.AUTHENTIK_SECRET_KEY }}
AUTHENTIK_PG_PASS: ${{ secrets.AUTHENTIK_PG_PASS }}
# Immich
IMMICH_DB_PASSWORD: ${{ secrets.IMMICH_DB_PASSWORD }}
# Planka
PLANKA_SECRET_KEY: ${{ secrets.PLANKA_SECRET_KEY }}
PLANKA_OIDC_CLIENT_ID: ${{ secrets.PLANKA_OIDC_CLIENT_ID }}
PLANKA_OIDC_CLIENT_SECRET: ${{ secrets.PLANKA_OIDC_CLIENT_SECRET }}
# Registry
REGISTRY_HTTP_SECRET: ${{ secrets.REGISTRY_HTTP_SECRET }}
REGISTRY_HTPASSWD: ${{ secrets.REGISTRY_HTPASSWD }}
# Memento
MEMENTO_AUTH_SECRET: ${{ secrets.MEMENTO_AUTH_SECRET }}
MEMENTO_AUTHENTIK_CLIENT_ID: ${{ secrets.MEMENTO_AUTHENTIK_CLIENT_ID }}
MEMENTO_AUTHENTIK_CLIENT_SECRET: ${{ secrets.MEMENTO_AUTHENTIK_CLIENT_SECRET }}
# Bookclub
BOOKCLUB_SMTP_HOST: ${{ secrets.BOOKCLUB_SMTP_HOST }}
BOOKCLUB_SMTP_USER: ${{ secrets.BOOKCLUB_SMTP_USER }}
BOOKCLUB_SMTP_PASS: ${{ secrets.BOOKCLUB_SMTP_PASS }}
BOOKCLUB_MAIL_FROM: ${{ secrets.BOOKCLUB_MAIL_FROM }}
BOOKCLUB_MAIL_TO: ${{ secrets.BOOKCLUB_MAIL_TO }}
BOOKCLUB_SECRET_PHRASE: ${{ secrets.BOOKCLUB_SECRET_PHRASE }}
# Perilous
PERILOUS_CODE_SERVER_PASSWORD: ${{ secrets.PERILOUS_CODE_SERVER_PASSWORD }}
- name: Deploy ${{ matrix.stack }}
run: |
STACK_DIR="${{ env.STACKS_DIR }}/${{ matrix.stack }}"
# Create stack directory if needed
mkdir -p "$STACK_DIR"
# Copy files to stack directory
cp -r stacks/${{ matrix.stack }}/* "$STACK_DIR/"
cd "$STACK_DIR"
# Pull and deploy
docker compose pull --ignore-pull-failures || true
docker compose up -d --remove-orphans
echo "✅ Deployed ${{ matrix.stack }}"
- name: Verify deployment
run: |
sleep 5
cd ${{ env.STACKS_DIR }}/${{ matrix.stack }}
docker compose ps

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Environment files (secrets are in Gitea)
.env
*.env
!.env.template
!.env.example
# Data directories
*/data/
*/postgres/
*/redis/
*/config/
*/upload/
*/media/
*/certs/
acme.json
# OS files
.DS_Store
Thumbs.db
# Editor files
*.swp
*.swo
*~
.idea/
.vscode/

141
README.md Normal file
View File

@ -0,0 +1,141 @@
# Docker Stacks
GitOps-managed Docker Compose stacks. Push changes to `main` branch and Gitea Actions will automatically deploy.
## Structure
```
stacks/
├── traefik/ # Reverse proxy + SSL
├── authentik/ # SSO/Identity provider
├── registry/ # Docker registry
├── immich/ # Photo management
├── planka/ # Kanban boards
├── syncthing/ # File sync
├── filebrowser/ # Web file manager
├── memento/ # Custom app
├── obsidian-tools/ # Obsidian vault tools
├── perilous/ # Blog/website
├── ramz/ # Go web app
├── bookclub/ # Form mailer
├── watchtower/ # Auto container updates
├── dockge/ # Container management UI
└── smokeping/ # Network monitoring
```
## How It Works
1. Edit compose files in `stacks/<service>/`
2. Commit and push to `main`
3. Gitea Actions detects changed stacks
4. Deploys only the changed stacks to `/var/core/<service>/`
## Manual Deploy
```bash
# Deploy single stack
./scripts/deploy.sh traefik
# Deploy all stacks
./scripts/deploy.sh all
```
## Required Gitea Secrets
Set these in Gitea → Repository → Settings → Actions → Secrets:
### Global
| Secret | Description |
|--------|-------------|
| `DOMAIN` | Base domain (e.g., `ghost.tel`) |
| `VOLUMES_ROOT` | Data root path (e.g., `/var/core`) |
| `ACME_EMAIL` | Email for Let's Encrypt |
### Authentik
| Secret | Description |
|--------|-------------|
| `AUTHENTIK_SECRET_KEY` | Generate: `openssl rand -hex 50` |
| `AUTHENTIK_PG_PASS` | PostgreSQL password |
### Immich
| Secret | Description |
|--------|-------------|
| `IMMICH_DB_PASSWORD` | PostgreSQL password |
### Planka
| Secret | Description |
|--------|-------------|
| `PLANKA_SECRET_KEY` | Generate: `openssl rand -hex 64` |
| `PLANKA_OIDC_CLIENT_ID` | Authentik client ID |
| `PLANKA_OIDC_CLIENT_SECRET` | Authentik client secret |
### Registry
| Secret | Description |
|--------|-------------|
| `REGISTRY_HTTP_SECRET` | Generate: `openssl rand -hex 32` |
### Memento
| Secret | Description |
|--------|-------------|
| `MEMENTO_AUTH_SECRET` | Auth.js secret |
| `MEMENTO_AUTHENTIK_CLIENT_ID` | Authentik client ID |
| `MEMENTO_AUTHENTIK_CLIENT_SECRET` | Authentik client secret |
### Bookclub
| Secret | Description |
|--------|-------------|
| `BOOKCLUB_SMTP_HOST` | SMTP server |
| `BOOKCLUB_SMTP_USER` | SMTP username |
| `BOOKCLUB_SMTP_PASS` | SMTP password |
| `BOOKCLUB_MAIL_FROM` | From email |
| `BOOKCLUB_MAIL_TO` | Recipient email |
| `BOOKCLUB_SECRET_PHRASE` | Form submission secret |
### Perilous
| Secret | Description |
|--------|-------------|
| `PERILOUS_CODE_SERVER_PASSWORD` | Code-server password |
## Runner Setup
The workflow requires a self-hosted runner on the prod server:
```bash
# On ubuntu-prod, register a Gitea runner
# See: https://docs.gitea.com/usage/actions/act-runner
# Install act_runner
wget https://gitea.com/gitea/act_runner/releases/download/v0.2.6/act_runner-0.2.6-linux-amd64
chmod +x act_runner-*
sudo mv act_runner-* /usr/local/bin/act_runner
# Register with Gitea
act_runner register --no-interactive \
--instance https://gitea.ghost.tel \
--token <runner-token> \
--name ubuntu-prod \
--labels ubuntu-prod
# Run as service
act_runner daemon
```
## First-Time Setup
1. Create the `web` Docker network:
```bash
docker network create web
```
2. Create `acme.json` for Traefik:
```bash
touch /var/core/traefik/acme.json
chmod 600 /var/core/traefik/acme.json
```
3. Deploy traefik first:
```bash
./scripts/deploy.sh traefik
```
4. Then deploy other stacks as needed.

48
scripts/deploy.sh Executable file
View File

@ -0,0 +1,48 @@
#!/bin/bash
# Manual deploy script for docker-stacks
# Usage: ./deploy.sh [stack-name|all]
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
STACKS_DIR="/var/core"
deploy_stack() {
local stack=$1
echo "🚀 Deploying $stack..."
local src="$REPO_ROOT/stacks/$stack"
local dest="$STACKS_DIR/$stack"
if [ ! -d "$src" ]; then
echo "❌ Stack $stack not found in $src"
return 1
fi
# Create destination directory
mkdir -p "$dest"
# Copy files
cp -r "$src"/* "$dest/"
# Deploy
cd "$dest"
docker compose pull --ignore-pull-failures 2>/dev/null || true
docker compose up -d --remove-orphans
echo "✅ Deployed $stack"
}
if [ -z "$1" ] || [ "$1" = "all" ]; then
echo "Deploying all stacks..."
for stack_dir in "$REPO_ROOT/stacks"/*/; do
stack=$(basename "$stack_dir")
deploy_stack "$stack"
done
else
deploy_stack "$1"
fi
echo ""
echo "🎉 Deployment complete!"

View File

@ -0,0 +1,3 @@
DOMAIN=${DOMAIN}
AUTHENTIK_SECRET_KEY=${AUTHENTIK_SECRET_KEY}
AUTHENTIK_PG_PASS=${AUTHENTIK_PG_PASS}

View File

@ -0,0 +1,75 @@
services:
server:
image: ghcr.io/goauthentik/server:latest
container_name: authentik-server
restart: unless-stopped
command: server
environment:
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_PG_PASS}
volumes:
- ./media:/media
- ./custom-templates:/templates
env_file:
- .env
depends_on:
- postgresql
- redis
networks:
- web
- default
labels:
- "traefik.enable=true"
- "traefik.http.routers.authentik.entrypoints=https"
- "traefik.http.routers.authentik.rule=Host(`authentik.${DOMAIN}`)"
- "traefik.http.routers.authentik.tls.certresolver=http"
- "traefik.http.services.authentik.loadbalancer.server.port=9000"
worker:
image: ghcr.io/goauthentik/server:latest
container_name: authentik-worker
restart: unless-stopped
command: worker
environment:
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_PG_PASS}
volumes:
- ./media:/media
- ./certs:/certs
- ./custom-templates:/templates
env_file:
- .env
depends_on:
- postgresql
- redis
postgresql:
image: postgres:15-alpine
container_name: authentik-postgres
restart: unless-stopped
environment:
POSTGRES_PASSWORD: ${AUTHENTIK_PG_PASS}
POSTGRES_USER: authentik
POSTGRES_DB: authentik
volumes:
- ./postgres:/var/lib/postgresql/data
redis:
image: redis:alpine
container_name: authentik-redis
restart: unless-stopped
command: --save 60 1 --loglevel warning
volumes:
- ./redis:/data
networks:
web:
external: true

View File

@ -0,0 +1,14 @@
DOMAIN=${DOMAIN}
SMTP_HOST=${BOOKCLUB_SMTP_HOST}
SMTP_PORT=465
SMTP_SECURE=true
SMTP_USER=${BOOKCLUB_SMTP_USER}
SMTP_PASS=${BOOKCLUB_SMTP_PASS}
MAIL_FROM=${BOOKCLUB_MAIL_FROM}
MAIL_TO=${BOOKCLUB_MAIL_TO}
SECRET_PHRASE=${BOOKCLUB_SECRET_PHRASE}
THANK_YOU_URL=/thanks.html
BIND_HOST=0.0.0.0
BIND_PORT=3000
TRUST_PROXY=true
TZ=America/New_York

View File

@ -0,0 +1,7 @@
FROM node:20-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY server.js ./
EXPOSE 3000
CMD ["node", "server.js"]

View File

@ -0,0 +1,20 @@
services:
form-mailer:
build: .
container_name: bookclub-form-mailer
restart: unless-stopped
env_file:
- .env
labels:
- "traefik.enable=true"
- "traefik.http.routers.form-mailer.rule=Host(`bookclub.${DOMAIN}`)"
- "traefik.http.routers.form-mailer.entrypoints=https"
- "traefik.http.routers.form-mailer.tls=true"
- "traefik.http.routers.form-mailer.tls.certresolver=http"
- "traefik.http.services.form-mailer.loadbalancer.server.port=3000"
networks:
- web
networks:
web:
external: true

View File

@ -0,0 +1,13 @@
{
"name": "bookclub-form-mailer",
"version": "1.0.0",
"description": "Simple form mailer service",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"nodemailer": "^6.9.7"
}
}

51
stacks/bookclub/server.js Normal file
View File

@ -0,0 +1,51 @@
const express = require('express');
const nodemailer = require('nodemailer');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
if (process.env.TRUST_PROXY === 'true') {
app.set('trust proxy', true);
}
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT || '465'),
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
app.post('/submit', async (req, res) => {
const { secret, ...formData } = req.body;
if (secret !== process.env.SECRET_PHRASE) {
return res.status(403).send('Invalid secret phrase');
}
try {
await transporter.sendMail({
from: process.env.MAIL_FROM,
to: process.env.MAIL_TO,
subject: 'New Form Submission',
text: Object.entries(formData)
.map(([key, value]) => `${key}: ${value}`)
.join('\n')
});
res.redirect(process.env.THANK_YOU_URL || '/thanks.html');
} catch (error) {
console.error('Mail error:', error);
res.status(500).send('Failed to send email');
}
});
const port = parseInt(process.env.BIND_PORT || '3000');
const host = process.env.BIND_HOST || '0.0.0.0';
app.listen(port, host, () => {
console.log(`Form mailer listening on ${host}:${port}`);
});

View File

@ -0,0 +1,3 @@
DOMAIN=${DOMAIN}
BRAIN_SFTP_USER=${BRAIN_SFTP_USER}
BRAIN_SFTP_PASSWORD=${BRAIN_SFTP_PASSWORD}

View File

@ -0,0 +1,32 @@
services:
web:
image: nginx:latest
container_name: brain
restart: unless-stopped
expose:
- 80
volumes:
- ./public:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.brain.entrypoints=https"
- "traefik.http.routers.brain.rule=Host(`brain.${DOMAIN}`)"
- "traefik.http.routers.brain.tls.certresolver=http"
- "traefik.http.routers.brain.middlewares=auth@file"
sftp:
image: atmoz/sftp
container_name: brain-sftp
restart: unless-stopped
volumes:
- ./public:/home/commander/upload
ports:
- "2232:22"
command: ${BRAIN_SFTP_USER}:${BRAIN_SFTP_PASSWORD}:1001
networks:
web:
external: true

View File

@ -0,0 +1 @@
DOMAIN=${DOMAIN}

View File

@ -0,0 +1,40 @@
services:
changedetection:
image: ghcr.io/dgtlmoon/changedetection.io
container_name: changedetection
hostname: changedetection
restart: unless-stopped
volumes:
- ./data:/datastore
environment:
- PLAYWRIGHT_DRIVER_URL=ws://playwright-chrome:3000/?stealth=1&--disable-web-security=true
ports:
- 5000:5000
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.changedetection.entrypoints=https"
- "traefik.http.routers.changedetection.rule=Host(`change.${DOMAIN}`)"
- "traefik.http.routers.changedetection.tls.certresolver=http"
- "traefik.http.routers.changedetection.middlewares=auth@file"
depends_on:
- playwright-chrome
playwright-chrome:
image: dgtlmoon/sockpuppetbrowser:latest
hostname: playwright-chrome
restart: unless-stopped
cap_add:
- SYS_ADMIN
environment:
- SCREEN_WIDTH=1920
- SCREEN_HEIGHT=1024
- SCREEN_DEPTH=16
- MAX_CONCURRENT_CHROME_PROCESSES=10
networks:
- web
networks:
web:
external: true

View File

@ -0,0 +1 @@
DOMAIN=${DOMAIN}

View File

@ -0,0 +1,25 @@
services:
dockge:
image: louislam/dockge:1
container_name: dockge
restart: unless-stopped
ports:
- 5001:5001
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/app/data
- /var/core:/var/core
environment:
- DOCKGE_STACKS_DIR=/var/core
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.dockge.rule=Host(`dockge.${DOMAIN}`)"
- "traefik.http.routers.dockge.entrypoints=https"
- "traefik.http.routers.dockge.tls.certresolver=http"
- "traefik.http.services.dockge.loadbalancer.server.port=5001"
networks:
web:
external: true

View File

@ -0,0 +1,2 @@
DOMAIN=${DOMAIN}
FILE_ROOT=/srv

View File

@ -0,0 +1,22 @@
services:
filebrowser:
image: filebrowser/filebrowser:latest
container_name: filebrowser
restart: unless-stopped
volumes:
- ./config/filebrowser.db:/database.db
- ${FILE_ROOT:-/srv}:/srv
expose:
- 80
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.filebrowser.entrypoints=https"
- "traefik.http.routers.filebrowser.rule=Host(`files.${DOMAIN}`)"
- "traefik.http.routers.filebrowser.tls.certresolver=http"
- "traefik.http.services.filebrowser.loadbalancer.server.port=80"
networks:
web:
external: true

View File

@ -0,0 +1,2 @@
DOMAIN=${DOMAIN}
GHOST_DB_PASSWORD=${GHOST_DB_PASSWORD}

View File

@ -0,0 +1,37 @@
services:
ghost:
image: ghost:4-alpine
container_name: ghost
restart: unless-stopped
expose:
- 2368
networks:
- web
- default
environment:
database__client: mysql
database__connection__host: db
database__connection__user: root
database__connection__password: ${GHOST_DB_PASSWORD}
database__connection__database: ghost
url: https://ghost.${DOMAIN}
volumes:
- ./content:/var/lib/ghost/content
labels:
- "traefik.enable=true"
- "traefik.http.routers.ghost.entrypoints=https"
- "traefik.http.routers.ghost.rule=Host(`ghost.${DOMAIN}`)"
- "traefik.http.routers.ghost.tls.certresolver=http"
db:
image: mysql:5.7
container_name: ghost-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${GHOST_DB_PASSWORD}
volumes:
- ./mysql:/var/lib/mysql
networks:
web:
external: true

View File

@ -0,0 +1,4 @@
DOMAIN=${DOMAIN}
GITEA_DB_ROOT_PASSWORD=${GITEA_DB_ROOT_PASSWORD}
GITEA_DB_PASSWORD=${GITEA_DB_PASSWORD}
GITEA_RUNNER_TOKEN=${GITEA_RUNNER_TOKEN}

View File

@ -0,0 +1,58 @@
services:
web:
image: gitea/gitea:latest
container_name: gitea
restart: unless-stopped
volumes:
- ./data:/data
expose:
- "3000"
- "22"
ports:
- "2245:22"
depends_on:
- db
environment:
- DOMAIN=gitea.${DOMAIN}
- SSH_DOMAIN=gitea.${DOMAIN}
- SSH_PORT=2245
- SSH_LISTEN_PORT=22
networks:
- default
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.gitea.entrypoints=https"
- "traefik.http.routers.gitea.rule=Host(`gitea.${DOMAIN}`)"
- "traefik.http.routers.gitea.tls.certresolver=http"
- "traefik.http.services.gitea.loadbalancer.server.port=3000"
db:
image: mariadb:10
container_name: gitea-db
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=${GITEA_DB_ROOT_PASSWORD}
- MYSQL_DATABASE=gitea
- MYSQL_USER=gitea
- MYSQL_PASSWORD=${GITEA_DB_PASSWORD}
- MARIADB_AUTO_UPGRADE=1
volumes:
- ./db:/var/lib/mysql
runner:
image: gitea/act_runner
container_name: gitea-runner
restart: unless-stopped
depends_on:
- web
volumes:
- ./runner-data:/data
- /var/run/docker.sock:/var/run/docker.sock
environment:
- GITEA_INSTANCE_URL=https://gitea.${DOMAIN}
- GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_TOKEN}
networks:
web:
external: true

View File

@ -0,0 +1 @@
DOMAIN=${DOMAIN}

View File

@ -0,0 +1,24 @@
services:
gollum:
image: gollumwiki/gollum:master
container_name: gollum
restart: unless-stopped
volumes:
- ./config.rb:/etc/gollum/config.rb
- ./wikidata:/wiki
command:
- "--config=/etc/gollum/config.rb"
expose:
- 4567
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.gollum.entrypoints=https"
- "traefik.http.routers.gollum.rule=Host(`gollum.${DOMAIN}`)"
- "traefik.http.routers.gollum.tls.certresolver=http"
- "traefik.http.routers.gollum.middlewares=auth@file"
networks:
web:
external: true

View File

@ -0,0 +1,5 @@
DOMAIN=${DOMAIN}
INVIDIOUS_DB_PASSWORD=${INVIDIOUS_DB_PASSWORD}
INVIDIOUS_VISITOR_DATA=${INVIDIOUS_VISITOR_DATA}
INVIDIOUS_PO_TOKEN=${INVIDIOUS_PO_TOKEN}
INVIDIOUS_HMAC_KEY=${INVIDIOUS_HMAC_KEY}

View File

@ -0,0 +1,67 @@
services:
invidious:
image: quay.io/invidious/invidious:latest
container_name: invidious
restart: unless-stopped
ports:
- "3001:3000"
environment:
INVIDIOUS_CONFIG: |
db:
dbname: invidious
user: kemal
password: ${INVIDIOUS_DB_PASSWORD}
host: invidious-db
port: 5432
check_tables: true
signature_server: inv_sig_helper:12999
visitor_data: ${INVIDIOUS_VISITOR_DATA}
po_token: ${INVIDIOUS_PO_TOKEN}
hmac_key: "${INVIDIOUS_HMAC_KEY}"
logging:
options:
max-size: "1G"
max-file: "4"
depends_on:
- invidious-db
labels:
- "traefik.enable=true"
- "traefik.http.services.invidious.loadbalancer.server.port=3000"
- "traefik.http.routers.invidious.entrypoints=https"
- "traefik.http.routers.invidious.rule=Host(`invid.${DOMAIN}`, `i.${DOMAIN}`)"
- "traefik.http.routers.invidious.tls.certresolver=http"
networks:
- web
- default
inv_sig_helper:
image: quay.io/invidious/inv-sig-helper:latest
container_name: inv-sig-helper
command: ["--tcp", "0.0.0.0:12999"]
environment:
- RUST_LOG=info
restart: unless-stopped
cap_drop:
- ALL
read_only: true
security_opt:
- no-new-privileges:true
invidious-db:
image: postgres:14
container_name: invidious-db
restart: unless-stopped
volumes:
- ./postgres:/var/lib/postgresql/data
- ./config/sql:/config/sql
- ./docker/init-invidious-db.sh:/docker-entrypoint-initdb.d/init-invidious-db.sh
environment:
POSTGRES_DB: invidious
POSTGRES_USER: kemal
POSTGRES_PASSWORD: ${INVIDIOUS_DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
networks:
web:
external: true

View File

@ -0,0 +1,6 @@
DOMAIN=${DOMAIN}
AUTH_SECRET=${MEMENTO_AUTH_SECRET}
AUTH_AUTHENTIK_CLIENT_ID=${MEMENTO_AUTHENTIK_CLIENT_ID}
AUTH_AUTHENTIK_CLIENT_SECRET=${MEMENTO_AUTHENTIK_CLIENT_SECRET}
AUTH_AUTHENTIK_ISSUER=https://authentik.${DOMAIN}/application/o/memento
AUTH_AUTHENTIK_REDIRECT_URI=https://memento.${DOMAIN}

View File

@ -0,0 +1,22 @@
services:
app:
image: gitea.${DOMAIN}/knight/memento:latest
container_name: memento
restart: unless-stopped
expose:
- "3002"
env_file:
- .env
networks:
- web
- default
labels:
- "traefik.enable=true"
- "traefik.http.routers.memento.entrypoints=https"
- "traefik.http.routers.memento.rule=Host(`memento.${DOMAIN}`)"
- "traefik.http.routers.memento.tls.certresolver=http"
- "traefik.http.services.memento.loadbalancer.server.port=3002"
networks:
web:
external: true

View File

@ -0,0 +1 @@
DOMAIN=${DOMAIN}

View File

@ -0,0 +1,87 @@
services:
obbytodo:
image: gitea.${DOMAIN}/knight/obbytodo:latest
container_name: obbytodo
restart: unless-stopped
expose:
- "3000"
environment:
- PORT=3000
- VAULT_PATH=/vault
- DEFAULT_TASK_FILE=Mobile Tasks.md
- DAILY_DIR=log
- DAILY_PRIMARY_DIR=Control/log
volumes:
- ./vault:/vault
networks:
- web
depends_on:
- syncthing
labels:
- "traefik.enable=true"
- "traefik.docker.network=web"
- "traefik.http.routers.todo-obbytodo.entrypoints=https"
- "traefik.http.routers.todo-obbytodo.rule=Host(`shell.${DOMAIN}`) && PathPrefix(`/todo`)"
- "traefik.http.routers.todo-obbytodo.tls.certresolver=http"
- "traefik.http.routers.todo-obbytodo.middlewares=todo-obbytodo-stripprefix@docker,dashboard-auth@file"
- "traefik.http.routers.todo-obbytodo.priority=100"
- "traefik.http.middlewares.todo-obbytodo-stripprefix.stripPrefix.prefixes=/todo"
- "traefik.http.services.todo-obbytodo.loadbalancer.server.port=3000"
- "traefik.http.routers.todo-obbytodo.service=todo-obbytodo"
- "traefik.http.routers.events-obbytodo.entrypoints=https"
- "traefik.http.routers.events-obbytodo.rule=Host(`shell.${DOMAIN}`) && PathPrefix(`/events`)"
- "traefik.http.routers.events-obbytodo.tls.certresolver=http"
- "traefik.http.routers.events-obbytodo.middlewares=dashboard-auth@file"
- "traefik.http.routers.events-obbytodo.priority=100"
- "traefik.http.routers.events-obbytodo.service=todo-obbytodo"
search-service:
image: gitea.${DOMAIN}/knight/obsidiansearch:latest
container_name: obsidian-search-service
restart: unless-stopped
expose:
- "3033"
environment:
- NODE_ENV=production
- OBSIDIAN_VAULT_PATH=/data
- GRAPH_DATA_PATH=/usr/src/app/graph-data/graph.json
volumes:
- ./vault:/data:ro
- ./graph-data:/usr/src/app/graph-data
depends_on:
- syncthing
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.docker.network=web"
- "traefik.http.routers.shell-secure.entrypoints=https"
- "traefik.http.routers.shell-secure.rule=Host(`shell.${DOMAIN}`)"
- "traefik.http.routers.shell-secure.tls.certresolver=http"
- "traefik.http.routers.shell-secure.middlewares=dashboard-auth@file"
- "traefik.http.services.shell-secure.loadbalancer.server.port=3033"
- "traefik.http.routers.shell-secure.service=shell-secure"
syncthing:
image: lscr.io/linuxserver/syncthing:latest
container_name: obsidian-syncthing
hostname: obsidian-syncthing
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- TZ=America/New_York
volumes:
- ./syncthing-config:/config
- ./vault:/data1
ports:
- "8385:8384"
- "22001:22000/tcp"
- "22001:22000/udp"
- "21028:21027/udp"
networks:
- web
networks:
web:
external: true

View File

@ -0,0 +1 @@
PERILOUS_CODE_SERVER_PASSWORD=${PERILOUS_CODE_SERVER_PASSWORD}

View File

@ -0,0 +1,7 @@
FROM node:18-alpine
WORKDIR /usr/src/app
COPY package.json ./
RUN npm install
COPY server.js ./
EXPOSE 3000
CMD ["node", "server.js"]

View File

@ -0,0 +1,43 @@
services:
web:
build: .
container_name: perilous-web
restart: unless-stopped
ports:
- "3003:3000"
volumes:
- ./content:/usr/src/app/content
labels:
- "traefik.enable=true"
- "traefik.http.routers.perilous-secure.entrypoints=https,http"
- "traefik.http.routers.perilous-secure.rule=Host(`chapel.perilous.dev`,`forest.perilous.dev`,`castle.perilous.dev`,`siege.perilous.dev`,`pass.perilous.dev`,`perilous.dev`,`www.perilous.dev`,`word.perilous.dev`,`mirror.perilous.dev`,`request.perilous.dev`,`the.chapel.perilous.dev`,`the.forest.perilous.dev`,`the.castle.perilous.dev`,`the.siege.perilous.dev`,`the.pass.perilous.dev`,`the.word.perilous.dev`,`the.mirror.perilous.dev`,`the.request.perilous.dev`,`the.adventure.perilous.dev`,`the.cs.perilous.dev`,`the.gallery.perilous.dev`,`gallery.perilous.dev`,`the.perilous.dev`,`ring.perilous.dev`,`the.ring.perilous.dev`,`autumn.perilous.dev`,`the.autumn.perilous.dev`)"
- "traefik.http.routers.perilous-secure.tls.certresolver=http"
networks:
- web
code-server:
image: lscr.io/linuxserver/code-server:latest
container_name: perilous-code-server
restart: unless-stopped
volumes:
- ./content:/home/project
- ./config:/config
ports:
- "8180:8443"
environment:
- PASSWORD=${PERILOUS_CODE_SERVER_PASSWORD}
- PUID=1000
- PGID=1000
- PROXY_DOMAIN=cs.perilous.dev
- DEFAULT_WORKSPACE=/home/project
labels:
- "traefik.enable=true"
- "traefik.http.routers.pcs-secure.entrypoints=https,http"
- "traefik.http.routers.pcs-secure.rule=Host(`cs.perilous.dev`)"
- "traefik.http.routers.pcs-secure.tls.certresolver=http"
networks:
- web
networks:
web:
external: true

View File

@ -0,0 +1,12 @@
{
"name": "perilous",
"version": "1.0.0",
"description": "Perilous static site server",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2"
}
}

24
stacks/perilous/server.js Normal file
View File

@ -0,0 +1,24 @@
const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();
const PORT = process.env.PORT || 3000;
const CONTENT_DIR = '/usr/src/app/content';
app.use(express.static(CONTENT_DIR));
app.get('*', (req, res) => {
const filePath = path.join(CONTENT_DIR, req.path);
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
const indexPath = path.join(filePath, 'index.html');
if (fs.existsSync(indexPath)) {
return res.sendFile(indexPath);
}
}
res.sendFile(path.join(CONTENT_DIR, 'index.html'));
});
app.listen(PORT, () => {
console.log(`Perilous server running on port ${PORT}`);
});

View File

@ -0,0 +1 @@
DOMAIN=${DOMAIN}

View File

@ -0,0 +1,35 @@
services:
radicale:
image: tomsquest/docker-radicale
container_name: radicale
restart: unless-stopped
init: true
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- SETUID
- SETGID
- CHOWN
- KILL
healthcheck:
test: curl -f http://127.0.0.1:5232 || exit 1
interval: 30s
retries: 3
expose:
- 5232
networks:
- web
volumes:
- ./data:/data
labels:
- "traefik.enable=true"
- "traefik.http.routers.radicale.entrypoints=https"
- "traefik.http.routers.radicale.rule=Host(`radicale.${DOMAIN}`)"
- "traefik.http.routers.radicale.tls.certresolver=http"
networks:
web:
external: true

13
stacks/ramz/Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY *.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o ramz .
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/ramz .
COPY static/ ./static/
EXPOSE 3333
CMD ["./ramz"]

View File

@ -0,0 +1,24 @@
services:
app:
build: .
container_name: ramz
restart: unless-stopped
ports:
- "3333:3333"
environment:
- GIN_MODE=release
volumes:
- ./templates:/root/templates
- ./data:/root/data
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.ramz-secure.entrypoints=https,http"
- "traefik.http.routers.ramz-secure.rule=Host(`parker.ramz.cc`)"
- "traefik.http.routers.ramz-secure.tls.certresolver=http"
- "traefik.http.services.ramz.loadbalancer.server.port=3333"
networks:
web:
external: true

View File

@ -0,0 +1,2 @@
DOMAIN=${DOMAIN}
REGISTRY_HTTP_SECRET=${REGISTRY_HTTP_SECRET}

View File

@ -0,0 +1,27 @@
services:
registry:
image: registry:2
container_name: registry
restart: unless-stopped
volumes:
- ./data:/var/lib/registry
- ./auth:/auth
environment:
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
REGISTRY_HTTP_SECRET: ${REGISTRY_HTTP_SECRET}
expose:
- 5000
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.registry.entrypoints=https"
- "traefik.http.routers.registry.rule=Host(`registry.${DOMAIN}`)"
- "traefik.http.routers.registry.tls.certresolver=http"
- "traefik.http.services.registry.loadbalancer.server.port=5000"
networks:
web:
external: true

View File

@ -0,0 +1 @@
DOMAIN=${DOMAIN}

View File

@ -0,0 +1,26 @@
services:
smokeping:
image: lscr.io/linuxserver/smokeping:latest
container_name: smokeping
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- TZ=America/New_York
volumes:
- ./config:/config
- ./data:/data
expose:
- 80
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.smokeping.entrypoints=https"
- "traefik.http.routers.smokeping.rule=Host(`smokeping.${DOMAIN}`)"
- "traefik.http.routers.smokeping.tls.certresolver=http"
- "traefik.http.services.smokeping.loadbalancer.server.port=80"
networks:
web:
external: true

View File

@ -0,0 +1 @@
DOMAIN=${DOMAIN}

View File

@ -0,0 +1,30 @@
services:
syncthing:
image: lscr.io/linuxserver/syncthing:latest
container_name: syncthing
hostname: syncthing
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- TZ=America/New_York
volumes:
- ./config:/config
- ./data:/data
ports:
- "8384:8384"
- "22000:22000/tcp"
- "22000:22000/udp"
- "21027:21027/udp"
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.syncthing.entrypoints=https"
- "traefik.http.routers.syncthing.rule=Host(`syncthing.${DOMAIN}`)"
- "traefik.http.routers.syncthing.tls.certresolver=http"
- "traefik.http.services.syncthing.loadbalancer.server.port=8384"
networks:
web:
external: true

View File

@ -0,0 +1 @@
ACME_EMAIL=${ACME_EMAIL}

View File

@ -0,0 +1,25 @@
services:
traefik:
image: traefik:latest
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- web
ports:
- 80:80
- 443:443
- 8080:8080
environment:
- TZ=America/New_York
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/traefik.yml:ro
- ./acme.json:/acme.json
- ./conf.d/:/conf.d/
- /var/log:/var/log
networks:
web:
external: true

View File

@ -0,0 +1,40 @@
global:
checkNewVersion: true
api:
dashboard: true
insecure: true
entryPoints:
http:
address: ":80"
https:
address: ":443"
providers:
providersThrottleDuration: 2s
file:
directory: "/conf.d"
watch: true
docker:
watch: true
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: web
certificatesResolvers:
http:
acme:
email: ${ACME_EMAIL}
storage: acme.json
httpChallenge:
entryPoint: http
log:
level: INFO
filePath: /var/log/traefik.log
format: json
accessLog:
filePath: /var/log/traefik_access.log
format: json

View File

@ -0,0 +1,3 @@
DOMAIN=${DOMAIN}
WALLABAG_DB_PASSWORD=${WALLABAG_DB_PASSWORD}
WALLABAG_DB_ROOT_PASSWORD=${WALLABAG_DB_ROOT_PASSWORD}

View File

@ -0,0 +1,55 @@
services:
wallabag:
image: wallabag/wallabag:latest
container_name: wallabag
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=${WALLABAG_DB_ROOT_PASSWORD}
- SYMFONY__ENV__DATABASE_DRIVER=pdo_mysql
- SYMFONY__ENV__DATABASE_HOST=db
- SYMFONY__ENV__DATABASE_PORT=3306
- SYMFONY__ENV__DATABASE_NAME=wallabag
- SYMFONY__ENV__DATABASE_USER=wallabag
- SYMFONY__ENV__DATABASE_PASSWORD=${WALLABAG_DB_PASSWORD}
- SYMFONY__ENV__DATABASE_CHARSET=utf8mb4
- SYMFONY__ENV__MAILER_HOST=127.0.0.1
- SYMFONY__ENV__MAILER_USER=~
- SYMFONY__ENV__MAILER_PASSWORD=~
- SYMFONY__ENV__FROM_EMAIL=wallabag@${DOMAIN}
- SYMFONY__ENV__DOMAIN_NAME=https://wallabag.${DOMAIN}
expose:
- 80
networks:
- web
- default
volumes:
- ./images:/var/www/wallabag/web/assets/images
labels:
- "traefik.enable=true"
- "traefik.http.routers.wallabag.entrypoints=https"
- "traefik.http.routers.wallabag.rule=Host(`wallabag.${DOMAIN}`)"
- "traefik.http.routers.wallabag.tls.certresolver=http"
- "traefik.http.middlewares.wallabag-header.headers.customRequestHeaders.X-Forwarded-Proto=https"
- "traefik.http.routers.wallabag.middlewares=wallabag-header"
depends_on:
- db
- redis
db:
image: mariadb
container_name: wallabag-db
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=${WALLABAG_DB_ROOT_PASSWORD}
- MARIADB_AUTO_UPGRADE=1
volumes:
- ./data:/var/lib/mysql
redis:
image: redis:alpine
container_name: wallabag-redis
restart: unless-stopped
networks:
web:
external: true

View File

@ -0,0 +1,14 @@
services:
watchtower:
image: containrrr/watchtower:latest
container_name: watchtower
restart: unless-stopped
environment:
- TZ=America/New_York
- DOCKER_API_VERSION=1.44
- WATCHTOWER_SCHEDULE=0 0 4 * * *
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_INCLUDE_STOPPED=false
- WATCHTOWER_REVIVE_STOPPED=false
volumes:
- /var/run/docker.sock:/var/run/docker.sock

View File

@ -0,0 +1 @@
DOMAIN=${DOMAIN}

View File

@ -0,0 +1,26 @@
services:
xbackbone:
image: lscr.io/linuxserver/xbackbone:latest
container_name: xbackbone
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- TZ=America/New_York
volumes:
- ./config:/config
expose:
- 443
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.xbackbone.entrypoints=https"
- "traefik.http.routers.xbackbone.rule=Host(`xb.${DOMAIN}`)"
- "traefik.http.routers.xbackbone.tls.certresolver=http"
- "traefik.http.middlewares.xbackbone-header.headers.customRequestHeaders.X-Forwarded-Proto=https"
- "traefik.http.routers.xbackbone.middlewares=xbackbone-header"
networks:
web:
external: true

View File

@ -0,0 +1,2 @@
DOMAIN=${DOMAIN}
# Copy additional env vars from /var/core/zerotier/denv on old prod

View File

@ -0,0 +1,24 @@
services:
ztncui:
image: keynetworks/ztncui
container_name: zerotier-ui
restart: always
expose:
- 3180
volumes:
- ./ztncui:/opt/key-networks/ztncui/etc
- ./zt1:/var/lib/zerotier-one
env_file:
- .env
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.zerotier.entrypoints=https"
- "traefik.http.routers.zerotier.rule=Host(`zerotierui.${DOMAIN}`)"
- "traefik.http.routers.zerotier.tls.certresolver=http"
- "traefik.http.routers.zerotier.middlewares=dashboard-auth@file"
networks:
web:
external: true