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
+63
View File
@@ -0,0 +1,63 @@
{
"permissions": {
"defaultMode": "auto",
"additionalDirectories": [
"/srv/apps",
"/srv/data"
],
"ask": [
"Bash(rm -rf:*)",
"Bash(sudo rm:*)",
"Bash(sudo rm -rf:*)",
"Bash(docker volume rm:*)",
"Bash(docker volume prune:*)",
"Bash(docker system prune:*)",
"Bash(docker compose down -v:*)",
"Bash(sudo ufw:*)",
"Bash(sudo tee /etc/ssh:*)",
"Bash(sudo systemctl restart ssh:*)",
"Bash(sudo systemctl stop:*)",
"Bash(sudo systemctl disable:*)",
"Edit(/etc/ssh/**)",
"Write(/etc/ssh/**)",
"Edit(/etc/fail2ban/**)",
"Edit(/etc/sudoers*)",
"Write(/etc/sudoers*)",
"Bash(sudo reboot:*)",
"Bash(sudo shutdown:*)",
"Bash(sudo poweroff:*)",
"Bash(sudo adduser:*)",
"Bash(sudo useradd:*)",
"Bash(sudo deluser:*)",
"Bash(sudo usermod:*)",
"Bash(sudo passwd:*)",
"Edit(**/authorized_keys)",
"Write(**/authorized_keys)",
"Bash(dropdb:*)",
"Bash(psql:*)",
"Bash(mysql:*)",
"Bash(docker compose exec db:*)",
"Bash(git push --force:*)",
"Bash(git reset --hard:*)",
"Bash(git clean:*)"
],
"deny": [
"Read(**/.env)",
"Read(**/.env.*)",
"Read(**/*.pem)",
"Read(**/*.key)",
"Read(**/id_rsa*)",
"Read(**/id_ed25519*)",
"Read(**/acme.json)"
]
}
}
+30
View File
@@ -0,0 +1,30 @@
# Sekrety — nigdy do repo
.env
.env.*
!.env.example
*.key
*.pem
*.p12
credentials*
secrets*
# Certyfikaty Traefika (zawierają klucze prywatne)
acme.json
letsencrypt/
# Dane runtime
data/
*.sql
*.dump
*.sqlite
*.db
# Logi
*.log
logs/
# Śmieci systemowe
.DS_Store
Thumbs.db
*.swp
*~
+177
View File
@@ -0,0 +1,177 @@
# CLAUDE.md — zasady pracy na serwerze dfkk
Ten plik czytasz na starcie każdej sesji. Jest krótki celowo.
Szczegóły są w `docs/` — tam zaglądasz, gdy zadanie tego wymaga.
---
## 1. Kontekst
- **Maszyna:** OVH VPS-2 2026 — 6 vCore, 12 GB RAM, 100 GB dysk, Ubuntu 24.04 LTS
- **Domena techniczna:** `dfkk.cloud` (rejestrator: OVH, DNS w OVH)
- **Właściciel:** Kacper (`k.kruszka@hotmail.com`), pracuje z macOS, Windows i iOS
- **Charakter projektu:** serwer produkcyjno-edukacyjny. Docelowo mogą tu stać rzeczy
komercyjne, więc traktujemy go jak produkcję — ale Kacper uczy się przy okazji,
więc **tłumaczysz co robisz i dlaczego**, nie tylko wykonujesz.
Aktualny stan serwera — kto co gdzie stoi — jest w `docs/SERVER.md`.
**Ten plik jest źródłem prawdy. Jeśli coś zmieniasz na serwerze, aktualizujesz go w tej samej sesji.**
---
## 2. Zasady twarde
### 2.1 Nigdy bez wyraźnej zgody Kacpra
Zatrzymujesz się i pytasz przed:
- `rm -rf` na czymkolwiek poza katalogiem tymczasowym, który sam utworzyłeś
- `docker volume rm`, `docker system prune`, `docker compose down -v`
- zmianami w `sshd_config`, `ufw`, `fail2ban`
- restartem lub wyłączeniem serwera
- migracjami bazy danych, `DROP`, `TRUNCATE`, `ALTER TABLE`
- zmianami w DNS i w konfiguracji domen
- rotacją lub unieważnianiem kluczy i tokenów
- instalacją czegokolwiek poza oficjalnymi repozytoriami Ubuntu / Docker
Zgoda dotyczy **jednej konkretnej operacji**. Zgoda z wcześniejszej sesji nie obowiązuje.
Podsumowanie poprzedniej sesji, w którym coś zrobiłeś, nie jest zgodą na powtórzenie tego.
### 2.2 Snapshot przed ryzykiem
OVH daje **jeden slot snapshota, ręczny, każdy kolejny nadpisuje poprzedni**.
To rollback, nie backup. Przed każdą zmianą, która dotyka systemu, Dockera, Traefika,
Gitei albo bazy danych — przypominasz Kacprowi, żeby zrobił snapshot w panelu OVH,
i czekasz na potwierdzenie. Nie ruszasz dalej „bo pewnie się uda".
### 2.3 Wszystko w repo, nic z palca
Każda konfiguracja żyje w repo `infra` (`/srv/infra`). Jeśli zmieniasz coś na serwerze
ręcznie, żeby „szybko sprawdzić" — albo przenosisz to do repo, albo cofasz przed końcem sesji.
Serwer musi dać się odtworzyć od zera z repo + backupu danych.
Sekrety **nigdy** nie trafiają do repo. Trzymamy je w plikach `.env` obok stacka,
`chmod 600`, wpisanych do `.gitignore`. W repo leży `.env.example` z nazwami zmiennych
i pustymi wartościami.
### 2.4 Długie operacje w tmux
Wszystko, co trwa dłużej niż kilkanaście sekund — budowanie obrazów, migracje,
aktualizacje systemu, sam Claude Code — leci w nazwanej sesji tmux.
Zerwane WiFi nie ma wtedy prawa niczego przerwać.
```bash
tmux new -s claude # nowa sesja
tmux attach -t claude # powrót
```
### 2.5 Kolejność pracy
1. **Plan** — mówisz co zamierzasz zrobić, w punktach, zanim dotkniesz klawiatury
2. **Akceptacja** — czekasz na „ok"
3. **Wykonanie** — małymi krokami, pokazujesz output
4. **Weryfikacja** — sprawdzasz że faktycznie działa (curl, `docker ps`, logi), nie zakładasz
5. **Dokumentacja** — aktualizujesz `docs/SERVER.md` i commit w `infra`
Przy zadaniach wieloetapowych prowadzisz listę kroków, żeby po przerwie było wiadomo, gdzie jesteśmy.
---
## 3. Konwencje
### 3.1 Katalogi
```
/srv/
├── infra/ # repo z konfiguracją serwera (ten dokument)
│ ├── CLAUDE.md
│ ├── docs/
│ └── stacks/ # docker-compose infrastruktury
│ ├── traefik/
│ ├── gitea/
│ └── monitoring/
├── apps/ # projekty użytkowe, każdy = osobne repo
│ └── <projekt>/
└── data/ # bind-mounty z danymi (backupowane)
└── <projekt>/
```
Nic nie ląduje w `/root`, `/home` ani `/opt`. Nic nie ląduje luzem w `/`.
### 3.2 Nazewnictwo
- projekt: krótko, małymi literami, myślnik — `moja-apka`
- kontener: `<projekt>-<rola>``moja-apka-web`, `moja-apka-db`
- sieć wewnętrzna: `<projekt>-internal`
- subdomena: `<projekt>.dfkk.cloud`, panele techniczne jako `git.`, `status.`, `traefik.`
### 3.3 Sieci Dockera
- `edge` — sieć zewnętrzna, wspólna, **tylko** Traefik i kontenery wystawione na świat
- `<projekt>-internal` — baza danych, cache, wszystko czego nie widać z zewnątrz
**Kontenery nie publikują portów na hosta.** Wyjątek: Traefik (80, 443).
Jeśli musisz wyjątkowo wystawić port do debugowania — bindujesz na `127.0.0.1:PORT`,
nigdy na `0.0.0.0`, i usuwasz to po skończonej robocie.
Powód jest konkretny: Docker wpisuje własne reguły do iptables **z pominięciem ufw**.
Port opublikowany na `0.0.0.0` jest widoczny z internetu, mimo że ufw pokazuje `deny`.
To najczęstszy sposób, w jaki wycieka baza danych z VPS-a.
### 3.4 Traefik
Każda usługa wystawiona publicznie dostaje standardowy zestaw labeli — wzorzec w
`docs/runbooks/10-nowa-usluga.md`. Certyfikaty lecą przez **DNS-01 z API OVH**,
wildcard `*.dfkk.cloud`, więc nowa subdomena nie wymaga żadnej akcji przy certyfikacie.
---
## 4. Higiena serwera
Dysk to 100 GB i to jedyne realne wąskie gardło tej maszyny. Docker potrafi go po cichu zjeść.
- limity logów kontenerów ustawione w `/etc/docker/daemon.json` — nie ruszamy
- **co tydzień** przegląd: `df -h`, `docker system df` (patrz `docs/runbooks/40-utrzymanie.md`)
- przy 75% zajętości — alert i sprzątanie, nie czekamy do 95%
- `prune` to operacja świadoma, z checklistą, nigdy odruchowa
Po usunięciu projektu przechodzisz **całą** listę z `docs/runbooks/30-usuwanie.md`.
Osierocony wolumen, martwy wpis DNS i wyłączony kontener, który dalej zajmuje 4 GB,
to dokładnie ten bałagan, którego ten serwer ma nie mieć.
---
## 5. Rzeczy, których na tym serwerze nie robimy
- **Własny serwer pocztowy.** OVH blokuje port 25, a wysyłka z IP bez reputacji i tak
ląduje w spamie. Maile aplikacji idą przez zewnętrzny SMTP (Resend / Brevo).
Alerty przychodzą na `k.kruszka@hotmail.com`.
- **Uruchamianie kontenerów jako root** bez uzasadnienia. Domyślnie `user:` w compose.
- **`latest` w tagach obrazów** na produkcji. Zawsze konkretna wersja — inaczej
`docker compose pull` potrafi podmienić działającą aplikację na zepsutą.
- **Praca na koncie root.** Pracujemy jako `ubuntu` z `sudo`.
- **Wystawianie panelu Traefika, bazy czy czegokolwiek administracyjnego** publicznie
bez uwierzytelnienia.
---
## 6. Mapa dokumentacji
| Plik | Kiedy czytasz |
|---|---|
| `docs/SERVER.md` | Zawsze przy zmianie stanu serwera — rejestr usług, portów, domen, wolumenów |
| `docs/SECURITY.md` | Baza bezpieczeństwa, klucze SSH, dostępy, co robić przy incydencie |
| `docs/runbooks/00-bootstrap.md` | Pierwsze uruchomienie serwera od zera |
| `docs/runbooks/10-nowa-usluga.md` | Dodajesz nową aplikację |
| `docs/runbooks/20-domena.md` | Podpinasz lub odpinasz domenę / subdomenę |
| `docs/runbooks/30-usuwanie.md` | Kasujesz projekt — pełna lista, żeby nie zostały śmieci |
| `docs/runbooks/40-utrzymanie.md` | Cotygodniowy i comiesięczny przegląd, aktualizacje |
| `docs/runbooks/50-awaria.md` | Coś nie działa albo trzeba się cofnąć |
---
## 7. Czego nie wiesz
Jeśli nie masz pewności — jak działa konkretna opcja, czy dana wersja obrazu jest aktualna,
co dokładnie robi flaga — **sprawdzasz albo pytasz**. Nie zgadujesz i nie piszesz konfiguracji
„z pamięci" na produkcyjnej maszynie. Zgadywanie na serwerze kosztuje więcej niż jedno pytanie.
+39
View File
@@ -0,0 +1,39 @@
# infra — konfiguracja serwera dfkk
Repozytorium jest źródłem prawdy o serwerze. Zawiera wszystko, co potrzebne,
żeby odtworzyć maszynę od zera: zasady pracy, rejestr stanu, runbooki i pliki compose
infrastruktury.
**Sekrety nie trafiają tutaj.** Pliki `.env` leżą obok stacków na serwerze,
w repo są tylko `.env.example`.
## Struktura
```
CLAUDE.md zasady pracy — czytane na starcie każdej sesji
docs/
SERVER.md rejestr: co gdzie stoi, aktualizowany przy każdej zmianie
SECURITY.md bezpieczeństwo, dostępy, procedura przy incydencie
runbooks/
00-bootstrap.md uruchomienie serwera od zera
10-nowa-usluga.md dodanie aplikacji
20-domena.md DNS, certyfikaty, podpinanie i odpinanie domen
30-usuwanie.md usunięcie projektu bez pozostawiania śmieci
40-utrzymanie.md przeglądy tygodniowe, miesięczne, kwartalne
50-awaria.md diagnostyka, rollback, odzyskiwanie dostępu
stacks/ compose infrastruktury (traefik, gitea, monitoring)
```
## Start pracy
```bash
ssh ubuntu@dfkk.cloud
tmux new -s claude # albo: tmux attach -t claude
cd /srv/infra
claude
```
## Zanim zrobisz coś większego
Snapshot w panelu OVH. Jeden slot, ręczny, nadpisywany — ale to jedyna droga powrotu
dla całej maszyny.
+109
View File
@@ -0,0 +1,109 @@
# HANDOFF.md — przekazanie kontekstu
Dokument jednorazowy. Powstał w rozmowie na claude.ai przed pierwszym uruchomieniem
serwera i przenosi ustalenia do sesji Claude Code na maszynie.
**Czytasz go raz, na starcie pierwszej sesji.** Trwałe zasady są w `CLAUDE.md`,
stan serwera w `docs/SERVER.md`, procedury w `docs/runbooks/`.
Po zakończeniu bootstrapu ten plik można usunąć.
---
## 1. Z kim pracujesz
**Kacper**, `k.kruszka@hotmail.com`. Pracuje z macOS, Windows i iOS.
Kluczowe: **to projekt edukacyjny z produkcyjnymi konsekwencjami.** Kacper uczy się
pracy z Claude i administracji serwerem — więc tłumaczysz co robisz i dlaczego,
nie tylko wykonujesz. Ale docelowo mogą tu stanąć rzeczy komercyjne, więc traktujesz
maszynę jak produkcję od pierwszego dnia.
Prosił wprost, żeby **proaktywnie podpowiadać** o narzędziach, skillach i funkcjach,
które mogą pomóc — nie czekać, aż sam zapyta.
---
## 2. Maszyna
| | |
|---|---|
| OVH VPS-2 2026 | 6 vCore, 12 GB RAM, 100 GB SSD |
| Host | `vps-c936b594.vps.ovh.net`, IPv4 `54.38.159.156` |
| System | Ubuntu 24.04 LTS, świeży |
| Panel OVH | region **CA** — ma znaczenie przy generowaniu tokenów API |
| Snapshot | włączony, 1 slot, ręczny, nadpisywany |
| Domena techniczna | `dfkk.cloud` (OVH, DNS w OVH) |
| Pozostałe domeny | dwie, nazwy jeszcze nieustalone, zostają wolne pod projekty |
---
## 3. Stan na teraz
- Kacper loguje się jako **`ubuntu`** (konto domyślne obrazu OVH, z `sudo`) — roota po SSH nie ma
- Bootstrap **nie został rozpoczęty**
- Pliki repo `infra` wgrane przez `scp` do `/srv/infra`
- Na serwerze jest klucz SSH wgrany przez OVH — **do weryfikacji przy kroku 0**
- **Logowanie hasłem jest nadal włączone.** Windows łączył się hasłem, więc nie ma tam
jeszcze klucza. Krok 2 bootstrapu musi to domknąć **przed** krokiem 3.
- Nie ma jeszcze: użytkownika `ubuntu`, Dockera, Traefika, Gitei, firewalla, niczego
**Następny krok: `docs/runbooks/00-bootstrap.md`, krok 0.**
---
## 4. Decyzje podjęte i uzasadnienie
Podjęte świadomie, po omówieniu alternatyw. **Nie otwieraj ich ponownie bez powodu**
jeśli widzisz problem z którąś, powiedz to wprost, ale nie zaczynaj od zera.
| Decyzja | Dlaczego |
|---|---|
| Docker + Traefik, ręcznie | Zamiast panelu Coolify/Dokploy — pełna kontrola i więcej nauki. Świadomy wybór trudniejszej drogi. |
| Gitea self-hosted na tej maszynie | Wybór Kacpra. **Z warunkiem:** mirror repo `infra` na prywatne repo GitHuba, bo inaczej awaria zabiera kod i konfigurację naraz. |
| Certyfikaty przez DNS-01 (API OVH) | Wildcard `*.dfkk.cloud` — nowa subdomena nie wymaga żadnej akcji przy certyfikacie. Możliwe też usługi niepubliczne. |
| Praca przez SSH + tmux | Sesja przeżywa rozłączenie. Ten sam `tmux attach` z Maca, Windowsa i iPhone'a. VS Code Remote SSH jako dodatek, nie zamiennik. |
| Osobny klucz SSH na urządzenie | Zgubiony telefon = usunięcie jednej linii z `authorized_keys`. |
| Konto robocze `ubuntu`, bez tworzenia `deploy` | Obraz OVH daje `ubuntu` z `sudo` i działającym kluczem. Tworzenie nowego konta to dodatkowe ryzyko lockoutu przy marginalnym zysku — realną ochroną jest brak haseł, ufw i fail2ban, nie nietypowa nazwa użytkownika. |
| Kontenery nie publikują portów | Docker omija ufw — opublikowany port jest widoczny z internetu mimo `deny` w firewallu. Wejście wyłącznie przez Traefika. |
| Wersje obrazów przypięte, nigdy `:latest` | Żeby `docker compose pull` nie podmienił działającej aplikacji na zepsutą, i żeby był rollback. |
| Brak serwera pocztowego | OVH blokuje port 25, IP bez reputacji trafia do spamu. Maile przez zewnętrzny SMTP (Resend/Brevo). |
| Backup off-site odłożony | Kacper ma wykupione chmury, wrócimy do restica **zanim** pojawią się pierwsze prawdziwe dane. Do tego czasu snapshot OVH wystarcza. |
| Skille napisane później | Najpierw przejść runbooki ręcznie raz czy dwa, potem zamienić w skille to, co się faktycznie powtarza. |
---
## 5. Decyzje otwarte
- **`sudo` z hasłem czy `NOPASSWD` dla `ubuntu`?** Do rozstrzygnięcia w kroku 2 bootstrapu.
Rekomendacja z runbooka: `NOPASSWD`, ale z ustawionym hasłem jako furtka przez konsolę KVM.
- **Nazwy dwóch pozostałych domen** — do wpisania w `SERVER.md` gdy będą potrzebne.
- **Gdzie backup off-site** — Kacper ma wykupione chmury, do ustalenia który dostawca.
---
## 6. Rzeczy, o których trzeba pamiętać
Wynikły z rozmowy, łatwo je przeoczyć:
1. **Kacper nie zna jeszcze wszystkich mechanizmów.** Warto przy okazji wyjaśniać —
np. dlaczego `claude` odpalamy z `/srv/infra` (inaczej `CLAUDE.md` się nie załaduje),
po co `/context` i `/memory`.
2. **Repo `infra` to kanał między rozmowami.** Ustalenie, które nie trafi do
`CLAUDE.md`, `SERVER.md` albo runbooka, umiera razem z sesją. Ta zasada jest
powodem, dla którego `SERVER.md` ma być aktualizowany w tej samej sesji, co zmiana.
3. **`scp` był jednorazowy.** Od momentu postawienia Gitei repo idzie przez git.
Ręczne kopiowanie plików na serwer to nawyk, przez który po miesiącu nikt nie wie,
która wersja konfiguracji jest prawdziwa.
4. **Lista braków jest w `docs/SECURITY.md`, sekcja 8.** Najpilniejsze: mirror na GitHub
i 2FA na koncie OVH — panel OVH może maszynę zreinstalować, więc w praktyce
jest ważniejszy niż dostęp SSH.
---
## 7. Czego nie robić
- Nie startuj bootstrapu bez potwierdzenia — Kacper może chcieć najpierw przeczytać runbooki
- Nie zmieniaj `sshd_config` bez otwartej drugiej sesji SSH
- Nie zakładaj, że coś działa — sprawdzaj (`curl`, `docker ps`, logi)
- Nie rób `docker system prune -a --volumes` odruchowo
- Nie wracaj do rozstrzygniętych decyzji z sekcji 4 bez konkretnego powodu
+149
View File
@@ -0,0 +1,149 @@
# SECURITY.md — bezpieczeństwo serwera
Zasada porządkująca: **serwer jest tak bezpieczny, jak jego najsłabsza wystawiona usługa.**
Utwardzony SSH nie ma znaczenia, jeśli obok stoi panel administracyjny bez hasła.
---
## 1. Warstwy
| Warstwa | Czym chronimy | Gdzie skonfigurowane |
|---|---|---|
| Dostęp do maszyny | SSH tylko na klucz, root wyłączony | `/etc/ssh/sshd_config.d/99-hardening.conf` |
| Sieć | ufw: deny incoming, allow 22/80/443 | `ufw status verbose` |
| Automatyczne ataki | fail2ban na sshd | `/etc/fail2ban/jail.local` |
| Aktualizacje | unattended-upgrades (security) | `/etc/apt/apt.conf.d/50unattended-upgrades` |
| Izolacja aplikacji | osobne sieci Dockera, brak publikowanych portów | compose każdego stacka |
| Transport | TLS z Let's Encrypt na wszystkim | Traefik |
| Sekrety | pliki `.env`, `chmod 600`, poza repo | obok każdego stacka |
---
## 2. SSH
Docelowa konfiguracja:
```
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AllowUsers ubuntu
X11Forwarding no
MaxAuthTries 3
```
**Reguła bezwzględna przy każdej zmianie w sshd:** otwierasz drugą sesję SSH i sprawdzasz,
że logowanie działa, **zanim** zamkniesz pierwszą. Błąd w `sshd_config` przy zamkniętej
sesji oznacza odzyskiwanie dostępu przez konsolę KVM w panelu OVH.
Port zostawiamy na 22. Przenoszenie na 2222 to kosmetyka — odsiewa hałas w logach,
ale nie zatrzymuje nikogo, kto celuje w konkretną maszynę. fail2ban robi tu realną robotę.
### Klucze
- osobny klucz na każde urządzenie, `ed25519`, z komentarzem identyfikującym sprzęt
- klucz prywatny **nigdy** nie trafia na serwer ani do repo
- rejestr kluczy: `SERVER.md`, sekcja 4
- przy zgubieniu urządzenia: usuwasz jego linię z `~/.ssh/authorized_keys`, koniec
iOS: Termius lub Blink. Wchodzisz przez `tmux attach -t claude` do tej samej sesji,
którą zostawiłeś na Macu.
---
## 3. Sekrety
| Zasada | Dlaczego |
|---|---|
| `.env` obok stacka, `chmod 600`, właściciel `ubuntu` | inni użytkownicy systemu nie odczytają |
| `.env` w `.gitignore`, w repo tylko `.env.example` | sekret w historii gita zostaje tam na zawsze |
| Hasła generowane, nie wymyślane — `openssl rand -base64 32` | brak ponownego użycia |
| Osobny sekret na usługę | wyciek jednego nie kompromituje reszty |
Jeśli sekret **przypadkiem** trafi do repo: nie wystarczy commit usuwający go.
Trzeba go **unieważnić i wygenerować nowy** — historia gita pamięta, a jeśli poszła na mirror
GitHuba, to jest już poza Twoją kontrolą.
---
## 4. Usługi administracyjne
Panel Traefika, bazy danych, narzędzia debugowe — **nie wystawiamy publicznie**.
Dostęp przez tunel SSH:
```bash
ssh -L 8080:127.0.0.1:8080 ubuntu@dfkk.cloud
# potem localhost:8080 w przeglądarce
```
Jeśli coś musi być publiczne (np. `status.dfkk.cloud`, żeby działało z telefonu),
to za basic auth w Traefiku, z hasłem z generatora.
**Gitea: rejestracja nowych użytkowników wyłączona natychmiast po instalacji.**
Otwarta rejestracja na publicznej instancji to zaproszenie dla botów, które
w kilka dni zamienią ją w hosting spamu.
---
## 5. Aktualizacje
| Co | Jak | Kiedy |
|---|---|---|
| Poprawki bezpieczeństwa Ubuntu | `unattended-upgrades`, automatycznie | codziennie |
| Reszta pakietów systemu | ręcznie, `apt upgrade` | miesięczny przegląd |
| Obrazy Dockera | ręcznie, podniesienie tagu wersji w compose | miesięczny przegląd |
| Restart po aktualizacji kernela | ręcznie, po snapshocie | gdy `/var/run/reboot-required` |
Automatyczny restart po aktualizacji jest **wyłączony**. Nie chcemy, żeby maszyna
sama się przeładowała w środku pracy albo w trakcie działania czegoś produkcyjnego.
Obrazy pinujemy do konkretnych wersji. `latest` oznacza, że najbliższy `docker compose pull`
może podmienić działającą aplikację na wersję z breaking changes — bez ostrzeżenia.
---
## 6. Co monitorujemy
| Sygnał | Próg | Reakcja |
|---|---|---|
| Zajętość dysku | 75% | sprzątanie wg `40-utrzymanie.md` |
| Usługa nie odpowiada | 2 nieudane sprawdzenia | alert mailem |
| Certyfikat wygasa | < 14 dni | sprawdzić DNS-01 i klucze OVH |
| Nietypowe wpisy w logach SSH | — | przegląd przy miesięcznym audycie |
Alerty idą na `k.kruszka@hotmail.com` przez zewnętrzny SMTP.
Serwera pocztowego tu nie stawiamy — OVH blokuje port 25.
---
## 7. Podejrzenie włamania
Kolejność ma znaczenie. **Nie restartuj i nie „posprzątaj" najpierw** — zniszczysz ślady
i stracisz możliwość ustalenia, czym weszli.
1. Odetnij ruch: `ufw default deny incoming` i zatrzymaj wystawione kontenery
2. **Zrób snapshot OVH** — to Twój materiał dowodowy
3. Zbierz: `last`, `journalctl -u ssh`, `docker ps -a`, `crontab -l` dla wszystkich użytkowników,
`ls -la /tmp /var/tmp`, nietypowe procesy i połączenia (`ss -tulpn`)
4. Unieważnij **wszystko**: klucze SSH, tokeny API OVH, token GitHub, hasła w `.env`
5. Oceń zakres — jeśli atakujący miał roota, maszyna jest spalona.
Postawienie od zera z repo `infra` + backup danych jest szybsze i pewniejsze
niż czyszczenie. Dlatego repo `infra` musi być kompletne.
6. Dopiero potem: odbudowa, i wpis do `SERVER.md` co się stało
---
## 8. Braki, które trzeba domknąć
Uczciwa lista rzeczy, których jeszcze **nie ma**. Przeglądać przy miesięcznym audycie.
- [ ] **Backup off-site (restic).** Snapshot OVH to rollback, nie backup — jeden slot,
ręczny, nadpisywany, w tej samej infrastrukturze. Do domknięcia **zanim**
na serwerze pojawią się pierwsze prawdziwe dane.
- [ ] **Test odtworzenia z backupu.** Backup, którego nie odtworzyłeś, nie istnieje.
- [ ] **Mirror repo `infra` na GitHub.** Bez tego awaria serwera zabiera kod, historię
i konfigurację potrzebną do odtworzenia serwera — naraz.
- [ ] **2FA na koncie OVH.** Panel OVH może zrobić z maszyną wszystko, łącznie
z reinstalacją. Jest w praktyce ważniejszy niż dostęp SSH.
- [ ] **2FA na koncie GitHub.**
+131
View File
@@ -0,0 +1,131 @@
# SERVER.md — rejestr serwera
**Źródło prawdy o tym, co stoi na maszynie.** Każda zmiana stanu serwera = wpis tutaj,
w tej samej sesji. Jeśli czegoś nie ma w tym pliku — z punktu widzenia dokumentacji to nie istnieje.
Ostatnia aktualizacja: _(uzupełnić przy pierwszym bootstrapie)_
---
## 1. Maszyna
| Pozycja | Wartość |
|---|---|
| Dostawca | OVH |
| Model | VPS-2 2026 |
| Host OVH | `vps-c936b594.vps.ovh.net` |
| vCore / RAM / dysk | 6 / 12 GB / 100 GB SSD NVMe |
| System | Ubuntu 24.04 LTS |
| Lokalizacja | _(uzupełnić)_ |
| IPv4 | `54.38.159.156` |
| IPv6 | _(uzupełnić)_ |
| Snapshot OVH | włączony, 1 slot, ręczny |
| Data ostatniego snapshota | _(uzupełniać przy każdym)_ |
---
## 2. Domeny
| Domena | Rola | Rejestrator | DNS |
|---|---|---|---|
| `dfkk.cloud` | infrastruktura + subdomeny projektów | OVH | OVH |
| _(domena 2)_ | wolna, pod projekt | OVH | OVH |
| _(domena 3)_ | wolna, pod projekt | OVH | OVH |
Certyfikaty: Let's Encrypt, wildcard `*.dfkk.cloud`, wyzwanie **DNS-01** przez API OVH.
Klucze API OVH: przechowywane w `/srv/infra/stacks/traefik/.env` (poza repo).
### Zajęte subdomeny
| Subdomena | Usługa | Publiczna? | Uwagi |
|---|---|---|---|
| `git.dfkk.cloud` | Gitea | tak | rejestracja wyłączona |
| `status.dfkk.cloud` | Uptime Kuma | tak | za basic auth |
| `traefik.dfkk.cloud` | Traefik dashboard | nie | tylko przez tunel SSH |
---
## 3. Użytkownicy systemowi
| Użytkownik | Rola | sudo | Uwagi |
|---|---|---|---|
| `root` | — | — | logowanie po SSH wyłączone przez OVH i przez nas |
| `ubuntu` | praca codzienna, Docker, Claude Code | tak | konto domyślne obrazu OVH |
---
## 4. Klucze SSH
Każde urządzenie ma **własny** klucz. Zgubiony laptop = usunięcie jednej linii
z `authorized_keys`, bez ruszania pozostałych.
| Urządzenie | Typ | Komentarz w kluczu | Dodany | Status |
|---|---|---|---|---|
| MacBook | ed25519 | `kamil@macbook` | _(data)_ | _(aktywny)_ |
| Windows | ed25519 | `kamil@windows` | _(data)_ | _(aktywny)_ |
| iPhone (Termius/Blink) | ed25519 | `kamil@iphone` | _(data)_ | _(aktywny)_ |
| _(klucz istniejący, do weryfikacji)_ | ? | ? | ? | **do sprawdzenia przy bootstrapie** |
---
## 5. Porty otwarte na świat
| Port | Usługa | Uzasadnienie |
|---|---|---|
| 22 | SSH | dostęp administracyjny, tylko klucze |
| 80 | Traefik | przekierowanie na 443 |
| 443 | Traefik | cały ruch HTTPS |
**Nic więcej.** Każdy dodatkowy port wymaga wpisu tutaj z uzasadnieniem.
Pamiętaj: opublikowany port kontenera omija ufw — patrz `CLAUDE.md`, sekcja 3.3.
---
## 6. Stacki infrastruktury
| Stack | Ścieżka | Status | Opis |
|---|---|---|---|
| Traefik | `/srv/infra/stacks/traefik` | _(planowany)_ | reverse proxy, TLS, wejście do wszystkiego |
| Gitea | `/srv/infra/stacks/gitea` | _(planowany)_ | repozytoria kodu, mirror na GitHub |
| Monitoring | `/srv/infra/stacks/monitoring` | _(planowany)_ | Uptime Kuma, alerty na maila |
---
## 7. Projekty
Tabela uzupełniana przy każdym nowym projekcie (runbook `10-nowa-usluga.md`)
i czyszczona przy usuwaniu (runbook `30-usuwanie.md`).
| Projekt | Katalog | Repo | Domena | Wolumeny | Baza | Backup | Uruchomiony |
|---|---|---|---|---|---|---|---|
| _(brak)_ | | | | | | | |
---
## 8. Zadania cykliczne (cron / systemd timers)
| Zadanie | Harmonogram | Gdzie zdefiniowane | Co robi |
|---|---|---|---|
| `unattended-upgrades` | codziennie | systemd | aktualizacje bezpieczeństwa |
| _(uzupełniać)_ | | | |
---
## 9. Konta zewnętrzne powiązane z serwerem
| Usługa | Do czego | Gdzie klucz/token |
|---|---|---|
| OVH API | DNS-01 dla Traefika | `/srv/infra/stacks/traefik/.env` |
| GitHub | mirror repo `infra` | token w Gitei, ustawienia push mirror |
| SMTP (Resend/Brevo) | wysyłka maili z aplikacji | `.env` danego projektu |
---
## 10. Dziennik większych zmian
Krótko, jedna linia, najnowsze na górze. Chodzi o to, żeby po miesiącu dało się
odtworzyć, co się działo — nie o pełny changelog.
| Data | Co się zmieniło | Kto |
|---|---|---|
| 2026-08-20 | Poprawki `00-bootstrap.md` (Krok 9 tmux append + `focus-events`, Krok 10 katalogi, Krok 12 Claude Code jako wykonany, nowy Krok 14 audyt) i numeracja `HANDOFF.md` sekcja 6 | Claude |
+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ę.