Compare commits

...

10 Commits

Author SHA1 Message Date
Gato 56a384a9d8 update data path 2026-06-06 21:31:09 +02:00
Gato ad9fbcbbdc Update gitea compose
Signed-off-by: Gato <cedric@goutailler-olivier.fr>
2026-06-06 21:03:52 +02:00
Gato 5ba69d1e16 Rattrapage pb de gitea
Signed-off-by: Gato <cedric@goutailler-olivier.fr>
2026-06-06 21:01:24 +02:00
Gato 86fa4f8519 Changement du runner
Signed-off-by: Gato <cedric@goutailler-olivier.fr>
2026-06-05 06:36:52 +02:00
Gato 16668b1961 docs: documenter le workflow release automatique de Luz 2026-06-05 05:59:02 +02:00
Gato bd6ee2ba2b docs: ajout du projet Luz (luz.goutailler-olivier.com)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 05:51:19 +02:00
Gato 135187f69f CI rollback
Signed-off-by: Gato <cedric@goutailler-olivier.fr>
2026-05-31 17:38:23 +02:00
Gato df3a672387 Update 31 mai
Signed-off-by: Gato <cedric@goutailler-olivier.fr>
2026-05-31 15:09:38 +02:00
Gato 7312d2832f Misa a jours des container 2026-05-25 08:38:30 +02:00
Gato 1aef6e395e update doc installation production 2026-05-25 07:56:19 +02:00
21 changed files with 958 additions and 363 deletions
+18
View File
@@ -0,0 +1,18 @@
{
"permissions": {
"allow": [
"Bash(ls /var/home/Gato/IdeaProjects/Infra/)",
"Read(//var/home/Gato/IdeaProjects/Infra/**)",
"Read(//var/home/Gato/Applications/Infra/keycloak/themes/bonsai/**)",
"Read(//var/home/Gato/Applications/Infra/keycloak/themes/bonsai/login/**)",
"Bash(realpath /var/home/Gato/Applications 2>/dev/null || ls /var/home/Gato/ | grep -i app)",
"Bash(ls /var/home/Gato/IdeaProjects/Infra && ls /var/home/Gato/IdeaProjects/bonsai-api)",
"Read(//var/home/Gato/IdeaProjects/**)",
"Bash(mkdir -p /var/home/Gato/IdeaProjects/Infra/bonsai-api)",
"Bash(git *)"
],
"additionalDirectories": [
"/var/home/Gato/IdeaProjects/Infra"
]
}
}
-37
View File
@@ -1,37 +0,0 @@
# Infrastructure — Vue d'ensemble
Ce dépôt contient toutes les configurations Docker Compose de l'infrastructure auto-hébergée.
## Architecture
```
Internet
Traefik (reverse proxy, TLS Let's Encrypt)
├── git.goutailler-olivier.com → Gitea (forge + CI/CD)
├── auth.goutailler-olivier.com → Keycloak (SSO / OAuth2)
├── bonsai.goutailler-olivier.com
│ ├── /api → Bonsai API (Spring Boot)
│ └── / → Bonsai Webapp (front-end)
├── cloud.goutailler-olivier.com → Nextcloud
└── notes.goutailler-olivier.com → Trilium
```
## Services
| Service | Dossier | URL |
|---|---|---|
| Traefik | `traefik/` | `traefik.goutailler-olivier.com` |
| Gitea | `gitea/` | `git.goutailler-olivier.com` |
| Keycloak | `keycloak/` | `auth.goutailler-olivier.com` |
| Bonsai API | `bonsai-api/` | `bonsai.goutailler-olivier.com/api` |
| Bonsai Webapp | `bonsai-webapp/` | `bonsai.goutailler-olivier.com` |
| Nextcloud | `nextcloud/` | `cloud.goutailler-olivier.com` |
| Trilium | `trilium/` | `notes.goutailler-olivier.com` |
## Pages
- [Installation Production](Installation-Production)
- [Installation Développement](Installation-Developpement)
-150
View File
@@ -1,150 +0,0 @@
# Installation Développement
Cette page décrit comment démarrer l'environnement de développement local pour le projet **Bonsai API**.
## Prérequis
| Outil | Version minimale |
|---|---|
| Java JDK | 25 |
| Docker + Docker Compose | 24+ |
| Gradle | 8+ (wrapper inclus dans le dépôt) |
---
## Cloner le dépôt
```bash
git clone https://git.goutailler-olivier.com/bonsai/bonsai-api.git
cd bonsai-api
```
---
## Option A — Hot-reload avec Docker Compose (recommandé)
Cette option monte les sources depuis l'hôte dans le conteneur. Gradle détecte les changements et relance automatiquement l'application.
```bash
docker compose -f docker-compose.dev.yml up --build
```
- L'API est disponible sur `http://localhost:8080`
- PostgreSQL est disponible sur `localhost:5432`
- Le cache Gradle est conservé dans un volume dédié (`gradle_home`) : le premier build télécharge les dépendances, les suivants sont rapides
Pour arrêter et tout supprimer (y compris les volumes) :
```bash
docker compose -f docker-compose.dev.yml down -v
```
---
## Option B — Gradle en local + PostgreSQL Docker
Démarrer uniquement la base de données :
```bash
docker compose up db -d
```
Lancer l'API avec le wrapper Gradle :
```bash
./gradlew bootRun
```
Flyway applique automatiquement la migration `V1__init.sql` au premier démarrage.
---
## Variables d'environnement
En développement, les valeurs par défaut sont utilisées automatiquement (définies dans `src/main/resources/application.yml`). Aucun `.env` n'est nécessaire.
| Variable | Valeur locale | Description |
|---|---|---|
| `DATASOURCE_URL` | `jdbc:postgresql://localhost:5432/bonsai` | URL JDBC |
| `DATASOURCE_USERNAME` | `bonsai` | Utilisateur PostgreSQL |
| `DATASOURCE_PASSWORD` | `bonsai` | Mot de passe PostgreSQL |
| `KEYCLOAK_JWKS_URI` | `https://auth.goutailler-olivier.com/realms/bonsai/protocol/openid-connect/certs` | Endpoint JWKS |
| `CORS_ALLOWED_ORIGIN_PROD` | `https://bonsai.goutailler-olivier.com` | Origine CORS de prod |
L'origine `http://localhost:4200` est toujours autorisée en CORS (front Angular en dev).
---
## Documentation de l'API
Swagger UI est disponible à l'adresse suivante une fois l'API démarrée :
```
http://localhost:8080/swagger-ui.html
```
La spécification OpenAPI (JSON) est accessible sur :
```
http://localhost:8080/v3/api-docs
```
---
## Lancer les tests
```bash
./gradlew test
```
Le rapport HTML est généré dans `build/reports/tests/test/index.html`.
---
## Structure du projet
```
src/main/java/fr/bonsai/api/
├── domain/model/ # Entités métier (sans dépendance Spring)
├── application/
│ ├── port/in/ # Interfaces des use cases
│ ├── port/out/ # Interface du repository
│ └── usecase/ # Logique métier (IssueService)
├── adapter/
│ ├── in/web/ # Controllers REST et DTOs
│ └── out/persistence/ # Entités JPA et adaptateur repository
└── config/ # Configuration Spring (Security, CORS, Beans)
```
---
## Sécurité
Toutes les routes nécessitent un token JWT Bearer valide émis par Keycloak :
- **Realm** : `bonsai`
- **Client** : `bonsai-webapp`
- **Issuer** : `https://auth.goutailler-olivier.com/realms/bonsai`
Pour tester sans Keycloak local, utiliser l'instance de production comme fournisseur JWKS (valeur par défaut).
```http
Authorization: Bearer <token>
```
---
## Endpoints disponibles
| Méthode | Route | Description |
|---|---|---|
| `GET` | `/issues` | Liste toutes les issues |
| `POST` | `/issues` | Crée une issue |
| `PUT` | `/issues/{id}` | Remplace une issue |
| `DELETE` | `/issues/{id}` | Supprime une issue (204) |
---
## CI/CD
Le pipeline Gitea Actions (`.gitea/workflows/`) construit l'image Docker et la pousse sur le registre interne à chaque push sur `main`. Le runner `ubuntu-latest` est fourni par le conteneur `act_runner` de la stack Gitea.
-176
View File
@@ -1,176 +0,0 @@
# Installation Production
## Prérequis
- Serveur Linux avec Docker 24+ et Docker Compose V2
- Domaine DNS pointant vers le serveur (`goutailler-olivier.com`)
- Ports **80**, **443** et **2222** ouverts en entrée
---
## 1. Réseau Docker partagé
Tous les services communiquent via un réseau externe `proxy`. À créer une seule fois :
```bash
docker network create proxy
```
---
## 2. Traefik
Traefik est le point d'entrée unique : il gère le TLS (Let's Encrypt) et route les requêtes vers chaque service.
```bash
cd traefik/
docker compose up -d
```
Le dashboard est exposé sur `https://traefik.goutailler-olivier.com` (accès restreint par défaut).
---
## 3. Keycloak
Keycloak gère l'authentification SSO pour toute l'infrastructure.
```bash
cd keycloak/
cp .env.example .env
# Éditer .env avec des mots de passe sécurisés
docker compose up -d
```
Variables à définir dans `.env` :
| Variable | Description |
|---|---|
| `POSTGRES_PASSWORD` | Mot de passe de la base PostgreSQL |
| `KEYCLOAK_ADMIN` | Login administrateur (défaut : `admin`) |
| `KEYCLOAK_ADMIN_PASSWORD` | Mot de passe administrateur |
**Configuration post-démarrage :**
1. Se connecter à `https://auth.goutailler-olivier.com/admin`
2. Créer le realm `bonsai`
3. Créer le client `bonsai-webapp` (type *OpenID Connect*, flux *Authorization Code*)
4. Configurer les *Valid redirect URIs* : `https://bonsai.goutailler-olivier.com/*`
---
## 4. Gitea
Gitea est la forge Git avec le runner CI/CD intégré.
```bash
cd gitea/
docker compose -f gitea-compose.yml up -d
```
**Configuration post-démarrage :**
1. Accéder à `https://git.goutailler-olivier.com` et terminer l'installation via l'interface web
2. Créer l'organisation `bonsai`
3. Récupérer un token d'enregistrement pour le runner dans *Administration → Actions → Runners*
4. Mettre à jour `GITEA_RUNNER_REGISTRATION_TOKEN` dans `gitea-compose.yml`, puis redémarrer le service `act_runner`
Le runner est configuré avec le label `ubuntu-latest` mappé sur `ubuntu:22.04`.
---
## 5. Bonsai API
L'image est construite par la CI Gitea et poussée sur le registre `git.goutailler-olivier.com/bonsai/bonsai-api:latest`.
```bash
cd bonsai-api/
# Créer un fichier .env avec le mot de passe PostgreSQL
echo "POSTGRES_PASSWORD=<mot_de_passe_sécurisé>" > .env
docker compose up -d
```
Variables d'environnement :
| Variable | Description |
|---|---|
| `POSTGRES_PASSWORD` | Mot de passe PostgreSQL (injecté via `.env`) |
| `DATASOURCE_URL` | `jdbc:postgresql://db:5432/bonsai` (défaut réseau interne) |
| `KEYCLOAK_JWKS_URI` | `https://auth.goutailler-olivier.com/realms/bonsai/protocol/openid-connect/certs` |
| `CORS_ALLOWED_ORIGIN_PROD` | `https://bonsai.goutailler-olivier.com` |
Flyway applique automatiquement les migrations SQL au démarrage.
---
## 6. Bonsai Webapp
```bash
cd bonsai-webapp/
docker compose up -d
```
L'image `git.goutailler-olivier.com/bonsai/bonsai-webapp:latest` est également construite par la CI. Aucune variable d'environnement spécifique n'est requise.
---
## 7. Nextcloud
```bash
cd nextcloud/
# Adapter les mots de passe dans docker-compose.yml avant le premier démarrage
docker compose up -d
```
Variables à personnaliser dans `docker-compose.yml` avant le premier lancement :
| Variable | Description |
|---|---|
| `POSTGRES_PASSWORD` | Mot de passe PostgreSQL |
| `NEXTCLOUD_ADMIN_USER` | Compte administrateur Nextcloud |
| `NEXTCLOUD_ADMIN_PASSWORD` | Mot de passe administrateur |
---
## 8. Trilium
```bash
cd trilium/
docker compose up -d
```
Les données sont persistées dans `/home/gato/Applications/Trilium/data` sur l'hôte. S'assurer que ce chemin existe avant le démarrage :
```bash
mkdir -p /home/gato/Applications/Trilium/data
```
---
## Ordre de démarrage recommandé
```
1. Réseau proxy (une seule fois)
2. Traefik
3. Keycloak
4. Gitea
5. Bonsai API
6. Bonsai Webapp
7. Nextcloud
8. Trilium
```
---
## Vérifications
```bash
# État de tous les conteneurs
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
# Logs d'un service
docker logs <nom_conteneur> --tail 50 -f
# Certificats TLS Traefik
docker logs traefik 2>&1 | grep -i "certificate\|acme"
```
+54
View File
@@ -0,0 +1,54 @@
name: bonsai-api-stack
services:
db:
image: postgres:16-alpine
container_name: bonsai-api-db
restart: unless-stopped
environment:
POSTGRES_DB: bonsai
POSTGRES_USER: bonsai
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
TZ: Europe/Paris
healthcheck:
test: ["CMD-SHELL", "pg_isready -U bonsai -d bonsai"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- ~/Applications/data/bonsai-api/db_data:/var/lib/postgresql/data
networks:
- bonsai-api-net
api:
image: git.goutailler-olivier.com/bonsai/bonsai-api:latest
container_name: bonsai-api
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
DATASOURCE_URL: jdbc:postgresql://db:5432/bonsai
DATASOURCE_USERNAME: bonsai
DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
KEYCLOAK_JWKS_URI: https://auth.goutailler-olivier.com/realms/bonsai/protocol/openid-connect/certs
CORS_ALLOWED_ORIGIN_PROD: https://bonsai.goutailler-olivier.com
TZ: Europe/Paris
networks:
- bonsai-api-net
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.bonsai-api.rule=Host(`bonsai.goutailler-olivier.com`) && PathPrefix(`/api`)
- traefik.http.routers.bonsai-api.entrypoints=websecure
- traefik.http.routers.bonsai-api.tls.certresolver=le
- traefik.http.services.bonsai-api.loadbalancer.server.port=8080
- traefik.docker.network=proxy
- com.centurylinklabs.watchtower.enable=true
networks:
bonsai-api-net:
driver: bridge
proxy:
external: true
name: proxy
+27
View File
@@ -0,0 +1,27 @@
name: bonsai-webapp
services:
bonsai-webapp:
image: git.goutailler-olivier.com/bonsai/bonsai-webapp:latest
container_name: bonsai-webapp
restart: unless-stopped
environment:
TZ: Europe/Paris
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.bonsai-webapp.rule=Host(`bonsai.goutailler-olivier.com`)
- traefik.http.routers.bonsai-webapp.entrypoints=websecure
- traefik.http.routers.bonsai-webapp.tls.certresolver=le
- traefik.http.services.bonsai-webapp.loadbalancer.server.port=80
- traefik.docker.network=proxy
- com.centurylinklabs.watchtower.enable=true
networks:
proxy:
external: true
name: proxy
+9
View File
@@ -0,0 +1,9 @@
FROM gitea/act_runner:latest
# Installer Node.js et npm
RUN apk add --no-cache nodejs npm
# Docker CLI déjà installé via le wget précédent
RUN wget -O /tmp/docker.tgz https://download.docker.com/linux/static/stable/x86_64/docker-26.1.4.tgz \
&& tar -xzf /tmp/docker.tgz --strip-components=1 -C /usr/local/bin docker/docker \
&& rm /tmp/docker.tgz
+13
View File
@@ -0,0 +1,13 @@
## Mise a jours
```bash
docker compose -f gitea-compose.yml down
tar -czvf gitea_backup_$(date +%Y%m%d).tar.gz ./gitea ./db_data
docker compose -f gitea-compose.yml pull gitea
docker compose -f gitea-compose.yml up -d
docker exec gitea gitea --version
```
Relancer le runner
```bash
docker compose -f gitea-compose.yml restart act_runner
```
+109
View File
@@ -0,0 +1,109 @@
name: gitea-stack
services:
db:
image: postgres:16-alpine
container_name: gitea-db
restart: unless-stopped
environment:
POSTGRES_DB: gitea
POSTGRES_USER: gitea
POSTGRES_PASSWORD: change_me
TZ: Europe/Paris
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- ~/Applications/data/gitea/db_data:/var/lib/postgresql/data
networks:
- gitea-net
gitea:
image: gitea/gitea:latest
container_name: gitea
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
USER_UID: 1000
USER_GID: 1000
# ---- URLs / proxy (Traefik) ----
GITEA__server__DOMAIN: git.goutailler-olivier.com
GITEA__server__ROOT_URL: https://git.goutailler-olivier.com/
GITEA__server__HTTP_PORT: "3000"
# ---- SSH (optionnel) ----
# Laisse lSSH intégré de Gitea activé et expose un port hôte 2222 (voir plus bas)
GITEA__server__SSH_DOMAIN: git.goutailler-olivier.com
GITEA__server__START_SSH_SERVER: "true"
GITEA__server__SSH_PORT: "2222"
GITEA__server__SSH_LISTEN_PORT: "2222"
# ---- Base de données ----
GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: db:5432
GITEA__database__NAME: gitea
GITEA__database__USER: gitea
GITEA__database__PASSWD: change_me
TZ: Europe/Paris
volumes:
- ~/Applications/data/gitea:/data
# (facultatif) pour horloge locale dans les logs :
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
# Pas d'exposition du port HTTP: Traefik s'en charge
# On expose seulement l'SSH si tu veux cloner/pusher en SSH
networks:
- gitea-net
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.gitea.rule=Host(`git.goutailler-olivier.com`)
- traefik.http.routers.gitea.entrypoints=websecure
- traefik.http.routers.gitea.tls.certresolver=le
- traefik.http.services.gitea.loadbalancer.server.port=3000
- traefik.docker.network=proxy
# SSH (TCP router)
- traefik.tcp.routers.gitea-ssh.rule=HostSNI(`*`)
- traefik.tcp.routers.gitea-ssh.tls=false
- traefik.tcp.routers.gitea-ssh.entrypoints=ssh
- traefik.tcp.routers.gitea-ssh.service=gitea-ssh
- traefik.tcp.services.gitea-ssh.loadbalancer.server.port=2222
act_runner:
build:
context: .
dockerfile: Dockerfile.runner
extra_hosts:
- "git.goutailler-olivier.com:host-gateway"
container_name: gitea-runner
restart: unless-stopped
depends_on:
- gitea
environment:
GITEA_INSTANCE_URL: http://gitea:3000
GITEA_RUNNER_REGISTRATION_TOKEN: Rvi31evVGlyH8o1h2lw200uMjOJyCrBQJXLKQqJk
GITEA_RUNNER_NAME: docker-runner
GITEA_RUNNER_LABELS: ubuntu-latest:host
CONFIG_FILE: /config.yaml
GITEA__actions__ENABLED: "true"
GITEA__actions__DEFAULT_ACTIONS_URL: http://gitea:3000
volumes:
- ~/Applications/data/gitea/runner_data:/data
- ~/Applications/data/gitea/runner_data/config.yaml:/config.yaml
- /var/run/docker.sock:/var/run/docker.sock
networks:
- gitea-net
networks:
gitea-net:
driver: bridge
proxy:
external: true
name: proxy
+3
View File
@@ -0,0 +1,3 @@
POSTGRES_PASSWORD=changeme
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=changeme
+67
View File
@@ -0,0 +1,67 @@
name: keycloak-stack
services:
db:
image: postgres:16-alpine
container_name: keycloak-db
restart: unless-stopped
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
TZ: Europe/Paris
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- ~/Applications/data/keycloak/db_data:/var/lib/postgresql/data
networks:
- keycloak-net
keycloak:
image: quay.io/keycloak/keycloak:26.2
container_name: keycloak
restart: unless-stopped
command: start
depends_on:
db:
condition: service_healthy
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://db:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: ${POSTGRES_PASSWORD}
KC_HOSTNAME: auth.goutailler-olivier.com
KC_HOSTNAME_STRICT: "true"
KC_HTTP_ENABLED: "true"
KC_PROXY_HEADERS: xforwarded
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin}
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
TZ: Europe/Paris
KC_SPI_THEME_STATIC_MAX_AGE: "-1"
KC_SPI_THEME_CACHE_THEMES: "false"
KC_SPI_THEME_CACHE_TEMPLATES: "false"
volumes:
- ./themes:/opt/keycloak/themes
networks:
- keycloak-net
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.keycloak.rule=Host(`auth.goutailler-olivier.com`)
- traefik.http.routers.keycloak.entrypoints=websecure
- traefik.http.routers.keycloak.tls.certresolver=le
- traefik.http.services.keycloak.loadbalancer.server.port=8080
- traefik.docker.network=proxy
networks:
keycloak-net:
driver: bridge
proxy:
external: true
name: proxy
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="${(locale.currentLanguageTag)!'fr'}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bonsai — Réinitialisation du mot de passe</title>
<link rel="stylesheet" href="${url.resourcesPath}/css/login.css">
</head>
<body>
<div class="page">
<div class="card">
<div class="logo">
<svg viewBox="0 0 48 56" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M14 56 L16 49 L32 49 L34 56 Z" fill="#C05621"/>
<rect x="12" y="45" width="24" height="5" rx="2" fill="#9C4221"/>
<path d="M24 45 C24 39 22 33 20 27 C18 22 19 17 22 13" stroke="#744210" stroke-width="4" stroke-linecap="round"/>
<path d="M21 28 C26 25 31 22 33 18" stroke="#744210" stroke-width="2.5" stroke-linecap="round"/>
<path d="M20 35 C15 32 11 28 10 24" stroke="#744210" stroke-width="2" stroke-linecap="round"/>
<circle cx="10" cy="21" r="9" fill="#276749"/>
<circle cx="34" cy="16" r="10" fill="#276749"/>
<circle cx="22" cy="11" r="11" fill="#2F855A"/>
<circle cx="26" cy="17" r="8" fill="#38A169"/>
<circle cx="18" cy="16" r="6" fill="#48BB78"/>
</svg>
<span class="logo-name">Bonsai</span>
</div>
<h1 class="title">Mot de passe oublié ?</h1>
<p class="subtitle">Saisissez votre email pour recevoir un lien de réinitialisation.</p>
<#if message?has_content>
<div class="alert alert--${message.type}">
${message.summary}
</div>
</#if>
<form action="${url.loginAction}" method="post">
<div class="field">
<label for="username">Email ou nom d'utilisateur</label>
<input
type="text"
id="username"
name="username"
value="${(auth.attemptedUsername!'')}"
autocomplete="username"
autofocus
/>
</div>
<button type="submit" class="btn-primary">Envoyer le lien</button>
</form>
<p class="register-link">
<a href="${url.loginUrl}">← Retour à la connexion</a>
</p>
</div>
</div>
</body>
</html>
+105
View File
@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="${(locale.currentLanguageTag)!'fr'}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bonsai — Connexion</title>
<link rel="stylesheet" href="${url.resourcesPath}/css/login.css">
</head>
<body>
<div class="page">
<div class="card">
<div class="logo">
<svg viewBox="0 0 48 56" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M14 56 L16 49 L32 49 L34 56 Z" fill="#C05621"/>
<rect x="12" y="45" width="24" height="5" rx="2" fill="#9C4221"/>
<path d="M24 45 C24 39 22 33 20 27 C18 22 19 17 22 13" stroke="#744210" stroke-width="4" stroke-linecap="round"/>
<path d="M21 28 C26 25 31 22 33 18" stroke="#744210" stroke-width="2.5" stroke-linecap="round"/>
<path d="M20 35 C15 32 11 28 10 24" stroke="#744210" stroke-width="2" stroke-linecap="round"/>
<circle cx="10" cy="21" r="9" fill="#276749"/>
<circle cx="34" cy="16" r="10" fill="#276749"/>
<circle cx="22" cy="11" r="11" fill="#2F855A"/>
<circle cx="26" cy="17" r="8" fill="#38A169"/>
<circle cx="18" cy="16" r="6" fill="#48BB78"/>
</svg>
<span class="logo-name">Bonsai</span>
</div>
<h1 class="title">Connexion</h1>
<#if message?has_content>
<div class="alert alert--${message.type}">
${message.summary}
</div>
</#if>
<#if realm.password>
<form action="${url.loginAction}" method="post" novalidate>
<input type="hidden" id="id-hidden-input" name="credentialId"
<#if auth.selectedCredential?has_content>value="${auth.selectedCredential}"</#if>/>
<div class="field">
<label for="username">Email ou nom d'utilisateur</label>
<input
type="text"
id="username"
name="username"
value="${(login.username!'')}"
autocomplete="username"
<#if usernameEditDisabled??>disabled</#if>
autofocus
/>
</div>
<div class="field">
<div class="field-label-row">
<label for="password">Mot de passe</label>
<#if realm.resetPasswordAllowed>
<a href="${url.loginResetCredentialsUrl}" class="forgot-link" tabindex="5">
Mot de passe oublié ?
</a>
</#if>
</div>
<input
type="password"
id="password"
name="password"
autocomplete="current-password"
/>
</div>
<#if realm.rememberMe && !usernameEditDisabled??>
<div class="remember">
<input type="checkbox" id="rememberMe" name="rememberMe"
<#if login.rememberMe??>checked</#if>>
<label for="rememberMe">Se souvenir de moi</label>
</div>
</#if>
<button type="submit" class="btn-primary">Se connecter</button>
</form>
</#if>
<#if social.providers?has_content>
<div class="divider"><span>ou</span></div>
<div class="socials">
<#list social.providers as p>
<a href="${p.loginUrl}" class="btn-social">
Continuer avec ${p.displayName!''}
</a>
</#list>
</div>
</#if>
<#if realm.password && realm.registrationAllowed && !registrationDisabled??>
<p class="register-link">
Pas encore de compte ?
<a href="${url.registrationUrl}">Créer un compte</a>
</p>
</#if>
</div>
</div>
</body>
</html>
@@ -0,0 +1,262 @@
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
font-size: 0.9rem;
color: #111827;
background: #f0fdf4;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
/* ── Page ── */
.page {
width: 100%;
padding: 1.5rem;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
/* ── Card ── */
.card {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 0.75rem;
padding: 2.5rem 2rem;
width: 100%;
max-width: 380px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
gap: 1.25rem;
}
/* ── Logo ── */
.logo {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
}
.logo svg {
width: 52px;
height: 60px;
}
.logo-name {
font-size: 1.3rem;
font-weight: 800;
color: #111827;
letter-spacing: -0.02em;
}
/* ── Titles ── */
.title {
font-size: 1.1rem;
font-weight: 700;
color: #111827;
text-align: center;
}
.subtitle {
font-size: 0.85rem;
color: #6b7280;
text-align: center;
line-height: 1.5;
}
/* ── Alert ── */
.alert {
padding: 0.65rem 0.875rem;
border-radius: 0.5rem;
font-size: 0.85rem;
line-height: 1.4;
}
.alert--error {
background: #fef2f2;
color: #dc2626;
border: 1px solid #fecaca;
}
.alert--warning {
background: #fffbeb;
color: #d97706;
border: 1px solid #fde68a;
}
.alert--success {
background: #f0fdf4;
color: #16a34a;
border: 1px solid #bbf7d0;
}
.alert--info {
background: #eff6ff;
color: #2563eb;
border: 1px solid #bfdbfe;
}
/* ── Form fields ── */
.field {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.field-label-row {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.field label {
font-size: 0.85rem;
font-weight: 500;
color: #374151;
}
.field input {
width: 100%;
padding: 0.55rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.9rem;
color: #111827;
background: #ffffff;
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
}
.field input:focus {
border-color: #2F855A;
box-shadow: 0 0 0 3px rgba(47, 133, 90, 0.15);
}
.field input:disabled {
background: #f3f4f6;
color: #9ca3af;
cursor: not-allowed;
}
.forgot-link {
font-size: 0.78rem;
color: #2F855A;
text-decoration: none;
}
.forgot-link:hover {
text-decoration: underline;
}
/* ── Remember me ── */
.remember {
display: flex;
align-items: center;
gap: 0.5rem;
}
.remember input[type="checkbox"] {
width: 1rem;
height: 1rem;
accent-color: #2F855A;
cursor: pointer;
flex-shrink: 0;
}
.remember label {
font-size: 0.85rem;
color: #374151;
cursor: pointer;
}
/* ── Primary button ── */
.btn-primary {
width: 100%;
padding: 0.6rem;
background: #2F855A;
color: #ffffff;
font-size: 0.9rem;
font-weight: 600;
border: none;
border-radius: 0.5rem;
cursor: pointer;
transition: background 0.15s;
}
.btn-primary:hover {
background: #276749;
}
.btn-primary:active {
background: #1e5236;
}
/* ── Divider ── */
.divider {
display: flex;
align-items: center;
gap: 0.75rem;
color: #9ca3af;
font-size: 0.8rem;
}
.divider::before,
.divider::after {
content: '';
flex: 1;
border-top: 1px solid #e5e7eb;
}
/* ── Social buttons ── */
.socials {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.btn-social {
display: block;
width: 100%;
padding: 0.55rem;
text-align: center;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
background: #ffffff;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
text-decoration: none;
transition: background 0.1s, border-color 0.1s;
}
.btn-social:hover {
background: #f3f4f6;
border-color: #9ca3af;
}
/* ── Register link ── */
.register-link {
text-align: center;
font-size: 0.83rem;
color: #6b7280;
}
.register-link a {
color: #2F855A;
text-decoration: none;
font-weight: 500;
}
.register-link a:hover {
text-decoration: underline;
}
@@ -0,0 +1,2 @@
parent=keycloak
styles=css/login.css
+27
View File
@@ -0,0 +1,27 @@
name: luz
services:
luz:
image: git.goutailler-olivier.com/gato/luz:latest
container_name: luz
restart: unless-stopped
environment:
TZ: Europe/Paris
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.luz.rule=Host(`luz.goutailler-olivier.com`)
- traefik.http.routers.luz.entrypoints=websecure
- traefik.http.routers.luz.tls.certresolver=le
- traefik.http.services.luz.loadbalancer.server.port=80
- traefik.docker.network=proxy
- com.centurylinklabs.watchtower.enable=true
networks:
proxy:
external: true
name: proxy
+95
View File
@@ -0,0 +1,95 @@
# Nextcloud on port 8088 with Postgres and pgAdmin
# ------------------------------------------------
# Quick start:
# docker compose up -d # (Compose V2 syntax; no `version:` key)
name: nextcloud-stack
services:
db:
image: postgres:16-alpine
container_name: nextcloud-db
restart: unless-stopped
environment:
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: changeme
TZ: Europe/Paris
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- ~/Applications/data/nextcloud/db_data:/var/lib/postgresql/data
networks:
- nextcloud-net
nextcloud:
image: nextcloud:latest
container_name: nextcloud-app
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
POSTGRES_HOST: db
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: changeme
NEXTCLOUD_ADMIN_USER: admin
NEXTCLOUD_ADMIN_PASSWORD: adminpass
NEXTCLOUD_TRUSTED_DOMAINS: cloud.goutailler-olivier.com
NEXTCLOUD_OVERWRITEHOST: cloud.goutailler-olivier.com
NEXTCLOUD_OVERWRITEPROTOCOL: https
NEXTCLOUD_TRUSTED_PROXIES: 172.23.0.0/16
APACHE_DISABLE_REWRITE_IP: "1"
PHP_MEMORY_LIMIT: 1G
PHP_UPLOAD_LIMIT: 2G
TZ: Europe/Paris
volumes:
- ~/Applications/data/nextcloud/nextcloud_app:/var/www/html
- ~/Applications/data/nextcloud/nextcloud_data:/var/www/html/data
networks:
- nextcloud-net
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.nextcloud.rule=Host(`cloud.goutailler-olivier.com`)
- traefik.http.routers.nextcloud.entrypoints=websecure
- traefik.http.routers.nextcloud.tls.certresolver=le
- traefik.http.services.nextcloud.loadbalancer.server.port=80
- traefik.docker.network=proxy
# (optionnel) quelques en-têtes de sécurité
- traefik.http.routers.nextcloud.middlewares=nc-sec
- traefik.http.middlewares.nc-sec.headers.stsSeconds=31536000
- traefik.http.middlewares.nc-sec.headers.stsIncludeSubdomains=true
- traefik.http.middlewares.nc-sec.headers.stsPreload=true
- traefik.http.middlewares.nc-sec.headers.contentTypeNosniff=true
- traefik.http.middlewares.nc-sec.headers.browserXssFilter=true
pgadmin:
image: dpage/pgadmin4:latest
container_name: nextcloud-pgadmin
restart: unless-stopped
environment:
PGADMIN_DEFAULT_EMAIL: admin@example.com
PGADMIN_DEFAULT_PASSWORD: adminpass
PGADMIN_CONFIG_SERVER_MODE: 'False'
TZ: Europe/Paris
volumes:
- ~/Applications/data/nextcloud/pgadmin_data:/var/lib/pgadmin
- ~/Applications/data/nextcloud/pgadmin/servers.json:/pgadmin4/servers.json:ro
networks:
- nextcloud-net
networks:
nextcloud-net:
driver: bridge
proxy:
external: true
name: proxy
+45
View File
@@ -0,0 +1,45 @@
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=proxy
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
# Redirection HTTP -> HTTPS
- --entrypoints.web.http.redirections.entryPoint.to=websecure
- --entrypoints.web.http.redirections.entryPoint.scheme=https
# Let's Encrypt (HTTP-01)
- --certificatesresolvers.le.acme.email=cedric@goutailler-olivier.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
- --entrypoints.ssh.address=:2222
# (Optionnel) Dashboard interne
- --api.dashboard=true
ports:
- "80:80"
- "443:443"
- "2222:2222"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik-letsencrypt:/letsencrypt
networks:
- proxy
environment:
- TZ=Europe/Paris
labels:
- traefik.enable=true
- traefik.http.routers.traefik.rule=Host(`traefik.goutailler-olivier.com`)
- traefik.http.routers.traefik.entrypoints=websecure
- traefik.http.routers.traefik.tls.certresolver=le
- traefik.http.routers.traefik.service=api@internal
networks:
# nextcloud-net:
# driver: bridge
proxy:
external: true
name: proxy
+30
View File
@@ -0,0 +1,30 @@
services:
trilium:
image: triliumnext/trilium:latest
container_name: trilium
restart: unless-stopped
environment:
TRILIUM_DATA_DIR: /home/node/trilium-data
TZ: Europe/Paris
volumes:
- /home/gato/Applications/Trilium/data:/home/node/trilium-data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.trilium.rule=Host(`notes.goutailler-olivier.com`)
- traefik.http.routers.trilium.entrypoints=websecure
- traefik.http.routers.trilium.tls.certresolver=le
- traefik.http.services.trilium.loadbalancer.server.port=8080
- traefik.docker.network=proxy
networks:
proxy:
external: true
name: proxy
+1
View File
@@ -0,0 +1 @@
WATCHTOWER_TOKEN=votre_token_secret_ici
+30
View File
@@ -0,0 +1,30 @@
name: watchtower
services:
watchtower:
image: containrrr/watchtower
container_name: watchtower
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /root/.docker/config.json:/root/.docker/config.json:ro
environment:
WATCHTOWER_HTTP_API_UPDATE: "true"
WATCHTOWER_HTTP_API_TOKEN: ${WATCHTOWER_TOKEN}
WATCHTOWER_LABEL_ENABLE: "true"
WATCHTOWER_CLEANUP: "true"
TZ: Europe/Paris
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.watchtower.rule=Host(`watchtower.goutailler-olivier.com`)
- traefik.http.routers.watchtower.entrypoints=websecure
- traefik.http.routers.watchtower.tls.certresolver=le
- traefik.http.services.watchtower.loadbalancer.server.port=8080
- traefik.docker.network=proxy
networks:
proxy:
external: true
name: proxy