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 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
|
**Domaine :** qpismont.fr
|
||||||
**Stack :** Docker Swarm
|
**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)
|
- **Hébergement Git** avec interface web (Gitea)
|
||||||
- **CI/CD automatisé** pour builds et déploiements (Woodpecker)
|
- **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)
|
- **Reverse proxy** avec HTTPS automatique (Caddy)
|
||||||
- **Accès VPN sécurisé** pour l'administration (WireGuard)
|
- **Accès VPN sécurisé** pour l'administration (WireGuard + CoreDNS)
|
||||||
- **Monitoring** des services et ressources (Prometheus + Perses)
|
- **Observabilité** : métriques, traces et dashboards (Prometheus, Tempo, Perses)
|
||||||
- **Backups automatisés** quotidiens vers serveur FTP
|
- **Alerting** temps réel avec notifications push (Alertmanager + ntfy)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Services déployés
|
## Services déployés
|
||||||
|
|
||||||
### Production
|
### Stack `core`
|
||||||
|
|
||||||
| Service | Version | Description | URL |
|
| Service | Image | Description | Exposition |
|
||||||
|---------|---------|-------------|-----|
|
|---------|-------|-------------|------------|
|
||||||
| **Gitea** | 1.24.7 | Hébergement Git (alternative GitHub) | https://gitea.qpismont.fr |
|
| **reverse_caddy** | `caddy:2.11-alpine` | Reverse proxy, HTTPS auto, HTTP/3 | `80`, `443/tcp`, `443/udp` |
|
||||||
| **Woodpecker CI** | 3.11.0 | Plateforme CI/CD (fork de Drone) | https://woodpecker.qpismont.fr |
|
| **wireguard_server** | `linuxserver/wireguard:latest` | VPN d'accès distant + CoreDNS | `51820/udp` |
|
||||||
| **Caddy** | 2-alpine | Reverse proxy + HTTPS automatique | - |
|
|
||||||
| **WireGuard** | latest | Serveur VPN pour accès distant | UDP:51820 |
|
### Stack `git`
|
||||||
| **Prometheus** | 3.7.2 | Collecte de métriques | (interne) |
|
|
||||||
| **Perses** | latest | Dashboards de monitoring | (via VPN) |
|
| Service | Image | Description | URL |
|
||||||
| **cAdvisor** | latest | Métriques des containers | (interne) |
|
|---------|-------|-------------|-----|
|
||||||
| **Backup** | latest | Sauvegarde quotidienne (4h00) | (interne) |
|
| **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
|
### Configuration
|
||||||
|
|
||||||
- **Timezone** : Europe/Paris
|
- **Timezone** : Europe/Paris
|
||||||
- **OAuth** : Gitea ↔ Woodpecker intégré
|
- **OAuth** : Gitea ↔ Woodpecker intégré
|
||||||
- **Admin** : qpismont
|
- **Admin** : qpismont
|
||||||
- **Backup retention** : 14 jours
|
- **Scrape interval** : 10 secondes
|
||||||
- **Monitoring scrape** : 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]
|
[Caddy Proxy] [HTTP/3] [WireGuard VPN]
|
||||||
│ │
|
│ │
|
||||||
┌───────────┼──────────┐ │
|
┌───────────┼───────────┬──────────┐ │
|
||||||
│ │ │ │
|
│ │ │ │ │
|
||||||
gitea:3000 woodpecker:8000 │ [Perses]
|
gitea:3000 woodpecker:8000 │ ntfy:80│
|
||||||
│ │
|
│ │
|
||||||
www.qpismont.fr [Prometheus]
|
www.qpismont.fr [CoreDNS]
|
||||||
(désactivé) │
|
(portfolio) │
|
||||||
[cAdvisor]
|
[Perses]
|
||||||
|
[Prometheus]
|
||||||
|
[Tempo]
|
||||||
|
|
||||||
┌──────────────────────────────────────────────────────────────┐
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
│ CI/CD Pipeline │
|
│ Stack git (CI/CD) │
|
||||||
│ │
|
│ │
|
||||||
│ [Gitea] ←──── OAuth ────→ [Woodpecker Server] │
|
│ [Gitea] ←──── OAuth ────→ [Woodpecker Server] │
|
||||||
│ │ │
|
│ │ │ │
|
||||||
│ ▼ │
|
│ │ webhook ▼ │
|
||||||
│ [Woodpecker Agent] │
|
│ ▼ [Woodpecker Agent] │
|
||||||
│ │ │
|
│ [Herald] │ │
|
||||||
│ ▼ │
|
│ (review IA) ▼ │
|
||||||
│ [Docker-in-Docker] │
|
│ [Docker-in-Docker] │
|
||||||
│ (build containers) │
|
│ (build containers) │
|
||||||
└──────────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
┌──────────────────────────────────────────────────────────────┐
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
│ Monitoring Stack │
|
│ Stack observability │
|
||||||
│ │
|
│ │
|
||||||
│ [Prometheus] ←─ scrape ─┬─ prometheus:9090 (self) │
|
│ [OTel Collector] ──┬─ hostmetrics (CPU/RAM/disk/net) │
|
||||||
│ │ ├─ reverse_caddy:2019 (metrics) │
|
│ │ ├─ docker_stats (métriques containers) │
|
||||||
│ │ └─ cadvisor:8080 (containers) │
|
│ │ ├─ prometheus scrape: │
|
||||||
|
│ │ │ reverse_caddy:2019 │
|
||||||
|
│ │ │ herald:9100 │
|
||||||
|
│ │ │ alertmanager:9093 │
|
||||||
|
│ │ │ blackbox_exporter:9115 │
|
||||||
|
│ │ └─ OTLP (4318) │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ └──→ [Perses] (visualisation via VPN) │
|
│ ├── remote_write ──→ [Prometheus] ──→ [Perses] │
|
||||||
└──────────────────────────────────────────────────────────────┘
|
│ └── OTLP ──────────→ [Tempo] │
|
||||||
|
│ │ │
|
||||||
┌──────────────────────────────────────────────────────────────┐
|
│ [Prometheus] ── alertes ──→ [Alertmanager] ──→ [ntfy] │
|
||||||
│ 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é) │
|
|
||||||
└──────────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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éseaux Docker Swarm
|
||||||
|
|
||||||
| Réseau | Type | Services |
|
| Réseau | Type | Services |
|
||||||
|--------|------|----------|
|
|--------|------|----------|
|
||||||
| `reverse` | overlay | Caddy, Gitea, Woodpecker |
|
| `reverse_network` | overlay | Caddy, Gitea, Woodpecker (server + agent), Herald, ntfy |
|
||||||
| `wireguard_network` | overlay | WireGuard, Perses |
|
| `wireguard_network` | overlay | WireGuard, Prometheus, Perses, Tempo |
|
||||||
| `prometheus_network` | overlay | Prometheus, Perses, cAdvisor, Caddy, Backup |
|
| `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
|
- **Docker Configs** pour configurations versionnées
|
||||||
|
|
||||||
### Infrastructure as Code
|
### 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)
|
- Déploiement : `./deploy.sh` (génère timestamp pour versioning)
|
||||||
- Contexte Docker distant : `prod`
|
- 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
|
- **VPN** : WireGuard pour services internes
|
||||||
|
|
||||||
### Storage
|
### Storage
|
||||||
- **Volumes Docker** : giteadata, woodpeckerdata, reversecaddydata, reversecaddyconfig, backuptemp
|
- **Volumes Docker** : `reversecaddydata`, `reversecaddyconfig` (core) · `giteadata`, `woodpeckerdata` (git) · `prometheusdata`, `tempodata`, `persesdata`, `alertmanagerdata`, `ntfydata` (observability)
|
||||||
- **Host mounts** : /home/user/wireguard-config, /var/run/docker.sock (CI/CD)
|
- **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
|
### 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)
|
- **Dashboards** : Perses (accessible via VPN)
|
||||||
- **Container metrics** : cAdvisor
|
- **Container metrics** : receiver `docker_stats` de l'OTel Collector (labels Swarm `service_name` / `task_name`)
|
||||||
- **Proxy metrics** : Caddy admin API (:2019)
|
- **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
|
### Backups
|
||||||
- **Solution** : docker-volume-backup avec rclone
|
- ⚠️ **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.
|
||||||
- **Fréquence** : Quotidien à 4h00
|
|
||||||
- **Destination** : Serveur FTP distant
|
|
||||||
- **Rétention** : 14 jours
|
|
||||||
- **Format** : .tar.gz compressé
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -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
|
- Limite : 1 workflow concurrent, CPU core 1
|
||||||
- Support multi-architectures
|
- 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
|
### Caddy
|
||||||
- HTTPS automatique avec Let's Encrypt
|
- HTTPS automatique avec Let's Encrypt
|
||||||
- HTTP/3 (QUIC) support
|
- 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
|
- VPN moderne et rapide
|
||||||
- Configuration : 1 peer (extensible)
|
- Configuration : 1 peer (extensible)
|
||||||
- Accès sécurisé aux services internes
|
- Accès sécurisé aux services internes
|
||||||
|
- CoreDNS embarqué pour résoudre `perses.internal`, `prometheus.internal`, `tempo.internal`
|
||||||
- Faible overhead
|
- Faible overhead
|
||||||
|
|
||||||
### Monitoring
|
### Monitoring
|
||||||
- **Prometheus** : Collecte et stockage des métriques
|
- **OTel Collector** : hostmetrics (CPU, RAM, disque, réseau, load, process) + `docker_stats` (métriques containers) + scrape Prometheus + réception OTLP
|
||||||
- **Perses** : Visualisation moderne (alternative Grafana)
|
- **Prometheus** : stockage des métriques, remote-write receiver
|
||||||
- **cAdvisor** : Métriques CPU, RAM, I/O des containers
|
- **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
|
- Scrape automatique toutes les 10 secondes
|
||||||
|
|
||||||
### Backups
|
### Alerting
|
||||||
- Sauvegarde automatique quotidienne
|
- Règles : `ServiceDown` (probe blackbox), `OtelCollectorDown`, `CaddyDown`
|
||||||
- Support FTP/SFTP via rclone
|
- Seuil : 1 minute
|
||||||
- Compression gzip
|
- Inhibition : `CaddyDown` supprime les `ServiceDown` (évite le bruit)
|
||||||
- Notifications optionnelles (Discord, Slack, Email)
|
- Notification : webhook ntfy avec template `alertmanager`, `send_resolved: true`
|
||||||
- Rétention configurable
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## URLs et accès
|
## URLs et accès
|
||||||
|
|
||||||
### Services publics
|
### Services publics
|
||||||
- **Gitea** : https://gitea.qpismont.fr
|
- **Gitea** : https://gitea.qpismont.fr (alias https://git.qpismont.fr)
|
||||||
- **Woodpecker** : https://woodpecker.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)
|
### Services internes (VPN requis)
|
||||||
- **Perses** : http://perses:8080 (via WireGuard)
|
- **Perses** : http://perses.internal:8080 (via WireGuard/CoreDNS)
|
||||||
- **Prometheus** : http://prometheus:9090 (via WireGuard)
|
- **Prometheus** : http://prometheus.internal:9090
|
||||||
|
- **Tempo** : http://tempo.internal:3200
|
||||||
- **Caddy Admin** : http://reverse_caddy:2019 (métriques)
|
- **Caddy Admin** : http://reverse_caddy:2019 (métriques)
|
||||||
|
|
||||||
### VPN
|
### VPN
|
||||||
- **Protocole** : WireGuard
|
- **Protocole** : WireGuard
|
||||||
- **Port** : 51820/udp
|
- **Port** : 51820/udp
|
||||||
|
- **Sous-réseau** : `10.13.13.0/24`
|
||||||
- **Config** : `/home/user/wireguard-config/peer1/`
|
- **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
|
./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 :**
|
**Prérequis :**
|
||||||
- Contexte Docker `prod` configuré
|
- Contexte Docker `prod` configuré
|
||||||
- Réseaux overlay créés (`reverse`, `wireguard_network`, `prometheus_network`)
|
- Réseaux overlay créés (`reverse_network`, `wireguard_network`, `prometheus_network`)
|
||||||
- Secrets configurés dans `./secrets/`
|
- 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
|
### Secrets Docker
|
||||||
|
|
||||||
| Secret | Usage |
|
| Secret | Fichier source | Usage |
|
||||||
|--------|-------|
|
|--------|----------------|-------|
|
||||||
| `woodpecker_agent_secret` | Communication server/agent Woodpecker |
|
| `woodpecker_agent_secret` | `./secrets/woodpecker_agent_secret` | Communication server/agent Woodpecker |
|
||||||
| `woodpecker_gitea_client` | OAuth client ID Gitea |
|
| `woodpecker_gitea_client` | `./secrets/woodpecker_gitea_client` | OAuth client ID Gitea |
|
||||||
| `woodpecker_gitea_secret` | OAuth client secret Gitea |
|
| `woodpecker_gitea_secret` | `./secrets/woodpecker_gitea_secret` | OAuth client secret Gitea |
|
||||||
| `backup_ftp_config` | Configuration rclone pour FTP |
|
| `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
|
### Configs Docker
|
||||||
|
|
||||||
| Config | Fichier source |
|
| Config | Fichier source | Stack |
|
||||||
|--------|----------------|
|
|--------|----------------|-------|
|
||||||
| `caddy_config` | `./Caddyfile` |
|
| `caddy_config` | `./Caddyfile` | core |
|
||||||
| `prometheus_config` | `./prometheus.yml` |
|
| `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
|
#!/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.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
|
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
|
- reverse_network
|
||||||
- prometheus_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:
|
wireguard_server:
|
||||||
image: linuxserver/wireguard
|
image: linuxserver/wireguard
|
||||||
cap_add:
|
cap_add:
|
||||||
@@ -128,29 +53,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- wireguard_network
|
- 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:
|
configs:
|
||||||
caddy_config:
|
caddy_config:
|
||||||
name: caddy_config_${DATETIME}
|
name: caddy_config_${DATETIME}
|
||||||
@@ -163,8 +65,6 @@ configs:
|
|||||||
file: ./portfolio/index.html
|
file: ./portfolio/index.html
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
giteadata:
|
|
||||||
woodpeckerdata:
|
|
||||||
reversecaddyconfig:
|
reversecaddyconfig:
|
||||||
reversecaddydata:
|
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
|
- wireguard_network
|
||||||
|
|
||||||
blackbox_exporter:
|
blackbox_exporter:
|
||||||
image: prom/blackbox-exporter:v0.28.0
|
image: prom/blackbox-exporter:v0.29.1
|
||||||
configs:
|
configs:
|
||||||
- source: blackbox_config
|
- source: blackbox_config
|
||||||
target: /etc/blackbox_exporter/config.yml
|
target: /etc/blackbox_exporter/config.yml
|
||||||
@@ -52,8 +52,44 @@ services:
|
|||||||
- prometheus_network
|
- prometheus_network
|
||||||
- reverse_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:
|
otel-collector:
|
||||||
image: otel/opentelemetry-collector-contrib:0.161.0
|
image: otel/opentelemetry-collector-contrib:0.164.0
|
||||||
configs:
|
configs:
|
||||||
- source: otel_collector_config
|
- source: otel_collector_config
|
||||||
target: /etc/otelcol-contrib/config.yaml
|
target: /etc/otelcol-contrib/config.yaml
|
||||||
@@ -66,12 +102,12 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 512M
|
memory: 768M
|
||||||
networks:
|
networks:
|
||||||
- prometheus_network
|
- prometheus_network
|
||||||
|
|
||||||
prometheus:
|
prometheus:
|
||||||
image: prom/prometheus:v3.13.3
|
image: prom/prometheus:v3.15.1
|
||||||
command:
|
command:
|
||||||
- --config.file=/etc/prometheus/prometheus.yml
|
- --config.file=/etc/prometheus/prometheus.yml
|
||||||
- --storage.tsdb.path=/prometheus
|
- --storage.tsdb.path=/prometheus
|
||||||
@@ -87,16 +123,6 @@ services:
|
|||||||
- prometheus_network
|
- prometheus_network
|
||||||
- wireguard_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:
|
configs:
|
||||||
prometheus_config:
|
prometheus_config:
|
||||||
name: prometheus_config_${DATETIME}
|
name: prometheus_config_${DATETIME}
|
||||||
|
|||||||
@@ -14,6 +14,35 @@ receivers:
|
|||||||
network:
|
network:
|
||||||
load:
|
load:
|
||||||
processes:
|
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:
|
prometheus:
|
||||||
config:
|
config:
|
||||||
scrape_configs:
|
scrape_configs:
|
||||||
@@ -21,10 +50,6 @@ receivers:
|
|||||||
scrape_interval: 10s
|
scrape_interval: 10s
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ['reverse_caddy:2019']
|
- targets: ['reverse_caddy:2019']
|
||||||
- job_name: cadvisor
|
|
||||||
scrape_interval: 10s
|
|
||||||
static_configs:
|
|
||||||
- targets: ['cadvisor:8080']
|
|
||||||
- job_name: herald
|
- job_name: herald
|
||||||
scrape_interval: 10s
|
scrape_interval: 10s
|
||||||
static_configs:
|
static_configs:
|
||||||
@@ -64,6 +89,8 @@ processors:
|
|||||||
exporters:
|
exporters:
|
||||||
prometheusremotewrite:
|
prometheusremotewrite:
|
||||||
endpoint: http://prometheus:9090/api/v1/write
|
endpoint: http://prometheus:9090/api/v1/write
|
||||||
|
resource_to_telemetry_conversion:
|
||||||
|
enabled: true
|
||||||
otlphttp/tempo:
|
otlphttp/tempo:
|
||||||
endpoint: http://tempo:4318
|
endpoint: http://tempo:4318
|
||||||
|
|
||||||
@@ -78,7 +105,7 @@ service:
|
|||||||
port: 8888
|
port: 8888
|
||||||
pipelines:
|
pipelines:
|
||||||
metrics:
|
metrics:
|
||||||
receivers: [otlp, prometheus, hostmetrics]
|
receivers: [otlp, prometheus, hostmetrics, docker_stats]
|
||||||
processors: [memory_limiter, batch]
|
processors: [memory_limiter, batch]
|
||||||
exporters: [prometheusremotewrite]
|
exporters: [prometheusremotewrite]
|
||||||
traces:
|
traces:
|
||||||
|
|||||||
+271
-164
@@ -5,38 +5,63 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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.">
|
<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>
|
<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 -->
|
<!-- Open Graph -->
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:title" content="Quentin Pismont — Senior/Lead Platform Engineer">
|
<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.">
|
<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>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg: #0d1117;
|
--bg: #0b0b14;
|
||||||
--bg-soft: #161b22;
|
--bg-soft: #14142a;
|
||||||
--bg-card: #161b22;
|
--bg-card: #101024;
|
||||||
--border: #21262d;
|
--border: #3a3a6a;
|
||||||
--border-hover: #30363d;
|
--text: #e8e8f0;
|
||||||
--text: #e6edf3;
|
--text-dim: #bcbcd8;
|
||||||
--text-dim: #8b949e;
|
--text-faint: #7a7aa4;
|
||||||
--text-faint: #6e7681;
|
--accent: #3cbcfc;
|
||||||
--accent: #58a6ff;
|
--accent-soft: rgba(60, 188, 252, 0.12);
|
||||||
--accent-soft: rgba(88, 166, 255, 0.1);
|
--accent-2: #f8b800;
|
||||||
--green: #3fb950;
|
--green: #00e436;
|
||||||
--mono: 'SF Mono', 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
|
--pink: #ff2d78;
|
||||||
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
--mono: 'VT323', 'Courier New', monospace;
|
||||||
|
--pixel: 'Press Start 2P', 'Courier New', monospace;
|
||||||
}
|
}
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
html { scroll-behavior: smooth; }
|
html { scroll-behavior: smooth; }
|
||||||
body {
|
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);
|
color: var(--text);
|
||||||
font-family: var(--sans);
|
font-family: var(--mono);
|
||||||
line-height: 1.65;
|
font-size: 21px;
|
||||||
font-size: 16px;
|
line-height: 1.6;
|
||||||
-webkit-font-smoothing: antialiased;
|
}
|
||||||
|
/* 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 {
|
.wrap {
|
||||||
max-width: 860px;
|
max-width: 880px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
}
|
}
|
||||||
@@ -45,225 +70,279 @@
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
background: rgba(13, 17, 23, 0.8);
|
background: var(--bg);
|
||||||
backdrop-filter: blur(12px);
|
border-bottom: 4px solid var(--border);
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
}
|
||||||
nav .wrap {
|
nav .wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 60px;
|
height: 64px;
|
||||||
}
|
}
|
||||||
nav .logo {
|
nav .logo {
|
||||||
font-family: var(--mono);
|
font-family: var(--pixel);
|
||||||
font-weight: 600;
|
font-weight: 400;
|
||||||
font-size: 15px;
|
font-size: 10px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
nav .logo span { color: var(--accent); }
|
nav .logo span { color: var(--accent-2); }
|
||||||
nav .links { display: flex; gap: 28px; }
|
nav .logo::after {
|
||||||
|
content: "_";
|
||||||
|
color: var(--accent);
|
||||||
|
animation: blink 1s steps(2, start) infinite;
|
||||||
|
}
|
||||||
|
nav .links { display: flex; gap: 24px; }
|
||||||
nav a {
|
nav a {
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 14px;
|
font-family: var(--pixel);
|
||||||
transition: color 0.2s;
|
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; } }
|
@media (max-width: 620px) { nav .links { display: none; } }
|
||||||
/* ---------- HERO ---------- */
|
/* ---------- HERO ---------- */
|
||||||
header {
|
header {
|
||||||
padding: 100px 0 80px;
|
padding: 72px 0 64px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 4px solid var(--border);
|
||||||
}
|
}
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
font-family: var(--mono);
|
font-family: var(--pixel);
|
||||||
font-size: 13px;
|
font-size: 8px;
|
||||||
|
text-transform: uppercase;
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
background: rgba(63, 185, 80, 0.1);
|
background: transparent;
|
||||||
border: 1px solid rgba(63, 185, 80, 0.2);
|
border: 3px solid var(--green);
|
||||||
padding: 5px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 20px;
|
margin-bottom: 32px;
|
||||||
margin-bottom: 28px;
|
box-shadow: 4px 4px 0 rgba(0, 228, 54, 0.25);
|
||||||
}
|
}
|
||||||
.badge .dot {
|
.badge .dot {
|
||||||
width: 7px; height: 7px;
|
width: 8px; height: 8px;
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--green);
|
background: var(--green);
|
||||||
box-shadow: 0 0 8px var(--green);
|
animation: blink 1s steps(2, start) infinite;
|
||||||
}
|
}
|
||||||
header h1 {
|
header h1 {
|
||||||
font-size: clamp(2rem, 5vw, 3rem);
|
font-family: var(--pixel);
|
||||||
line-height: 1.15;
|
font-size: clamp(0.85rem, 2.6vw, 1.45rem);
|
||||||
letter-spacing: -0.02em;
|
line-height: 1.9;
|
||||||
margin-bottom: 20px;
|
letter-spacing: 0;
|
||||||
font-weight: 700;
|
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 {
|
header h1 .grad {
|
||||||
background: linear-gradient(120deg, #58a6ff, #a371f7);
|
color: var(--accent-2);
|
||||||
-webkit-background-clip: text;
|
background: none;
|
||||||
background-clip: text;
|
-webkit-text-fill-color: currentColor;
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
}
|
}
|
||||||
header p.lead {
|
header p.lead {
|
||||||
font-size: 1.15rem;
|
font-size: 23px;
|
||||||
|
line-height: 1.65;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
max-width: 640px;
|
max-width: 660px;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
.cta {
|
.cta {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 14px;
|
gap: 16px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.btn {
|
.btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 10px 20px;
|
padding: 12px 18px;
|
||||||
border-radius: 8px;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 14px;
|
font-family: var(--pixel);
|
||||||
font-weight: 500;
|
font-size: 8px;
|
||||||
transition: all 0.2s;
|
text-transform: uppercase;
|
||||||
border: 1px solid transparent;
|
border: 3px solid var(--text);
|
||||||
|
background: var(--bg-soft);
|
||||||
|
color: var(--text);
|
||||||
|
box-shadow: 5px 5px 0 #000;
|
||||||
|
transition: none;
|
||||||
}
|
}
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: var(--accent);
|
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 {
|
.btn-ghost {
|
||||||
background: transparent;
|
background: var(--bg-soft);
|
||||||
color: var(--text);
|
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 ---------- */
|
/* ---------- SECTIONS ---------- */
|
||||||
section { padding: 80px 0; border-bottom: 1px solid var(--border); }
|
section { padding: 64px 0; border-bottom: 4px solid var(--border); }
|
||||||
.section-head {
|
.section-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 14px;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 36px;
|
||||||
}
|
}
|
||||||
.section-head .num {
|
.section-head .num {
|
||||||
font-family: var(--mono);
|
font-family: var(--pixel);
|
||||||
font-size: 14px;
|
font-size: 10px;
|
||||||
color: var(--accent);
|
color: #0b0b14;
|
||||||
|
background: var(--accent-2);
|
||||||
|
padding: 6px 8px;
|
||||||
|
box-shadow: 3px 3px 0 #000;
|
||||||
}
|
}
|
||||||
.section-head h2 {
|
.section-head h2 {
|
||||||
font-size: 1.6rem;
|
font-family: var(--pixel);
|
||||||
letter-spacing: -0.01em;
|
font-size: clamp(0.85rem, 2.2vw, 1.1rem);
|
||||||
font-weight: 600;
|
letter-spacing: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text);
|
||||||
}
|
}
|
||||||
.section-intro {
|
.section-intro {
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
max-width: 640px;
|
max-width: 660px;
|
||||||
margin: -24px 0 40px;
|
margin: -20px 0 36px;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1.65;
|
||||||
}
|
}
|
||||||
/* ---------- ABOUT ---------- */
|
/* ---------- ABOUT ---------- */
|
||||||
.about-grid {
|
.about-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 40px;
|
gap: 36px;
|
||||||
}
|
}
|
||||||
@media (max-width: 620px) { .about-grid { grid-template-columns: 1fr; gap: 28px; } }
|
@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 { color: var(--text-dim); margin-bottom: 18px; font-size: 22px; line-height: 1.65; }
|
||||||
.about-grid p strong { color: var(--text); font-weight: 500; }
|
.about-grid p strong { color: var(--accent-2); font-weight: 400; }
|
||||||
.principles { list-style: none; }
|
.principles { list-style: none; }
|
||||||
.principles li {
|
.principles li {
|
||||||
padding: 10px 0 10px 24px;
|
padding: 10px 0 10px 26px;
|
||||||
position: relative;
|
position: relative;
|
||||||
color: var(--text-dim);
|
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:last-child { border-bottom: none; }
|
||||||
.principles li::before {
|
.principles li::before {
|
||||||
content: "→";
|
content: ">";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
color: var(--accent);
|
color: var(--green);
|
||||||
|
font-family: var(--pixel);
|
||||||
|
font-size: 9px;
|
||||||
|
top: 14px;
|
||||||
}
|
}
|
||||||
/* ---------- PROJECTS ---------- */
|
/* ---------- PROJECTS ---------- */
|
||||||
.project {
|
.project {
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border: 1px solid var(--border);
|
border: 3px solid var(--border);
|
||||||
border-radius: 14px;
|
border-radius: 0;
|
||||||
padding: 32px;
|
padding: 28px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 28px;
|
||||||
transition: border-color 0.25s, transform 0.25s;
|
box-shadow: 6px 6px 0 #000;
|
||||||
|
transition: none;
|
||||||
}
|
}
|
||||||
.project:hover {
|
.project:hover {
|
||||||
border-color: var(--border-hover);
|
border-color: var(--accent);
|
||||||
transform: translateY(-2px);
|
transform: none;
|
||||||
|
box-shadow: 6px 6px 0 rgba(60, 188, 252, 0.35);
|
||||||
}
|
}
|
||||||
.project-top {
|
.project-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.project h3 {
|
.project h3 {
|
||||||
font-size: 1.3rem;
|
font-family: var(--pixel);
|
||||||
font-weight: 600;
|
font-size: 11px;
|
||||||
|
line-height: 1.8;
|
||||||
|
font-weight: 400;
|
||||||
|
text-transform: uppercase;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
.project .tag {
|
.project .tag {
|
||||||
font-family: var(--mono);
|
font-family: var(--pixel);
|
||||||
font-size: 11px;
|
font-size: 7px;
|
||||||
padding: 3px 10px;
|
padding: 6px 8px;
|
||||||
border-radius: 20px;
|
border-radius: 0;
|
||||||
background: var(--accent-soft);
|
background: transparent;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
border: 1px solid rgba(88,166,255,0.2);
|
border: 2px solid var(--accent);
|
||||||
white-space: nowrap;
|
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.wip { background: transparent; color: var(--accent-2); border-color: var(--accent-2); }
|
||||||
.tag.done { background: rgba(63,185,80,0.1); color: var(--green); border-color: rgba(63,185,80,0.2); }
|
.tag.done { background: transparent; color: var(--green); border-color: var(--green); }
|
||||||
.project .subtitle {
|
.project .subtitle {
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
font-size: 0.98rem;
|
font-size: 22px;
|
||||||
|
line-height: 1.65;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
.project .subtitle strong { color: var(--text); font-weight: 400; }
|
||||||
.block { margin-bottom: 20px; }
|
.block { margin-bottom: 20px; }
|
||||||
.block-label {
|
.block-label {
|
||||||
font-family: var(--mono);
|
font-family: var(--pixel);
|
||||||
font-size: 12px;
|
font-size: 8px;
|
||||||
color: var(--text-faint);
|
color: var(--accent-2);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0;
|
||||||
margin-bottom: 8px;
|
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 { list-style: none; }
|
||||||
.block ul li {
|
.block ul li {
|
||||||
padding: 4px 0 4px 20px;
|
padding: 6px 0 6px 22px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.block ul li::before {
|
.block ul li::before {
|
||||||
content: "▹";
|
content: ">";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
color: var(--accent);
|
color: var(--green);
|
||||||
font-size: 0.85em;
|
font-family: var(--pixel);
|
||||||
|
font-size: 8px;
|
||||||
|
top: 9px;
|
||||||
}
|
}
|
||||||
|
.block strong { color: var(--text); font-weight: 400; }
|
||||||
.block code {
|
.block code {
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
font-size: 0.85em;
|
font-size: 0.95em;
|
||||||
background: var(--bg-soft);
|
background: #000;
|
||||||
padding: 2px 6px;
|
padding: 1px 5px;
|
||||||
border-radius: 4px;
|
border-radius: 0;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
border: 1px solid var(--border);
|
border: 2px solid var(--border);
|
||||||
}
|
}
|
||||||
.stack {
|
.stack {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -271,73 +350,101 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 2px dashed var(--border);
|
||||||
}
|
}
|
||||||
.stack span {
|
.stack span {
|
||||||
font-family: var(--mono);
|
font-family: var(--pixel);
|
||||||
font-size: 12px;
|
font-size: 7px;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
background: var(--bg-soft);
|
background: #000;
|
||||||
padding: 4px 10px;
|
padding: 6px 8px;
|
||||||
border-radius: 6px;
|
border-radius: 0;
|
||||||
border: 1px solid var(--border);
|
border: 2px solid var(--border);
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
/* ---------- SKILLS ---------- */
|
/* ---------- SKILLS ---------- */
|
||||||
.skills-grid {
|
.skills-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
.skill-card {
|
.skill-card {
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border: 1px solid var(--border);
|
border: 3px solid var(--border);
|
||||||
border-radius: 12px;
|
border-radius: 0;
|
||||||
padding: 22px;
|
padding: 20px;
|
||||||
|
box-shadow: 5px 5px 0 #000;
|
||||||
}
|
}
|
||||||
|
.skill-card:hover { border-color: var(--accent-2); }
|
||||||
.skill-card h4 {
|
.skill-card h4 {
|
||||||
font-size: 0.95rem;
|
font-family: var(--pixel);
|
||||||
|
font-size: 8px;
|
||||||
|
line-height: 1.8;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
text-transform: uppercase;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.skill-card h4 .ic { color: var(--accent); }
|
.skill-card h4 .ic { color: var(--pink); }
|
||||||
.skill-card p { color: var(--text-dim); font-size: 0.9rem; }
|
.skill-card p { color: var(--text-dim); font-size: 21px; line-height: 1.6; }
|
||||||
/* ---------- CONTACT ---------- */
|
/* ---------- CONTACT ---------- */
|
||||||
#contact { text-align: center; border-bottom: none; }
|
#contact { text-align: center; border-bottom: none; }
|
||||||
#contact h2 { justify-content: center; }
|
#contact h2 { justify-content: center; }
|
||||||
#contact .section-head { justify-content: center; }
|
#contact .section-head { justify-content: center; }
|
||||||
#contact p { color: var(--text-dim); max-width: 500px; margin: 0 auto 32px; }
|
#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: 14px; justify-content: center; flex-wrap: wrap; }
|
.contact-links { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; }
|
||||||
/* ---------- FOOTER ---------- */
|
/* ---------- FOOTER ---------- */
|
||||||
footer {
|
footer {
|
||||||
padding: 40px 0;
|
padding: 32px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
font-size: 13px;
|
font-size: 18px;
|
||||||
font-family: var(--mono);
|
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 {
|
.reveal {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(20px);
|
transform: translateY(12px);
|
||||||
transition: opacity 0.6s ease, transform 0.6s ease;
|
transition: opacity 0.15s steps(2, end), transform 0.15s steps(2, end);
|
||||||
}
|
}
|
||||||
.reveal.visible { opacity: 1; transform: translateY(0); }
|
.reveal.visible { opacity: 1; transform: translateY(0); }
|
||||||
/* Accessibility: no animation if the user prefers reduced motion */
|
/* Accessibility: no animation if the user prefers reduced motion */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
html { scroll-behavior: auto; }
|
html { scroll-behavior: auto; }
|
||||||
.reveal { opacity: 1; transform: none; transition: none; }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<div class="logo">~/<span>quentin_pismont</span></div>
|
<div class="logo">1UP <span>QUENTIN_PISMONT</span></div>
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<a href="#about">About</a>
|
<a href="#about">About</a>
|
||||||
<a href="#projects">Projects</a>
|
<a href="#projects">Projects</a>
|
||||||
@@ -348,15 +455,15 @@
|
|||||||
</nav>
|
</nav>
|
||||||
<header>
|
<header>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<div class="badge"><span class="dot"></span> Open to opportunities</div>
|
<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>, built to hold up in production.</h1>
|
<h1>I design systems<br><span class="grad">end to end</span>,<br>built to hold up<br>in production.</h1>
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
Platform / infrastructure engineer. From code to production, through CI/CD,
|
Platform / infrastructure engineer. From code to production, through CI/CD,
|
||||||
observability and security — an integrated ecosystem designed to be operated
|
observability and security — an integrated ecosystem designed to be operated
|
||||||
over the long run.
|
over the long run.
|
||||||
</p>
|
</p>
|
||||||
<div class="cta">
|
<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>
|
<a href="https://gitea.qpismont.fr/explore/repos" target="_blank" class="btn btn-ghost">Gitea / Source code</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -416,9 +523,9 @@
|
|||||||
<!-- PROJECT 1 : INFRA -->
|
<!-- PROJECT 1 : INFRA -->
|
||||||
<div class="project reveal">
|
<div class="project reveal">
|
||||||
<div class="project-top">
|
<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>
|
</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">
|
||||||
<div class="block-label">The problem</div>
|
<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>
|
<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>
|
<div class="block-label">The architecture</div>
|
||||||
<ul>
|
<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>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>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>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>
|
<li><strong>Safety net</strong> — Blackbox scraped directly by Prometheus: monitoring survives a Collector failure.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div class="block-label">What sets it apart — validated resilience</div>
|
<div class="block-label">What sets it apart — validated resilience</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Gitea outage → ntfy alert received ✓</li>
|
<li>Gitea outage -> ntfy alert received [OK]</li>
|
||||||
<li>OtelCollector outage → alert received, monitoring maintained ✓</li>
|
<li>OtelCollector outage -> alert received, monitoring maintained [OK]</li>
|
||||||
<li>Multiple redeployments → zero data loss (persistent volumes) ✓</li>
|
<li>Multiple redeployments -> zero data loss (persistent volumes) [OK]</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="stack">
|
<div class="stack">
|
||||||
@@ -447,7 +554,7 @@
|
|||||||
<!-- PROJECT 2 : HERALD -->
|
<!-- PROJECT 2 : HERALD -->
|
||||||
<div class="project reveal">
|
<div class="project reveal">
|
||||||
<div class="project-top">
|
<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>
|
</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>
|
<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">
|
<div class="block">
|
||||||
@@ -482,7 +589,7 @@
|
|||||||
<!-- PROJECT 3 : PIPELINE -->
|
<!-- PROJECT 3 : PIPELINE -->
|
||||||
<div class="project reveal">
|
<div class="project reveal">
|
||||||
<div class="project-top">
|
<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>
|
</div>
|
||||||
<p class="subtitle">An extended imgproxy-style media processing pipeline, designed for horizontal scaling and streaming.</p>
|
<p class="subtitle">An extended imgproxy-style media processing pipeline, designed for horizontal scaling and streaming.</p>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
@@ -516,27 +623,27 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="skills-grid">
|
<div class="skills-grid">
|
||||||
<div class="skill-card reveal">
|
<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>
|
<p>Rust (async / Tokio), TypeScript / Node. Looking for the right tool for the right problem.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="skill-card reveal">
|
<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>
|
<p>Docker Swarm, bare metal, Fedora Bootc (immutable OS), CI/CD (Gitea, Woodpecker).</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="skill-card reveal">
|
<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>
|
<p>OpenTelemetry, Prometheus, Loki, Tempo, Perses, Alertmanager. Full stack, tested under failure.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="skill-card reveal">
|
<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>
|
<p>WireGuard, CoreDNS, overlay networks, Caddy. Designing self-hosted private networks.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="skill-card reveal">
|
<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>
|
<p>Container sandboxing, privilege separation (DBus / Polkit), threat modeling.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="skill-card reveal">
|
<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>
|
<p>NATS, distributed architectures, claim-check patterns, horizontal scaling.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -562,7 +669,7 @@
|
|||||||
</section>
|
</section>
|
||||||
<footer>
|
<footer>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
© 2026
|
INSERT COIN
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
Reference in new issue
Block a user