| name | prod-health |
| description | Kolla produktionsserverns hälsa — CPU, minne, disk, Docker-containrar, Redis och php-fpm-poolen — via SSH till Hetzner. Använd när användaren frågar saker som 'hur mår prod', 'kolla cpu på prod', 'redis health', 'är allt OK på servern', 'docker stats prod', eller när sajten är långsam/nere och orsaken ska hittas. Subkommandon: (tomt)/all, cpu, mem, disk, docker, redis, fpm. |
/prod-health — produktionsserver-hälsa
Snabb hälsokoll mot prod (Hetzner CX33, deploy@brottsplatskartan.se,
kod i /opt/brottsplatskartan/). Allt är read-only — inga ändringar.
Först vid "sajten är långsam": har det just deployats?
Kolla alltid detta innan du gräver i CPU, Redis eller fpm. deploy.sh
avslutar med responsecache:clear samtidigt som app-containern startar om,
så de första minuterna efter en deploy köar requests bakom varandras
omrenderingar. Uppmätt: /stockholm 56 s och load 4,05 direkt efter deploy,
0,22 s och load 2,73 fem minuter senare.
ssh deploy@brottsplatskartan.se 'cd /opt/brottsplatskartan && \
cat storage/app/deploy.json; echo; git log -1 --format="%h %cr %s"'
deploy.json ger deployed_at med tidszonsoffset
(2026-08-24T16:39:43+02:00) — använd den, inte loggtidsstämplar, när du
jämför mot något. Prod loggar i svensk tid medan gh run rapporterar UTC,
och två timmars förskjutning gör att man lätt läser före-data som
efter-data.
Är deployen yngre än ~5–10 minuter: vänta och mät om. Se prod-ops
(“Efter deploy: två saker som ser ut som fel men inte är det”) för hur du
skiljer kön från ett verkligt problem. Äkta kallrendering är 0,2–0,3 s.
Mät kallrendering med ?v=<slump> — ?nocache= ligger i
ignored_query_parameters och ger samma cache-nyckel som utan parameter.
Tolka argumentet
Argumentet efter /prod-health (ord 1) avgör vad som körs:
| Argument | Visar |
|---|
(tomt) / all | Sammanfattning av allt nedan |
cpu | Load + topprocesser |
mem / memory | RAM-användning + swap |
disk | Diskutrymme |
docker / containers | docker compose ps + docker stats --no-stream |
redis | php artisan redis:health (rikare output, tabell + varningar) |
fpm | php-fpm-poolen: workers, listen-kö, mättnadsräknare |
Kommandon
Alla körs över SSH. Alla är read-only. Om en permission-prompt dyker upp,
godkänn — eller (bättre) lägg permanent regel i .claude/settings.local.json.
cpu
ssh deploy@brottsplatskartan.se "uptime && echo --- && top -bn1 | head -15"
Tolka: load average över 4 (CX33 har 4 vCPU) = ihållande mättning.
%Cpu(s)-raden visar fördelning user/sys/idle/wa. Hög wa = disk-I/O-bunden.
mem
ssh deploy@brottsplatskartan.se "free -h"
Tolka: available är viktigare än free (Linux räknar buff/cache som
återanvändbart). Swap > 0 på en server utan swap-fil betyder fel-konfig;
servern har idag 0 MB swap, så swap-raden ska vara helt 0.
disk
ssh deploy@brottsplatskartan.se "df -h / && echo --- && du -sh /opt/brottsplatskartan 2>/dev/null"
Tolka: / på 80 GB SSD. Över 80 % är värt att flagga (tile-data + Docker
images kan svälla). /opt/brottsplatskartan visar projektets storlek.
docker
ssh deploy@brottsplatskartan.se "cd /opt/brottsplatskartan && docker compose ps && echo --- && docker stats --no-stream"
Tolka:
- Alla containrar ska vara
running / Up. Saknas en eller står på
Exited/Restarting → kolla logs för den.
docker stats visar CPU%, MEM USAGE/LIMIT per container.
- Förväntad RAM-fördelning grovt (CX33 har 8 GB total):
redis ~ 0.2–1 GB (cap 3 GB)
mariadb ~ 0.3 GB
app ~ 0.1–0.3 GB per php-fpm-worker
caddy, scheduler, tileserver små
redis
ssh deploy@brottsplatskartan.se "cd /opt/brottsplatskartan && docker compose exec -T app php artisan redis:health"
Tolka utdata:
- Använt nu / Maxmemory — under 80 % är OK.
- Peak / max — peak nära max betyder att eviction kan börja vid trafiktoppar.
- Evicted keys — > 0 betyder Redis har börjat slänga data; ofta ett
tecken på att maxmemory är för litet eller TTL för långa.
- Hit rate — > 90 % bra. Sjunker det efter en
cache:clear är det
normalt en stund.
- Fragmentation ratio — 1.0–1.5 normalt. > 1.5 kan indikera
minnesfragmentering; > 5 är illa.
- Uptime — om mycket lägre än serverns systemuptime betyder det att
Redis-containern startat om. Värt att fråga varför (
docker compose logs redis).
fpm
Kolla alltid den här när sajten är långsam eller nere. Det är php-fpm
som tar slut först — inte CPU, RAM eller MariaDB.
ssh deploy@brottsplatskartan.se 'docker exec -i -e SCRIPT_NAME=/status -e SCRIPT_FILENAME=/status -e REQUEST_METHOD=GET brottsplatskartan-app-1 cgi-fcgi -bind -connect 127.0.0.1:9000'
Tolka:
| Nyckel | Betydelse |
|---|
max children reached | Antal gånger poolen slagit i taket sedan start |
max active processes | Högsta samtidiga workers sedan start |
max listen queue | Djupaste kön av väntande anslutningar |
listen queue len | Backlog-taket (4096) |
slow requests | Requests över request_slowlog_timeout |
Trösklar:
max children reached > 0 → poolen har varit full. Requests köade.
max active processes nära PHP_FPM_PM_MAX_CHILDREN (40, satt i
compose.yaml) → taket är på väg att ta slut igen.
max listen queue nära listen queue len (4096) → kön har svämmat
över. Då vägrar php-fpm nya anslutningar, och utifrån ser det ut som
att servern är helt nere: ingen HTTP-statuskod alls, inte 502/503.
Uptime-tjänster rapporterar det som "no response code".
- Alla värden är kumulativa sedan containerstart. De säger att det
hänt, inte när. Tidsstämpla mot något annat (se nedan).
pm är ondemand, så total processes varierar med last — det är inte
ett problem i sig att den ligger nära taket en stund.
Responstid över tid — cache:warm som gratis mätpunkt
Prod saknar historisk responstidsmätning, men cache:warm (schemalagd
:08 :23 :38 :53) pingar ~26 sidor över HTTP mot sajten själv och
schedulern loggar körtiden. Det ger en tidsstämplad responstidsserie
som redan finns, utan att något behöver installeras.
ssh deploy@brottsplatskartan.se "cd /opt/brottsplatskartan && docker compose logs --since 3h --timestamps scheduler 2>&1 | grep -A1 'cache:warm' | grep -E 'cache:warm|s DONE|FAIL'"
Tolka:
- 2–5 s = normalt. Så ser de allra flesta körningar ut.
- 15 s+ = sajten är trög för riktiga besökare just då.
- 30 s+ eller
FAIL = något är allvarligt fel. Kommandot returnerar
exit 1 när en URL timeoutar (Http::timeout(15)), vilket också ger en
ERROR: Scheduled command ... failed i laravel.log.
Under nedtiden 2026-08-03 gick serien 6 s → 16 s → 40 s (failade) och var
det som knöt incidenten till en exakt tidpunkt.
Fälla: första körningen efter varje deploy är alltid dyr (30 s+) —
deployen rensar responscachen, så sidorna regenereras kallt. Det är
förväntat och inget larm. Kolla nästa körning innan du drar slutsatser.
Samma sak gäller om du curl:ar /stockholm direkt efter en deploy.
all / (tomt)
Kör allt i en enda SSH-anslutning för att minimera handshake-overhead.
Tunga jobb (docker stats, redis-cli INFO, du -sh) startas i
bakgrunden direkt så de överlappar med de snabba sekventiella anropen.
redis-cli använder REDISCLI_AUTH (env-var i redis-containern, satt i
compose.yaml) — inget lösenord behövs i shell-anropet.
ssh deploy@brottsplatskartan.se '
cd /opt/brottsplatskartan
# Starta tunga jobb i bakgrund så de överlappar med de snabba nedan.
# docker stats sätter golvet (~2 s, inneboende sampling).
docker stats --no-stream > /tmp/_bpk_stats.txt &
docker exec -i brottsplatskartan-redis-1 redis-cli INFO memory stats server > /tmp/_bpk_redis.txt &
docker exec -i -e SCRIPT_NAME=/status -e SCRIPT_FILENAME=/status -e REQUEST_METHOD=GET \
brottsplatskartan-app-1 cgi-fcgi -bind -connect 127.0.0.1:9000 > /tmp/_bpk_fpm.txt 2>/dev/null &
du -sh /opt/brottsplatskartan > /tmp/_bpk_du.txt 2>/dev/null &
uptime
echo === MEM ===
free -h
echo === DISK ===
df -h /
echo === DOCKER PS ===
docker ps -a --filter label=com.docker.compose.project=brottsplatskartan --format "table {{.Names}}\t{{.Status}}"
echo === CACHE-FLUSH ===
cat storage/app/cache-meta/last-cache-flush 2>/dev/null; echo
cat storage/app/cache-meta/last-responsecache-flush 2>/dev/null; echo
wait
echo === PROJECT SIZE ===
cat /tmp/_bpk_du.txt
echo === DOCKER STATS ===
cat /tmp/_bpk_stats.txt
echo === REDIS INFO ===
cat /tmp/_bpk_redis.txt
echo === PHP-FPM ===
cat /tmp/_bpk_fpm.txt
rm -f /tmp/_bpk_stats.txt /tmp/_bpk_redis.txt /tmp/_bpk_du.txt /tmp/_bpk_fpm.txt
'
Varför detta mönster:
- En SSH-handshake istället för flera separata anslutningar.
- Bakgrundsjobben startas först så
docker stats (~2 s-golvet)
överlappar med uptime/free/df/cache-flush-läsningarna istället för
att dyka upp efteråt. Total tid ≈ 2 s (begränsat av docker stats).
docker exec istället för docker compose exec för redis-anropet
— slipper compose-yaml-parsen (~300 ms).
- Cache-flush läses direkt från host —
storage/ är bind-monterad
(./:/var/www/html i compose.yaml), så ingen docker exec behövs
(~500 ms).
docker ps --filter istället för docker compose ps — samma
resonemang, slipper compose-parsen.
- Inget Laravel-boot — rå
redis-cli istället för php artisan redis:health. Skillen tolkar INFO-output själv (se nedan).
SSH ControlMaster (i ~/.ssh/config) sparar handshake vid
upprepade körningar inom en timme.
Tolka rå INFO-output i all
INFO memory stats server returnerar key:value-rader. Plocka:
| Nyckel | Vad det är |
|---|
used_memory | Använt nu (bytes) |
used_memory_peak | Peak (bytes) |
maxmemory | Tak (bytes) |
mem_fragmentation_ratio | Fragmentation |
evicted_keys | Evicted sedan start |
keyspace_hits | Cache-träffar |
keyspace_misses | Cache-missar |
uptime_in_seconds | Redis-uptime |
redis_version | Version |
Räkna:
- Hit rate:
keyspace_hits / (keyspace_hits + keyspace_misses) * 100
- Peak / max:
used_memory_peak / maxmemory * 100
Trösklar (samma som php artisan redis:health använder):
- Peak/max > 80 % → överväg höja taket
evicted_keys > 0 → cache slits ut, taket för litet eller TTL för långa
mem_fragmentation_ratio > 1.5 (när used_memory > 100 MB) → fragmentering
mem_fragmentation_ratio < 0.95 (när used_memory > 100 MB) → möjlig swap
- Redis-uptime ≪ system-uptime → containern har startat om nyligen
CACHE-FLUSH-blocket levererar två ISO8601-strängar (eller tomma rader
om aldrig flushat). Visa relativ tid ("för 1 timme sedan") i sammanfattningen.
Sammanfatta i slutet: "Allt OK" eller lista avvikelser (load > 4,
mem available < 1 GB, disk > 80 %, container nere, Redis evictions, etc.).
För all räcker det med en kondenserad version per sektion — inte hela
top-output. Visa det viktigaste:
- CPU: load + de 3 högsta processerna
- Mem: total/used/available + swap
- Disk:
/ raden
- Docker: bara containrar som inte är
Up/running (om alla är OK, säg det)
- Redis: minne använt / max + evictions + hit rate
- php-fpm:
max children reached + max active processes / taket +
max listen queue. Är alla noll/låga räcker det med "poolen mår bra".
Konventioner
- Aldrig modifiera något på servern via denna skill. Inga
cache:clear, restart, eller liknande. För skrivande operationer:
använd deploy/-skripten och låt användaren köra.
- Säg vad du gjorde — visa kommandot du körde innan/under output, så
användaren kan reproducera manuellt.
- Tolka, sammanfatta inte bara dumpa — du har sett hela output, ge
användaren slutsatsen ("RAM mår bra, 60 % ledigt") inte bara siffrorna.
- Avvikelser = flagga — om något ser ovanligt ut (load 6 på 4 vCPU,
swap > 0, container Restarting, evictions > 0) säg det tydligt.
Edge cases
- SSH timeout — säg det och fråga om servern är nere; rekommendera
Hetzner Cloud Console.
- Permission denied av sandbox — be användaren godkänna eller lägga
in permission-regel.
- Container nere — visa
docker compose logs --tail 50 <namn> som
nästa steg, men kör det inte automatiskt.
docker compose logs är för långsam för incidentanalys. Den läser
hela JSON-loggen innan den filtrerar, så anrop med --since 12h eller
breda grep:ar tar flera minuter och timeoutar. Håll fönstren korta
(--since 30m, eller --since/--until runt en känd minut) och kör i
bakgrunden. Vill du räkna trafik: skriv först till en fil på servern
(> /tmp/x.log 2>&1, i den ordningen) och greppa den — inte via pipe
genom SSH.
- Access-loggning är delvis avstängd. nginx har
access_log off i
sina location-block och Caddy loggar bara warn/error, inte access.
nginx server-nivå loggar ändå requests till stdout, så
docker compose logs app innehåller dem — men Caddy-loggen visar bara
fel, så den duger inte för att mäta trafikvolym.
- Kumulativa räknare ≠ tidsstämplar. php-fpm-status och Redis
INFO
räknar sedan containerstart. En max listen queue: 4098 säger att kön
svämmat över någon gång, inte att det sker nu. Knyt till en tidpunkt
via cache:warm-serien eller schedulerloggen.