replace cadvisor for otelcollector docker_stats plugin, use docker socket proxy for otelcollector
This commit is contained in:
1 parent
0747dda9e7
commit
887668dde9
7 files changed
+588
-506
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Infrastructure qpismont.fr
|
||||
|
||||
Infrastructure self-hosted complète hébergée sur serveur dédié, fournissant une plateforme DevOps avec Git, CI/CD, reverse proxy, VPN et monitoring.
|
||||
Infrastructure self-hosted complète hébergée sur serveur dédié, fournissant une plateforme DevOps avec Git, CI/CD, reverse proxy, VPN et observabilité.
|
||||
|
||||
**Domaine :** qpismont.fr
|
||||
**Stack :** Docker Swarm
|
||||
@@ -14,35 +14,53 @@ Cette infrastructure héberge l'ensemble des services nécessaires pour le déve
|
||||
|
||||
- **Hébergement Git** avec interface web (Gitea)
|
||||
- **CI/CD automatisé** pour builds et déploiements (Woodpecker)
|
||||
- **Agent IA de review** de code sur webhooks Gitea (Herald)
|
||||
- **Reverse proxy** avec HTTPS automatique (Caddy)
|
||||
- **Accès VPN sécurisé** pour l'administration (WireGuard)
|
||||
- **Monitoring** des services et ressources (Prometheus + Perses)
|
||||
- **Backups automatisés** quotidiens vers serveur FTP
|
||||
- **Accès VPN sécurisé** pour l'administration (WireGuard + CoreDNS)
|
||||
- **Observabilité** : métriques, traces et dashboards (Prometheus, Tempo, Perses)
|
||||
- **Alerting** temps réel avec notifications push (Alertmanager + ntfy)
|
||||
|
||||
---
|
||||
|
||||
## Services déployés
|
||||
|
||||
### Production
|
||||
### Stack `core`
|
||||
|
||||
| Service | Version | Description | URL |
|
||||
|---------|---------|-------------|-----|
|
||||
| **Gitea** | 1.24.7 | Hébergement Git (alternative GitHub) | https://gitea.qpismont.fr |
|
||||
| **Woodpecker CI** | 3.11.0 | Plateforme CI/CD (fork de Drone) | https://woodpecker.qpismont.fr |
|
||||
| **Caddy** | 2-alpine | Reverse proxy + HTTPS automatique | - |
|
||||
| **WireGuard** | latest | Serveur VPN pour accès distant | UDP:51820 |
|
||||
| **Prometheus** | 3.7.2 | Collecte de métriques | (interne) |
|
||||
| **Perses** | latest | Dashboards de monitoring | (via VPN) |
|
||||
| **cAdvisor** | latest | Métriques des containers | (interne) |
|
||||
| **Backup** | latest | Sauvegarde quotidienne (4h00) | (interne) |
|
||||
| Service | Image | Description | Exposition |
|
||||
|---------|-------|-------------|------------|
|
||||
| **reverse_caddy** | `caddy:2.11-alpine` | Reverse proxy, HTTPS auto, HTTP/3 | `80`, `443/tcp`, `443/udp` |
|
||||
| **wireguard_server** | `linuxserver/wireguard:latest` | VPN d'accès distant + CoreDNS | `51820/udp` |
|
||||
|
||||
### Stack `git`
|
||||
|
||||
| Service | Image | Description | URL |
|
||||
|---------|-------|-------------|-----|
|
||||
| **gitea** | `docker.gitea.com/gitea:28.2` | Hébergement Git | https://gitea.qpismont.fr · https://git.qpismont.fr |
|
||||
| **woodpecker_server** | `woodpeckerci/woodpecker-server:v3.19-alpine` | Serveur CI/CD | https://woodpecker.qpismont.fr |
|
||||
| **woodpecker_agent** | `woodpeckerci/woodpecker-agent:v3.19-alpine` | Agent d'exécution (Docker socket) | (interne) |
|
||||
| **herald** | `tintounn/herald:1.2` | Agent IA de review sur webhooks Gitea (Docker socket) | (interne, métriques `:9100`) |
|
||||
|
||||
### Stack `observability`
|
||||
|
||||
| Service | Image | Description | Exposition |
|
||||
|---------|-------|-------------|------------|
|
||||
| **prometheus** | `prom/prometheus:v3.13.3` | TSDB + remote-write receiver | (interne) |
|
||||
| **otel-collector** | `otel/opentelemetry-collector-contrib:0.164.0` | Collecte métriques + traces | (interne) |
|
||||
| **docker-proxy-otel** | `tecnativa/docker-socket-proxy:0.4.0` | Proxy socket Docker (lecture seule) | (interne) |
|
||||
| **tempo** | `grafana/tempo:3.0.3` | Stockage de traces (OTLP) | (interne) |
|
||||
| **perses** | `persesdev/perses:latest` | Dashboards | (via VPN) |
|
||||
| **alertmanager** | `prom/alertmanager:v0.34.1` | Routage des alertes | (interne) |
|
||||
| **blackbox_exporter** | `prom/blackbox-exporter:v0.28.0` | Probes HTTP externes | (interne) |
|
||||
| **ntfy** | `binwiederhier/ntfy:latest` | Notifications push | https://ntfy.qpismont.fr |
|
||||
|
||||
### Configuration
|
||||
|
||||
- **Timezone** : Europe/Paris
|
||||
- **OAuth** : Gitea ↔ Woodpecker intégré
|
||||
- **Admin** : qpismont
|
||||
- **Backup retention** : 14 jours
|
||||
- **Monitoring scrape** : 10 secondes
|
||||
- **Scrape interval** : 10 secondes
|
||||
- **Rétention Tempo** : 48 h
|
||||
- **Rétention Prometheus** : par défaut (15 j), volume persistant
|
||||
|
||||
---
|
||||
|
||||
@@ -58,57 +76,63 @@ Cette infrastructure héberge l'ensemble des services nécessaires pour le déve
|
||||
▼ ▼ ▼
|
||||
[Caddy Proxy] [HTTP/3] [WireGuard VPN]
|
||||
│ │
|
||||
┌───────────┼──────────┐ │
|
||||
│ │ │ │
|
||||
gitea:3000 woodpecker:8000 │ [Perses]
|
||||
┌───────────┼───────────┬──────────┐ │
|
||||
│ │ │ │ │
|
||||
gitea:3000 woodpecker:8000 │ ntfy:80│
|
||||
│ │
|
||||
www.qpismont.fr [Prometheus]
|
||||
(désactivé) │
|
||||
[cAdvisor]
|
||||
www.qpismont.fr [CoreDNS]
|
||||
(portfolio) │
|
||||
[Perses]
|
||||
[Prometheus]
|
||||
[Tempo]
|
||||
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ CI/CD Pipeline │
|
||||
│ Stack git (CI/CD) │
|
||||
│ │
|
||||
│ [Gitea] ←──── OAuth ────→ [Woodpecker Server] │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ [Woodpecker Agent] │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ │ │ │
|
||||
│ │ webhook ▼ │
|
||||
│ ▼ [Woodpecker Agent] │
|
||||
│ [Herald] │ │
|
||||
│ (review IA) ▼ │
|
||||
│ [Docker-in-Docker] │
|
||||
│ (build containers) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Monitoring Stack │
|
||||
│ Stack observability │
|
||||
│ │
|
||||
│ [Prometheus] ←─ scrape ─┬─ prometheus:9090 (self) │
|
||||
│ │ ├─ reverse_caddy:2019 (metrics) │
|
||||
│ │ └─ cadvisor:8080 (containers) │
|
||||
│ [OTel Collector] ──┬─ hostmetrics (CPU/RAM/disk/net) │
|
||||
│ │ ├─ docker_stats (métriques containers) │
|
||||
│ │ ├─ prometheus scrape: │
|
||||
│ │ │ reverse_caddy:2019 │
|
||||
│ │ │ herald:9100 │
|
||||
│ │ │ alertmanager:9093 │
|
||||
│ │ │ blackbox_exporter:9115 │
|
||||
│ │ └─ OTLP (4318) │
|
||||
│ │ │
|
||||
│ └──→ [Perses] (visualisation via VPN) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Backup System │
|
||||
│ │
|
||||
│ [Backup Service] ──→ rclone/FTP ──→ [Serveur FTP distant] │
|
||||
│ │ │ │
|
||||
│ └─ Volumes Docker: └─ /backups/infra/│
|
||||
│ • giteadata Format: .tar.gz │
|
||||
│ • woodpeckerdata (désactivé) Rétention: 14j │
|
||||
│ • caddy configs (désactivé) Cron: 4h00 │
|
||||
│ • wireguard (désactivé) │
|
||||
│ ├── remote_write ──→ [Prometheus] ──→ [Perses] │
|
||||
│ └── OTLP ──────────→ [Tempo] │
|
||||
│ │ │
|
||||
│ [Prometheus] ── alertes ──→ [Alertmanager] ──→ [ntfy] │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Stacks Docker Swarm
|
||||
|
||||
| Stack | Fichier compose | Services |
|
||||
|-------|-----------------|----------|
|
||||
| `core` | `docker-compose.core.yml` | Caddy, WireGuard |
|
||||
| `git` | `docker-compose.git.yml` | Gitea, Woodpecker (server + agent), Herald |
|
||||
| `observability` | `docker-compose.observability.yml` | Prometheus, Perses, Tempo, Alertmanager, Blackbox, OTel Collector, proxy socket, ntfy |
|
||||
|
||||
### Réseaux Docker Swarm
|
||||
|
||||
| Réseau | Type | Services |
|
||||
|--------|------|----------|
|
||||
| `reverse` | overlay | Caddy, Gitea, Woodpecker |
|
||||
| `wireguard_network` | overlay | WireGuard, Perses |
|
||||
| `prometheus_network` | overlay | Prometheus, Perses, cAdvisor, Caddy, Backup |
|
||||
| `reverse_network` | overlay | Caddy, Gitea, Woodpecker (server + agent), Herald, ntfy |
|
||||
| `wireguard_network` | overlay | WireGuard, Prometheus, Perses, Tempo |
|
||||
| `prometheus_network` | overlay | Caddy, Herald, Prometheus, Perses, Tempo, Alertmanager, Blackbox, OTel Collector, docker-proxy-otel, ntfy |
|
||||
|
||||
---
|
||||
|
||||
@@ -121,7 +145,7 @@ Cette infrastructure héberge l'ensemble des services nécessaires pour le déve
|
||||
- **Docker Configs** pour configurations versionnées
|
||||
|
||||
### Infrastructure as Code
|
||||
- Configuration : `docker-compose.yml`
|
||||
- Configuration : `docker-compose.core.yml` (Caddy, WireGuard), `docker-compose.git.yml` (Gitea, Woodpecker, Herald), `docker-compose.observability.yml` (Prometheus, Perses, Tempo, ntfy…)
|
||||
- Déploiement : `./deploy.sh` (génère timestamp pour versioning)
|
||||
- Contexte Docker distant : `prod`
|
||||
|
||||
@@ -132,21 +156,21 @@ Cette infrastructure héberge l'ensemble des services nécessaires pour le déve
|
||||
- **VPN** : WireGuard pour services internes
|
||||
|
||||
### Storage
|
||||
- **Volumes Docker** : giteadata, woodpeckerdata, reversecaddydata, reversecaddyconfig, backuptemp
|
||||
- **Host mounts** : /home/user/wireguard-config, /var/run/docker.sock (CI/CD)
|
||||
- **Volumes Docker** : `reversecaddydata`, `reversecaddyconfig` (core) · `giteadata`, `woodpeckerdata` (git) · `prometheusdata`, `tempodata`, `persesdata`, `alertmanagerdata`, `ntfydata` (observability)
|
||||
- **Host mounts** : `/home/user/wireguard-config`, `/lib/modules` (WireGuard) · `/var/run/docker.sock` (Woodpecker agent, Herald, `docker-proxy-otel`) · `/` en lecture seule (OTel Collector)
|
||||
|
||||
### Monitoring
|
||||
- **Métriques** : Prometheus avec 10s scrape interval
|
||||
- **Collecte** : OTel Collector (hostmetrics + scrape Prometheus + OTLP)
|
||||
- **Stockage métriques** : Prometheus (remote-write receiver activé)
|
||||
- **Stockage traces** : Tempo (OTLP HTTP `:4318`, rétention 48 h)
|
||||
- **Dashboards** : Perses (accessible via VPN)
|
||||
- **Container metrics** : cAdvisor
|
||||
- **Proxy metrics** : Caddy admin API (:2019)
|
||||
- **Container metrics** : receiver `docker_stats` de l'OTel Collector (labels Swarm `service_name` / `task_name`)
|
||||
- **Proxy metrics** : Caddy admin API (`:2019`)
|
||||
- **Probes externes** : blackbox_exporter (`http_2xx` sur les URLs publiques)
|
||||
- **Alerting** : Prometheus → Alertmanager → ntfy (`http://ntfy:80/alerts`)
|
||||
|
||||
### Backups
|
||||
- **Solution** : docker-volume-backup avec rclone
|
||||
- **Fréquence** : Quotidien à 4h00
|
||||
- **Destination** : Serveur FTP distant
|
||||
- **Rétention** : 14 jours
|
||||
- **Format** : .tar.gz compressé
|
||||
- ⚠️ **Aucun service de backup n'est déployé actuellement.** L'ancien service `docker-volume-backup` a été retiré (commit `041eb51`). Les sauvegardes sont à refaire manuellement ou à réintroduire.
|
||||
|
||||
---
|
||||
|
||||
@@ -166,6 +190,13 @@ Cette infrastructure héberge l'ensemble des services nécessaires pour le déve
|
||||
- Limite : 1 workflow concurrent, CPU core 1
|
||||
- Support multi-architectures
|
||||
|
||||
### Herald
|
||||
- Agent IA de review de code, déclenché par webhooks Gitea
|
||||
- Modèle via OpenRouter (`openai/gpt-5.6-luna-pro`)
|
||||
- Sandbox d'exécution limitée à 32 itérations
|
||||
- 5 jobs concurrents max, timeout Gitea 60 s / OpenRouter 600 s
|
||||
- Métriques Prometheus exposées sur `:9100`
|
||||
|
||||
### Caddy
|
||||
- HTTPS automatique avec Let's Encrypt
|
||||
- HTTP/3 (QUIC) support
|
||||
@@ -177,38 +208,44 @@ Cette infrastructure héberge l'ensemble des services nécessaires pour le déve
|
||||
- VPN moderne et rapide
|
||||
- Configuration : 1 peer (extensible)
|
||||
- Accès sécurisé aux services internes
|
||||
- CoreDNS embarqué pour résoudre `perses.internal`, `prometheus.internal`, `tempo.internal`
|
||||
- Faible overhead
|
||||
|
||||
### Monitoring
|
||||
- **Prometheus** : Collecte et stockage des métriques
|
||||
- **Perses** : Visualisation moderne (alternative Grafana)
|
||||
- **cAdvisor** : Métriques CPU, RAM, I/O des containers
|
||||
- **OTel Collector** : hostmetrics (CPU, RAM, disque, réseau, load, process) + `docker_stats` (métriques containers) + scrape Prometheus + réception OTLP
|
||||
- **Prometheus** : stockage des métriques, remote-write receiver
|
||||
- **Tempo** : stockage des traces OTLP
|
||||
- **Perses** : visualisation moderne (alternative Grafana)
|
||||
- **blackbox_exporter** : probes HTTP sur les URLs publiques
|
||||
- **Alertmanager** : routage des alertes vers ntfy
|
||||
- Scrape automatique toutes les 10 secondes
|
||||
|
||||
### Backups
|
||||
- Sauvegarde automatique quotidienne
|
||||
- Support FTP/SFTP via rclone
|
||||
- Compression gzip
|
||||
- Notifications optionnelles (Discord, Slack, Email)
|
||||
- Rétention configurable
|
||||
### Alerting
|
||||
- Règles : `ServiceDown` (probe blackbox), `OtelCollectorDown`, `CaddyDown`
|
||||
- Seuil : 1 minute
|
||||
- Inhibition : `CaddyDown` supprime les `ServiceDown` (évite le bruit)
|
||||
- Notification : webhook ntfy avec template `alertmanager`, `send_resolved: true`
|
||||
|
||||
---
|
||||
|
||||
## URLs et accès
|
||||
|
||||
### Services publics
|
||||
- **Gitea** : https://gitea.qpismont.fr
|
||||
- **Gitea** : https://gitea.qpismont.fr (alias https://git.qpismont.fr)
|
||||
- **Woodpecker** : https://woodpecker.qpismont.fr
|
||||
- **Portfolio** : ~~https://www.qpismont.fr~~ (désactivé)
|
||||
- **Portfolio** : https://www.qpismont.fr (`qpismont.fr` redirige vers `www`)
|
||||
- **ntfy** : https://ntfy.qpismont.fr
|
||||
|
||||
### Services internes (VPN requis)
|
||||
- **Perses** : http://perses:8080 (via WireGuard)
|
||||
- **Prometheus** : http://prometheus:9090 (via WireGuard)
|
||||
- **Perses** : http://perses.internal:8080 (via WireGuard/CoreDNS)
|
||||
- **Prometheus** : http://prometheus.internal:9090
|
||||
- **Tempo** : http://tempo.internal:3200
|
||||
- **Caddy Admin** : http://reverse_caddy:2019 (métriques)
|
||||
|
||||
### VPN
|
||||
- **Protocole** : WireGuard
|
||||
- **Port** : 51820/udp
|
||||
- **Sous-réseau** : `10.13.13.0/24`
|
||||
- **Config** : `/home/user/wireguard-config/peer1/`
|
||||
|
||||
---
|
||||
@@ -221,12 +258,12 @@ Le déploiement se fait via un contexte Docker distant vers le serveur de produc
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
Le script génère un timestamp pour versionner les configs et secrets, puis déploie le stack `core` sur le contexte `prod`.
|
||||
Le script génère un timestamp pour versionner les configs et secrets, puis déploie les trois stacks (`core`, `git`, `observability`) sur le contexte `prod`.
|
||||
|
||||
**Prérequis :**
|
||||
- Contexte Docker `prod` configuré
|
||||
- Réseaux overlay créés (`reverse`, `wireguard_network`, `prometheus_network`)
|
||||
- Secrets configurés dans `./secrets/`
|
||||
- Réseaux overlay créés (`reverse_network`, `wireguard_network`, `prometheus_network`)
|
||||
- Secrets configurés dans `./secrets/` (dossier ignoré par Git)
|
||||
|
||||
---
|
||||
|
||||
@@ -234,156 +271,30 @@ Le script génère un timestamp pour versionner les configs et secrets, puis dé
|
||||
|
||||
### Secrets Docker
|
||||
|
||||
| Secret | Usage |
|
||||
|--------|-------|
|
||||
| `woodpecker_agent_secret` | Communication server/agent Woodpecker |
|
||||
| `woodpecker_gitea_client` | OAuth client ID Gitea |
|
||||
| `woodpecker_gitea_secret` | OAuth client secret Gitea |
|
||||
| `backup_ftp_config` | Configuration rclone pour FTP |
|
||||
| Secret | Fichier source | Usage |
|
||||
|--------|----------------|-------|
|
||||
| `woodpecker_agent_secret` | `./secrets/woodpecker_agent_secret` | Communication server/agent Woodpecker |
|
||||
| `woodpecker_gitea_client` | `./secrets/woodpecker_gitea_client` | OAuth client ID Gitea |
|
||||
| `woodpecker_gitea_secret` | `./secrets/woodpecker_gitea_secret` | OAuth client secret Gitea |
|
||||
| `herald_gitea_token` | `./secrets/herald/herald_gitea_token` | Token API Gitea pour Herald |
|
||||
| `herald_openrouter_token` | `./secrets/herald/herald_openrouter_token` | Clé API OpenRouter |
|
||||
| `herald_gitea_header_secret` | `./secrets/herald/herald_gitea_header_secret` | Vérification signature webhook Gitea |
|
||||
| `herald_sentry_dsn` | `./secrets/herald/herald_sentry_dsn` | DSN Sentry |
|
||||
|
||||
### Configs Docker
|
||||
|
||||
| Config | Fichier source |
|
||||
|--------|----------------|
|
||||
| `caddy_config` | `./Caddyfile` |
|
||||
| `prometheus_config` | `./prometheus.yml` |
|
||||
| Config | Fichier source | Stack |
|
||||
|--------|----------------|-------|
|
||||
| `caddy_config` | `./Caddyfile` | core |
|
||||
| `caddy_portfolio_html` | `./portfolio/index.html` | core |
|
||||
| `wireguard_coredns_config` | `./wireguard/CoreDns` | core |
|
||||
| `prometheus_config` | `./prometheus/prometheus.yml` | observability |
|
||||
| `prometheus_rules_config` | `./prometheus/alert.rules.yml` | observability |
|
||||
| `alertmanager_config` | `./prometheus/alertmanager/alertmanager.yml` | observability |
|
||||
| `blackbox_config` | `./blackbox/blackbox.yml` | observability |
|
||||
| `otel_collector_config` | `./otel-collector/config.yaml` | observability |
|
||||
| `tempo_config` | `./tempo/tempo.yml` | observability |
|
||||
|
||||
Les configs et secrets sont versionnés avec `${DATETIME}` pour forcer les mises à jour lors du déploiement.
|
||||
Les configs et secrets sont versionnés avec `${DATETIME}` pour forcer les mises à jour lors du déploiement. Ils s'accumulent donc à chaque déploiement : voir la section [Nettoyage](#nettoyage).
|
||||
|
||||
---
|
||||
|
||||
## Problèmes connus
|
||||
|
||||
### ⚠️ Service my_cv désactivé
|
||||
Le Caddyfile contient des routes vers `my_cv:80` mais le service n'existe plus dans docker-compose.yml (retiré dans commit f047317).
|
||||
|
||||
**Impact :** Routes `qpismont.fr` et `www.qpismont.fr` ne fonctionnent pas.
|
||||
|
||||
### ⚠️ Prometheus non-persistant
|
||||
Prometheus n'a pas de volume Docker, les métriques sont perdues à chaque redémarrage du container.
|
||||
|
||||
**Impact :** Pas d'historique des métriques sur le long terme.
|
||||
|
||||
### ⚠️ Volumes backup partiellement désactivés
|
||||
Seul `giteadata` est actuellement sauvegardé. Les autres volumes (woodpecker, caddy, wireguard) sont commentés dans docker-compose.yml.
|
||||
|
||||
**Impact :** Perte de données possible en cas d'incident sur ces volumes.
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
### Fichiers de documentation
|
||||
|
||||
- **[README.md](./README.md)** : Ce fichier - présentation générale
|
||||
- **[ANALYSE.md](./ANALYSE.md)** : Analyse détaillée de l'infrastructure + recommandations d'amélioration
|
||||
- **[BACKUP.md](./BACKUP.md)** : Documentation complète du système de backup (configuration, utilisation, restauration)
|
||||
|
||||
### Documentation externe
|
||||
|
||||
- [Docker Swarm](https://docs.docker.com/engine/swarm/)
|
||||
- [Caddy](https://caddyserver.com/docs/)
|
||||
- [Gitea](https://docs.gitea.io/)
|
||||
- [Woodpecker CI](https://woodpecker-ci.org/docs/)
|
||||
- [WireGuard](https://www.wireguard.com/)
|
||||
- [Prometheus](https://prometheus.io/docs/)
|
||||
- [Perses](https://perses.dev/docs/)
|
||||
|
||||
---
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Routine
|
||||
|
||||
- **Quotidien** : Backup automatique à 4h00
|
||||
- **Hebdomadaire** : Vérification logs et backups
|
||||
- **Mensuel** : Mise à jour des images Docker
|
||||
- **Trimestriel** : Test de restauration des backups
|
||||
|
||||
### Monitoring
|
||||
|
||||
```bash
|
||||
# Status des services
|
||||
docker --context prod ps
|
||||
|
||||
# Logs d'un service
|
||||
docker --context prod logs -f $(docker --context prod ps -q -f name=core_SERVICE)
|
||||
|
||||
# Métriques système
|
||||
docker --context prod stats
|
||||
```
|
||||
|
||||
### Mises à jour
|
||||
|
||||
Modifier les versions dans `docker-compose.yml` puis :
|
||||
|
||||
```bash
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
Docker Swarm appliquera les changements de manière progressive.
|
||||
|
||||
---
|
||||
|
||||
## Sécurité
|
||||
|
||||
### Mesures en place
|
||||
|
||||
✅ HTTPS automatique (Let's Encrypt)
|
||||
✅ Secrets Docker (credentials chiffrés)
|
||||
✅ Network isolation (3 réseaux séparés)
|
||||
✅ VPN pour services internes (WireGuard)
|
||||
✅ Resource limits sur CI/CD agent
|
||||
✅ Backups automatisés quotidiens
|
||||
✅ Non-root user pour Gitea (UID 1000)
|
||||
|
||||
### Recommandations
|
||||
|
||||
Voir [ANALYSE.md](./ANALYSE.md) pour la liste complète des améliorations de sécurité suggérées.
|
||||
|
||||
---
|
||||
|
||||
## Historique
|
||||
|
||||
### 2025-10-27
|
||||
- ✅ Upgrade vers Debian 13 (devcontainer)
|
||||
- ✅ Update Woodpecker v3.11.0
|
||||
- ✅ Update Gitea v1.24.7
|
||||
- ✅ Ajout WireGuard VPN
|
||||
- ✅ Ajout stack monitoring (Prometheus + Perses + cAdvisor)
|
||||
- ✅ Ajout système de backup automatique
|
||||
|
||||
### 2025-06-23
|
||||
- ✅ Retrait service my_cv (portfolio)
|
||||
- ✅ Update images Woodpecker et Gitea
|
||||
|
||||
### Antérieur
|
||||
- Déploiement devcontainer Debian
|
||||
- Upgrades réguliers des services
|
||||
- Ajout Woodpecker CI
|
||||
- Migration vers Docker Swarm
|
||||
|
||||
---
|
||||
|
||||
## Statistiques
|
||||
|
||||
- **Services actifs** : 8
|
||||
- **Réseaux overlay** : 3
|
||||
- **Volumes Docker** : 5
|
||||
- **Secrets Docker** : 4
|
||||
- **Configs Docker** : 2
|
||||
- **Ports exposés** : 4 (80, 443/tcp, 443/udp, 51820/udp)
|
||||
- **Backup size** : ~50-500 MB (dépend des données Gitea)
|
||||
- **Uptime target** : 99%+
|
||||
|
||||
---
|
||||
|
||||
## Licence et Auteur
|
||||
|
||||
**Propriétaire** : qpismont
|
||||
**Usage** : Personnel
|
||||
**Repository** : https://gitea.qpismont.fr/qpismont/infra
|
||||
|
||||
---
|
||||
|
||||
**Dernière mise à jour :** 27 octobre 2025
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
DATETIME=$(date +%Y%m%d%H%M%S) docker --context prod stack deploy -c docker-compose.core.yml core --with-registry-auth
|
||||
DATETIME=$(date +%Y%m%d%H%M%S) docker --context prod stack deploy -c docker-compose.git.yml git --with-registry-auth
|
||||
DATETIME=$(date +%Y%m%d%H%M%S) docker --context prod stack deploy -c docker-compose.observability.yml observability --with-registry-auth
|
||||
@@ -19,81 +19,6 @@ services:
|
||||
- reverse_network
|
||||
- prometheus_network
|
||||
|
||||
woodpecker_server:
|
||||
image: woodpeckerci/woodpecker-server:v3.19-alpine
|
||||
volumes:
|
||||
- woodpeckerdata:/var/lib/woodpecker/
|
||||
entrypoint: /bin/sh -c "export WOODPECKER_GITEA_CLIENT=$$(cat /run/secrets/woodpecker_gitea_client) && export WOODPECKER_GITEA_SECRET=$$(cat /run/secrets/woodpecker_gitea_secret) && export WOODPECKER_AGENT_SECRET=$$(cat /run/secrets/woodpecker_agent_secret) && /bin/woodpecker-server"
|
||||
environment:
|
||||
WOODPECKER_ADMIN: qpismont
|
||||
WOODPECKER_HOST: https://woodpecker.qpismont.fr
|
||||
WOODPECKER_GITEA: "true"
|
||||
WOODPECKER_GITEA_URL: https://gitea.qpismont.fr
|
||||
networks:
|
||||
- reverse_network
|
||||
secrets:
|
||||
- woodpecker_agent_secret
|
||||
- woodpecker_gitea_client
|
||||
- woodpecker_gitea_secret
|
||||
|
||||
woodpecker_agent:
|
||||
image: woodpeckerci/woodpecker-agent:v3.19-alpine
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
entrypoint: /bin/sh -c "export WOODPECKER_AGENT_SECRET=$$(cat /run/secrets/woodpecker_agent_secret) && /bin/woodpecker-agent"
|
||||
environment:
|
||||
WOODPECKER_SERVER: woodpecker_server:9000
|
||||
WOODPECKER_MAX_WORKFLOWS: 1
|
||||
WOODPECKER_LIMIT_CPU_SET: 1
|
||||
networks:
|
||||
- reverse_network
|
||||
secrets:
|
||||
- woodpecker_agent_secret
|
||||
|
||||
gitea:
|
||||
image: docker.gitea.com/gitea:28.2
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
restart: always
|
||||
volumes:
|
||||
- giteadata:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
networks:
|
||||
- reverse_network
|
||||
|
||||
herald:
|
||||
image: tintounn/herald:1.2
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- >-
|
||||
export GITEA_TOKEN=$$(cat /run/secrets/herald_gitea_token) &&
|
||||
export OPEN_ROUTER_API_KEY=$$(cat /run/secrets/herald_openrouter_token) &&
|
||||
export WEBHOOK_SIG_HEADER_SECRET=$$(cat /run/secrets/herald_gitea_header_secret) &&
|
||||
export SENTRY_DSN=$$(cat /run/secrets/herald_sentry_dsn) &&
|
||||
/app/herald-server
|
||||
networks:
|
||||
- reverse_network
|
||||
- prometheus_network
|
||||
secrets:
|
||||
- herald_gitea_token
|
||||
- herald_openrouter_token
|
||||
- herald_gitea_header_secret
|
||||
- herald_sentry_dsn
|
||||
environment:
|
||||
HTTP_PORT: 3000
|
||||
BOT_MAX_CONCURRENT: 5
|
||||
GITEA_URL: http://gitea:3000
|
||||
GITEA_TIMEOUT: 60
|
||||
OPEN_ROUTER_MODEL: openai/gpt-5.6-luna-pro
|
||||
OPEN_ROUTER_TIMEOUT: 600
|
||||
METRICS_BIND_ADDR: 0.0.0.0:9100
|
||||
SANDBOX_MAX_ITERATIONS: 32
|
||||
|
||||
wireguard_server:
|
||||
image: linuxserver/wireguard
|
||||
cap_add:
|
||||
@@ -128,29 +53,6 @@ services:
|
||||
networks:
|
||||
- wireguard_network
|
||||
|
||||
secrets:
|
||||
woodpecker_agent_secret:
|
||||
name: woodpecker_agent_secret_${DATETIME}
|
||||
file: ./secrets/woodpecker_agent_secret
|
||||
woodpecker_gitea_secret:
|
||||
name: woodpecker_gitea_secret_${DATETIME}
|
||||
file: ./secrets/woodpecker_gitea_secret
|
||||
woodpecker_gitea_client:
|
||||
name: woodpecker_gitea_client_${DATETIME}
|
||||
file: ./secrets/woodpecker_gitea_client
|
||||
herald_gitea_token:
|
||||
name: herald_gitea_token_${DATETIME}
|
||||
file: ./secrets/herald/herald_gitea_token
|
||||
herald_openrouter_token:
|
||||
name: herald_openrouter_token_${DATETIME}
|
||||
file: ./secrets/herald/herald_openrouter_token
|
||||
herald_gitea_header_secret:
|
||||
name: herald_gitea_header_secret_${DATETIME}
|
||||
file: ./secrets/herald/herald_gitea_header_secret
|
||||
herald_sentry_dsn:
|
||||
name: herald_sentry_dsn_${DATETIME}
|
||||
file: ./secrets/herald/herald_sentry_dsn
|
||||
|
||||
configs:
|
||||
caddy_config:
|
||||
name: caddy_config_${DATETIME}
|
||||
@@ -163,8 +65,6 @@ configs:
|
||||
file: ./portfolio/index.html
|
||||
|
||||
volumes:
|
||||
giteadata:
|
||||
woodpeckerdata:
|
||||
reversecaddyconfig:
|
||||
reversecaddydata:
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
gitea:
|
||||
image: docker.gitea.com/gitea:28.2
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
restart: always
|
||||
volumes:
|
||||
- giteadata:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
networks:
|
||||
- reverse_network
|
||||
|
||||
woodpecker_server:
|
||||
image: woodpeckerci/woodpecker-server:v3.19-alpine
|
||||
volumes:
|
||||
- woodpeckerdata:/var/lib/woodpecker/
|
||||
entrypoint: /bin/sh -c "export WOODPECKER_GITEA_CLIENT=$$(cat /run/secrets/woodpecker_gitea_client) && export WOODPECKER_GITEA_SECRET=$$(cat /run/secrets/woodpecker_gitea_secret) && export WOODPECKER_AGENT_SECRET=$$(cat /run/secrets/woodpecker_agent_secret) && /bin/woodpecker-server"
|
||||
environment:
|
||||
WOODPECKER_ADMIN: qpismont
|
||||
WOODPECKER_HOST: https://woodpecker.qpismont.fr
|
||||
WOODPECKER_GITEA: "true"
|
||||
WOODPECKER_GITEA_URL: https://gitea.qpismont.fr
|
||||
networks:
|
||||
- reverse_network
|
||||
secrets:
|
||||
- woodpecker_agent_secret
|
||||
- woodpecker_gitea_client
|
||||
- woodpecker_gitea_secret
|
||||
|
||||
woodpecker_agent:
|
||||
image: woodpeckerci/woodpecker-agent:v3.19-alpine
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
entrypoint: /bin/sh -c "export WOODPECKER_AGENT_SECRET=$$(cat /run/secrets/woodpecker_agent_secret) && /bin/woodpecker-agent"
|
||||
environment:
|
||||
WOODPECKER_SERVER: woodpecker_server:9000
|
||||
WOODPECKER_MAX_WORKFLOWS: 1
|
||||
WOODPECKER_LIMIT_CPU_SET: 1
|
||||
networks:
|
||||
- reverse_network
|
||||
secrets:
|
||||
- woodpecker_agent_secret
|
||||
|
||||
herald:
|
||||
image: tintounn/herald:1.2
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- >-
|
||||
export GITEA_TOKEN=$$(cat /run/secrets/herald_gitea_token) &&
|
||||
export OPEN_ROUTER_API_KEY=$$(cat /run/secrets/herald_openrouter_token) &&
|
||||
export WEBHOOK_SIG_HEADER_SECRET=$$(cat /run/secrets/herald_gitea_header_secret) &&
|
||||
export SENTRY_DSN=$$(cat /run/secrets/herald_sentry_dsn) &&
|
||||
/app/herald-server
|
||||
networks:
|
||||
- reverse_network
|
||||
- prometheus_network
|
||||
secrets:
|
||||
- herald_gitea_token
|
||||
- herald_openrouter_token
|
||||
- herald_gitea_header_secret
|
||||
- herald_sentry_dsn
|
||||
environment:
|
||||
HTTP_PORT: 3000
|
||||
BOT_MAX_CONCURRENT: 5
|
||||
GITEA_URL: http://gitea:3000
|
||||
GITEA_TIMEOUT: 60
|
||||
OPEN_ROUTER_MODEL: openai/gpt-5.6-luna-pro
|
||||
OPEN_ROUTER_TIMEOUT: 600
|
||||
METRICS_BIND_ADDR: 0.0.0.0:9100
|
||||
SANDBOX_MAX_ITERATIONS: 32
|
||||
|
||||
secrets:
|
||||
woodpecker_agent_secret:
|
||||
name: woodpecker_agent_secret_${DATETIME}
|
||||
file: ./secrets/woodpecker_agent_secret
|
||||
woodpecker_gitea_secret:
|
||||
name: woodpecker_gitea_secret_${DATETIME}
|
||||
file: ./secrets/woodpecker_gitea_secret
|
||||
woodpecker_gitea_client:
|
||||
name: woodpecker_gitea_client_${DATETIME}
|
||||
file: ./secrets/woodpecker_gitea_client
|
||||
herald_gitea_token:
|
||||
name: herald_gitea_token_${DATETIME}
|
||||
file: ./secrets/herald/herald_gitea_token
|
||||
herald_openrouter_token:
|
||||
name: herald_openrouter_token_${DATETIME}
|
||||
file: ./secrets/herald/herald_openrouter_token
|
||||
herald_gitea_header_secret:
|
||||
name: herald_gitea_header_secret_${DATETIME}
|
||||
file: ./secrets/herald/herald_gitea_header_secret
|
||||
herald_sentry_dsn:
|
||||
name: herald_sentry_dsn_${DATETIME}
|
||||
file: ./secrets/herald/herald_sentry_dsn
|
||||
|
||||
volumes:
|
||||
giteadata:
|
||||
woodpeckerdata:
|
||||
|
||||
networks:
|
||||
reverse_network:
|
||||
external: true
|
||||
prometheus_network:
|
||||
external: true
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
- wireguard_network
|
||||
|
||||
blackbox_exporter:
|
||||
image: prom/blackbox-exporter:v0.28.0
|
||||
image: prom/blackbox-exporter:v0.29.1
|
||||
configs:
|
||||
- source: blackbox_config
|
||||
target: /etc/blackbox_exporter/config.yml
|
||||
@@ -52,8 +52,44 @@ services:
|
||||
- prometheus_network
|
||||
- reverse_network
|
||||
|
||||
docker-proxy-otel:
|
||||
image: tecnativa/docker-socket-proxy:v0.5.0
|
||||
environment:
|
||||
CONTAINERS: 1
|
||||
INFO: 1
|
||||
VERSION: 1
|
||||
PING: 1
|
||||
# docker_stats écoute /events pour suivre les conteneurs (sinon 403)
|
||||
EVENTS: 1
|
||||
POST: 0
|
||||
EXEC: 0
|
||||
IMAGES: 0
|
||||
VOLUMES: 0
|
||||
NETWORKS: 0
|
||||
SECRETS: 0
|
||||
CONFIGS: 0
|
||||
SWARM: 0
|
||||
SERVICES: 0
|
||||
TASKS: 0
|
||||
NODES: 0
|
||||
SYSTEM: 0
|
||||
PLUGINS: 0
|
||||
SESSION: 0
|
||||
AUTH: 0
|
||||
BUILD: 0
|
||||
COMMIT: 0
|
||||
DISTRIBUTION: 0
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
networks:
|
||||
- prometheus_network
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 64M
|
||||
|
||||
otel-collector:
|
||||
image: otel/opentelemetry-collector-contrib:0.161.0
|
||||
image: otel/opentelemetry-collector-contrib:0.164.0
|
||||
configs:
|
||||
- source: otel_collector_config
|
||||
target: /etc/otelcol-contrib/config.yaml
|
||||
@@ -66,12 +102,12 @@ services:
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
memory: 768M
|
||||
networks:
|
||||
- prometheus_network
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:v3.13.3
|
||||
image: prom/prometheus:v3.15.1
|
||||
command:
|
||||
- --config.file=/etc/prometheus/prometheus.yml
|
||||
- --storage.tsdb.path=/prometheus
|
||||
@@ -87,16 +123,6 @@ services:
|
||||
- prometheus_network
|
||||
- wireguard_network
|
||||
|
||||
cadvisor:
|
||||
image: gcr.io/cadvisor/cadvisor:latest
|
||||
volumes:
|
||||
- /:/rootfs:ro
|
||||
- /var/run:/var/run:rw
|
||||
- /sys:/sys:ro
|
||||
- /var/lib/docker/:/var/lib/docker:ro
|
||||
networks:
|
||||
- prometheus_network
|
||||
|
||||
configs:
|
||||
prometheus_config:
|
||||
name: prometheus_config_${DATETIME}
|
||||
|
||||
@@ -14,6 +14,35 @@ receivers:
|
||||
network:
|
||||
load:
|
||||
processes:
|
||||
docker_stats:
|
||||
endpoint: tcp://docker-proxy-otel:2375
|
||||
collection_interval: 30s
|
||||
timeout: 20s
|
||||
api_version: "1.44"
|
||||
container_labels_to_metric_labels:
|
||||
com.docker.swarm.service.name: service_name
|
||||
com.docker.swarm.task.name: task_name
|
||||
metrics:
|
||||
container.cpu.usage.total:
|
||||
enabled: true
|
||||
container.cpu.percent:
|
||||
enabled: true
|
||||
container.memory.usage.total:
|
||||
enabled: true
|
||||
container.memory.percent:
|
||||
enabled: true
|
||||
container.memory.usage.limit:
|
||||
enabled: true
|
||||
container.network.io.usage.rx_bytes:
|
||||
enabled: true
|
||||
container.network.io.usage.tx_bytes:
|
||||
enabled: true
|
||||
container.blockio.io_service_bytes_recursive:
|
||||
enabled: true
|
||||
container.pids.count:
|
||||
enabled: true
|
||||
container.uptime:
|
||||
enabled: true
|
||||
prometheus:
|
||||
config:
|
||||
scrape_configs:
|
||||
@@ -21,10 +50,6 @@ receivers:
|
||||
scrape_interval: 10s
|
||||
static_configs:
|
||||
- targets: ['reverse_caddy:2019']
|
||||
- job_name: cadvisor
|
||||
scrape_interval: 10s
|
||||
static_configs:
|
||||
- targets: ['cadvisor:8080']
|
||||
- job_name: herald
|
||||
scrape_interval: 10s
|
||||
static_configs:
|
||||
@@ -64,6 +89,8 @@ processors:
|
||||
exporters:
|
||||
prometheusremotewrite:
|
||||
endpoint: http://prometheus:9090/api/v1/write
|
||||
resource_to_telemetry_conversion:
|
||||
enabled: true
|
||||
otlphttp/tempo:
|
||||
endpoint: http://tempo:4318
|
||||
|
||||
@@ -78,7 +105,7 @@ service:
|
||||
port: 8888
|
||||
pipelines:
|
||||
metrics:
|
||||
receivers: [otlp, prometheus, hostmetrics]
|
||||
receivers: [otlp, prometheus, hostmetrics, docker_stats]
|
||||
processors: [memory_limiter, batch]
|
||||
exporters: [prometheusremotewrite]
|
||||
traces:
|
||||
|
||||
+271
-164
@@ -5,38 +5,63 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Portfolio of Quentin Pismont, senior/lead platform engineer: distributed systems, observability, security and self-hosted CI/CD, tested in production.">
|
||||
<title>Quentin Pismont — Senior/Lead Platform Engineer</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap" rel="stylesheet">
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Quentin Pismont — Senior/Lead Platform Engineer">
|
||||
<meta property="og:description" content="Self-hosted infrastructure running in production, a security-first AI agent in Rust, and a distributed media pipeline. Source code available.">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--bg-soft: #161b22;
|
||||
--bg-card: #161b22;
|
||||
--border: #21262d;
|
||||
--border-hover: #30363d;
|
||||
--text: #e6edf3;
|
||||
--text-dim: #8b949e;
|
||||
--text-faint: #6e7681;
|
||||
--accent: #58a6ff;
|
||||
--accent-soft: rgba(88, 166, 255, 0.1);
|
||||
--green: #3fb950;
|
||||
--mono: 'SF Mono', 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
|
||||
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
--bg: #0b0b14;
|
||||
--bg-soft: #14142a;
|
||||
--bg-card: #101024;
|
||||
--border: #3a3a6a;
|
||||
--text: #e8e8f0;
|
||||
--text-dim: #bcbcd8;
|
||||
--text-faint: #7a7aa4;
|
||||
--accent: #3cbcfc;
|
||||
--accent-soft: rgba(60, 188, 252, 0.12);
|
||||
--accent-2: #f8b800;
|
||||
--green: #00e436;
|
||||
--pink: #ff2d78;
|
||||
--mono: 'VT323', 'Courier New', monospace;
|
||||
--pixel: 'Press Start 2P', 'Courier New', monospace;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
background-color: var(--bg);
|
||||
/* Scanlines + grid live in the background layer, never over the glyphs */
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0.22) 0px,
|
||||
rgba(0, 0, 0, 0.22) 1px,
|
||||
transparent 1px,
|
||||
transparent 4px
|
||||
),
|
||||
linear-gradient(rgba(60, 188, 252, 0.035) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(60, 188, 252, 0.035) 1px, transparent 1px);
|
||||
background-size: 100% 4px, 32px 32px, 32px 32px;
|
||||
background-attachment: fixed, scroll, scroll;
|
||||
color: var(--text);
|
||||
font-family: var(--sans);
|
||||
line-height: 1.65;
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-family: var(--mono);
|
||||
font-size: 21px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
/* CRT vignette — smooth gradient, does not cut through text */
|
||||
body::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(ellipse at center, transparent 72%, rgba(0, 0, 0, 0.28) 100%);
|
||||
}
|
||||
.wrap {
|
||||
max-width: 860px;
|
||||
max-width: 880px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
@@ -45,225 +70,279 @@
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: rgba(13, 17, 23, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg);
|
||||
border-bottom: 4px solid var(--border);
|
||||
}
|
||||
nav .wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
height: 64px;
|
||||
}
|
||||
nav .logo {
|
||||
font-family: var(--mono);
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
font-family: var(--pixel);
|
||||
font-weight: 400;
|
||||
font-size: 10px;
|
||||
color: var(--text);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
nav .logo span { color: var(--accent); }
|
||||
nav .links { display: flex; gap: 28px; }
|
||||
nav .logo span { color: var(--accent-2); }
|
||||
nav .logo::after {
|
||||
content: "_";
|
||||
color: var(--accent);
|
||||
animation: blink 1s steps(2, start) infinite;
|
||||
}
|
||||
nav .links { display: flex; gap: 24px; }
|
||||
nav a {
|
||||
color: var(--text-dim);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
transition: color 0.2s;
|
||||
font-family: var(--pixel);
|
||||
font-size: 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
nav a:hover {
|
||||
color: var(--accent-2);
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
nav a:hover { color: var(--text); }
|
||||
@media (max-width: 620px) { nav .links { display: none; } }
|
||||
/* ---------- HERO ---------- */
|
||||
header {
|
||||
padding: 100px 0 80px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 72px 0 64px;
|
||||
border-bottom: 4px solid var(--border);
|
||||
}
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
gap: 10px;
|
||||
font-family: var(--pixel);
|
||||
font-size: 8px;
|
||||
text-transform: uppercase;
|
||||
color: var(--green);
|
||||
background: rgba(63, 185, 80, 0.1);
|
||||
border: 1px solid rgba(63, 185, 80, 0.2);
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 28px;
|
||||
background: transparent;
|
||||
border: 3px solid var(--green);
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 32px;
|
||||
box-shadow: 4px 4px 0 rgba(0, 228, 54, 0.25);
|
||||
}
|
||||
.badge .dot {
|
||||
width: 7px; height: 7px;
|
||||
border-radius: 50%;
|
||||
width: 8px; height: 8px;
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 8px var(--green);
|
||||
animation: blink 1s steps(2, start) infinite;
|
||||
}
|
||||
header h1 {
|
||||
font-size: clamp(2rem, 5vw, 3rem);
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 700;
|
||||
font-family: var(--pixel);
|
||||
font-size: clamp(0.85rem, 2.6vw, 1.45rem);
|
||||
line-height: 1.9;
|
||||
letter-spacing: 0;
|
||||
margin-bottom: 28px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
text-shadow: 3px 3px 0 #000, 6px 6px 0 rgba(60, 188, 252, 0.25);
|
||||
}
|
||||
header h1 .grad {
|
||||
background: linear-gradient(120deg, #58a6ff, #a371f7);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: var(--accent-2);
|
||||
background: none;
|
||||
-webkit-text-fill-color: currentColor;
|
||||
}
|
||||
header p.lead {
|
||||
font-size: 1.15rem;
|
||||
font-size: 23px;
|
||||
line-height: 1.65;
|
||||
color: var(--text-dim);
|
||||
max-width: 640px;
|
||||
max-width: 660px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.cta {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
padding: 12px 18px;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
font-family: var(--pixel);
|
||||
font-size: 8px;
|
||||
text-transform: uppercase;
|
||||
border: 3px solid var(--text);
|
||||
background: var(--bg-soft);
|
||||
color: var(--text);
|
||||
box-shadow: 5px 5px 0 #000;
|
||||
transition: none;
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: #0d1117;
|
||||
color: #0b0b14;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-2);
|
||||
border-color: var(--accent-2);
|
||||
transform: translate(2px, 2px);
|
||||
box-shadow: 3px 3px 0 #000;
|
||||
}
|
||||
.btn-primary:hover { background: #79b8ff; transform: translateY(-1px); }
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
background: var(--bg-soft);
|
||||
color: var(--text);
|
||||
border-color: var(--border-hover);
|
||||
border-color: var(--border);
|
||||
}
|
||||
.btn-ghost:hover {
|
||||
color: var(--accent-2);
|
||||
border-color: var(--accent-2);
|
||||
transform: translate(2px, 2px);
|
||||
box-shadow: 3px 3px 0 #000;
|
||||
}
|
||||
.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }
|
||||
/* ---------- SECTIONS ---------- */
|
||||
section { padding: 80px 0; border-bottom: 1px solid var(--border); }
|
||||
section { padding: 64px 0; border-bottom: 4px solid var(--border); }
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
margin-bottom: 40px;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
.section-head .num {
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
color: var(--accent);
|
||||
font-family: var(--pixel);
|
||||
font-size: 10px;
|
||||
color: #0b0b14;
|
||||
background: var(--accent-2);
|
||||
padding: 6px 8px;
|
||||
box-shadow: 3px 3px 0 #000;
|
||||
}
|
||||
.section-head h2 {
|
||||
font-size: 1.6rem;
|
||||
letter-spacing: -0.01em;
|
||||
font-weight: 600;
|
||||
font-family: var(--pixel);
|
||||
font-size: clamp(0.85rem, 2.2vw, 1.1rem);
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
color: var(--text);
|
||||
}
|
||||
.section-intro {
|
||||
color: var(--text-dim);
|
||||
max-width: 640px;
|
||||
margin: -24px 0 40px;
|
||||
max-width: 660px;
|
||||
margin: -20px 0 36px;
|
||||
font-size: 22px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
/* ---------- ABOUT ---------- */
|
||||
.about-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 40px;
|
||||
gap: 36px;
|
||||
}
|
||||
@media (max-width: 620px) { .about-grid { grid-template-columns: 1fr; gap: 28px; } }
|
||||
.about-grid p { color: var(--text-dim); margin-bottom: 16px; }
|
||||
.about-grid p strong { color: var(--text); font-weight: 500; }
|
||||
.about-grid p { color: var(--text-dim); margin-bottom: 18px; font-size: 22px; line-height: 1.65; }
|
||||
.about-grid p strong { color: var(--accent-2); font-weight: 400; }
|
||||
.principles { list-style: none; }
|
||||
.principles li {
|
||||
padding: 10px 0 10px 24px;
|
||||
padding: 10px 0 10px 26px;
|
||||
position: relative;
|
||||
color: var(--text-dim);
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-bottom: 2px dashed var(--border);
|
||||
font-size: 21px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.principles li:last-child { border-bottom: none; }
|
||||
.principles li::before {
|
||||
content: "→";
|
||||
content: ">";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--accent);
|
||||
color: var(--green);
|
||||
font-family: var(--pixel);
|
||||
font-size: 9px;
|
||||
top: 14px;
|
||||
}
|
||||
/* ---------- PROJECTS ---------- */
|
||||
.project {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 32px;
|
||||
margin-bottom: 24px;
|
||||
transition: border-color 0.25s, transform 0.25s;
|
||||
border: 3px solid var(--border);
|
||||
border-radius: 0;
|
||||
padding: 28px;
|
||||
margin-bottom: 28px;
|
||||
box-shadow: 6px 6px 0 #000;
|
||||
transition: none;
|
||||
}
|
||||
.project:hover {
|
||||
border-color: var(--border-hover);
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--accent);
|
||||
transform: none;
|
||||
box-shadow: 6px 6px 0 rgba(60, 188, 252, 0.35);
|
||||
}
|
||||
.project-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.project h3 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
font-family: var(--pixel);
|
||||
font-size: 11px;
|
||||
line-height: 1.8;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
color: var(--accent);
|
||||
}
|
||||
.project .tag {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
background: var(--accent-soft);
|
||||
font-family: var(--pixel);
|
||||
font-size: 7px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
border: 1px solid rgba(88,166,255,0.2);
|
||||
border: 2px solid var(--accent);
|
||||
white-space: nowrap;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.tag.wip { background: rgba(210,153,34,0.1); color: #d29922; border-color: rgba(210,153,34,0.2); }
|
||||
.tag.done { background: rgba(63,185,80,0.1); color: var(--green); border-color: rgba(63,185,80,0.2); }
|
||||
.tag.wip { background: transparent; color: var(--accent-2); border-color: var(--accent-2); }
|
||||
.tag.done { background: transparent; color: var(--green); border-color: var(--green); }
|
||||
.project .subtitle {
|
||||
color: var(--text-dim);
|
||||
font-size: 0.98rem;
|
||||
font-size: 22px;
|
||||
line-height: 1.65;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.project .subtitle strong { color: var(--text); font-weight: 400; }
|
||||
.block { margin-bottom: 20px; }
|
||||
.block-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
color: var(--text-faint);
|
||||
font-family: var(--pixel);
|
||||
font-size: 8px;
|
||||
color: var(--accent-2);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.block p, .block ul { color: var(--text-dim); font-size: 0.95rem; }
|
||||
.block-label::before { content: "■ "; color: var(--pink); }
|
||||
.block p, .block ul { color: var(--text-dim); font-size: 21px; line-height: 1.65; }
|
||||
.block ul { list-style: none; }
|
||||
.block ul li {
|
||||
padding: 4px 0 4px 20px;
|
||||
padding: 6px 0 6px 22px;
|
||||
position: relative;
|
||||
}
|
||||
.block ul li::before {
|
||||
content: "▹";
|
||||
content: ">";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--accent);
|
||||
font-size: 0.85em;
|
||||
color: var(--green);
|
||||
font-family: var(--pixel);
|
||||
font-size: 8px;
|
||||
top: 9px;
|
||||
}
|
||||
.block strong { color: var(--text); font-weight: 400; }
|
||||
.block code {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.85em;
|
||||
background: var(--bg-soft);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.95em;
|
||||
background: #000;
|
||||
padding: 1px 5px;
|
||||
border-radius: 0;
|
||||
color: var(--accent);
|
||||
border: 1px solid var(--border);
|
||||
border: 2px solid var(--border);
|
||||
}
|
||||
.stack {
|
||||
display: flex;
|
||||
@@ -271,73 +350,101 @@
|
||||
gap: 8px;
|
||||
margin-top: 24px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--border);
|
||||
border-top: 2px dashed var(--border);
|
||||
}
|
||||
.stack span {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
font-family: var(--pixel);
|
||||
font-size: 7px;
|
||||
color: var(--text-dim);
|
||||
background: var(--bg-soft);
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: #000;
|
||||
padding: 6px 8px;
|
||||
border-radius: 0;
|
||||
border: 2px solid var(--border);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
/* ---------- SKILLS ---------- */
|
||||
.skills-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
.skill-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 22px;
|
||||
border: 3px solid var(--border);
|
||||
border-radius: 0;
|
||||
padding: 20px;
|
||||
box-shadow: 5px 5px 0 #000;
|
||||
}
|
||||
.skill-card:hover { border-color: var(--accent-2); }
|
||||
.skill-card h4 {
|
||||
font-size: 0.95rem;
|
||||
font-family: var(--pixel);
|
||||
font-size: 8px;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text);
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.skill-card h4 .ic { color: var(--accent); }
|
||||
.skill-card p { color: var(--text-dim); font-size: 0.9rem; }
|
||||
.skill-card h4 .ic { color: var(--pink); }
|
||||
.skill-card p { color: var(--text-dim); font-size: 21px; line-height: 1.6; }
|
||||
/* ---------- CONTACT ---------- */
|
||||
#contact { text-align: center; border-bottom: none; }
|
||||
#contact h2 { justify-content: center; }
|
||||
#contact .section-head { justify-content: center; }
|
||||
#contact p { color: var(--text-dim); max-width: 500px; margin: 0 auto 32px; }
|
||||
.contact-links { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }
|
||||
#contact p { color: var(--text-dim); max-width: 560px; margin: 0 auto 32px; font-size: 22px; line-height: 1.65; }
|
||||
.contact-links { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; }
|
||||
/* ---------- FOOTER ---------- */
|
||||
footer {
|
||||
padding: 40px 0;
|
||||
padding: 32px 0;
|
||||
text-align: center;
|
||||
color: var(--text-faint);
|
||||
font-size: 13px;
|
||||
font-size: 18px;
|
||||
font-family: var(--mono);
|
||||
border-top: 1px solid var(--border);
|
||||
border-top: 4px solid var(--border);
|
||||
}
|
||||
/* Reveal animation */
|
||||
footer .wrap::before {
|
||||
content: "PRESS START TO CONTINUE";
|
||||
display: block;
|
||||
font-family: var(--pixel);
|
||||
font-size: 7px;
|
||||
color: var(--text-faint);
|
||||
margin-bottom: 14px;
|
||||
animation: blink 1.4s steps(2, start) infinite;
|
||||
}
|
||||
footer .wrap::after {
|
||||
content: "1UP HI-SCORE 000000 © 2026";
|
||||
display: block;
|
||||
font-family: var(--pixel);
|
||||
font-size: 7px;
|
||||
color: var(--text-faint);
|
||||
margin-top: 14px;
|
||||
}
|
||||
@keyframes blink {
|
||||
0%, 49% { opacity: 1; }
|
||||
50%, 100% { opacity: 0; }
|
||||
}
|
||||
/* Reveal animation: hard step, no easing */
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: opacity 0.6s ease, transform 0.6s ease;
|
||||
transform: translateY(12px);
|
||||
transition: opacity 0.15s steps(2, end), transform 0.15s steps(2, end);
|
||||
}
|
||||
.reveal.visible { opacity: 1; transform: translateY(0); }
|
||||
/* Accessibility: no animation if the user prefers reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
.reveal { opacity: 1; transform: none; transition: none; }
|
||||
.project:hover, .btn-primary:hover { transform: none; }
|
||||
.project:hover, .btn-primary:hover, .btn-ghost:hover { transform: none; }
|
||||
.badge .dot, nav .logo::after, footer .wrap::before { animation: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<div class="wrap">
|
||||
<div class="logo">~/<span>quentin_pismont</span></div>
|
||||
<div class="logo">1UP <span>QUENTIN_PISMONT</span></div>
|
||||
<div class="links">
|
||||
<a href="#about">About</a>
|
||||
<a href="#projects">Projects</a>
|
||||
@@ -348,15 +455,15 @@
|
||||
</nav>
|
||||
<header>
|
||||
<div class="wrap">
|
||||
<div class="badge"><span class="dot"></span> Open to opportunities</div>
|
||||
<h1>I design systems<br><span class="grad">end to end</span>, built to hold up in production.</h1>
|
||||
<div class="badge"><span class="dot"></span> Player 1 ready — open to work</div>
|
||||
<h1>I design systems<br><span class="grad">end to end</span>,<br>built to hold up<br>in production.</h1>
|
||||
<p class="lead">
|
||||
Platform / infrastructure engineer. From code to production, through CI/CD,
|
||||
observability and security — an integrated ecosystem designed to be operated
|
||||
over the long run.
|
||||
</p>
|
||||
<div class="cta">
|
||||
<a href="#projects" class="btn btn-primary">View projects →</a>
|
||||
<a href="#projects" class="btn btn-primary">Start game ></a>
|
||||
<a href="https://gitea.qpismont.fr/explore/repos" target="_blank" class="btn btn-ghost">Gitea / Source code</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -416,9 +523,9 @@
|
||||
<!-- PROJECT 1 : INFRA -->
|
||||
<div class="project reveal">
|
||||
<div class="project-top">
|
||||
<h3>🏗️ Self-hosted infrastructure <span class="tag done">Complete & tested</span></h3>
|
||||
<h3>Self-hosted infrastructure <span class="tag done">Complete & tested</span></h3>
|
||||
</div>
|
||||
<p class="subtitle">A complete ecosystem — code → CI → production → observability — on bare metal, validated by real failure tests.</p>
|
||||
<p class="subtitle">A complete ecosystem — code -> CI -> production -> observability — on bare metal, validated by real failure tests.</p>
|
||||
<div class="block">
|
||||
<div class="block-label">The problem</div>
|
||||
<p>Having a reliable personal platform to host and operate my own services, with the standards of real production: resilience, alerting, zero data loss.</p>
|
||||
@@ -427,17 +534,17 @@
|
||||
<div class="block-label">The architecture</div>
|
||||
<ul>
|
||||
<li><strong>Networking</strong> — WireGuard (the only entry point), Docker overlay, and <code>CoreDNS</code> for <code>*.internal</code> resolution with fallback to Docker's DNS.</li>
|
||||
<li><strong>CI/CD</strong> — Gitea + Woodpecker: lint → tests → build → automatic push registry on tag/release → deploy on prod.</li>
|
||||
<li><strong>Observability</strong> — OtelCollector (central hub), Prometheus (storage + alerting), Loki (logs) + Tempo (traces), Perses dashboards, Alertmanager → ntfy.</li>
|
||||
<li><strong>CI/CD</strong> — Gitea + Woodpecker: lint -> tests -> build -> automatic push registry on tag/release -> deploy on prod.</li>
|
||||
<li><strong>Observability</strong> — OtelCollector (central hub), Prometheus (storage + alerting), Loki (logs) + Tempo (traces), Perses dashboards, Alertmanager -> ntfy.</li>
|
||||
<li><strong>Safety net</strong> — Blackbox scraped directly by Prometheus: monitoring survives a Collector failure.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="block">
|
||||
<div class="block-label">What sets it apart — validated resilience</div>
|
||||
<ul>
|
||||
<li>Gitea outage → ntfy alert received ✓</li>
|
||||
<li>OtelCollector outage → alert received, monitoring maintained ✓</li>
|
||||
<li>Multiple redeployments → zero data loss (persistent volumes) ✓</li>
|
||||
<li>Gitea outage -> ntfy alert received [OK]</li>
|
||||
<li>OtelCollector outage -> alert received, monitoring maintained [OK]</li>
|
||||
<li>Multiple redeployments -> zero data loss (persistent volumes) [OK]</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="stack">
|
||||
@@ -447,7 +554,7 @@
|
||||
<!-- PROJECT 2 : HERALD -->
|
||||
<div class="project reveal">
|
||||
<div class="project-top">
|
||||
<h3>🤖 Herald — AI agent for Gitea <span class="tag wip">In active development</span></h3>
|
||||
<h3>Herald — AI agent for Gitea <span class="tag wip">In active development</span></h3>
|
||||
</div>
|
||||
<p class="subtitle">A code-review and AI-assistance bot for Gitea, written in Rust, with a <strong>security-first</strong> agentic architecture.</p>
|
||||
<div class="block">
|
||||
@@ -482,7 +589,7 @@
|
||||
<!-- PROJECT 3 : PIPELINE -->
|
||||
<div class="project reveal">
|
||||
<div class="project-top">
|
||||
<h3>⚡ Distributed media pipeline <span class="tag wip">Design / upcoming</span></h3>
|
||||
<h3>Distributed media pipeline <span class="tag wip">Design / upcoming</span></h3>
|
||||
</div>
|
||||
<p class="subtitle">An extended imgproxy-style media processing pipeline, designed for horizontal scaling and streaming.</p>
|
||||
<div class="block">
|
||||
@@ -516,27 +623,27 @@
|
||||
</div>
|
||||
<div class="skills-grid">
|
||||
<div class="skill-card reveal">
|
||||
<h4><span class="ic">◆</span> Languages</h4>
|
||||
<h4><span class="ic">■</span> Languages</h4>
|
||||
<p>Rust (async / Tokio), TypeScript / Node. Looking for the right tool for the right problem.</p>
|
||||
</div>
|
||||
<div class="skill-card reveal">
|
||||
<h4><span class="ic">◆</span> Infrastructure & Ops</h4>
|
||||
<h4><span class="ic">■</span> Infrastructure & Ops</h4>
|
||||
<p>Docker Swarm, bare metal, Fedora Bootc (immutable OS), CI/CD (Gitea, Woodpecker).</p>
|
||||
</div>
|
||||
<div class="skill-card reveal">
|
||||
<h4><span class="ic">◆</span> Observability</h4>
|
||||
<h4><span class="ic">■</span> Observability</h4>
|
||||
<p>OpenTelemetry, Prometheus, Loki, Tempo, Perses, Alertmanager. Full stack, tested under failure.</p>
|
||||
</div>
|
||||
<div class="skill-card reveal">
|
||||
<h4><span class="ic">◆</span> Networking</h4>
|
||||
<h4><span class="ic">■</span> Networking</h4>
|
||||
<p>WireGuard, CoreDNS, overlay networks, Caddy. Designing self-hosted private networks.</p>
|
||||
</div>
|
||||
<div class="skill-card reveal">
|
||||
<h4><span class="ic">◆</span> Security & Isolation</h4>
|
||||
<h4><span class="ic">■</span> Security & Isolation</h4>
|
||||
<p>Container sandboxing, privilege separation (DBus / Polkit), threat modeling.</p>
|
||||
</div>
|
||||
<div class="skill-card reveal">
|
||||
<h4><span class="ic">◆</span> Distributed & Messaging</h4>
|
||||
<h4><span class="ic">■</span> Distributed & Messaging</h4>
|
||||
<p>NATS, distributed architectures, claim-check patterns, horizontal scaling.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -562,7 +669,7 @@
|
||||
</section>
|
||||
<footer>
|
||||
<div class="wrap">
|
||||
© 2026
|
||||
INSERT COIN
|
||||
</div>
|
||||
</footer>
|
||||
<script>
|
||||
|
||||
Reference in new issue
Block a user