- 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
10 KiB
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 1–1,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.
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
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:
ssh-keygen -t ed25519 -C "kacper@macbook"
Windows (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:
ssh-copy-id ubuntu@54.38.159.156
Windows (PowerShell — nie ma ssh-copy-id):
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:
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:
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
sudo passwd ubuntu
Decyzja do podjęcia:
sudoz hasłem czyNOPASSWD? Z hasłem jest bezpieczniej, ale każda operacja Claude Code wymagająca uprawnień zatrzyma się na monicie.NOPASSWDjest 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
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
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 pokazujedeny. Dlatego w tym projekcie kontenery nie publikują portów — patrzCLAUDE.md, 3.3.
Krok 5 — fail2ban
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
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.
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).
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
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
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.
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.
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:
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:
- Traefik — bez niego reszta nie ma jak wyjść na świat. Wymaga kluczy API OVH
do DNS-01. Weryfikacja: certyfikat wildcard dla
*.dfkk.cloudsię wystawia. - Gitea na
git.dfkk.cloud— rejestracja wyłączona natychmiast po pierwszym logowaniu. Potem push mirror repoinfrana prywatne repo GitHuba. - Monitoring (Uptime Kuma) na
status.dfkk.cloudza 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
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.156z macOS, Windows i iOS → wchodzi bez pytania o hasło -
test, że baza nie jest wystawiona — na Windowsie w PowerShellu:
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.mduzupełniony: IP, lokalizacja, klucze, data snapshota- Repo
infrazainicjowane 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