12 Commits

78 changed files with 1716 additions and 5 deletions

4
.gitignore vendored
View File

@@ -4,3 +4,7 @@ apps/proxy
apps/administration/*
apps/tools/app/*
env/secrets.env
infra/core/traefik/data/acme.json
infra/**/.env
infra/**/*.env.local
infra/secrets/*

6
.gitmodules vendored
View File

@@ -4,3 +4,9 @@
[submodule "apps/frontend/src"]
path = apps/frontend/src
url = https://gitea.mindboost.team/Mindboost/mindboost-webapp.git
[submodule "apps/tools/invoiceninja/dockerfiles"]
path = apps/tools/invoiceninja/dockerfiles
url = https://github.com/invoiceninja/dockerfiles.git
[submodule "apps/security/Eduroam Analyzer/asn-updater"]
path = apps/security/Eduroam Analyzer/asn-updater
url = https://gitea.mindboost.team/mindboost/education-flagger.git

8
Jenkinsfile vendored
View File

@@ -22,5 +22,13 @@ pipeline {
build job: 'frontend-pipeline', wait: true
}
}
stage('Deploy Infrastructure') {
steps {
sshagent(['jenkins-ssh-key']) {
sh "ssh user@server 'cd /opt/myapp && git pull origin main && docker compose up -d'"
}
}
}
}
}

42
Makefile Normal file
View File

@@ -0,0 +1,42 @@
SHELL := /bin/bash
# Environment selection
ENV ?= development
COMMON_ENV := infra/env/$(ENV)/common.env
# Helper to pass env files if present
define with_env
$(foreach f,$(1),$(if $(wildcard $(f)),--env-file $(f),))
endef
.PHONY: bootstrap proxy-up proxy-down proxy-logs app-up app-down app-logs ps
bootstrap:
@bash scripts/infra/bootstrap.sh
proxy-up:
@docker compose -f infra/core/traefik/docker-compose.yml $(call with_env,$(COMMON_ENV) infra/apps/traefik/.env) up -d
proxy-down:
@docker compose -f infra/core/traefik/docker-compose.yml $(call with_env,$(COMMON_ENV) infra/apps/traefik/.env) down
proxy-logs:
@docker compose -f infra/core/traefik/docker-compose.yml $(call with_env,$(COMMON_ENV) infra/apps/traefik/.env) logs -f
# Usage: make app-up APP=nextcloud
APP ?=
app-up:
@test -n "$(APP)" || (echo "APP not set. Example: make app-up APP=nextcloud" && exit 1)
@docker compose -f infra/apps/$(APP)/docker-compose.yml $(call with_env,$(COMMON_ENV) infra/apps/$(APP)/.env) up -d
app-down:
@test -n "$(APP)" || (echo "APP not set. Example: make app-down APP=nextcloud" && exit 1)
@docker compose -f infra/apps/$(APP)/docker-compose.yml $(call with_env,$(COMMON_ENV) infra/apps/$(APP)/.env) down
app-logs:
@test -n "$(APP)" || (echo "APP not set. Example: make app-logs APP=nextcloud" && exit 1)
@docker compose -f infra/apps/$(APP)/docker-compose.yml $(call with_env,$(COMMON_ENV) infra/apps/$(APP)/.env) logs -f
ps:
@docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Networks}}'

View File

@@ -2,6 +2,30 @@
All the software used and hosted by mindboost organized in containers.
## New Infra (v2) Overview
This repo now includes a modular, bestpractice infrastructure under `infra/` to make replication and selective deployment easy. It is centered on Traefik as the reverse proxy with automatic TLS via Let's Encrypt, environment layering, and pickwhatyouneed application stacks.
- Core: `infra/core/traefik` — Traefik with HTTPS (ACME), dashboard, and sane defaults
- Apps: `infra/apps/<service>` — selfcontained stacks (e.g., `nextcloud`)
- Env: `infra/env/<environment>/common.env` — environment defaults (dev/prod)
- Secrets: `infra/secrets/` — local secret storage (ignored by git)
- Make targets: toplevel `Makefile` to bootstrap, start proxy, and start apps
Quickstart
- Copy `infra/env/development/common.env` and adjust domains and ACME email.
- Create the shared proxy network and ACME storage: `make bootstrap`
- Start Traefik: `make proxy-up`
- Start a service, e.g. Nextcloud: `make app-up APP=nextcloud`
Notes
- Traefik dashboard is exposed at `TRAEFIK_DASHBOARD_DOMAIN` with optional basic auth.
- Services connect to an external `proxy` network for routing, plus their own internal network.
- Each app has its own `.env.example`; copy to `.env` and adjust.
- The legacy `apps/` structure remains as-is; new infra is additive and can coexist.
## Project Structure
./apps/

View File

@@ -43,6 +43,6 @@ services:
volumes:
backend_redis_data:
driver: local
name: "${INFRASTRUCTURE_LABEL}_backend_redis_data"
name: "${INFRASTRUCTURE_LABEL:-default}_backend_redis_data"

View File

@@ -0,0 +1,8 @@
services:
adminer:
profiles: ["all", "database", "backend", "adminer", "app"]
image: adminer
container_name: ${INFRASTRUCTURE_LABEL:-default}-adminer-${ENVIRONMENT:-development}
restart: always
ports:
- ${ADMINER_PORT:-0}:8080

View File

@@ -0,0 +1,20 @@
services:
adminer:
profiles: ["all", "database", "backend", "adminer", "app"]
image: adminer
container_name: ${INFRASTRUCTURE_LABEL:-default}-adminer-${ENVIRONMENT:-development}
restart: always
ports:
- ${ADMINER_PORT:-0}:8080
networks:
- database
- proxy
labels:
- "traefik.enable=${TRAEFIK_ENABLE:-false}"
- "traefik.http.routers.${INFRASTRUCTURE_LABEL:-default}_adminer.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}"
- "traefik.http.routers.${INFRASTRUCTURE_LABEL:-default}_adminer.rule=Host(`${ADMINER_DOMAIN:-adminer.local}`)"
- "traefik.http.routers.${INFRASTRUCTURE_LABEL:-default}_adminer.tls=true"
- "traefik.http.routers.${INFRASTRUCTURE_LABEL:-default}_adminer.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-http_resolver}"
- 'traefik.http.routers.${INFRASTRUCTURE_LABEL:-default}_adminer.service=adminer'
- "traefik.http.adminer.cloud.loadbalancer.server.port=8080"
- "traefik.docker.network=${TRAEFIK_NETWORK:-default}"

View File

@@ -0,0 +1,9 @@
### Develop (./apps/develop/docker-compose.yml)
# - [ ] Create services for Gitea, Jenkins, and Adminer
# - [ ] Configure volumes for persistent storage of Git repositories, Jenkins data, and Adminer settings
# - [ ] Set up environment variables using the new structure (../../env/${ENVIRONMENT:-development}/develop.env)
# - [ ] Configure networking to allow these services to communicate with each other and the necessary application services
# - [ ] Set up access controls and security measures for development tools
include:
- ./gitea/docker-compose.yml

View File

@@ -0,0 +1,44 @@
services:
gitea:
image: gitea/gitea:latest
container_name: ${INFRASTRUCTURE_LABEL:-mindboost}-gitea
profiles: ["all", "gitea","develop"]
restart: always
volumes:
- ${GITEA_VOLUME_PATH}:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
depends_on:
- gitea_db
labels:
- "traefik.enable=${TRAEFIK_ENABLE:-false}"
- "traefik.http.routers.gitea.entrypoints=${TRAEFIK_ENTRYPOINT}"
- "traefik.http.routers.gitea.rule=(Host(`${GITEA_DOMAIN})`)"
- "traefik.http.routers.gitea.tls=true"
- "traefik.http.routers.gitea.tls.certresolver=${TRAEFIK_CERT_RESOLVER}"
- "traefik.http.routers.gitea.service=gitea"
- 'traefik.http.services.gitea.loadbalancer.gitea.port=3000'
- "traefik.http.routers.gitea.tls.domains[0].main=`${GITEA_TLS_DOMAIN_MAIN}`"
# SSH routing, can't route based on host so anything to port 222 will come to this container
- "traefik.tcp.routers.gitea-ssh.rule=HostSNI(`*`)"
- "traefik.tcp.routers.gitea-ssh.entrypoints=ssh"
- "traefik.tcp.routers.gitea-ssh.service=gitea-ssh-svc"
- "traefik.tcp.services.gitea-ssh-svc.loadbalancer.gitea.port=22"
gitea_db:
image: mysql:latest
container_name: ${INFRASTRUCTURE_LABEL:-mindboost}-gitea_db
profiles: ["all", "gitea","develop"]
restart: always
environment:
- MYSQL_ROOT_PASSWORD=${GITEA_MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${GITEA_MYSQL_DATABASE}
- MYSQL_USER=${GITEA_MYSQL_USER}
- MYSQL_PASSWORD=${GITEA_MYSQL_PASSWORD}
volumes:
- ${GITEA_DATABASE_VOLUME_PATH}:/var/lib/mysql
networks:
gitea:

View File

@@ -0,0 +1,40 @@
### Jenkins (./apps/frontend/docker-compose.yml)
services:
jenkins:
image: jenkins/jenkins:lts
container_name: jenkins
ports:
- "50000:50000" # Jenkins Agent Port
volumes:
- ../../../volumes/develop/jenkins:/var/jenkins_home
- ./plugins.yml:/usr/share/jenkins/ref/plugins.yml
depends_on:
- jenkins-plugins
environment:
- JAVA_OPTS=-Djenkins.install.runSetupWizard=false
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.jenkins.rule=Host(`j.haslach2025.de`)"
- "traefik.http.routers.jenkins.entrypoints=websecure"
- "traefik.http.routers.jenkins.tls=true"
- "traefik.http.routers.jenkins.tls.certresolver=http_resolver"
- "traefik.http.services.jenkins.loadbalancer.server.port=8080" # interner Port von Jenkins
- "traefik.docker.network=proxy"
jenkins-plugins:
image: jenkins/jenkins:lts-jdk17
command: >
jenkins-plugin-cli -f /usr/share/jenkins/ref/plugins.yml --available-updates --output txt > /usr/share/jenkins/ref/plugins.yml
volumes:
- ./plugins.yml:/usr/share/jenkins/ref/plugins.yml
restart: "no"
volumes:
jenkins_home:
driver: local
networks:
proxy:
external: true

View File

View File

@@ -0,0 +1,50 @@
##
## ONE SCRIPT TO RULE THEM ALL
##
## Dieses Compose-File startet alle verfügbaren Services, abhängig von dem angegebenen ENVIRONMENT.
## Um diese Konfiguration zu verwenden, kannst du folgende Befehle nutzen:
## Um alle Services zu starten:
## docker compose -f docker-compose.all.yml --env-file ../env/.env.all --profile all up -d
## Um nur bestimmte Services zu starten (z.B. frontend und backend):
## docker compose -f docker-compose.all.yml --env-file ../env/.env.all --profile frontend --profile backend up -d
##
## Stellen Sie sicher, dass die .env.all Datei im angegebenen Verzeichnis existiert und den ENVIRONMENT Wert enthält.
##
configs:
all:
file: ../env/.env.all
include:
- path: ./proxy/docker-compose.yml
env_file:
- ../env/.env.all
- ../env/${ENVIRONMENT:-development}/.env.proxy
- path: ./frontend/docker-compose.yml
env_file:
- ../env/.env.all
- ../env/${ENVIRONMENT:-development}/.env.frontend
- path: ./backend/docker-compose.yml
- path: ./database/docker-compose.yml
- path: ./website/docker-compose.yml
env_file:
- ../env/.env.all
- ../env/${ENVIRONMENT:-development}/.env.website
- ../env/${ENVIRONMENT:-development}/.env.proxy
- path: ./administration/docker-compose.yml
env_file:
- ../env/.env.all
- ../env/${ENVIRONMENT:-development}/.env.administration
- ../env/${ENVIRONMENT:-development}/.env.proxy
- path: ./develop/docker-compose.yml
env_file:
- ../env/.env.all
- ../env/${ENVIRONMENT:-development}/.env.develop
- ../env/${ENVIRONMENT:-development}/.env.proxy
- path: ./tools/docker-compose.yml
env_file:
- ../env/.env.all
- ../env/${ENVIRONMENT:-development}/.env.tools
- ../env/${ENVIRONMENT:-development}/.env.proxy

View File

@@ -0,0 +1,11 @@
# MaxMind (create a free GeoLite2 license key in your MaxMind account)
MAXMIND_LICENSE_KEY=your_maxmind_license_key
# PeeringDB (optional; reduces rate limits)
PDB_API_KEY=your_peeringdb_api_key
# existing Traefik/proxy network name (must already exist)
PROXY_NETWORK=proxy
# update interval in seconds (30 days)
UPDATE_INTERVAL_SECONDS=2592000

View File

@@ -0,0 +1 @@
.env

View File

@@ -0,0 +1,16 @@
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY main.go ./
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/asn-header-service
FROM alpine:3.20
RUN adduser -D -H -u 10001 app
USER 10001
WORKDIR /app
COPY --from=build /out/asn-header-service /app/asn-header-service
EXPOSE 8080
ENV ADDR=:8080
ENTRYPOINT ["/app/asn-header-service"]

View File

@@ -0,0 +1,89 @@
# NREN / ASN Detection Service
Dieses Projekt stellt einen **minimalen Microservice** bereit, um **Hochschul- und Forschungsnetzwerke (NRENs)** anhand der **Autonomous System Number (ASN)** zu erkennen.
Der Zweck ist es, **Anfragen aus Hochschulnetzen (z. B. eduroam)** zu identifizieren, um **Research-bezogene Services kostenlos oder bevorzugt bereitzustellen**.
Das System dient ausschließlich der **Netzwerk-Klassifikation** und **ersetzt keine Authentifizierung**.
---
## Ziel
- Erkennen, ob eine Anfrage aus einem **Hochschul- oder Forschungsnetz** stammt
- Bereitstellung eines **Header-Hinweises** für nachgelagerte Services
- Grundlage für Entscheidungen wie:
- kostenfreie Research-Features
- angepasste UI-Hinweise
- alternative Rate-Limits
---
## Funktionsweise (Kurzfassung)
```
Client
→ Traefik
→ ForwardAuth
→ ASN Detection Service
→ Header wird ergänzt
```
1. Die Client-IP wird ermittelt
2. Die zugehörige ASN wird lokal nachgeschlagen
3. Die ASN wird mit einer NREN-ASN-Liste verglichen
4. Das Ergebnis wird als HTTP-Header zurückgegeben
---
## Datenquellen
- **GeoLite2 ASN (MaxMind)**
- kostenlos
- lokal
- monatliche Aktualisierung
- **NREN-ASN-Liste**
- abgeleitet aus PeeringDB
- Kategorie: `Research and Education`
- monatliche Aktualisierung
---
## Bereitgestellte Header
| Header | Beschreibung |
|------|-------------|
| `X-ASN` | ASN der Client-IP |
| `X-ASN-ORG` | Organisation (optional) |
| `X-NREN` | `1` wenn ASN zu einem Hochschul-/Forschungsnetz gehört, sonst `0` |
---
## Integration
Der Service wird als **Traefik ForwardAuth Middleware** eingebunden.
Die Header werden über `authResponseHeaders` an die eigentliche Anwendung weitergereicht.
Der Service ist **nicht öffentlich exponiert** und kommuniziert ausschließlich über das interne Docker-Netzwerk.
---
## Update-Strategie
- monatliche Aktualisierung der ASN-Daten
- keine externen Requests während der Anfrageverarbeitung
---
## Einschränkungen
- Die Erkennung ist **heuristisch**
- Es gibt **keine Garantie**, dass jede Anfrage aus einem Hochschulnetz erkannt wird
- Die Information darf **nicht als Authentifizierungsmerkmal** verwendet werden
---
## Zusammenfassung
Dieses Projekt ermöglicht eine **performante, datenschutzfreundliche Erkennung von Hochschulnetzen**, um **Research-Angebote kontextabhängig bereitzustellen**, ohne Nutzer zu identifizieren oder externe Dienste zur Laufzeit zu kontaktieren.

View File

@@ -0,0 +1,89 @@
# NREN / ASN Detection Service
Dieses Projekt stellt einen **minimalen Microservice** bereit, um **Hochschul- und Forschungsnetzwerke (NRENs)** anhand der **Autonomous System Number (ASN)** zu erkennen.
Der Zweck ist es, **Anfragen aus Hochschulnetzen (z. B. eduroam)** zu identifizieren, um **Research-bezogene Services kostenlos oder bevorzugt bereitzustellen**.
Das System dient ausschließlich der **Netzwerk-Klassifikation** und **ersetzt keine Authentifizierung**.
---
## Ziel
- Erkennen, ob eine Anfrage aus einem **Hochschul- oder Forschungsnetz** stammt
- Bereitstellung eines **Header-Hinweises** für nachgelagerte Services
- Grundlage für Entscheidungen wie:
- kostenfreie Research-Features
- angepasste UI-Hinweise
- alternative Rate-Limits
---
## Funktionsweise (Kurzfassung)
```
Client
→ Traefik
→ ForwardAuth
→ ASN Detection Service
→ Header wird ergänzt
```
1. Die Client-IP wird ermittelt
2. Die zugehörige ASN wird lokal nachgeschlagen
3. Die ASN wird mit einer NREN-ASN-Liste verglichen
4. Das Ergebnis wird als HTTP-Header zurückgegeben
---
## Datenquellen
- **GeoLite2 ASN (MaxMind)**
- kostenlos
- lokal
- monatliche Aktualisierung
- **NREN-ASN-Liste**
- abgeleitet aus PeeringDB
- Kategorie: `Research and Education`
- monatliche Aktualisierung
---
## Bereitgestellte Header
| Header | Beschreibung |
|------|-------------|
| `X-ASN` | ASN der Client-IP |
| `X-ASN-ORG` | Organisation (optional) |
| `X-NREN` | `1` wenn ASN zu einem Hochschul-/Forschungsnetz gehört, sonst `0` |
---
## Integration
Der Service wird als **Traefik ForwardAuth Middleware** eingebunden.
Die Header werden über `authResponseHeaders` an die eigentliche Anwendung weitergereicht.
Der Service ist **nicht öffentlich exponiert** und kommuniziert ausschließlich über das interne Docker-Netzwerk.
---
## Update-Strategie
- monatliche Aktualisierung der ASN-Daten
- keine externen Requests während der Anfrageverarbeitung
---
## Einschränkungen
- Die Erkennung ist **heuristisch**
- Es gibt **keine Garantie**, dass jede Anfrage aus einem Hochschulnetz erkannt wird
- Die Information darf **nicht als Authentifizierungsmerkmal** verwendet werden
---
## Zusammenfassung
Dieses Projekt ermöglicht eine **performante, datenschutzfreundliche Erkennung von Hochschulnetzen**, um **Research-Angebote kontextabhängig bereitzustellen**, ohne Nutzer zu identifizieren oder externe Dienste zur Laufzeit zu kontaktieren.

View File

@@ -0,0 +1,36 @@
services:
asn-header:
build: .
container_name: asn-header
restart: unless-stopped
env_file: .env
environment:
MMDB_PATH: /data/GeoLite2-ASN.mmdb
ASN_LIST_PATH: /data/nren_asns.txt
ADDR: ":8080"
volumes:
- asn_data:/data:ro
networks:
- proxy
asn-updater:
build: ./asn-updater
container_name: asn-updater
restart: unless-stopped
env_file: .env
environment:
OUT_DIR: /data
PDB_INFO_TYPE: "Research and Education"
INTERVAL_SECONDS: "${UPDATE_INTERVAL_SECONDS}"
volumes:
- asn_data:/data
networks:
- proxy
networks:
proxy:
external: true
name: ${PROXY_NETWORK}
volumes:
asn_data:

View File

@@ -0,0 +1,6 @@
module asn-header-service
go 1.22
require github.com/oschwald/maxminddb-golang v1.13.1

View File

@@ -0,0 +1,158 @@
package main
import (
"bufio"
"log"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/oschwald/maxminddb-golang"
)
type asnRecord struct {
ASN uint `maxminddb:"autonomous_system_number"`
Org string `maxminddb:"autonomous_system_organization"`
}
type server struct {
db *maxminddb.Reader
nrenASNs map[uint]struct{}
ready atomic.Bool
versionTag string
}
func loadASNSet(path string) (map[uint]struct{}, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
set := make(map[uint]struct{}, 4096)
sc := bufio.NewScanner(f)
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
v, err := strconv.ParseUint(line, 10, 32)
if err != nil {
continue
}
set[uint(v)] = struct{}{}
}
return set, sc.Err()
}
func firstForwardedFor(r *http.Request) string {
xff := r.Header.Get("X-Forwarded-For")
if xff == "" {
return ""
}
parts := strings.Split(xff, ",")
if len(parts) == 0 {
return ""
}
return strings.TrimSpace(parts[0])
}
func remoteIP(r *http.Request) string {
// Prefer XFF (because Traefik is proxy)
ip := firstForwardedFor(r)
if ip != "" {
return ip
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err == nil {
return host
}
return r.RemoteAddr
}
func (s *server) authHandler(w http.ResponseWriter, r *http.Request) {
if !s.ready.Load() {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
ipStr := remoteIP(r)
parsed := net.ParseIP(ipStr)
if parsed == nil {
// Always 200: we enrich, not block
w.Header().Set("X-NREN", "0")
w.WriteHeader(http.StatusOK)
return
}
var rec asnRecord
if err := s.db.Lookup(parsed, &rec); err != nil || rec.ASN == 0 {
w.Header().Set("X-NREN", "0")
w.WriteHeader(http.StatusOK)
return
}
w.Header().Set("X-ASN", strconv.FormatUint(uint64(rec.ASN), 10))
if rec.Org != "" {
// optional: keep it short; some org strings can be long
w.Header().Set("X-ASN-ORG", rec.Org)
}
_, ok := s.nrenASNs[rec.ASN]
if ok {
w.Header().Set("X-NREN", "1")
} else {
w.Header().Set("X-NREN", "0")
}
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("X-Service", s.versionTag)
w.WriteHeader(http.StatusOK)
}
func main() {
mmdbPath := getenv("MMDB_PATH", "/data/GeoLite2-ASN.mmdb")
asnListPath := getenv("ASN_LIST_PATH", "/data/nren_asns.txt")
addr := getenv("ADDR", ":8080")
version := getenv("VERSION_TAG", "asn-header-service")
db, err := maxminddb.Open(mmdbPath)
if err != nil {
log.Fatalf("failed to open mmdb: %v", err)
}
defer db.Close()
set, err := loadASNSet(asnListPath)
if err != nil {
log.Fatalf("failed to load asn list: %v", err)
}
s := &server{db: db, nrenASNs: set, versionTag: version}
s.ready.Store(true)
mux := http.NewServeMux()
mux.HandleFunc("/auth", s.authHandler)
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })
srv := &http.Server{
Addr: addr,
Handler: mux,
ReadHeaderTimeout: 2 * time.Second,
}
log.Printf("listening on %s (asn_count=%d)", addr, len(set))
log.Fatal(srv.ListenAndServe())
}
func getenv(k, def string) string {
v := strings.TrimSpace(os.Getenv(k))
if v == "" {
return def
}
return v
}

View File

@@ -0,0 +1,30 @@
services:
wireguard:
image: linuxserver/wireguard
container_name: wireguard
cap_add:
- NET_ADMIN
- SYS_MODULE
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Berlin
- SERVERURL=${SERVER_IP:?"❌ ERROR = SERVERURL is not set. Run set-server-ip.sh first."}
- SERVERPORT=51820
- PEERS=3 # Number of VPN clients to generate
- PEERDNS=auto
- INTERNAL_SUBNET=22.22.22.0
volumes:
- ../../volumes/security/wireguard/config:/config
- /lib/modules:/lib/modules
ports:
- "51820:51820/udp"
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
restart: unless-stopped
networks:
- wireguard_network
networks:
wireguard_network:
driver: bridge

View File

@@ -0,0 +1,50 @@
volumes:
etc_wireguard:
services:
wg-easy:
environment:
# Change Language:
# (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi, ja, si)
- LANG=${WG_LANG:-de}
# ⚠️ Required:
# Change this to your host's public address
- WG_HOST=${SERVER_IP:-localhost}
# Optional:
# - PASSWORD_HASH=$$2y$$10$$hBCoykrB95WSzuV4fafBzOHWKu9sbyVa34GJr8VV5R/pIelfEMYyG # (needs double $$, hash of 'foobar123'; see "How_to_generate_an_bcrypt_hash.md" for generate the hash)
# - PORT=51821
# - WG_PORT=51820
# - WG_CONFIG_PORT=92820
- WG_DEFAULT_ADDRESS=${WG_DEFAULT_ADDRESS:-22.22.22.0}
# - WG_DEFAULT_DNS=1.1.1.1
# - WG_MTU=1420
# - WG_ALLOWED_IPS=192.168.15.0/24, 10.0.1.0/24
# - WG_PERSISTENT_KEEPALIVE=25
# - WG_PRE_UP=echo "Pre Up" > /etc/wireguard/pre-up.txt
# - WG_POST_UP=echo "Post Up" > /etc/wireguard/post-up.txt
# - WG_PRE_DOWN=echo "Pre Down" > /etc/wireguard/pre-down.txt
# - WG_POST_DOWN=echo "Post Down" > /etc/wireguard/post-down.txt
# - UI_TRAFFIC_STATS=true
# - UI_CHART_TYPE=0 # (0 Charts disabled, 1 # Line chart, 2 # Area chart, 3 # Bar chart)
# - WG_ENABLE_ONE_TIME_LINKS=true
# - UI_ENABLE_SORT_CLIENTS=true
# - WG_ENABLE_EXPIRES_TIME=true
# - ENABLE_PROMETHEUS_METRICS=false
# - PROMETHEUS_METRICS_PASSWORD=$$2a$$12$$vkvKpeEAHD78gasyawIod.1leBMKg8sBwKW.pQyNsq78bXV3INf2G # (needs double $$, hash of 'prometheus_password'; see "How_to_generate_an_bcrypt_hash.md" for generate the hash)
image: ghcr.io/wg-easy/wg-easy
container_name: wg-easy
volumes:
- ../../volumes/wireguardeasy/:/etc/wireguard
ports:
- "51820:51820/udp"
- "51821:51821/tcp"
restart: unless-stopped
cap_add:
- NET_ADMIN
- SYS_MODULE
# - NET_RAW # ⚠️ Uncomment if using Podman
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1

View File

@@ -0,0 +1,2 @@
#!/bin/bash
export SERVER_IP=$(curl -s https://api.ipify.org)

View File

@@ -0,0 +1,11 @@
### Tools (./apps/tools/docker-compose.yml)
# - [ ] Create services for Nextcloud, LimeSurvey, and LinkStack
# - [ ] Configure volumes for persistent storage of files, survey data, and link management data
# - [ ] Set up environment variables using the new structure (../../env/${ENVIRONMENT:-development}/tools.env)
# - [ ] Configure networking to expose these services to the internet via the proxy
# - [ ] Set up regular backup jobs for critical data in these services
include:
- path: ./nextcloud/docker-compose.yml
- path: ./limesurvey/docker-compose.yml
- path: ./invoiceninja/dockerfiles/debian/docker-compose.yml

View File

View File

@@ -0,0 +1,59 @@
services:
nextcloud-db:
image: mariadb:10.6
container_name: ${INFRASTRUCTURE_LABEL:-default}-nextcloud-db-${ENVIRONMENT:-development}
profiles: ["all", "tools", "nextcloud"]
command: --transaction-isolation=READ-COMMITTED --innodb_read_only_compressed=OFF
restart: unless-stopped
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
- ../../volumes/tools/${INFRASTRUCTURE_LABEL:-default}_cloud/database:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=headpiece-constant1-denim-mindboost #SQL root Passwort eingeben
- MYSQL_PASSWORD=idealist9-frayed-murkiness-mindboost #SQL Benutzer Passwort eingeben
- MYSQL_DATABASE=nextcloud-mindboost #Datenbank Name
- MYSQL_USER=mindboostcloud #SQL Nutzername
- MYSQL_INITDB_SKIP_TZINFO=1
- MARIADB_AUTO_UPGRADE=1
nextcloud-redis:
image: redis:alpine
container_name: ${INFRASTRUCTURE_LABEL:-default}-nextcloud-redis-${ENVIRONMENT:-development}
profiles: ["all", "tools", "nextcloud"]
hostname: nextcloud-redis
restart: unless-stopped
command: redis-server --requirepass redis-mindboost-passwort # Redis Passwort eingeben
cloud:
image: nextcloud
container_name: ${INFRASTRUCTURE_LABEL:-default}-nextcloud-app-${ENVIRONMENT:-development}
profiles: ["all", "tools", "nextcloud"]
restart: unless-stopped
depends_on:
- nextcloud-db
- nextcloud-redis
environment:
TRUSTED_PROXIES: 172.16.255.254/16
OVERWRITEPROTOCOL: https
OVERWRITECLIURL: https://${CLOUD_DOMAIN:-cloud}
OVERWRITEHOST: ${CLOUD_DOMAIN:-cloud}
REDIS_HOST: nextcloud-redis
REDIS_HOST_PASSWORD: redis-mindboost-passwort # Redis Passwort von oben wieder eingeben
volumes:
- ../../volumes/tools/${INFRASTRUCTURE_LABEL:-default}_cloudapp/:/var/www/html/data
labels:
- "traefik.enable=true"
- "traefik.http.routers.${INFRASTRUCTURE_LABEL:-default}_cloud.entrypoints=websecure"
- "traefik.http.routers.${INFRASTRUCTURE_LABEL:-default}_cloud.rule=Host(`${CLOUD_DOMAIN}`)"
- "traefik.http.routers.${INFRASTRUCTURE_LABEL:-default}_cloud.tls=true"
- "traefik.http.routers.${INFRASTRUCTURE_LABEL:-default}_cloud.tls.certresolver=http_resolver"
- 'traefik.http.routers.${INFRASTRUCTURE_LABEL:-default}_cloud.service=cloud'
- "traefik.http.services.cloud.loadbalancer.server.port=80"
- "traefik.docker.network=${TRAEFIK_NETWORK:-default}"
- "traefik.http.routers.${INFRASTRUCTURE_LABEL:-default}_cloud.middlewares=nextcloud-dav,default@file"
- "traefik.http.middlewares.nextcloud-dav.replacepathregex.regex=^/.well-known/ca(l|rd)dav"
- "traefik.http.middlewares.nextcloud-dav.replacepathregex.replacement=/remote.php/dav/"
networks:
- ${TRAEFIK_NETWORK}
networks:
nextcloud:
name: ${INFRASTRUCTURE_LABEL:-default}_nextcloud

View File

@@ -0,0 +1,29 @@
services:
kirbycms:
build:
context: ./kirby
dockerfile: Dockerfile
image: kirbycms
container_name: ${INFRASTRUCTURE_LABEL:-default}-kirbycms-${ENVIRONMENT:-development}
profiles: ["website","kirbycms","all"]
volumes:
- kirbycms_data:/var/www/html:rw # Persistente Daten
restart: unless-stopped
ports:
- 0:80
networks:
- ${TRAEFIK_NETWORK:-default}
labels:
- "traefik.enable=${TRAEFIK_ENABLE:-false}"
- "traefik.docker.network=${TRAEFIK_NETWORK:-default}"
- "traefik.http.routers.kirbycms.service=kirbycms"
- "traefik.http.routers.kirbycms.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-default}"
- "traefik.http.routers.kirbycms.tls.domains[0].main=`${WEBSITE_DOMAIN:-kirby.local}`"
- "traefik.http.routers.kirbycms.rule=Host(`${WEBSITE_DOMAIN:-kirby.local}`)"
- "traefik.http.routers.kirbycms.entrypoints=${TRAEFIK_ENTRYPOINT:-default}"
- "traefik.http.routers.kirbycms.tls=true"
- "traefik.http.services.kirbycms.loadbalancer.server.port=80"
volumes:
kirbycms_data:
driver: local

View File

@@ -0,0 +1,49 @@
# Use latest offical ubuntu image
FROM ubuntu:latest
# Set timezone
ENV TZ=Europe/Berlin
# Set geographic area using above variable
# This is necessary, otherwise building the image doesn't work
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# Remove annoying messages during package installation
ARG DEBIAN_FRONTEND=noninteractive
# Install packages: web server & PHP plus extensions
RUN apt-get update && apt-get install -y \
apache2 \
apache2-utils \
ca-certificates \
php \
libapache2-mod-php \
php-curl \
php-dom \
php-gd \
php-intl \
php-json \
php-mbstring \
php-xml \
php-zip && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Copy virtual host configuration from current path onto existing 000-default.conf
COPY default.conf /etc/apache2/sites-available/000-default.conf
# Remove default content (existing index.html)
RUN rm /var/www/html/*
# Activate Apache modules headers & rewrite
RUN a2enmod headers rewrite
# Ensure Group Ownership for www-data every member of kirbygroup should edit files
RUN groupadd -g 1003 kirbygroup && usermod -aG kirbygroup www-data
RUN chown -R www-data:kirbygroup /var/www/html
RUN chmod -R g+rw /var/www/html && find /var/www/html -type d -exec chmod g+xs {} \;
# Tell container to listen to port 80 at runtime
EXPOSE 80
# Start Apache web server
CMD [ "/usr/sbin/apache2ctl", "-DFOREGROUND" ]

View File

@@ -0,0 +1,9 @@
<VirtualHost *:80>
ServerName localhost
# Set the document root
DocumentRoot "/var/www/html"
<Directory "/var/www/html">
# Allow overriding the default configuration via `.htaccess`
AllowOverride All
</Directory>
</VirtualHost>

View File

@@ -0,0 +1,7 @@
#!/bin/bash
set -e -u
[[ $USERID ]] && usermod --uid "${USERID}" www-data
exec "$@"

View File

@@ -0,0 +1 @@
USERID=${USERID:-0}

114
dev-fpm.docker-compose.yml Normal file
View File

@@ -0,0 +1,114 @@
version: "3.8"
services:
mariadb_webapp_dev:
image: docker.io/bitnami/mariadb:11.1
container_name: ${DEV_COMPOSE_PREFIX:-dev}-mariadb
hostname: ${DEV_DB_HOST:-mariadb-webapp-dev}
environment:
MARIADB_USER: ${MARIADB_USER}
MARIADB_DATABASE: ${MARIADB_DATABASE}
MARIADB_PASSWORD: ${MARIADB_PASSWORD}
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
networks:
- dev_backend
volumes:
- mindboost_mariadb_data_dev:/var/lib/mysql
laravel-redis-dev:
image: redis:alpine
container_name: ${DEV_COMPOSE_PREFIX:-dev}-redis
hostname: ${DEV_REDIS_HOST:-laravel-redis-dev}
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
networks:
- dev_backend
restart: unless-stopped
volumes:
- ./data/redis-dev:/data
laravel_backend_dev:
image: ${BACKEND_IMAGE}
container_name: ${DEV_COMPOSE_PREFIX:-dev}-backend
environment:
APP_ENV: ${APP_ENV:-production}
APP_NAME: ${APP_NAME:-Mindboost Backend Dev}
APP_URL: https://${DEV_BACKEND_DOMAIN}
FRONTEND_URL: https://${DEV_FRONTEND_DOMAIN}
DB_CONNECTION: mysql
DB_HOST: ${DEV_DB_HOST:-mariadb-webapp-dev}
DB_PORT: ${DB_PORT:-3306}
DB_DATABASE: ${MARIADB_DATABASE}
DB_USERNAME: ${MARIADB_USER}
DB_PASSWORD: ${MARIADB_PASSWORD}
REDIS_HOST: ${DEV_REDIS_HOST:-laravel-redis-dev}
REDIS_PASSWORD: ${REDIS_PASSWORD}
REDIS_PORT: ${REDIS_PORT:-6379}
CACHE_DRIVER: redis
QUEUE_CONNECTION: redis
SESSION_DRIVER: redis
volumes:
- ${BACKEND_CODE_PATH:-./apps/backend/src}:/app
- ${BACKEND_PUBLIC_PATH:-./apps/backend/src/public}:/var/www/public
- ${BACKEND_ENV_FILE:-./env/development/.env.backend}:/var/www/.env
- ./logs/backend-dev:/var/www/storage/logs
depends_on:
- mariadb_webapp_dev
- laravel-redis-dev
networks:
- dev_backend
laravel-nginx-dev:
image: nginx:alpine
container_name: ${DEV_COMPOSE_PREFIX:-dev}-nginx
volumes:
- ./nginx:/etc/nginx/conf.d:ro
- ${BACKEND_PUBLIC_PATH:-./apps/backend/src/public}:/var/www/public:ro
depends_on:
- laravel_backend_dev
labels:
- "traefik.enable=true"
- "traefik.http.routers.dev_backend_http.entrypoints=web"
- "traefik.http.routers.dev_backend_http.rule=Host(`${DEV_BACKEND_DOMAIN}`)"
- "traefik.http.routers.dev_backend_http.middlewares=traefik-https-redirect"
- "traefik.http.routers.dev_backend_https.entrypoints=websecure"
- "traefik.http.routers.dev_backend_https.rule=Host(`${DEV_BACKEND_DOMAIN}`)"
- "traefik.http.routers.dev_backend_https.tls=true"
- "traefik.http.routers.dev_backend_https.tls.certresolver=${TRAEFIK_CERT_RESOLVER}"
- "traefik.http.routers.dev_backend_https.service=dev_backend_service"
- "traefik.http.services.dev_backend_service.loadbalancer.server.port=80"
- "traefik.docker.network=${TRAEFIK_NETWORK}"
networks:
- dev_backend
- proxy
nuxt_frontend_dev:
image: ${NUXT_IMAGE}
container_name: ${DEV_COMPOSE_PREFIX:-dev}-frontend
environment:
VUE_APP_BACKEND_HOST_ADDRESS: https://${DEV_BACKEND_DOMAIN}
NUXT_PUBLIC_BACKEND_URL: https://${DEV_BACKEND_DOMAIN}
networks:
- dev_backend
- proxy
depends_on:
- laravel_backend_dev
labels:
- "traefik.enable=true"
- "traefik.http.routers.dev_frontend_http.entrypoints=web"
- "traefik.http.routers.dev_frontend_http.rule=Host(`${DEV_FRONTEND_DOMAIN}`)"
- "traefik.http.routers.dev_frontend_http.middlewares=traefik-https-redirect"
- "traefik.http.routers.dev_frontend_https.entrypoints=websecure"
- "traefik.http.routers.dev_frontend_https.rule=Host(`${DEV_FRONTEND_DOMAIN}`)"
- "traefik.http.routers.dev_frontend_https.tls=true"
- "traefik.http.routers.dev_frontend_https.tls.certresolver=${TRAEFIK_CERT_RESOLVER}"
- "traefik.http.services.dev_frontend_https.loadbalancer.server.port=${VUE_INTERNAL_PORT}"
- "traefik.docker.network=${TRAEFIK_NETWORK}"
networks:
dev_backend:
driver: bridge
proxy:
external: true
volumes:
mindboost_mariadb_data_dev:

29
docs/infra.md Normal file
View File

@@ -0,0 +1,29 @@
Infrastructure v2
Goals
- Modular stacks you can pick individually (Nextcloud, etc.)
- Unified reverse proxy (Traefik) with automatic TLS
- Clear env layering and gitignored secrets
- Simple Make targets for a smooth DX
Layout
- infra/core/traefik: Traefik compose + static/dynamic config
- infra/apps/<service>: Selfcontained compose stacks and .env.example
- infra/env/<env>/common.env: Shared environment defaults per environment
- infra/secrets: Local secret files (ignored)
- scripts/infra/bootstrap.sh: Creates proxy network and ACME storage
Usage
1. cp infra/env/development/common.env infra/env/development/common.env (adjust values)
2. make bootstrap
3. make proxy-up
4. make app-up APP=nextcloud
Security
- Do not commit real secrets. Place them in local `.env` files or secret managers.
- Optionally protect Traefik dashboard with basic auth via `TRAEFIK_BASIC_AUTH_USERS`.

39
env/.env.all vendored Normal file
View File

@@ -0,0 +1,39 @@
##
## Einstellung die für das gesamte Projekt gelten. Also der Name und der Admin
## Das Environment muss "production","staging" oder "development" heißen
INFRASTRUCTURE_LABEL=mindboost
ENVIRONMENT=development
ADMIN_USER=${INFRASTRUCTURE_LABEL:-default}_${ENVIRONMENT:-development}
ADMIN_PASSWORD_HASH='$2y$05$U7noO29Ru/4VB5x8TpZo3.b4VjH6AAnhufJJUG2Vs7qHCM2Cd8yIK' # for development = admin
SERVER_IP=127.0.0.1
#################################################################################################
# 🔧 ENVIRONMENT VARIABLES 🔧 #
#################################################################################################
#
# This file contains **default (fallback) values** for environment variables.
# These values ensure that services run with sane defaults if no other configuration is provided.
#
# 📌 **ENVIRONMENT VARIABLE PRIORITY ORDER (Lowest to Highest)**
# 1⃣ **Fallback Values in the File** (Used only if no other source provides a value)
# 2⃣ **Global Defaults in `.env.all`** (Shared settings across all services)
# 3⃣ **Service-Specific `.env` Files** (Overrides per service group, e.g., `.env.backend`, `.env.proxy`)
# 4⃣ **Preloaded Shell Environment** (`export VAR=value` before running `docker compose`)
# 5⃣ **CLI Overrides** (`docker compose --env-file` or `-e VAR=value` → Highest Priority)
#
# 🔄 **Overwriting Behavior**
# - Variables defined in **`.env.all`** override values in this file.
# - Variables defined in **`.env.<service>`** (e.g., `.env.backend`) override `.env.all`.
# - Variables explicitly **exported in the shell** take priority over all `.env` files.
# - Variables passed via **CLI (`--env-file` or `-e VAR=value`)** have the **highest priority**.
#
# 🚀 **Key Takeaways**
# ✅ Use `.env.all` for common values across environments.
# ✅ Use `.env.<service>` for service-specific configurations.
# ✅ If needed, manually override variables in the shell or CLI.
#
#################################################################################################

50
env/README.md vendored Normal file
View File

@@ -0,0 +1,50 @@
# 🔧 Environment Configuration Guide
## 🌍 Overview
This project uses **environment variables** to manage configuration across different environments (development, staging, production, etc.). These variables are loaded from `.env` files and can be overridden at multiple levels.
---
## 📌 **Environment Variable Priority (Lowest to Highest)**
| 🔢 Priority | 📄 Source | 🔍 Description |
|------------|-----------------------------|------------------------------------------------|
| 1**Fallback Values** | hardcoded defaults | Used only if no other configuration is provided |
| 2**Global Defaults** | `.env.all` | Shared settings for all services |
| 3**Service-Specific Overrides** | `.env.backend`, `.env.proxy`, etc. | Overrides `.env.all` with service-specific values |
| 4**Shell Environment Variables** | `export VAR=value` before running | Takes precedence over `.env` files |
| 5**CLI Overrides** | `docker compose --env-file` or `-e VAR=value` | **Highest priority** (for temporary overrides) |
---
## 🔄 **Overwriting Behavior**
- 🏗 **Variables defined in `.env.all`** override fallback values.
- 🏗 **Variables defined in `.env.<service>`** (e.g., `.env.backend`) override `.env.all`.
- 🔧 **Manually exported environment variables** in the shell take priority over `.env` files.
- 🚀 **Variables passed via CLI (`--env-file` or `-e VAR=value`)** override everything.
---
## 🚀 **Best Practices**
✔️ **Use `.env.all` for global configurations** (e.g., `ENVIRONMENT=development`, `INFRASTRUCTURE_LABEL=myinfra`).
✔️ **Use `.env.<service>` for service-specific configurations** (e.g., `.env.backend` for Laravel, `.env.database` for MariaDB).
✔️ **If needed, manually override variables in the shell** using `export VAR=value`.
✔️ **Use CLI `--env-file` for temporary overrides** in testing/debugging scenarios.
---
## 🏗 **Example File Structure**
```sh
/env/
├── .env.all # Global default variables
├── development/
│ ├── .env.backend # Backend service config for development
│ ├── .env.database # Database config for development
│ ├── .env.proxy # Proxy config for development
├── staging/
│ ├── .env.backend # Backend service config for staging
│ ├── .env.database # Database config for staging
├── production/
│ ├── .env.backend # Backend service config for production
│ ├── .env.database # Database config for production

7
env/development/.env.administration vendored Normal file
View File

@@ -0,0 +1,7 @@
# ----------------------------------
# Portainer
# ----------------------------------
PORTAINER_IMAGE=portainer/portainer-ce:latest
PORTAINER_DATA_PATH=../../../volumes/administration/portainer/data

31
env/development/.env.backend vendored Normal file
View File

@@ -0,0 +1,31 @@
# ----------------------------------
# Redis
# ----------------------------------
REDIS_PASSWORD=laravel-redis-passwort
REDIS_PORT=6379
SERVER_IP=${SERVER_IP:-localhost}
# ----------------------------------
# Laravel Backend
# ----------------------------------
BACKEND_NETWORK=backend
APP_ENV=${ENVIRONMENT-local}
APP_NAME="mindboost backend - Compose Deployment"
APP_URL=https://backend.local
LARAVEL_PORT=8000
LARAVEL_VITE_PORT=5173
JWT_SECRET=zMtO8sgsnc4UixWSsYWE1pK9EdpNLzxNSoIPlUpTe6dDlarM3bu4cwM80tH3jA0F
# ----------------------------------
# Datenbank Zugriff - ! MUSS MIT .env.database übereinstimmen
# ----------------------------------
DB_HOST=database
DB_PORT=3306
DB_PASSWORD=1stronges-mindboostdb-passwort
DB_USERNAME=${INFRASTRUCTURE_LABEL:-default}_${ENVIRONMENT:-development}
DB_DATABASE=${INFRASTRUCTURE_LABEL:-default}_${ENVIRONMENT:-development}

9
env/development/.env.database vendored Normal file
View File

@@ -0,0 +1,9 @@
# ----------------------------------
# Datenbank (MariaDB)
# ----------------------------------
MARIADB_USER=${INFRASTRUCTURE_LABEL:-default}_${ENVIRONMENT:-development}
MARIADB_DATABASE=${INFRASTRUCTURE_LABEL:-default}_${ENVIRONMENT:-development}
MARIADB_ROOT_PASSWORD_FILE=/run/secrets/mariadb_root
MARIADB_PASSWORD=1stronges-mindboostdb-passwort
MARIADB_PORT=3306
MARIADB_HOST=database

25
env/development/.env.develop vendored Normal file
View File

@@ -0,0 +1,25 @@
# ----------------------------------
# GITEA
# ----------------------------------
USER_UID=1000
USER_GID=1000
GITEA_VOLUME_PATH=../../../volumes/develop/gitea/gitea
GITEA_DATABASE_VOLUME_PATH=../../../volumes/develop/gitea/gitea_db
GITEA_MYSQL_ROOT_PASSWORD=very-difficult-passwort-gitea
GITEA_MYSQL_USER=gitea
GITEA_MYSQL_PASSWORD=very-difficult-gitea
GITEA_MYSQL_DATABASE=gitea
GITEA_MYSQL_ALLOW_EMPTY_PASSWORD=true
# ----------------------------------
# GITEA DB
# ----------------------------------
DB_HOST=gitea_db:3306
DB_NAME=gitea
DB_PASSWD=very-difficult-gitea
DB_TYPE=mysql
DB_USER=gitea

4
env/development/.env.frontend vendored Normal file
View File

@@ -0,0 +1,4 @@
# ----------------------------------
# VUE APP
# ----------------------------------
BACKEND_URL="backend.local"

51
env/development/.env.proxy vendored Normal file
View File

@@ -0,0 +1,51 @@
# ----------------------------------
# TRAEFIK
# ----------------------------------
TRAEFIK_ENABLE=true
TRAEFIK_NETWORK=proxy
TRAEFIK_BASIC_AUTH_USERS=${ADMIN_USER}:${ADMIN_PASSWORD_HASH}
TRAEFIK_CERT_RESOLVER=
## Domains when TRAEFIK is ENABLED
PORTAINER_DOMAIN=portainer.local
FRONTEND_DOMAIN=frontend.local
FRONTEND_DOMAIN_2=app.frontend.local
BACKEND_DOMAIN=backend.local
WEBSITE_DOMAIN=web.local
ADMINER_DOMAIN=adminer.local
GITEA_DOMAIN=gitea.local
LIMESURVEY_DOMAIN=survey.local
LINKSTACK_DOMAIN=linkstack.local
TRAEFIK_DOMAIN=traefik.local
CLOUD_DOMAIN=cloud.local
KILLBILL_DOMAIN=killbill.local
### TLS for Domains
PORTAINER_TLS_DOMAIN_MAIN=${PORTAINER_DOMAIN}
FRONTEND_TLS_DOMAIN_MAIN=${FRONTEND_DOMAIN}
FRONTEND_TLS_DOMAIN_SANS=${FRONTEND_DOMAIN_2}
BACKEND_TLS_DOMAIN_MAIN=${BACKEND_DOMAIN}
WEBSITE_TLS_DOMAIN_MAIN=${WEBSITE_DOMAIN}
GITEA_TLS_DOMAIN_MAIN=${GITEA_DOMAIN}
LIMESURVEY_TLS_DOMAIN_MAIN=${LIMESURVEY_DOMAIN}
LINKSTACK_TLS_DOMAIN_MAIN=${LINKSTACK_DOMAIN}
TRAEFIK_TLS_DOMAIN_MAIN=${TRAEFIK_DOMAIN}
CLOUD_TLS_DOMAIN_MAIN=${CLOUD_DOMAIN}
KILLBILL_TLS_DOMAIN_MAIN=${KILLBILL_DOMAIN}
## MIDDLEWARES
TRAEFIK_HTTPS_REDIRECT_MIDDLEWARE=${INFRASTRUCTURE_LABEL:-default}-https-redirect
TRAEFIK_BASIC_AUTH_MIDDLEWARE=${INFRASTRUCTURE_LABEL:-default}-basic-auth
## ENTRYPOINTS
TRAEFIK_ENTRYPOINT=websecure
TRAEFIK_ENTRYPOINT_HTTP=web

29
env/development/.env.tools vendored Normal file
View File

@@ -0,0 +1,29 @@
# ----------------------------------
# NEXTCLOUD DB
# ----------------------------------
MYSQL_ROOT_PASSWORD=headpiece-constant1-denim-mindboost #SQL root Passwort eingeben
MYSQL_PASSWORD=idealist9-frayed-murkiness-mindboost #SQL Benutzer Passwort eingeben
MYSQL_DATABASE=nextcloud-mindboost #Datenbank Name
MYSQL_USER=mindboostcloud #SQL Nutzername
MYSQL_INITDB_SKIP_TZINFO=1
MARIADB_AUTO_UPGRADE=1
# ----------------------------------
# NEXTCLOUD CLOUD
# ----------------------------------
TRUSTED_PROXIES=172.16.255.254/16
OVERWRITEPROTOCOL=https
OVERWRITECLIURL=https://${CLOUD_DOMAIN:-cloud}
OVERWRITEHOST=${CLOUD_DOMAIN:-cloud}
REDIS_HOST=nextcloud-redis
REDIS_HOST_PASSWORD=redis-mindboost-passwort
# ----------------------------------
# KILLBILL PAYMENT
# ----------------------------------
KILLBILL_DAO_URL=jdbc:mysql://db:3306/killbill
KILLBILL_DAO_USER=${ADMIN_USER:-root}
KILLBILL_DAO_PASSWORD=${ADMIN_PASSWORD_HASH}

5
env/development/.env.website vendored Normal file
View File

@@ -0,0 +1,5 @@
# ----------------------------------
# KIRBY CMS
# ----------------------------------
USER_ID=0

31
env/development/portainer/backend.env vendored Normal file
View File

@@ -0,0 +1,31 @@
# ----------------------------------
# Redis
# ----------------------------------
REDIS_PASSWORD=laravel-redis-passwort
REDIS_PORT=6379
SERVER_IP=${SERVER_IP:-localhost}
# ----------------------------------
# Laravel Backend
# ----------------------------------
BACKEND_NETWORK=backend
APP_ENV=${ENVIRONMENT-local}
APP_NAME="mindboost backend - Compose Deployment"
APP_URL=https://backend.local
LARAVEL_PORT=8000
LARAVEL_VITE_PORT=5173
JWT_SECRET=zMtO8sgsnc4UixWSsYWE1pK9EdpNLzxNSoIPlUpTe6dDlarM3bu4cwM80tH3jA0F
# ----------------------------------
# Datenbank Zugriff - ! MUSS MIT .env.database übereinstimmen
# ----------------------------------
DB_HOST=database
DB_PORT=3306
DB_PASSWORD=1stronges-mindboostdb-passwort
DB_USERNAME=${INFRASTRUCTURE_LABEL:-default}_${ENVIRONMENT:-development}
DB_DATABASE=${INFRASTRUCTURE_LABEL:-default}_${ENVIRONMENT:-development}

0
env/production/.env.administration vendored Normal file
View File

1
env/production/.env.backend vendored Normal file
View File

@@ -0,0 +1 @@
${REDIS_PASSWORD}

7
env/production/.env.database vendored Normal file
View File

@@ -0,0 +1,7 @@
# ----------------------------------
# Datenbank (MariaDB)
# ----------------------------------
MARIADB_USER=${INFRASTRUCTURE_LABEL:-default}_${ENVIRONMENT:-development}
MARIADB_DATABASE=${INFRASTRUCTURE_LABEL:-default}_${ENVIRONMENT:-development}
MARIADB_PASSWORD=1stronges-mindboostdb-passwort
MARIADB_ROOT_PASSWORD=1stronges-passwort-fuer-diedb

1
env/production/.env.develop vendored Normal file
View File

@@ -0,0 +1 @@
ADMINER_PORT=8000

0
env/production/.env.frontend vendored Normal file
View File

3
env/production/.env.portainer vendored Normal file
View File

@@ -0,0 +1,3 @@
PORTAINER_IMAGE=portainer/portainer-ce:latest
PORTAINER_DATA_PATH=/opt/containers/portainer/data
PORTAINER_DOMAIN=portainer.yourdomain.com

32
env/production/.env.proxy vendored Normal file
View File

@@ -0,0 +1,32 @@
TRAEFIK_HTTPS_REDIRECT_MIDDLEWARE=${INFRASTRUCTURE_LABEL:-default}-https-redirect
TRAEFIK_BASIC_AUTH_MIDDLEWARE=${INFRASTRUCTURE_LABEL:-default}-basic-auth
TRAEFIK_BASIC_AUTH_USERS=${ADMIN_USER}:${ADMIN_PASSWORD_HASH}
# Service Crowdsec
SERVICES_CROWDSEC_CONTAINER_NAME=crowdsec
SERVICES_CROWDSEC_HOSTNAME=crowdsec
SERVICES_CROWDSEC_IMAGE=crowdsecurity/crowdsec
SERVICES_CROWDSEC_IMAGE_VERSION=latest
SERVICES_CROWDSEC_NETWORKS_CROWDSEC_IPV4=172.31.254.254
# Service Traefik
SERVICES_TRAEFIK_CONTAINER_NAME=${INFRASTRUCTURE_LABEL:-default}-traefik
SERVICES_TRAEFIK_HOSTNAME=${INFRASTRUCTURE_LABEL:-default}-traefik
SERVICES_TRAEFIK_IMAGE=traefik
SERVICES_TRAEFIK_IMAGE_VERSION=2.11
SERVICES_TRAEFIK_LABELS_TRAEFIK_HOST=`traefik.haslach2025.de`
SERVICES_TRAEFIK_NETWORKS_CROWDSEC_IPV4=172.31.254.253
SERVICES_TRAEFIK_NETWORKS_PROXY_IPV4=172.30.255.254
# Service Traefik Crowdsec Bouncer
SERVICES_TRAEFIK_CROWDSEC_BOUNCER_CONTAINER_NAME=traefik_crowdsec_bouncer
SERVICES_TRAEFIK_CROWDSEC_BOUNCER_HOSTNAME=traefik-crowdsec-bouncer
SERVICES_TRAEFIK_CROWDSEC_BOUNCER_IMAGE=fbonalair/traefik-crowdsec-bouncer
SERVICES_TRAEFIK_CROWDSEC_BOUNCER_IMAGE_VERSION=latest
SERVICES_TRAEFIK_CROWDSEC_BOUNCER_NETWORKS_CROWDSEC_IPV4=172.31.254.252
# Netzwerkeinstellungen
NETWORKS_PROXY_NAME=proxy
NETWORKS_PROXY_SUBNET_IPV4=172.30.0.0/16
NETWORKS_CROWDSEC_NAME=crowdsec
NETWORKS_CROWDSEC_SUBNET_IPV4=172.31.0.0/16

0
env/production/.env.tools vendored Normal file
View File

0
env/production/.env.website vendored Normal file
View File

6
env/staging/.env.administration vendored Normal file
View File

@@ -0,0 +1,6 @@
# ----------------------------------
# Portainer
# ----------------------------------

15
env/staging/.env.backend vendored Normal file
View File

@@ -0,0 +1,15 @@
# ----------------------------------
# Redis
# ----------------------------------
# ----------------------------------
# Laravel Backend
# ----------------------------------
# ----------------------------------
# Adminer
# ----------------------------------

3
env/staging/.env.database vendored Normal file
View File

@@ -0,0 +1,3 @@
# ----------------------------------
# Datenbank (MariaDB)
# ----------------------------------

9
env/staging/.env.develop vendored Normal file
View File

@@ -0,0 +1,9 @@
# ----------------------------------
# GITEA
# ----------------------------------
# ----------------------------------
# GITEA DB
# ----------------------------------

3
env/staging/.env.frontend vendored Normal file
View File

@@ -0,0 +1,3 @@
# ----------------------------------
# VUE APP
# ----------------------------------

4
env/staging/.env.proxy vendored Normal file
View File

@@ -0,0 +1,4 @@
# ----------------------------------
# TRAEFIK
# ----------------------------------

9
env/staging/.env.tools vendored Normal file
View File

@@ -0,0 +1,9 @@
# ----------------------------------
# NEXTCLOUD DB
# ----------------------------------
# ----------------------------------
# NEXTCLOUD CLOUD
# ----------------------------------

4
env/staging/.env.website vendored Normal file
View File

@@ -0,0 +1,4 @@
# ----------------------------------
# KIRBY CMS
# ----------------------------------

View File

@@ -0,0 +1,14 @@
# Nextcloud stack configuration
NEXTCLOUD_DOMAIN=cloud.example.com
# Database
NEXTCLOUD_DB_NAME=nextcloud
NEXTCLOUD_DB_USER=nextcloud
NEXTCLOUD_DB_PASSWORD=changeMe
NEXTCLOUD_DB_ROOT_PASSWORD=changeMeRoot
# PHP tuning
NEXTCLOUD_PHP_MEMORY_LIMIT=512M
NEXTCLOUD_PHP_UPLOAD_LIMIT=1024M

View File

@@ -0,0 +1,13 @@
Nextcloud Stack
Env vars required (copy .env.example to .env and adjust):
- NEXTCLOUD_DOMAIN: public domain for Nextcloud
- NEXTCLOUD_DB_NAME, NEXTCLOUD_DB_USER, NEXTCLOUD_DB_PASSWORD, NEXTCLOUD_DB_ROOT_PASSWORD
- Optional: NEXTCLOUD_PHP_MEMORY_LIMIT, NEXTCLOUD_PHP_UPLOAD_LIMIT
Usage
- Ensure the Traefik proxy stack is up and the external `${TRAEFIK_NETWORK}` network exists.
- Run: `docker compose --env-file ../../env/${ENV}/common.env --env-file ./.env -f docker-compose.yml up -d`

View File

@@ -0,0 +1,70 @@
services:
nextcloud:
image: nextcloud:28-apache
container_name: ${INFRASTRUCTURE_LABEL:-stack}-nextcloud
restart: unless-stopped
depends_on:
- db
- redis
environment:
- NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_DOMAIN}
- OVERWRITEHOST=${NEXTCLOUD_DOMAIN}
- OVERWRITEPROTOCOL=https
- OVERWRITECLIURL=https://${NEXTCLOUD_DOMAIN}
- REDIS_HOST=redis
- MYSQL_HOST=db
- MYSQL_DATABASE=${NEXTCLOUD_DB_NAME:-nextcloud}
- MYSQL_USER=${NEXTCLOUD_DB_USER:-nextcloud}
- MYSQL_PASSWORD=${NEXTCLOUD_DB_PASSWORD}
- PHP_MEMORY_LIMIT=${NEXTCLOUD_PHP_MEMORY_LIMIT:-512M}
- PHP_UPLOAD_LIMIT=${NEXTCLOUD_PHP_UPLOAD_LIMIT:-1024M}
volumes:
- nextcloud_data:/var/www/html
networks:
- nextcloud
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.nextcloud.rule=Host(`${NEXTCLOUD_DOMAIN}`)
- traefik.http.routers.nextcloud.entrypoints=websecure
- traefik.http.routers.nextcloud.tls=true
- traefik.http.routers.nextcloud.tls.certresolver=letsencrypt
- traefik.http.services.nextcloud.loadbalancer.server.port=80
- traefik.http.routers.nextcloud.middlewares=security-headers@file
- traefik.docker.network=${TRAEFIK_NETWORK:-proxy}
db:
image: mariadb:11
container_name: ${INFRASTRUCTURE_LABEL:-stack}-nextcloud-db
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=${NEXTCLOUD_DB_ROOT_PASSWORD}
- MYSQL_DATABASE=${NEXTCLOUD_DB_NAME:-nextcloud}
- MYSQL_USER=${NEXTCLOUD_DB_USER:-nextcloud}
- MYSQL_PASSWORD=${NEXTCLOUD_DB_PASSWORD}
volumes:
- db_data:/var/lib/mysql
networks:
- nextcloud
redis:
image: redis:7-alpine
container_name: ${INFRASTRUCTURE_LABEL:-stack}-nextcloud-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- nextcloud
volumes:
nextcloud_data:
db_data:
redis_data:
networks:
proxy:
external: true
nextcloud:
name: ${INFRASTRUCTURE_LABEL:-stack}-nextcloud

View File

@@ -0,0 +1,47 @@
services:
traefik:
image: traefik:v2.11
container_name: ${INFRASTRUCTURE_LABEL:-stack}-traefik
restart: unless-stopped
command:
- --providers.file.directory=/etc/traefik/dynamic
- --providers.file.watch=true
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=${TRAEFIK_NETWORK:-proxy}
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.letsencrypt.acme.httpchallenge=true
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
- --api.dashboard=true
- --log.level=${TRAEFIK_LOG_LEVEL:-INFO}
- --accesslog=true
ports:
- ${TRAEFIK_HTTP_PORT:-80}:80
- ${TRAEFIK_HTTPS_PORT:-443}:443
environment:
- TZ=${TZ:-UTC}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- ./dynamic:/etc/traefik/dynamic:ro
- ./data:/letsencrypt
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.traefik.rule=Host(`${TRAEFIK_DASHBOARD_DOMAIN}`)
- traefik.http.routers.traefik.entrypoints=websecure
- traefik.http.routers.traefik.tls=true
- traefik.http.routers.traefik.tls.certresolver=letsencrypt
- traefik.http.routers.traefik.service=api@internal
- traefik.docker.network=${TRAEFIK_NETWORK:-proxy}
# Optional: protect dashboard with basic auth if TRAEFIK_BASIC_AUTH_USERS is set
- traefik.http.routers.traefik.middlewares=dashboard-basicauth@file
networks:
proxy:
external: true

View File

@@ -0,0 +1,25 @@
http:
middlewares:
redirect-to-https:
redirectScheme:
scheme: https
permanent: true
security-headers:
headers:
frameDeny: true
contentTypeNosniff: true
browserXssFilter: true
referrerPolicy: no-referrer-when-downgrade
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
dashboard-basicauth:
basicAuth:
users:
# Provide users via env TRAEFIK_BASIC_AUTH_USERS, format: user:hashedpassword
# Example to generate: htpasswd -nbB admin 'yourpassword'
# If env is empty, you can comment this middleware out from labels
- ${TRAEFIK_BASIC_AUTH_USERS:-}

View File

@@ -0,0 +1,6 @@
tls:
options:
default:
minVersion: VersionTLS12
sniStrict: true

View File

@@ -0,0 +1,30 @@
api:
dashboard: true
providers:
docker:
exposedByDefault: false
network: ${TRAEFIK_NETWORK:-proxy}
file:
directory: /etc/traefik/dynamic
watch: true
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
log:
level: ${TRAEFIK_LOG_LEVEL:-INFO}
accessLog: {}
certificatesResolvers:
letsencrypt:
acme:
email: ${ACME_EMAIL}
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web

14
infra/env/common.env.example vendored Normal file
View File

@@ -0,0 +1,14 @@
# Global/defaults
INFRASTRUCTURE_LABEL=mindboost
TZ=UTC
# Traefik / proxy
TRAEFIK_NETWORK=proxy
TRAEFIK_HTTP_PORT=80
TRAEFIK_HTTPS_PORT=443
TRAEFIK_LOG_LEVEL=INFO
ACME_EMAIL=you@example.com
TRAEFIK_DASHBOARD_DOMAIN=traefik.example.com
# Optional basic auth users for dashboard (format: user:hashed)
#TRAEFIK_BASIC_AUTH_USERS=admin:$2y$05$...

11
infra/env/development/common.env vendored Normal file
View File

@@ -0,0 +1,11 @@
# Development defaults (copy to production and adjust as needed)
INFRASTRUCTURE_LABEL=dev
TZ=UTC
TRAEFIK_NETWORK=proxy
TRAEFIK_HTTP_PORT=80
TRAEFIK_HTTPS_PORT=443
TRAEFIK_LOG_LEVEL=INFO
ACME_EMAIL=dev@example.com
TRAEFIK_DASHBOARD_DOMAIN=traefik.local

27
nginx/dev-backend.conf Normal file
View File

@@ -0,0 +1,27 @@
server {
listen 80;
server_name _;
root /var/www/public;
index index.php index.html;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block";
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass laravel_backend_dev:9000;
fastcgi_index index.php;
}
location ~ /\.(?!well-known).* {
deny all;
}
}

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
# Create external proxy network if it doesn't exist and prepare Traefik state
NETWORK_NAME=${TRAEFIK_NETWORK:-proxy}
ACME_FILE="infra/core/traefik/data/acme.json"
echo "[bootstrap] Ensuring external network '${NETWORK_NAME}' exists..."
if ! docker network ls --format '{{.Name}}' | grep -qx "${NETWORK_NAME}"; then
docker network create "${NETWORK_NAME}"
echo "[bootstrap] Created network '${NETWORK_NAME}'."
else
echo "[bootstrap] Network '${NETWORK_NAME}' already exists."
fi
echo "[bootstrap] Ensuring ACME storage exists with correct permissions..."
mkdir -p "$(dirname "${ACME_FILE}")"
touch "${ACME_FILE}"
chmod 600 "${ACME_FILE}"
echo "[bootstrap] ACME storage ready at ${ACME_FILE}."
echo "[bootstrap] Done."

0
scripts/setup/set-global-env.sh Normal file → Executable file
View File

0
scripts/setup/set-project-root.sh Normal file → Executable file
View File