docs: vollständige Dokumentation (Dokploy-Deployment, Auto-Deploy, Troubleshooting, Architektur)
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
# Architektur & Infrastruktur
|
||||
|
||||
Übersicht der beteiligten Systeme und Netzwerk-Topologie.
|
||||
|
||||
## Komponenten
|
||||
|
||||
| System | Rolle | Adresse |
|
||||
|--------|-------|---------|
|
||||
| **Hermes-Server** | Assistent + Webhook-Receiver + Download-Hub | `192.168.0.80` |
|
||||
| **Dokploy-Host** | PaaS (Docker Swarm) | `192.168.0.184` |
|
||||
| **Gitea** | Git-Server | `git.kgessner.de` (K8s/Talos) |
|
||||
| **Proxmox** | Hypervisor (Dokploy-VM + K8s-Cluster) | — |
|
||||
| **Netcup-Gateway** | Reverse-Proxy (öffentliche IP) | `192.168.0.222` |
|
||||
|
||||
## Netzwerk-Topologie
|
||||
|
||||
```
|
||||
Internet
|
||||
│
|
||||
152.53.197.186 (öffentliche IP)
|
||||
│
|
||||
┌────────┴────────┐
|
||||
│ Netcup-Gateway │ (192.168.0.222)
|
||||
│ Reverse-Proxy │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────────────────┼────────────────────┐
|
||||
│ │ │
|
||||
git.kgessner.de dokploy.kgessner.de (weitere Dienste)
|
||||
│ │
|
||||
▼ ▼
|
||||
K8s/Talos-Cluster Docker-Swarm-Host
|
||||
(auf Proxmox) (192.168.0.184)
|
||||
- Gitea - Dokploy UI (:3000)
|
||||
- Traefik (:80/443)
|
||||
- App-Services
|
||||
```
|
||||
|
||||
### Wichtige Erkenntnisse
|
||||
|
||||
1. **`git.kgessner.de` und `dokploy.kgessner.de` teilen sich die öffentliche IP**
|
||||
`152.53.197.186`. Ein Reverse-Proxy routet anhand des Host-Headers.
|
||||
|
||||
2. **Gitea läuft in Kubernetes (Talos)**, Dokploy in **Docker Swarm** — zwei
|
||||
unterschiedliche Orchestrierungswelten auf demselben Proxmox-Host.
|
||||
|
||||
3. **Netzwerk-Isolation:** Der K8s-Cluster (Gitea) kann interne `192.168.0.x`-Adressen
|
||||
(Hermes, Dokploy) nicht zuverlässig erreichen. Das ist der Grund, warum der
|
||||
Gitea-Webhook unzuverlässig ist und ein Cron-Polling-Fallback nötig wurde.
|
||||
|
||||
4. **Öffentliche IP routet nicht zu Dokploy:** `techno-todo.152.53.197.186.sslip.io`
|
||||
funktioniert **nicht**, weil Port 80/443 der öffentlichen IP nicht auf den
|
||||
Dokploy-Host (`192.168.0.184`) weitergeleitet wird. Für LAN-Tests wird deshalb
|
||||
`techno-todo.192.168.0.184.sslip.io` genutzt.
|
||||
|
||||
## Docker-Swarm auf Dokploy
|
||||
|
||||
```
|
||||
docker service ls
|
||||
# dokploy (PaaS-UI)
|
||||
# dokploy-postgres (interne DB)
|
||||
# dokploy-traefik (Reverse-Proxy, Ports 80/443)
|
||||
# techno-todo-v2-... (diese App)
|
||||
```
|
||||
|
||||
### Traefik-Konfiguration
|
||||
|
||||
Dokploy schreibt die Routing-Regeln nach `/etc/dokploy/traefik/dynamic/<app>.yml`
|
||||
(Bind-Mount in den Traefik-Container).
|
||||
|
||||
```bash
|
||||
# Reload nach Config-Änderung
|
||||
docker kill -s HUP dokploy-traefik
|
||||
```
|
||||
|
||||
### Dokploy-interne DB
|
||||
|
||||
PostgreSQL im Container `dokploy-postgres`. Nützliche Tabellen: `application`, `gitea`,
|
||||
`apikey`, `project`, `domain`, `deployment`.
|
||||
|
||||
```bash
|
||||
docker exec dokploy-postgres.1.<id> psql -U dokploy -d dokploy -c "\dt"
|
||||
```
|
||||
|
||||
## sslip.io — der Magic-DNS-Trick
|
||||
|
||||
`<name>.192.168.0.184.sslip.io` löst automatisch zu `192.168.0.184` auf. So braucht jede
|
||||
App keine eigene DNS-Config — einfach Subdomain vor die IP setzen.
|
||||
@@ -0,0 +1,127 @@
|
||||
# Auto-Deploy-Pipeline
|
||||
|
||||
Ziel: **`git push` = neue Version automatisch live**, ohne manuellen Eingriff.
|
||||
|
||||
## Überblick
|
||||
|
||||
```
|
||||
Code ändern → git push (Gitea) → ┬─ Webhook → Hermes-Receiver → SSH → Dokploy → Build + Deploy
|
||||
└─ Cron-Polling (alle 2 Min) → Dokploy → Build + Deploy
|
||||
```
|
||||
|
||||
Es gibt **zwei unabhängige Trigger**, weil der Gitea-Webhook unzuverlässig ist (siehe
|
||||
[„Warum zwei Trigger?"](#warum-zwei-trigger)):
|
||||
|
||||
| Mechanismus | Latenz | Zuverlässigkeit |
|
||||
|-------------|--------|-----------------|
|
||||
| Gitea-Webhook → Receiver | ~5 s | ⚠️ sporadisch |
|
||||
| Cron-Polling | ≤ 2 min | ✅ garantiert |
|
||||
|
||||
---
|
||||
|
||||
## Komponenten
|
||||
|
||||
### 1. Gitea-Webhook
|
||||
|
||||
Per Gitea-API erstellt:
|
||||
|
||||
```bash
|
||||
curl -X POST "https://git.kgessner.de/api/v1/repos/LuiiCode/techno-todo/hooks" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"type": "gitea",
|
||||
"config": {"url": "http://192.168.0.80:8002/hook", "content_type": "json"},
|
||||
"events": ["push"],
|
||||
"active": true
|
||||
}'
|
||||
```
|
||||
|
||||
Zeigt auf den Webhook-Receiver auf dem Hermes-Server (`192.168.0.80:8002`).
|
||||
|
||||
### 2. Webhook-Receiver (Hermes)
|
||||
|
||||
Datei: `/home/hermes/webhook-receiver.py`
|
||||
|
||||
- Python-Stdlib-`http.server` (keine Dependencies)
|
||||
- Lauscht auf `0.0.0.0:8002`
|
||||
- Bei `X-Gitea-Event: push` auf `refs/heads/main` des Ziel-Repos:
|
||||
- startet **asynchron** (Thread) `ssh root@192.168.0.184 /opt/deploy-techno-todo.sh`
|
||||
- antwortet sofort mit `200` (Gitea wartet nicht auf den Deploy)
|
||||
- Loggt in `/home/hermes/webhook-receiver.log`
|
||||
|
||||
### 3. Deploy-Skript (Dokploy-Host)
|
||||
|
||||
Datei: `/opt/deploy-techno-todo.sh`
|
||||
|
||||
**Idempotent** — deployt nur, wenn es neue Commits gibt:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
REPO_DIR="/opt/repos/techno-todo"
|
||||
SERVICE_NAME="techno-todo-v2-mzs9mj"
|
||||
cd "$REPO_DIR"
|
||||
|
||||
git fetch --all --prune >/dev/null 2>&1
|
||||
LOCAL=$(git rev-parse HEAD)
|
||||
REMOTE=$(git rev-parse origin/main)
|
||||
[ "$LOCAL" = "$REMOTE" ] && { echo "UP-TO-DATE: $LOCAL"; exit 0; }
|
||||
|
||||
git reset --hard origin/main
|
||||
COMMIT=$(git rev-parse --short HEAD)
|
||||
docker build -t "techno-todo:${COMMIT}" .
|
||||
docker tag "techno-todo:${COMMIT}" techno-todo:latest
|
||||
docker service update --force --image "techno-todo:${COMMIT}" "$SERVICE_NAME"
|
||||
echo "DONE: deployed ${COMMIT}"
|
||||
```
|
||||
|
||||
Das Repo liegt persistent unter `/opt/repos/techno-todo` auf dem Dokploy-Host.
|
||||
|
||||
### 4. Cron-Polling (Fallback)
|
||||
|
||||
Auf dem Dokploy-Host:
|
||||
|
||||
```bash
|
||||
*/2 * * * * /opt/deploy-techno-todo.sh >> /var/log/techno-todo-deploy.log 2>&1
|
||||
```
|
||||
|
||||
Da das Skript idempotent ist, passiert bei „keine neuen Commits" nichts (exit 0).
|
||||
|
||||
---
|
||||
|
||||
## Warum zwei Trigger?
|
||||
|
||||
Der Gitea-Webhook erwies sich beim POC als **unzuverlässig**: Von mehreren Pushes wurden
|
||||
nur einige ausgelöst, andere gingen verloren.
|
||||
|
||||
**Root-Cause:** Gitea läuft in einem **Kubernetes-Cluster (Talos)** auf Proxmox — also in
|
||||
einem anderen Netzwerk-Kontext als der Dokploy-Docker-Swarm-Host (`192.168.0.184`) und der
|
||||
Hermes-Server (`192.168.0.80`). Ausgehende Verbindungen aus dem K8s-Cluster zu internen
|
||||
`192.168.0.x`-Adressen sind nicht zuverlässig möglich (CNI/NAT-Isolation).
|
||||
|
||||
Der Cron-Polling-Fallback umgeht das komplett: Er läuft direkt **auf** dem Dokploy-Host und
|
||||
holt sich den Git-Stand aktiv ab, statt auf einen eingehenden Webhook zu warten.
|
||||
|
||||
---
|
||||
|
||||
## Setup-Reihenfolge (für eine neue App)
|
||||
|
||||
1. Repo persistent auf Dokploy klonen: `/opt/repos/<app>`
|
||||
2. Deploy-Skript anpassen (Repo-Dir + Service-Name) → `/opt/deploy-<app>.sh`
|
||||
3. Cron-Job auf Dokploy einrichten (alle 2 Min)
|
||||
4. (Optional) Webhook-Receiver auf Hermes starten + Gitea-Webhook einrichten
|
||||
|
||||
## Verifikation
|
||||
|
||||
```bash
|
||||
# 1. Push machen
|
||||
git push
|
||||
|
||||
# 2. Nach ~2 Min prüfen:
|
||||
ssh root@192.168.0.184 "docker service ls --format '{{.Name}} {{.Image}}' | grep techno"
|
||||
# → techno-todo-v2-mzs9mj techno-todo:<neuer-commit>
|
||||
|
||||
# 3. Live testen:
|
||||
curl -H "Host: techno-todo.192.168.0.184.sslip.io" http://192.168.0.184/
|
||||
```
|
||||
@@ -0,0 +1,134 @@
|
||||
# Dokploy-Deployment
|
||||
|
||||
Diese Anleitung beschreibt **beide** Wege, die `techno-todo`-App auf Dokploy zu deployen:
|
||||
|
||||
1. **[Der native Dokploy-Weg](#der-native-dokploy-weg)** — wie man es „richtig" über die
|
||||
Dokploy-UI/API macht (für zukünftige Apps empfohlen).
|
||||
2. **[Was tatsächlich gemacht wurde](#was-tatsächlich-gemacht-wurde)** — der POC-Verlauf
|
||||
inkl. aller Workarounds, die wegen kaputter Dokploy-Zustände nötig waren.
|
||||
|
||||
---
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Dokploy-Instanz: `https://dokploy.kgessner.de` (intern `192.168.0.184`)
|
||||
- Gitea: `https://git.kgessner.de` (läuft in einem K8s/Talos-Cluster auf Proxmox)
|
||||
- Docker-Swarm auf dem Dokploy-Host aktiv
|
||||
|
||||
---
|
||||
|
||||
## Der native Dokploy-Weg
|
||||
|
||||
### 1. Git-Provider (Gitea) verbinden
|
||||
|
||||
In der Dokploy-UI unter **Settings → Git Providers → Gitea**:
|
||||
|
||||
- **Gitea URL:** `https://git.kgessner.de`
|
||||
- **Access Token:** ein Gitea-PAT mit Scope `repo`, `repo:status`, `read:user`, `read:org`
|
||||
|
||||
> Alternativ per API:
|
||||
> ```bash
|
||||
> curl -X POST "https://dokploy.kgessner.de/api/git.provider.gitea.create" \
|
||||
> -H "x-api-key: $DOKPLOY_TOKEN" \
|
||||
> -H "Content-Type: application/json" \
|
||||
> -d '{"giteaUrl":"https://git.kgessner.de","accessToken":"<GITEA_TOKEN>"}'
|
||||
> ```
|
||||
|
||||
### 2. Projekt anlegen
|
||||
|
||||
**Projects → Create Project** → Name `techno-todo`.
|
||||
|
||||
### 3. Application anlegen (Git-Source)
|
||||
|
||||
**New Application** mit diesen Einstellungen:
|
||||
|
||||
| Feld | Wert |
|
||||
|------|------|
|
||||
| Application Name | `techno-todo` |
|
||||
| Build Type | `Dockerfile` |
|
||||
| Provider | Gitea |
|
||||
| Repository | `LuiiCode/techno-todo` |
|
||||
| Branch | `main` |
|
||||
| Dockerfile Path | `./Dockerfile` |
|
||||
| Port | `3000` |
|
||||
|
||||
### 4. (Optional) Datenbank provisionieren
|
||||
|
||||
Falls du die PostgreSQL-Variante nutzt (Branch `v1-postgres`), provisioniere eine DB:
|
||||
|
||||
**New Database → PostgreSQL** → Name `techno-db`. Dokploy erzeugt automatisch einen
|
||||
Docker-Service und stellt die Verbindungsdaten als Env-Variablen bereit.
|
||||
|
||||
> **Achtung (POC-Lesson):** Die App braucht dann `DATABASE_URL`, die auf den
|
||||
> DB-Service-Namen zeigt (z.B. `techno-db-xxxx`), **nicht** `localhost`.
|
||||
|
||||
### 5. Domain konfigurieren
|
||||
|
||||
Unter **Application → Domains**:
|
||||
|
||||
- **Host:** `techno-todo.192.168.0.184.sslip.io`
|
||||
- **Port:** `3000`
|
||||
- **HTTPS:** HTTP (für LAN-Test ausreichend)
|
||||
|
||||
Dokploy erzeugt automatisch die Traefik-Routing-Konfiguration unter
|
||||
`/etc/dokploy/traefik/dynamic/<app>.yml`.
|
||||
|
||||
### 6. Auto-Deploy aktivieren
|
||||
|
||||
Dokploy hat einen nativen Auto-Deploy: In den **Application-Einstellungen** unter
|
||||
**Deployments** den Schalter für **Automatic Deployments** aktivieren und den
|
||||
**Webhook-URL** aus der UI kopieren. Diesen URL in Gitea als Webhook eintragen:
|
||||
|
||||
**Gitea → Repo → Settings → Webhooks → Add Webhook (Gitea)**:
|
||||
|
||||
- **URL:** `<dokploy-webhook-url>` (aus Dokploy-UI)
|
||||
- **Content Type:** `application/json`
|
||||
- **Secret:** (optional)
|
||||
- **Events:** Push
|
||||
|
||||
> **Wichtig:** Der native Webhook braucht einen **gültigen Dokploy-API-Key**. Ist der Key
|
||||
> abgelaufen, funktioniert der Auto-Deploy nicht (genau das ist beim POC passiert).
|
||||
|
||||
---
|
||||
|
||||
## Was tatsächlich gemacht wurde
|
||||
|
||||
Der POC lief **nicht** sauber über den nativen Weg, weil mehrere Dokploy-Zustände kaputt
|
||||
waren. Hier der tatsächliche Verlauf:
|
||||
|
||||
### Problem 1: DB-Service existierte nicht
|
||||
|
||||
Die App (PostgreSQL-Version) crashte im Loop:
|
||||
- v1: `ECONNREFUSED ::1:5432` (zeigte fälschlich auf localhost)
|
||||
- v2: `ENOTFOUND techno-db-8u3yoi` (falscher Service-Name)
|
||||
|
||||
**Lösung:** App auf **SQLite** umgebaut (kein externer DB-Service nötig).
|
||||
|
||||
### Problem 2: Traefik-Port falsch
|
||||
|
||||
Traefik routete auf `:3000`, aber Nginx (Test-App) hört auf `:80` → `Bad Gateway`.
|
||||
|
||||
**Lösung:** Port in `/etc/dokploy/traefik/dynamic/<app>.yml` korrigiert + Traefik reload:
|
||||
```bash
|
||||
sed -i 's|:3000|:80|' /etc/dokploy/traefik/dynamic/<app>.yml
|
||||
docker kill -s HUP dokploy-traefik
|
||||
```
|
||||
|
||||
### Problem 3: Dokploy-API-Key abgelaufen
|
||||
|
||||
Alle API-Aufrufe gaben `Unauthorized` / `Invalid API key`. Der Deploy-Worker loggte
|
||||
`Error verifying API key`.
|
||||
|
||||
**Lösung:** Neuen Key aus der Dokploy-DB extrahiert (siehe TROUBLESHOOTING).
|
||||
|
||||
### Problem 4: Deploy-Worker startete keine Container
|
||||
|
||||
Deployments wurden angenommen, aber Container wurden nie gebaut/gestartet.
|
||||
|
||||
**Lösung:** Manuell per Docker gebaut + deployed (später durch die Auto-Deploy-Pipeline
|
||||
ersetzt — siehe `AUTODEPLOY.md`).
|
||||
|
||||
### Finaler Zustand
|
||||
|
||||
Der Service läuft als Docker-Swarm-Service `techno-todo-v2-mzs9mj` mit dem SQLite-Image,
|
||||
erreichbar unter `http://techno-todo.192.168.0.184.sslip.io/`.
|
||||
@@ -0,0 +1,130 @@
|
||||
# Troubleshooting — alle POC-Fehler und Lösungen
|
||||
|
||||
Chronologische Liste aller Probleme, die beim Dokploy-POC aufgetreten sind.
|
||||
|
||||
---
|
||||
|
||||
## 1. App crasht: `ECONNREFUSED ::1:5432` / `127.0.0.1:5432`
|
||||
|
||||
**Symptom:** Docker-Container startet und stirbt sofort, Log zeigt
|
||||
`Failed to init DB: AggregateError [ECONNREFUSED]`.
|
||||
|
||||
**Ursache:** Die App (PostgreSQL-Version) nutzte `localhost` als DB-Host, aber die DB
|
||||
läuft als separater Docker-Service, nicht im selben Container.
|
||||
|
||||
**Lösung:** `DATABASE_URL` auf den Service-Namen zeigen lassen, **oder** (POC-Entscheidung)
|
||||
auf SQLite umsteigen — keine externe DB nötig.
|
||||
|
||||
---
|
||||
|
||||
## 2. App crasht: `ENOTFOUND techno-db-8u3yoi`
|
||||
|
||||
**Symptom:** DNS-Lookup des DB-Hostnames schlägt fehl.
|
||||
|
||||
**Ursache:** Der `techno-db` Docker-Service existierte gar nicht — Dokploy hat ihn nie
|
||||
erstellt, obwohl die API-Provisionierung „OK" meldete.
|
||||
|
||||
**Lösung:** SQLite-Umstellung (siehe oben).
|
||||
|
||||
---
|
||||
|
||||
## 3. `Bad Gateway` trotz laufender App
|
||||
|
||||
**Symptom:** Intern läuft die App, aber über die Domain kommt `Bad Gateway`.
|
||||
|
||||
**Ursache:** Traefik routete auf Port `:3000`, die App (bzw. `nginx:alpine`) lauscht aber
|
||||
auf `:80`. Dokploy setzt standardmäßig `:3000`.
|
||||
|
||||
**Diagnose:**
|
||||
```bash
|
||||
docker exec dokploy-traefik wget -qO- http://<service>:80/ # funktioniert
|
||||
docker exec dokploy-traefik wget -qO- http://<service>:3000/ # connection refused
|
||||
```
|
||||
|
||||
**Lösung:** Traefik-Config korrigieren:
|
||||
```bash
|
||||
# Config liegt auf dem Host (Bind-Mount)
|
||||
sed -i 's|http://<service>:3000|http://<service>:80|' /etc/dokploy/traefik/dynamic/<app>.yml
|
||||
docker kill -s HUP dokploy-traefik # reload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Dokploy-API: `Unauthorized` / `Invalid API key`
|
||||
|
||||
**Symptom:** Alle API-Calls geben 401, Deploy-Worker loggt `Error verifying API key`.
|
||||
|
||||
**Ursache:** Der gespeicherte API-Key war abgelaufen/ungültig.
|
||||
|
||||
**Diagnose + Lösung:** Key direkt aus der Dokploy-Postgres-DB lesen:
|
||||
```bash
|
||||
docker exec dokploy-postgres.1.<id> psql -U dokploy -d dokploy \
|
||||
-c "SELECT name, key, start FROM apikey;"
|
||||
```
|
||||
|
||||
**Hinweis:** Der `key`-Wert in der DB ist gehasht (BetterAuth). Für neue Keys: über die
|
||||
Dokploy-UI unter **Settings → API Keys** neu erzeugen.
|
||||
|
||||
---
|
||||
|
||||
## 5. Deploy-Worker startet keine Container
|
||||
|
||||
**Symptom:** Deployment wird angenommen, aber kein Container wird gebaut/gestartet.
|
||||
Worker-Logs zeigen `service <name> not found`.
|
||||
|
||||
**Ursache:** Services wurden manuell umbenannt/gelöscht → Dokploy ist **out-of-sync** mit
|
||||
dem tatsächlichen Docker-Zustand.
|
||||
|
||||
**Lösung:** Manuell bauen + deployen (später durch die Auto-Deploy-Pipeline ersetzt):
|
||||
```bash
|
||||
cd /opt/repos/techno-todo
|
||||
docker build -t techno-todo:<commit> .
|
||||
docker service update --force --image techno-todo:<commit> <service-name>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Gitea-Webhook löst nicht zuverlässig aus
|
||||
|
||||
**Symptom:** Manche Pushes triggern den Deploy, andere nicht.
|
||||
|
||||
**Ursache:** Gitea läuft in einem **K8s/Talos-Cluster** — ausgehende Verbindungen zu
|
||||
internen `192.168.0.x`-Adressen sind unzuverlässig (CNI/NAT).
|
||||
|
||||
**Lösung:** Cron-Polling-Fallback (idempotentes Deploy-Skript alle 2 Min). Siehe
|
||||
`AUTODEPLOY.md`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Race-Condition: alter Commit wird deployed
|
||||
|
||||
**Symptom:** Webhook feuert, aber das Deploy holt noch den alten Commit.
|
||||
|
||||
**Ursache:** `git fetch` läuft, bevor Gitea den Push vollständig ins Git-Backend
|
||||
integriert hat.
|
||||
|
||||
**Lösung:** Entweder `sleep 3` vor `git fetch`, oder (besser) idempotentes Skript — der
|
||||
nächste Cron-Lauf holt den verpassten Commit automatisch nach.
|
||||
|
||||
---
|
||||
|
||||
## Nützliche Diagnose-Befehle
|
||||
|
||||
```bash
|
||||
# Docker-Swarm-Services
|
||||
docker service ls
|
||||
docker service logs --tail 50 <service>
|
||||
|
||||
# Traefik-Routing
|
||||
docker exec dokploy-traefik wget -qO- http://localhost:8080/api/http/routers
|
||||
|
||||
# Dokploy-Logs (Deploy-Worker)
|
||||
docker service logs --tail 50 dokploy 2>&1 | grep -iE 'worker|error|deploy'
|
||||
|
||||
# Dokploy-DB direkt abfragen
|
||||
docker exec dokploy-postgres.1.<id> psql -U dokploy -d dokploy -c "\dt"
|
||||
|
||||
# Token aus DB extrahieren (Gitea)
|
||||
docker exec dokploy-postgres.1.<id> psql -U dokploy -d dokploy \
|
||||
-c "SELECT access_token FROM gitea;"
|
||||
```
|
||||
Reference in New Issue
Block a user