11 Commits

27 changed files with 543 additions and 9 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/*

3
.gitmodules vendored
View File

@@ -7,3 +7,6 @@
[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

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

@@ -12,7 +12,7 @@ services:
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}`)"
- "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'

View File

@@ -26,7 +26,4 @@ services:
volumes:
kirbycms_data:
driver: local
driver_opts:
type: none
o: bind
device: /mnt/docker-volumes/website/kirbycms # Neuer fester Speicherort

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`.

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}

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