Skip to main content

Kako sam zamenio menadžer obeleživača od 1,3 GB alatom od 17 MB

Pritisak na memoriju na mom VPS-u sa 7,6 GB RAM-a naterao me je da odustanem od Karakeep-a. Migracija na Readeck prerasla je u mali Go projekat: Telegram bot za više korisnika i MCP server koji sada povezujem sa Claude-om.

· 10 мин читања


Neko vreme sam zatvarao oči pred tim: svaki put kada bih pogledao docker stats na svom Hetzner VPS-u, Karakeep je ubedljivo bio najteži kontejner. Mašina sa 7,6 GB RAM-a, 25 servisa — i jedna Node aplikacija koja drži 1,32 GB rezidentne memorije za ono što je, u mom slučaju, bio samo nabudženi spisak linkova sa screenshotovima.

Dostupne memorije ostalo je 1,4 GB, swap je bio zakucan na 2,0 / 2,0 GB, a Postgres sa limitom od 768 MB stizao je do 61% iskorišćenosti. Ništa nije gorelo, ali rezerva je prestala da deluje kao rezerva.

Ovo je priča o tome kako sam to raspetljao. Na kraju su se izdvojila tri koraka: audit, migracija i — pošto gotova rešenja nisu sasvim legla — mali Go projekat.

Audit

Prvo je trebalo razumeti šta zapravo jede memoriju. docker stats --no-stream i for f in /proc/*/status; do awk '/^VmSwap:/{if($2>0)print …}' $f; done sortirali su sve po potrošnji memorije i količini podataka u swap-u. Prvih šest kontejnera izgledalo je ovako:

KontejnerRAM
karakeep-web1.32 GB
memory-embeddings (TEI za RAG)964 MB
authentik-server728 MB
city_dashboard622 MB
infra-postgres469 MB
authentik-worker281 MB

Swap od 2 GB bio je skoro potpuno „hladan“. vmstat je pokazivao si=2, so=3 kB/s: swap je bio pun, ali nije bilo aktivnog stalnog prebacivanja tamo-vamo. Kernel je u swap izbacio neaktivne podatke — TEI se pokreće jednom u 30 minuta, worker-i za Evening News takođe rade periodično — i zadržao vruće stranice u RAM-u. Taj pritisak na memoriju bio je školski primer zdravog ponašanja sistema, a ne pravi problem.

Pravo rešenje bilo je jednostavno: povukao sam svež Karakeep image — njihov :release tag kasnio je tri meseca — dodao drugi swap fajl od 4 GB i spustio vm.swappiness sa Ubuntu podrazumevanih 60 na 20. Posle toga stanje je izgledalo ovako:

MetrikaPrePosle
RAM u upotrebi5.4 GB4.4 GB
Dostupno1.4 GB2.7 GB
Swap2.0 / 2 GB (100%)1.9 / 6 GB (32%)

Običan update Karakeep-a spustio mu je potrošnju RAM-a sa 1,32 GB na 818 MB: oko 500 MB manje samo zahvaljujući svežem procesu i čistom Node heap-u. Mašina je opet prodisala.

Ali kada skineš 500 MB sa procesa koji i dalje zauzima oko 800 MB, neminovno počneš da se pitaš: da li mi tih 800 MB uopšte treba?

Izbor

Karakeep ima stvarne prednosti: automatsko tagovanje pomoću AI-ja, snimke stranica i screenshotove preko Chrome-a, nativne mobilne aplikacije sa share ekstenzijama. Ali u mom slučaju skoro sve što čuvam jesu linkovi prosleđeni iz Telegrama, koje kasnije čitam na iOS-u. AI tagovi su prijatna stvar, ali nisu neophodni. Arhiva screenshotova takođe. Pouzdan put iz mobilnog share sheeta jeste kritičan.

Prvi kandidat bio je linkding. Python/Django, oko 100 MB u memoriji i neobično bogat ekosistem aplikacija trećih strana: tri iOS aplikacije (LinkBuddy, Linkdy, LinkThing), četiri Android klijenta (LinkBuddy, Linkdy, Linklater, Pinkt), nekoliko Telegram botova, ekstenzije za browser i čak linkding-injector, koji prikazuje vaše sačuvane linkove unutar rezultata Google-a ili DuckDuckGo-a. Tehnički bi završio posao.

Zaustavio me je interfejs. linkding je solidan interni alat: zbijen i funkcionalan. Ali estetika mu je „ticket tracker iz 2019“, a ja bih u to gledao svakog dana, u svoje slobodno vreme. To nije tehnički argument, ali jeste stvaran.

Sledeći na listi bio je Readeck: iz iste Go porodice kao ostatak mog steka, oko 60 MB u memoriji i OPDS podrška za slanje obeleživača na e-čitač kao EPUB. Prethodni put sam ga odbacio jer nije imao nativne mobilne aplikacije. Ovog puta sam pogledao release notes i video da je v0.22 u martu 2026. doneo iOS aplikaciju sa OAuth-om i offline čitanjem, plus aktivan Android klijent na F-Droid-u. Glavni blokator je nestao.

Shiori bio je na listi radi potpunosti. Go binarni fajl, oko 30 MB, namerno minimalan: bez mobilnih aplikacija, bez AI-ja, browser ekstenzija još u beta fazi. Pravi alat za nekoga, pogrešan za mene.

Preostali pošten argument protiv Readeck-a bilo je Karakeep-ovo automatsko AI tagovanje. Sedeo sam neko vreme sa tim pitanjem. Dosadan odgovor: u praksi skoro nikada nisam tražio obeleživače po tagovima koje je Karakeep generisao; bilo ih je prijatno videti posle čuvanja, ali nisu bili ulazna tačka za pretragu. Zanimljiviji odgovor: sa MCP serverom ispred Readeck-a, AI tagovanje postaje BYO-LLM. Claude može da pozove readeck_get_article, ukratko prepriča članak i po potrebi pozove readeck_add_labels sa tagovima po mom ukusu. To nije funkcija koju gubim. To je funkcija koju premeštam tamo gde sam kontrolišem ponašanje.

Readeck je pobedio: čistiji UX za čitanje, sada dovoljno ubedljiv mobilni deo, Go binarni fajl, OPDS za e-čitač, a zamerka oko AI tagova ispostavila se kao rešiva, ne fatalna. Jedini kompromis koji ostaje jeste nedostatak punih Chrome snapshotova. Readeck izvlači tekst članka i čuva ugrađene resurse, ali ne čuva vizuelnu arhivu stranice. Za moj način korišćenja menadžera obeleživača to je poštena razmena.

Povezivanje

Deploy se oslanjao na dva već postojeća dela infrastrukture: deljeni Postgres (infra-postgres) i authentik SSO.

Za Postgres sam samo kreirao ulogu readeck_user i bazu readeck u istoj instanci koju već koriste Authentik i još nekoliko servisa. Readeck kontejner se povezuje preko READECK_DATABASE_SOURCE=postgres://readeck_user:…@infra-postgres-1:5432/readeck?sslmode=disable. Jedan backup pipeline pokriva sve.

SSO je bio teži nego što sam očekivao.

Readeck podržava forwarded authentication iz kutije: čita zaglavlja Remote-User, Remote-Email i Remote-Groups od pouzdanog reverse proxy-ja. Authentik proxy outpost šalje X-authentik-username, X-authentik-email i X-authentik-groups. Ali ugrađeni middleware-i u Traefik-u ne umeju da preimenuju zaglavlja. Mogu da postave statička custom zaglavlja, ali ne mogu da sastave njihove vrednosti iz dolaznih zaglavlja.

Moj prvi pokušaj bio je Caddy sidecar ispred Readeck-a:

:8080 {
  reverse_proxy readeck:8000 {
    header_up Remote-User   {http.request.header.X-Authentik-Username}
    header_up Remote-Email  {http.request.header.X-Authentik-Email}
    header_up Remote-Groups admin
  }
}

Radilo je: oko 10 MB RAM-a za taj most, dva kontejnera po servisu, ali funkcionalno ispravno.

Zatim sam krenuo da tražim čistiji put i našao ga: scope mappings u Authentik-u mogu da vrate poseban ključ ak_proxy.additionalHeaders, koji outpost šalje kao proizvoljna response zaglavlja. Scope mapping za Readeck postao je:

return {
    "ak_proxy": {
        "user_attributes": request.user.group_attributes(request),
        "is_superuser": request.user.is_superuser,
        "additionalHeaders": {
            "Remote-User":   request.user.username,
            "Remote-Email":  request.user.email,
            "Remote-Groups": "admin" if request.user.is_superuser else "user",
        },
    }
}

Na kraju je bilo dovoljno jedno Readeck-specific pravilo authResponseHeaders=…,Remote-User,Remote-Email,Remote-Groups na forwardAuth middleware-u u Traefik labelama, bez dodatnog sidecar kontejnera. Ideja sa Caddy-jem je tiho umrla.

Tokom testiranja pojavila su se još dva ograničenja:

  1. Mobilne aplikacije koriste API tokene, ne OAuth cookies. Da je svaki zahtev ka /api/* išao kroz Authentik, to bi slomilo iOS aplikaciju i svaki klijent sa Bearer tokenom. Rešenje: drugi Traefik router za Host(read.dzarlax.dev) && PathPrefix(/api), koji zaobilazi SSO middleware i šalje zahteve direktno u Readeck. Readeck sam obrađuje Bearer autentifikaciju za te zahteve.

  2. Let’s Encrypt me je rate-limitovao. Pustio sam deploy pre nego što se DNS zapis propagirao. Pet brzih ACME grešaka aktiviralo je blokadu „too many failed authorisations for read.dzarlax.dev, retry after …“. Sačekao sam da prozor istekne, restartovao kontejner i sertifikat je izdat iz prvog pokušaja. Lekcija: ne podizati TLS rutu dok DNS zaista nije živ.

Alatni paket

Tu migracija prestaje da bude samo migracija i postaje projekat.

Staru integraciju Telegram→Karakeep opsluživao je karakeepbot, Go bot treće strane preko Karakeep API-ja. U BotFather-u sam ostavio istog bota i iskoristio postojeći token, ali novi backend govori sa drugim API-jem. Znači, trebao mi je novi kod za Readeck.

Hteo sam i Readeck unutar Claude-a. Moji MCP serveri za ličnu memoriju i kalendar već su postali dovoljno korisni da su komande „pretraži moje obeleživače“ i „pročitaj ovaj članak“ izgledale kao očigledan sledeći korak. Najlakše je bilo uraditi to u istoj kodnoj bazi kao bot, jer dele isti API klijent.

Repozitorijum je Dzarlax-AI/readeck_toolkit: jedan Go modul, dva binarna fajla (cmd/bot, cmd/mcp), jedna Docker slika. Posle nekoliko iteracija dobio je ovaj oblik:

  • Bot podržava više korisnika kroz config.toml. Svaki [[tenants]] blok vezuje Telegram user id za Readeck API token. Dolazna poruka → pronađi id pošiljaoca → uzmi token tog korisnika → pozovi Readeck → obeleživač završi na pravom nalogu. Nepoznati pošiljaoci se tiho ignorišu. Dodavanje nove osobe, na primer za zajedničku kućnu upotrebu, izgleda ovako: ona napravi Readeck API token u Settings → API tokens, pošalje botu /whoami, dobije svoj numerički id, vi dodate novi [[tenants]] blok i pokrenete docker compose restart bot.

  • MCP je suprotan: bez stanja i bez kredencijala na serveru. Server zna samo Readeck base URL. Svaki MCP klijent pri povezivanju šalje svoj Readeck token kroz X-API-Key zaglavlje; handler alata vadi token iz konteksta zahteva i za svaki poziv pravi Readeck klijent. Jedna MCP instanca može bezbedno da se deli između više ljudi: Readeck ograničava pristup po tokenu. Nema korisničke konfiguracije na serveru, nema tajni na disku.

Vredi pomenuti i par ćorsokaka:

  • Prvi MCP dizajn koristio je SSE transport. NewSSEServer iz biblioteke mcp-go delovao je kao najlakši podrazumevani izbor. Radio je lokalno. Ali iza Traefik stripprefix middleware-a, SSE događaj endpoint oglašavao je /message?sessionId=… bez prefiksa /readeck, pa je klijent nastavljao na pogrešnu rutu. Da bi se to popravilo, trebalo bi izbaciti stripprefix i reći serveru punu javnu putanju. Zatim sam ponovo proverio specifikaciju i video da se SSE sada smatra legacy pristupom. Današnji MCP serveri treba da koriste Streamable HTTP: jedan POST endpoint, uz opcioni streaming odgovor. server.NewStreamableHTTPServer(s, server.WithStateLess(true), server.WithEndpointPath("/readeck/mcp")) na kraju je bio čistiji i lakši za razumevanje u višekorisničkoj postavci.

  • Prvi config.toml imao je poseban [mcp] odeljak sa poljem tenant da bi MCP znao u čije ime radi. Posle prelaska na X-API-Key po zahtevu, ceo taj odeljak je nestao. MCP-u je sada potreban samo [readeck].base_url; identitet korisnika dolazi iz zaglavlja.

Finalni skup alata ima devet funkcija:

readeck_save           save URL, optional title + labels
readeck_search         full-text search
readeck_list_recent    last N bookmarks
readeck_get_article    extracted article, HTML→Markdown server-side
readeck_mark_read      flip is_archived
readeck_add_labels     append labels, preserve existing
readeck_remove_labels  drop specified labels
readeck_delete         permanent delete
readeck_list_labels    every label with bookmark count

readeck_get_article je funkcija zbog koje MCP zaista zaslužuje mesto unutar Claude-a. Prirodan zahtev zvuči otprilike ovako: „pronađi moj sačuvani članak o Postgres _shared_buffers_ i sažmi njegove konkretne preporuke“. Claude pozove readeck_search, izabere najrelevantniji id iz liste, pozove readeck_get_article i napravi sažetak. Ja pritom uopšte ne otvaram web interfejs. U paru sa readeck_add_labels, to postaje BYO-LLM tagger koji zatvara Karakeep-ovu rupu iz prethodnog odeljka.

Gde je završilo

Bot i MCP zajedno troše svega 7 MB RAM-a u mirovanju. Karakeep, koji sam zaustavio ali ostavio na disku dve nedelje za slučaj da se pokajem, trošio je 818 MB. Razlika u potrošnji memorije je 47 puta za ceo alatni stek. Zauzvrat sam izgubio vizuelne screenshotove stranica, dobio malo manje ispolirano iOS iskustvo i potrošio otprilike tri slobodna popodneva.

Brisanje Karakeep-a zakazano je u Todoist-u za dve nedelje unapred, dovoljno daleko da stignem da vidim šta će mi nedostajati. Za sada je odgovor: „automatski AI tagovi, povremeno“, što je upravo rupa koju MCP može da popuni čim sednem i napišem Claude prompt za to.

Šta dalje?

Očigledna v2 je save hook u botu. Trenutno bot samo radi POST /api/bookmarks i odgovara. Ako ga proširim tako da šalje i POST {id, url, manual_labels} na save_hook_url podešen po korisniku, bilo šta na prijemnoj strani — Claude rutina, n8n workflow ili mali skript sa API ključem modela — može da povuče članak kroz MCP, generiše AI tagove i sažetak, pa da ih upiše nazad kroz readeck_add_labels i ažuriranje opisa. Svi građevni blokovi su već tu: bot se uredno aktivira pri čuvanju, MCP ume da čita i piše sve što treba. Nedostaje samo webhook lepak. To je otprilike jedno veče posla i zatvara potrebu za automatskim tagovanjem bez ugrađivanja LLM-a direktno u kodnu bazu. Cela poenta je bila BYO-LLM, pa neka hook ostane hook.

Repo: github.com/Dzarlax-AI/readeck_toolkit. Licenca je MIT, a multi-arch image je dostupan na ghcr.io/dzarlax-ai/readeck-toolkit. Bot ima smisla čak i za jednog korisnika: iOS share sheet pošalje URL pravo u njega jednim tapom, bot ga sačuva sa opcionim #hashtag oznakama i vrati Readeck link. Više ne moraš da otvaraš web interfejs samo da bi sačuvao link. Konfiguracija za više korisnika dobro dođe ako poželiš da podeliš instancu sa partnerom ili malim timom. MCP server je za svakoga ko želi da poveže Claude — ili bilo koji MCP-kompatibilan klijent — sa sopstvenim Readeck-om.