Init repo infra + poprawki runbooka bootstrap i HANDOFF

- Krok 9 (tmux): tee -> tee -a (nie nadpisuj configu), dodano
  focus-events on, poprawiono nazwę sesji na "bootstrap"
- Krok 10: struktura katalogów odzwierciedla stan faktyczny
  (/srv i /srv/infra już istnieją, brakuje apps i data)
- Krok 12: Claude Code oznaczony jako wykonany 20-08-2026,
  instalator natywny bez Node.js
- Nowy Krok 14: audyt po bootstrapie (SSH, ufw, fail2ban,
  unattended-upgrades, swap, Docker, porty, klucze)
- HANDOFF.md: poprawiona numeracja sekcji 6
This commit is contained in:
2026-08-20 15:53:51 +00:00
commit 6ed7a7f4d2
13 changed files with 1733 additions and 0 deletions
+392
View File
@@ -0,0 +1,392 @@
# Runbook 00 — bootstrap serwera od zera
Uruchomienie świeżego Ubuntu 24.04 do stanu, w którym można stawiać usługi.
Kolejność ma znaczenie — nie przeskakuj kroków.
Czas: około 11,5 h przy pierwszym przejściu, z tłumaczeniem po drodze.
---
## Krok 0 — przed startem
- [ ] Znasz IPv4 serwera (`54.38.159.156`) i masz dostęp do panelu OVH
- [ ] Masz pod ręką **konsolę KVM w panelu OVH** — to droga ratunku, jeśli zablokujesz sobie SSH
- [ ] Wiesz, z których urządzeń będziesz się logował
Obraz OVH nie daje logowania na roota po SSH. Konto robocze to **`ubuntu`**, z `sudo`.
```bash
ssh ubuntu@54.38.159.156
cat ~/.ssh/authorized_keys # jakie klucze już są
hostnamectl # wersja systemu
df -h / # punkt odniesienia dla dysku
sudo whoami # ma zwrócić: root
```
---
## Krok 1 — podstawy systemu
```bash
sudo apt update && sudo apt upgrade -y
sudo timedatectl set-timezone Europe/Warsaw
sudo hostnamectl set-hostname dfkk
sudo apt install -y tmux curl git ufw fail2ban unattended-upgrades ca-certificates
echo "127.0.1.1 dfkk" | sudo tee -a /etc/hosts
```
---
## Krok 2 — klucze SSH ze WSZYSTKICH urządzeń
**To jest krok, którego pominięcie kończy się utratą dostępu.** Krok 3 wyłącza
logowanie hasłem. Każde urządzenie, które nie ma wtedy klucza na serwerze,
traci możliwość zalogowania się — bo hasło przestaje działać, a klucza nie ma.
Zrób to **dla każdego urządzenia osobno**: macOS, Windows, iOS.
### Generowanie klucza (lokalnie, na danym urządzeniu)
macOS / Linux:
```bash
ssh-keygen -t ed25519 -C "kacper@macbook"
```
Windows (PowerShell):
```powershell
ssh-keygen -t ed25519 -C "kacper@windows"
```
iOS: klucz generujesz w aplikacji (Termius lub Blink) i kopiujesz część publiczną.
Komentarz na końcu (`-C`) jest istotny — po nim rozpoznasz w `authorized_keys`,
który wpis usunąć, gdy zgubisz konkretny sprzęt.
### Wgranie klucza na serwer
macOS / Linux:
```bash
ssh-copy-id ubuntu@54.38.159.156
```
Windows (PowerShell — nie ma `ssh-copy-id`):
```powershell
type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh ubuntu@54.38.159.156 "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
```
### Weryfikacja — obowiązkowa
Z **każdego** urządzenia po kolei:
```bash
ssh ubuntu@54.38.159.156
```
Ma wpuścić **bez pytania o hasło**. Jeśli pyta o hasło — klucz nie działa.
Napraw to teraz. Nie przechodź do kroku 3.
Na serwerze sprawdź komplet:
```bash
cat ~/.ssh/authorized_keys
```
Powinieneś zobaczyć klucz OVH plus jeden wpis na każde urządzenie, każdy z komentarzem.
Wpisz je do `docs/SERVER.md`, sekcja 4.
### Hasło do sudo
```bash
sudo passwd ubuntu
```
> **Decyzja do podjęcia:** `sudo` z hasłem czy `NOPASSWD`?
> Z hasłem jest bezpieczniej, ale każda operacja Claude Code wymagająca uprawnień
> zatrzyma się na monicie. `NOPASSWD` jest wygodniejszy i przy logowaniu wyłącznie
> na klucz — akceptowalny, bo kto ma klucz, i tak ma pełny dostęp.
> Rekomendacja: `NOPASSWD`, ale **hasło ustawione** jako furtka przez konsolę KVM.
---
## Krok 3 — utwardzenie SSH
```bash
sudo tee /etc/ssh/sshd_config.d/99-hardening.conf > /dev/null <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AllowUsers ubuntu
X11Forwarding no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
EOF
sudo sshd -t # walidacja składni — MUSI przejść bez błędu
sudo systemctl restart ssh
```
**Znowu: nowe okno, test logowania, i dopiero potem zamknięcie starej sesji.**
`sshd -t` łapie literówki, ale nie złapie sytuacji, w której wykluczyłeś sam siebie z `AllowUsers`.
---
## Krok 4 — firewall
```bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
```
> **Do zapamiętania:** ufw **nie chroni portów publikowanych przez Dockera.**
> Docker wpisuje własne reguły do iptables wcześniej w łańcuchu. Kontener z
> `ports: "5432:5432"` jest widoczny z internetu, mimo że ufw pokazuje `deny`.
> Dlatego w tym projekcie kontenery nie publikują portów — patrz `CLAUDE.md`, 3.3.
---
## Krok 5 — fail2ban
```bash
sudo tee /etc/fail2ban/jail.local > /dev/null <<'EOF'
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
backend = systemd
[sshd]
enabled = true
EOF
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
```
---
## Krok 6 — automatyczne aktualizacje bezpieczeństwa
```bash
sudo dpkg-reconfigure -plow unattended-upgrades
```
Sprawdź, że `Unattended-Upgrade::Automatic-Reboot` jest ustawione na `"false"`.
Serwer ma się **nie restartować sam** — restart robimy świadomie, po snapshocie.
---
## Krok 7 — swap
12 GB RAM to dużo, ale swap chroni przed tym, że jeden kontener po wycieku pamięci
zabije OOM-killerem coś zupełnie innego.
```bash
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swap.conf
sudo sysctl --system
```
---
## Krok 8 — Docker
Z oficjalnego repozytorium Dockera, nie z repo Ubuntu (tam jest stara wersja).
```bash
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker ubuntu
```
Wyloguj się i zaloguj ponownie, żeby członkostwo w grupie `docker` zadziałało.
### Limity logów — to jest ten krok, którego pominięcie zapycha dysk
```bash
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"live-restore": true
}
EOF
sudo systemctl restart docker
docker run --rm hello-world
```
Bez tego log jednego gadatliwego kontenera rośnie w nieskończoność. Przy 100 GB
dysku to kwestia tygodni, nie lat.
### Sieć `edge`
```bash
docker network create edge
```
---
## Krok 9 — tmux
Dopisujemy do `~/.tmux.conf`, nie nadpisujemy — jeśli plik już istnieje z własną
konfiguracją, `tee` bez `-a` go zniszczy.
```bash
tee -a ~/.tmux.conf > /dev/null <<'EOF'
set -g mouse on
set -g history-limit 50000
set -g base-index 1
setw -g mode-keys vi
set -g status-bg colour236
set -g status-fg colour252
set -g focus-events on
EOF
tmux source-file ~/.tmux.conf # przeładowanie, jeśli tmux już działa
```
Od tego momentu praca zaczyna się od `tmux new -s bootstrap` albo `tmux attach -t bootstrap`.
---
## Krok 10 — struktura katalogów
`/srv` i `/srv/infra` już istnieją — repo `infra` zostało wgrane przez `scp` przed
bootstrapem. Brakuje `/srv/apps` i `/srv/data`.
```bash
sudo mkdir -p /srv/apps /srv/data
sudo chown -R ubuntu:ubuntu /srv
```
---
## Krok 11 — snapshot
**Zrób snapshot w panelu OVH teraz.** Masz czysty, utwardzony system bez usług —
to najlepszy punkt powrotu, jaki będziesz miał. Zapisz datę w `SERVER.md`.
---
## Krok 12 — Claude Code
**Wykonane 20-08-2026.** Instalacja natywnym instalatorem, bez Node.js:
```bash
curl -fsSL https://claude.ai/install.sh | bash
```
Binarka ląduje w `~/.local/bin/claude`. Sesje odpalane **zawsze w tmuxie**,
z katalogu `/srv/infra`, żeby od razu widział `CLAUDE.md`.
---
## Krok 13 — Traefik, Gitea, monitoring
Osobne stacki, stawiane w tej kolejności:
1. **Traefik** — bez niego reszta nie ma jak wyjść na świat. Wymaga kluczy API OVH
do DNS-01. Weryfikacja: certyfikat wildcard dla `*.dfkk.cloud` się wystawia.
2. **Gitea** na `git.dfkk.cloud` — **rejestracja wyłączona natychmiast po pierwszym
logowaniu**. Potem push mirror repo `infra` na prywatne repo GitHuba.
3. **Monitoring** (Uptime Kuma) na `status.dfkk.cloud` za basic auth, alerty na maila.
Każdy stack: katalog w `/srv/infra/stacks/`, `compose.yaml` w repo, `.env` poza repo,
wpis w `SERVER.md`.
---
## Krok 14 — audyt po bootstrapie
Konfiguracja bez weryfikacji to założenie, nie zabezpieczenie. Literówka w
`sshd_config` albo reguła ufw, która nie weszła, wyglądają identycznie jak sukces.
**Każdy punkt, który nie przejdzie, blokuje przejście dalej.**
### Na serwerze
```bash
sudo sshd -T | grep -E "permitrootlogin|passwordauthentication|pubkeyauthentication"
# oczekiwane: no / no / yes
sudo ufw status verbose
# tylko 22/80/443, default deny incoming
sudo fail2ban-client status sshd
# jail aktywny
systemctl is-enabled unattended-upgrades
# enabled
grep Automatic-Reboot /etc/apt/apt.conf.d/50unattended-upgrades
# "false"
sudo swapon --show
# swapfile 2G aktywny
cat /etc/docker/daemon.json
# limity logów obecne
docker ps --format "table {{.Names}}\t{{.Ports}}"
# nic na 0.0.0.0
ss -tulpn | grep LISTEN
# każdy nasłuch uzasadniony, porównaj z SERVER.md sekcja 5
cat ~/.ssh/authorized_keys
# tylko znane klucze, każdy z komentarzem identyfikującym urządzenie
```
### Do wykonania przez Kacpra, z jego urządzeń (Claude nie ma do nich dostępu)
- `ssh ubuntu@54.38.159.156` z macOS, Windows i iOS → wchodzi bez pytania o hasło
- test, że baza nie jest wystawiona — na Windowsie w PowerShellu:
```powershell
Test-NetConnection -ComputerName 54.38.159.156 -Port 5432
# oczekiwane: TcpTestSucceeded : False
```
(Windows nie ma netcata — nie używamy tu `nc -zv`.)
### Po audycie
Zaktualizować `SERVER.md` sekcje 4, 5 i 10, zrobić commit.
---
## Po zakończeniu
- [ ] `SERVER.md` uzupełniony: IP, lokalizacja, klucze, data snapshota
- [ ] Repo `infra` zainicjowane i zacommitowane
- [ ] Mirror na GitHub skonfigurowany
- [ ] Test: rozłączenie SSH w trakcie działania procesu w tmux — proces przeżywa
- [ ] Test: logowanie z każdego z trzech urządzeń
- [ ] Snapshot po postawieniu Traefika i Gitei
+138
View File
@@ -0,0 +1,138 @@
# Runbook 10 — nowa usługa
Od pustego katalogu do działającej aplikacji na własnej subdomenie z HTTPS.
---
## 1. Ustalenia przed startem
| Pytanie | Po co |
|---|---|
| Nazwa projektu (małe litery, myślnik) | katalog, kontenery, sieć, subdomena |
| Publiczna czy wewnętrzna? | czy w ogóle wchodzi do sieci `edge` |
| Baza danych? Jaka? | osobny kontener w sieci wewnętrznej + wpis do backupu |
| Dane trwałe? Gdzie? | bind-mount w `/srv/data/<projekt>` |
| Wysyłka maili? | zewnętrzny SMTP, nigdy lokalny |
| Szacowane zużycie dysku | 100 GB to nasz limit |
---
## 2. Struktura
```bash
mkdir -p /srv/apps/<projekt>
mkdir -p /srv/data/<projekt>
cd /srv/apps/<projekt>
git init
```
Minimum plików: `compose.yaml`, `.env.example`, `.gitignore` (z `.env`), `README.md`.
---
## 3. Wzorzec compose
```yaml
services:
web:
image: <obraz>:<KONKRETNA-WERSJA> # nigdy :latest
container_name: <projekt>-web
restart: unless-stopped
env_file: .env
networks:
- edge
- internal
volumes:
- /srv/data/<projekt>/web:/app/data
labels:
- "traefik.enable=true"
- "traefik.http.routers.<projekt>.rule=Host(`<projekt>.dfkk.cloud`)"
- "traefik.http.routers.<projekt>.entrypoints=websecure"
- "traefik.http.routers.<projekt>.tls.certresolver=ovh"
- "traefik.http.services.<projekt>.loadbalancer.server.port=<PORT-WEWNĘTRZNY>"
db:
image: postgres:16.4
container_name: <projekt>-db
restart: unless-stopped
env_file: .env
networks:
- internal # BEZ edge — baza nie widzi świata
volumes:
- /srv/data/<projekt>/db:/var/lib/postgresql/data
networks:
edge:
external: true
internal:
name: <projekt>-internal
```
**Trzy rzeczy, na które patrzysz w tym pliku:**
1. Brak sekcji `ports:` — ruch wchodzi wyłącznie przez Traefika.
Opublikowany port omija ufw i jest widoczny z internetu.
2. `db` jest tylko w `internal`. Kontener w sieci `edge` jest osiągalny
dla każdej innej usługi w `edge` — baza nie ma tam czego szukać.
3. Wersja obrazu przypięta. `:latest` oznacza, że przyszły `pull` może
podmienić działającą aplikację na wersję z breaking changes.
---
## 4. Sekrety
```bash
cp .env.example .env
chmod 600 .env
openssl rand -base64 32 # tak generujemy hasła, nie wymyślamy ich
```
Sprawdź, że `.env` jest w `.gitignore`, **zanim** zrobisz pierwszy commit.
Sekret raz wypchnięty do repo trzeba unieważnić, nie usunąć.
---
## 5. DNS
Rekord A dla `<projekt>.dfkk.cloud` na IP serwera — panel OVH.
Certyfikat leci z wildcardu `*.dfkk.cloud`, więc **nic dodatkowo nie robisz**.
Szczegóły: `20-domena.md`.
---
## 6. Uruchomienie
```bash
docker compose config # walidacja składni i podstawionych zmiennych
docker compose up -d
docker compose ps
docker compose logs -f --tail=50
```
---
## 7. Weryfikacja — sprawdzasz, nie zakładasz
```bash
curl -I https://<projekt>.dfkk.cloud # 200 lub 3xx, certyfikat ważny
docker compose ps # wszystko "Up", nic w restart loop
docker stats --no-stream # zużycie w granicach rozsądku
```
Test negatywny — potwierdzenie, że baza **nie** jest wystawiona:
```bash
# z lokalnej maszyny, nie z serwera:
nc -zv <IP-serwera> 5432 # ma odmówić połączenia
```
---
## 8. Domknięcie
- [ ] Commit w repo projektu, push do Gitei
- [ ] Wpis w `SERVER.md`: projekt, katalog, repo, domena, wolumeny, baza
- [ ] Wpis w `SERVER.md`, sekcja 2: zajęta subdomena
- [ ] Jeśli są dane trwałe — dopisanie ścieżki do listy backupu
- [ ] Monitor w Uptime Kuma
- [ ] `df -h` — kontrola, ile dysku ubyło
+101
View File
@@ -0,0 +1,101 @@
# Runbook 20 — domeny i DNS
Wszystkie trzy domeny są w OVH, DNS też w OVH. To upraszcza sprawę: Traefik może
używać API OVH do wyzwania **DNS-01**, więc dostajemy certyfikaty wildcard.
---
## 1. Dlaczego DNS-01, a nie HTTP-01
| | HTTP-01 | DNS-01 (nasz wybór) |
|---|---|---|
| Jak działa | Let's Encrypt puka na port 80 | Traefik wpisuje rekord TXT przez API |
| Wildcard | niemożliwy | możliwy |
| Nowa subdomena | osobny certyfikat, osobne wyzwanie | działa od razu, zero akcji |
| Usługi niepubliczne | nie da się | da się |
| Wymaga | otwartego portu 80 | kluczy API dostawcy DNS |
Przy wildcardzie `*.dfkk.cloud` dodanie `nowyprojekt.dfkk.cloud` to jeden rekord A
i restart kontenera. Certyfikatem nie zajmujesz się w ogóle.
---
## 2. Klucze API OVH — jednorazowo
1. Wejdź na stronę tworzenia tokenów API OVH dla właściwego regionu konta
(uwaga: konto jest w regionie **CA** — patrz adres panelu w `SERVER.md`,
to zmienia zarówno adres, pod którym generujesz token, jak i endpoint w konfiguracji)
2. Uprawnienia zawężone do stref DNS — `GET`, `POST`, `PUT`, `DELETE`
na `/domain/zone/*`. Nie dawaj tokenowi pełnego dostępu do konta.
3. Ważność: bez limitu, ale **wpisz token do `SERVER.md`, sekcja 9**, żeby wiadomo było,
co unieważnić przy incydencie
4. Trzy wartości — application key, application secret, consumer key —
lądują w `/srv/infra/stacks/traefik/.env`, `chmod 600`, poza repo
---
## 3. Nowa subdomena
```
Typ: A
Nazwa: <projekt>
Cel: <IP serwera>
TTL: 300 (na czas testów; potem można podnieść)
```
Sprawdzenie propagacji:
```bash
dig +short <projekt>.dfkk.cloud
```
Dopóki `dig` nie zwraca IP serwera, nie ma sensu debugować Traefika —
problem jest w DNS, nie w kontenerze.
---
## 4. Nowa domena (druga lub trzecia)
Wildcard obejmuje **tylko** `*.dfkk.cloud`. Kolejna domena wymaga:
1. Rekordu A na IP serwera
2. Dopisania nowej domeny do konfiguracji certresolvera w Traefiku
(osobny wpis `domains` z `main` i `sans`)
3. Sprawdzenia, że token API OVH ma uprawnienia **także do tej strefy**
4. Wpisu w `SERVER.md`, sekcja 2
---
## 5. Odpinanie domeny
Kolejność odwrotna niż przy podpinaniu:
- [ ] Usuń labele Traefika z compose i zrestartuj usługę
- [ ] Usuń rekord A w panelu OVH
- [ ] Sprawdź, że nie ma innych rekordów wskazujących na serwer (CNAME, MX)
- [ ] Wykreśl subdomenę z `SERVER.md`, sekcja 2
- [ ] Usuń monitor z Uptime Kuma
Martwy rekord A wskazujący na Twój serwer to nie tylko bałagan — jeśli kiedyś oddasz
to IP, ktoś inny odziedziczy ruch kierowany na Twoją nazwę.
---
## 6. Diagnostyka
| Objaw | Gdzie szukać |
|---|---|
| `dig` nie zwraca IP | DNS, panel OVH — nie ruszaj serwera |
| DNS ok, ale 404 z Traefika | literówka w `Host()` albo brak `traefik.enable=true` |
| DNS ok, ale połączenie odrzucone | kontener nie działa lub nie jest w sieci `edge` |
| Błąd certyfikatu | logi Traefika, klucze API OVH, uprawnienia do strefy |
| Certyfikat „fake" / staging | w konfiguracji został adres staging Let's Encrypt |
```bash
docker logs traefik --tail=100 | grep -i -E "acme|error|certificate"
```
> **Uwaga na limity Let's Encrypt.** Przy debugowaniu certyfikatów łatwo wpaść
> w limit żądań i zostać zablokowanym na tydzień. Jeśli certyfikat nie chce się
> wystawić — przełącz się na serwer **staging** Let's Encrypt, dopracuj konfigurację
> tam, i dopiero potem wróć na produkcyjny.
+138
View File
@@ -0,0 +1,138 @@
# Runbook 30 — usuwanie projektu
Najważniejszy runbook dla utrzymania porządku. Usunięcie projektu to **nie** jest
`docker compose down`. Osierocone wolumeny, martwe rekordy DNS i wyłączone kontenery
zajmujące gigabajty to dokładnie ten bałagan, którego ten serwer ma nie mieć.
**Przechodzisz całą listę. Za każdym razem.**
---
## Krok 0 — zabezpieczenie danych
Zanim cokolwiek skasujesz:
```bash
cd /srv/apps/<projekt>
docker compose ps # co faktycznie działa
docker compose config # jakie wolumeny są w grze
du -sh /srv/data/<projekt> # ile tam danych
```
**Baza danych — zrzut przed usunięciem, zawsze:**
```bash
docker compose exec db pg_dump -U <user> <baza> > ~/<projekt>-final-$(date +%F).sql
```
Trzymaj zrzut poza `/srv/data/<projekt>` — inaczej skasujesz go razem z resztą.
Rozważ przeniesienie na lokalną maszynę. „Na pewno już niepotrzebne" bywa
weryfikowane dwa miesiące później.
> **Punkt zatrzymania:** potwierdź z Kacprem, że dane są zbędne albo zabezpieczone.
> To operacja nieodwracalna.
---
## Krok 1 — zatrzymanie
```bash
docker compose down # BEZ -v na tym etapie
docker compose ps -a
```
`-v` kasuje wolumeny natychmiast. Zostawiamy je do kroku 4, żeby był margines na refleksję.
---
## Krok 2 — DNS i Traefik
- [ ] Usuń rekord A w panelu OVH
- [ ] `dig +short <projekt>.dfkk.cloud` — ma nie zwracać nic
- [ ] Sprawdź, że żadna inna usługa nie używa tej subdomeny
---
## Krok 3 — monitoring
- [ ] Usuń monitor z Uptime Kuma (inaczej dostaniesz alert o „awarii" usługi,
którą sam skasowałeś)
- [ ] Usuń ścieżki projektu z listy backupu
---
## Krok 4 — dane i wolumeny
```bash
docker volume ls | grep <projekt>
docker volume rm <nazwa> # świadomie, po jednym
sudo rm -rf /srv/data/<projekt>
```
> **Punkt zatrzymania:** `rm -rf` na katalogu z danymi wymaga wyraźnej zgody.
> Przeczytaj ścieżkę na głos przed naciśnięciem Enter.
---
## Krok 5 — obrazy i sieci
```bash
docker images | grep <projekt>
docker rmi <obraz> # tylko jeśli nic innego go nie używa
docker network rm <projekt>-internal
```
---
## Krok 6 — kod
```bash
sudo rm -rf /srv/apps/<projekt>
```
Repo w Gitei: **archiwizuj, nie kasuj.** Archiwum jest tanie, kod czasem wraca.
Kasowanie repozytorium ma sens tylko wtedy, gdy trafiły do niego sekrety —
a wtedy i tak trzeba je najpierw unieważnić.
---
## Krok 7 — dokumentacja
- [ ] Wykreśl projekt z `SERVER.md`, sekcja 7
- [ ] Wykreśl subdomenę z `SERVER.md`, sekcja 2
- [ ] Usuń zadania cykliczne z sekcji 8, jeśli jakieś były
- [ ] Wpis w dzienniku zmian, sekcja 10
- [ ] Commit w repo `infra`
---
## Krok 8 — kontrola
```bash
df -h / # miejsce faktycznie wróciło?
docker system df # ile zajmują obrazy, kontenery, wolumeny
docker ps -a # brak śladów po projekcie
docker volume ls -f dangling=true
```
Jeśli miejsce **nie** wróciło — coś zostało. Szukaj, zanim uznasz sprawę za zamkniętą.
---
## Sprzątanie osieroconych zasobów
`docker system prune` bywa kuszący. Zanim go użyjesz — sprawdź, co usunie:
```bash
docker volume ls -f dangling=true # wolumeny bez kontenera
docker images -f dangling=true # warstwy bez tagu
```
**`docker system prune -a --volumes` potrafi skasować wolumen zatrzymanej usługi,
która wcale nie miała zniknąć.** Kasujemy pojedynczo, po sprawdzeniu każdej pozycji.
Bezpieczna wersja, bez dotykania wolumenów:
```bash
docker image prune -a # tylko nieużywane obrazy
docker builder prune # cache budowania — zwykle największy zysk
```
+121
View File
@@ -0,0 +1,121 @@
# Runbook 40 — utrzymanie
Serwer psuje się powoli i po cichu. Te przeglądy zajmują kilka minut i zapobiegają
sytuacji, w której o problemie dowiadujesz się od użytkownika.
---
## Przegląd tygodniowy — 5 minut
```bash
df -h / # < 75%
docker system df # co rośnie
docker ps -a # nic w restart loop
free -h # swap w użyciu? coś przecieka
uptime # load average
sudo fail2ban-client status sshd
```
Czerwone flagi:
| Objaw | Co znaczy |
|---|---|
| Dysk > 75% | czas na sprzątanie, nie za tydzień |
| Kontener z rosnącym `RESTARTS` | crash loop — sprawdź logi |
| Swap mocno zajęty przy 12 GB RAM | wyciek pamięci w którymś kontenerze |
| Skok liczby banów fail2ban | ktoś się interesuje maszyną |
---
## Przegląd miesięczny — 30 minut
### Aktualizacje systemu
```bash
sudo apt update
apt list --upgradable
```
Zanim `upgrade`: **snapshot OVH.** Potem:
```bash
sudo apt upgrade -y
ls /var/run/reboot-required 2>/dev/null && echo "wymagany restart"
```
Restart robimy świadomie, po snapshocie, nie w środku pracy.
### Aktualizacje obrazów
Obrazy są przypięte do wersji, więc `pull` sam nic nie podniesie — i o to chodzi.
Podnoszenie wersji to świadoma decyzja:
1. Sprawdź changelog obrazu pod kątem breaking changes
2. Podnieś tag w `compose.yaml`
3. Snapshot
4. `docker compose pull && docker compose up -d`
5. Weryfikacja: `curl -I`, logi, `docker compose ps`
6. Commit
Jedna usługa naraz. Aktualizacja pięciu rzeczy jednocześnie oznacza, że przy awarii
nie wiesz, która zawiniła.
### Sprzątanie dysku
```bash
docker system df
docker image prune -a # bezpieczne
docker builder prune # zwykle najwięcej odzyskuje
sudo journalctl --disk-usage
sudo journalctl --vacuum-time=30d
```
Wolumeny — **nigdy hurtem**:
```bash
docker volume ls -f dangling=true
# sprawdź każdy, potem docker volume rm <nazwa>
```
### Bezpieczeństwo
```bash
sudo journalctl -u ssh --since "1 month ago" | grep -i "accepted" | tail -30
sudo cat /home/ubuntu/.ssh/authorized_keys # tylko znane klucze?
sudo ufw status verbose # tylko 22/80/443?
docker ps --format "table {{.Names}}\t{{.Ports}}" # nic na 0.0.0.0?
```
Ostatnia komenda jest ważniejsza, niż wygląda. Opublikowany port kontenera na
`0.0.0.0` omija ufw i jest widoczny z internetu — a `ufw status` tego nie pokaże.
### Certyfikaty
```bash
echo | openssl s_client -connect git.dfkk.cloud:443 2>/dev/null | \
openssl x509 -noout -dates
```
Odnowienie następuje automatycznie ~30 dni przed wygaśnięciem. Jeśli zostało
mniej niż 14 dni — coś się zacięło, sprawdź logi Traefika i klucze API OVH.
### Dokumentacja
- [ ] `SERVER.md` zgodny ze stanem faktycznym?
- [ ] Lista braków w `SECURITY.md`, sekcja 8 — coś do odhaczenia?
- [ ] Repo `infra` zacommitowane i zmirrorowane na GitHub?
---
## Przegląd kwartalny
- [ ] **Test odtworzenia z backupu.** Nie „sprawdzenie, że plik istnieje" —
faktyczne przywrócenie danych i weryfikacja, że są kompletne.
Backup, którego nie odtworzyłeś, to założenie, nie zabezpieczenie.
- [ ] Rotacja tokenów API (OVH, GitHub, SMTP)
- [ ] Przegląd kluczy SSH — czy wszystkie urządzenia z listy nadal istnieją i są Twoje
- [ ] Przegląd projektów: czy wszystko, co działa, jest jeszcze potrzebne?
Nieużywana usługa to zajęty dysk, otwarta powierzchnia ataku i pakiety,
których nikt nie aktualizuje.
- [ ] Weryfikacja, czy `CLAUDE.md` odpowiada temu, jak faktycznie pracujemy —
jeśli zasada jest regularnie omijana, trzeba ją zmienić albo zacząć stosować
+145
View File
@@ -0,0 +1,145 @@
# Runbook 50 — awaria i rollback
Do czytania, gdy coś nie działa. Zasada nadrzędna: **najpierw diagnoza, potem zmiany.**
Chaotyczne restartowanie wszystkiego zaciera ślady i zamienia jeden problem w trzy.
---
## Kolejność diagnozy
Idź od zewnątrz do środka. Zatrzymaj się na pierwszym kroku, który zawiedzie —
tam jest problem, dalej nie ma sensu szukać.
```bash
# 1. Czy maszyna żyje?
ping <IP>
ssh ubuntu@<IP>
# 2. Czy nie brakuje zasobów? (najczęstsza przyczyna)
df -h /
free -h
uptime
# 3. Czy Docker działa?
sudo systemctl status docker
docker ps
# 4. Czy Traefik żyje?
docker logs traefik --tail=50
# 5. Czy konkretna usługa żyje?
cd /srv/apps/<projekt>
docker compose ps
docker compose logs --tail=100
```
---
## Typowe przyczyny, w kolejności prawdopodobieństwa
| Objaw | Sprawdź najpierw |
|---|---|
| Wszystko przestało działać naraz | `df -h` — pełny dysk zatrzymuje wszystko |
| Jedna usługa w restart loop | `docker compose logs` — zwykle brak zmiennej w `.env` lub zajęty port |
| 502 z Traefika | kontener działa, ale zły port w labelu `loadbalancer.server.port` |
| 404 z Traefika | zła reguła `Host()` albo brak `traefik.enable=true` |
| Błąd certyfikatu | logi Traefika, klucze API OVH, limity Let's Encrypt |
| Usługa wolna, serwer obciążony | `docker stats` — który kontener zjada CPU/RAM |
| Kontener zabity bez śladu | OOM killer: `dmesg -T \| grep -i oom` |
---
## Pełny dysk — najczęstsza awaria tej maszyny
Przy 100 GB to realne ryzyko. Objawy bywają mylące: bazy przestają zapisywać,
Docker nie startuje kontenerów, logi się urywają.
```bash
df -h /
sudo du -h --max-depth=1 /var | sort -hr | head
sudo du -h --max-depth=1 /srv | sort -hr | head
docker system df
```
Szybkie odzyskanie miejsca, od najbezpieczniejszego:
```bash
sudo journalctl --vacuum-size=200M
docker builder prune -f
docker image prune -a -f
```
Dopiero potem szukaj przyczyny — zwykle jest to jeden kontener logujący bez limitu
albo rosnąca baza. Limity logów są w `/etc/docker/daemon.json`.
---
## Rollback
### Poziom 1 — konfiguracja usługi
```bash
cd /srv/apps/<projekt>
git log --oneline -10
git revert <commit>
docker compose up -d
```
### Poziom 2 — wersja obrazu
Cofnij tag w `compose.yaml` do poprzedniej działającej wersji, `docker compose up -d`.
Dlatego przypinamy wersje — z `:latest` nie masz do czego wrócić.
### Poziom 3 — snapshot OVH
Przywrócenie snapshota **cofa całą maszynę** do stanu z chwili jego zrobienia.
Wszystko, co powstało później — inne projekty, dane, commity — znika.
Zanim to zrobisz:
- [ ] Czy problem faktycznie wymaga cofnięcia całej maszyny?
- [ ] Co powstało od czasu snapshota i czego nieodwracalnie stracisz?
- [ ] Czy dasz radę wyciągnąć potrzebne dane przed przywróceniem?
- [ ] Data snapshota — jest w `SERVER.md`
To ostateczność. W większości przypadków szybciej jest naprawić usługę.
---
## Odzyskanie dostępu po zablokowaniu SSH
Jeśli błąd w `sshd_config` albo ufw odciął Cię od maszyny:
1. Panel OVH → Twój VPS → **konsola KVM**
2. Logowanie jako `root` lub `ubuntu` (dlatego `ubuntu` ma ustawione hasło —
przez konsolę klucz SSH nie pomoże)
3. Napraw konfigurację, `sudo sshd -t`, restart usługi
4. Test z zewnątrz **przed** zamknięciem konsoli
To jest powód, dla którego przy każdej zmianie w SSH trzymamy otwartą drugą sesję.
---
## Utrata Gitei
Gitea stoi na tym samym serwerze, którym zarządza — więc jej awaria zabiera kod
i konfigurację naraz. Dlatego:
- repo `infra` jest zmirrorowane na prywatne repo GitHuba
- na maszynie zawsze jest aktualna kopia robocza `/srv/infra`
- odtworzenie: sklonuj `infra` z GitHuba, postaw Traefika i Gitea z `00-bootstrap.md`,
krok 13, przywróć dane repozytoriów z backupu
Jeśli mirror na GitHub nie jest jeszcze skonfigurowany — to najpilniejsza rzecz
z listy braków w `SECURITY.md`.
---
## Po każdej awarii
- [ ] Wpis w dzienniku zmian w `SERVER.md`: co się stało, co pomogło
- [ ] Jeśli przyczyna była systemowa — dopisz punkt kontrolny do `40-utrzymanie.md`
- [ ] Jeśli dało się temu zapobiec regułą — dopisz ją do `CLAUDE.md`
Runbooki mają rosnąć razem z doświadczeniem. Awaria, z której nic nie wynikło
dla dokumentacji, powtórzy się.