Jak zainstalować AFFiNE przez Docker Compose
Instrukcja samodzielnego hostowania AFFiNE na własnym serwerze VPS. Konfiguracja czterech kontenerów, mapowanie wolumenów, zarządzanie bazą Postgres oraz optymalizacja RAM.
Co zyskujesz, hostując samodzielnie AFFiNE
Samodzielne hostowanie AFFiNE zapewnia przestrzeń roboczą w stylu Notion na serwerze pod Twoją kontrolą, działającą jako cztery kontenery: aplikacja, jednorazowe zadanie migracji, Postgres oraz Redis. Współpraca w czasie rzeczywistym jest dostępna dla maksymalnie 10 stanowisk, które domyślnie otrzymuje samodzielnie hostowana przestrzeń robocza. Instalacja składa się z jednego pliku compose oraz jednego pliku konfiguracyjnego JSON. Kwestie wymagające uwagi to tagi obrazów, układ dysku, limit pamięci oraz proxy umieszczone przed aplikacją.
AFFiNE utrzymuje edytor dokumentów oraz nieskończone płótno w tej samej przestrzeni roboczej, dzięki czemu jedną stronę można czytać jako dokument lub rozłożyć jako tablicę. Jeśli nadal podejmujesz decyzję, co uruchomić, przeczytaj najpierw porównanie samodzielnie hostowanych alternatyw dla Notion. Ten przewodnik zakłada, że wybór został dokonany i skupia się na poprawnym uruchomieniu AFFiNE, a nie na ponownym porównywaniu rozwiązań.
Wszystkie informacje zostały zweryfikowane zgodnie z dokumentacją self-host AFFiNE oraz opublikowanymi plikami wydań w dniu 8 sierpnia 2026. Najnowszym stabilnym wydaniem w tym dniu było 0.27.3, opublikowane 23 lipca 2026. Poniższe twierdzenia z późniejszą datą zostały ponownie sprawdzone w źródłach pierwotnych 12 października 2026, kiedy to najnowszym stabilnym wydaniem było 0.27.4, opublikowane 18 sierpnia 2026.
Zadania poszczególnych czterech kontenerów
affine to serwer oraz klient webowy w jednym obrazie. Nasłuchuje on na porcie 3010.
affine_migration to zadanie typu one-shot, które uruchamia node ./scripts/self-host-predeploy.js, wykonuje migracje bazy danych i kończy działanie. Aplikacja deklaruje condition: service_completed_successfully dla tego zadania, więc migracja kończąca się statusem innym niż zero powoduje, że affine w ogóle nie startuje. Gdy interfejs webowy nie pojawia się, log tego zadania jest pierwszym miejscem, które należy sprawdzić.
postgres przechowuje dokumenty, użytkowników, przestrzenie robocze oraz uprawnienia. Dostarczany obraz to pgvector/pgvector:pg16, czyli standardowy Postgres 16 z wkompilowanym rozszerzeniem pgvector. pgvector dodaje do Postgresa typ kolumny vector, czyli postać numeryczną służącą do przechowywania embeddingów, co umożliwia wyszukiwanie tekstu według znaczenia.
redis stanowi twardą zależność: zarówno serwer, jak i zadanie migracji oczekują na pozytywny wynik jego testu sprawności (health check) przed uruchomieniem. Należy zwrócić uwagę, czego dostarczony plik compose nie zapewnia dla Redis, a mianowicie wolumenu. Nic wewnątrz niego nie przetrwa operacji docker compose down, co jasno wskazuje, że nie przechowuje on żadnych danych użytkownika i nie wymaga kopii zapasowej.
Dlaczego obraz Postgres to pgvector, a nie standardowy postgres
Wymóg ten wynika ze schematu bazy danych AFFiNE, a nie z preferencji. W schema.prisma źródło danych deklaruje extensions = [pgvector(map: "vector")], a cztery tabele zawierają kolumnę embedding o typie vector(1024). Zadanie migracji tworzy te tabele niezależnie od tego, czy funkcje AI zostaną włączone, dlatego rozszerzenie musi istnieć w bazie danych przed zakończeniem migracji. Zastosowanie postgres:16 powoduje brak rozszerzenia, uniemożliwia migracji utworzenie tych kolumn, a serwer pozostaje w stanie oczekiwania na zadanie, które zakończyło się niepowodzeniem.
AFFiNE przeszło na obraz pgvector w wersji 0.21. W przypadku instalacji starszych niż ta wersja, edycja linii z obrazem nie stanowi pełnej procedury aktualizacji, dlatego przed pobraniem jakichkolwiek plików należy zapoznać się ze stroną aktualizacji w dokumentacji self-host AFFiNE.
Dodatkowa uwaga dotycząca tagu. pg16 oznacza Postgres 16, a głównej wersji Postgres nie można zmienić poprzez prostą aktualizację numeru. Zmiana na pg17 w istniejącym katalogu danych spowoduje, że Postgres odmówi uruchomienia, a w docker compose logs postgres pojawi się komunikat podobny do The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17. Zmiana głównej wersji wymaga jednej z trzech ścieżek opisanych w informacjach o wydaniu PostgreSQL 17: zrzutu i przywrócenia danych do świeżego katalogu, pg_upgrade lub replikacji logicznej. W pojedynczej instalacji Compose zrzut i przywrócenie danych jest zazwyczaj najprostszym rozwiązaniem, ponieważ pg_upgrade wymaga jednoczesnego dostępu do binariów starego i nowego serwera.
Ile procesora i pamięci RAM wymaga samodzielnie hostowany AFFiNE
Strona z wymaganiami AFFiNE wskazuje na minimum 4 rdzenie procesora i 2 GB pamięci RAM, zwiększając zapotrzebowanie do 4 GB, gdy liczba słów w dokumentach przekroczy 10 000. Ta sama strona wyjaśnia przeznaczenie pamięci: system synchronizacji oraz scalanie dokumentów. Warto zapamiętać jedną wartość: scalanie dokumentu z 10 000 modyfikacji może wymagać szczytowo 1 GB pamięci.
Należy zestawić to z planem 2 GB pamięci przy pracy dwóch osób. Średnie zużycie jest akceptowalne. Procesy Postgres oraz Node mieszczą się w limicie z zapasem. Problemem są wartości szczytowe. Pojedyncze, duże scalanie może podnieść zużycie pamięci serwera do około 1 GB. Na maszynie z 2 GB pamięci i bez partycji swap, ten szczyt, w połączeniu z Postgres i innymi rezydentnymi procesami, może wyczerpać dostępną pamięć. Mechanizm OOM (Out-of-Memory) killer jądra systemu zakończy wtedy proces w celu zwolnienia pamięci. Serwer AFFiNE, jako największy proces, jest najbardziej prawdopodobną ofiarą, choć dokumentacja Docker jasno wskazuje, że wybrany może zostać dowolny proces, w tym sam Docker.
Gdy ofiarą jest serwer AFFiNE, współpracownik nie zobaczy błędu. Strona zostanie przeładowana, ponieważ restart: unless-stopped przywróci kontener w ciągu kilku sekund. Nie należy zgadywać przyczyny, lecz ją potwierdzić:
docker inspect affine_server --format '{{.State.OOMKilled}} {{.RestartCount}}'
sudo dmesg -T | grep -i -E 'out of memory|killed process'true z pierwszego polecenia lub linia Killed process wskazująca node z drugiego oznaczają wyczerpanie pamięci, a nie błąd w oprogramowaniu. Należy rozwiązać problem dwutorowo. Najpierw należy dodać swap, aby nagły wzrost zapotrzebowania miał bufor, zanim zadziała OOM killer:
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
free -hfree -h powinno teraz raportować łącznie 2.0Gi swap. Swap nie przyspiesza działania AFFiNE i nie do tego służy. Zapewnia on miejsce dla krótkotrwałych skoków zapotrzebowania, dzięki czemu taki skok spowoduje raczej sekundowe spowolnienie niż zatrzymanie kontenera. Jest to bufor, a nie gwarancja. Drugim krokiem jest ograniczenie Postgres, aby nie rozszerzał swojej pamięci podręcznej na przestrzeń potrzebną aplikacji podczas scalania, do czego służą limity pamięci w usłudze Compose. Jeśli po obu krokach procesy nadal są przerywane, serwer wymaga 4 GB pamięci, zgodnie z wymaganiami AFFiNE dla większych dokumentów.
Przewidywanie zapotrzebowania na pamięć masową jest znacznie prostsze. Oto wartości publikowane przez AFFiNE na tej samej stronie:
Dane, na których opiera się ten wykres
[
{
"label": "Server install",
"gb": 1.5
},
{
"label": "Postgres per 1,000 docs",
"gb": 0.1
},
{
"label": "Blob store per 1,000 uploads",
"gb": 10
}
]Instalacja serwera zajmuje 1.5 GB. Tysiąc dokumentów po około tysiąc słów każdy dodaje 0.1 GB danych Postgres, co jest wartością znikomą. Tysiąc przesłanych plików dodaje 10 GB, co stanowi główny składnik zajętości. Są to publikowane wartości planistyczne, a nie pomiary z działającej instancji, więc należy traktować je jako szacunek, a nie gwarancję. Istotny jest wzorzec: baza danych pozostaje niewielka, a o zajętości dysku decydują przesłane pliki.
Samodzielne przygotowanie pliku compose z przypiętymi tagami
Zalecana metoda instalacji pobiera gotowy plik za pomocą curl -L -o docker-compose.yml https://github.com/toeverything/AFFiNE/releases/latest/download/docker-compose.yml. To rozwiązanie działa. Warto jednak znać jeden szczegół przed rozpoczęciem pracy: według stanu na 8 sierpnia 2026 plik dołączony do wydania 0.27.3 nadal pobierał ścieżki z pliku .env, używając ${UPLOAD_LOCATION}, ${CONFIG_LOCATION} oraz ${DB_DATA_LOCATION}, podczas gdy dokumentacja referencyjna wskazywała nowszy układ, który przechowuje wszystko w ./data i nie wymaga .env. Wydanie 0.27.4 przeniosło dołączony plik do układu ./data i od 12 października 2026 oba źródła są zgodne. W przypadku instalacji ze starszego pliku ścieżki nadal pochodzą z .env, a strona aktualizacji AFFiNE ostrzega, że zastąpienie pliku compose bez przeniesienia tych ustawień może spowodować uruchomienie Postgres z pustym katalogiem danych. Samodzielne napisanie pliku całkowicie eliminuje ten problem, a edycja i tak jest konieczna, aby przypiąć obrazy i ustawić hasło do bazy danych.
mkdir -p ~/affine/config ~/affine/data
cd ~/affine
printf 'DB_PASSWORD=%s\n' "$(openssl rand -hex 24)" > .env
chmod 600 .envCompose automatycznie odczytuje .env z katalogu projektu i podstawia ${DB_PASSWORD}, dzięki czemu hasło nigdy nie pojawia się w pliku, który mógłby zostać wklejony w wątku pomocy technicznej. Warto stosować ten nawyk w każdym uruchamianym stosie; uzasadnienie znajduje się w utrzymywanie sekretów poza plikiem compose.
Teraz utwórz ~/affine/docker-compose.yml:
name: affine
services:
affine:
image: ghcr.io/toeverything/affine:stable
container_name: affine_server
ports:
- '127.0.0.1:3010:3010'
depends_on:
redis:
condition: service_healthy
postgres:
condition: service_healthy
affine_migration:
condition: service_completed_successfully
volumes:
- ./data/storage:/root/.affine/storage
- ./config:/root/.affine/config
environment:
- REDIS_SERVER_HOST=redis
- DATABASE_URL=postgresql://affine:${DB_PASSWORD}@postgres:5432/affine
- AFFINE_INDEXER_ENABLED=false
restart: unless-stopped
affine_migration:
image: ghcr.io/toeverything/affine:stable
container_name: affine_migration_job
command: ['sh', '-c', 'node ./scripts/self-host-predeploy.js']
volumes:
- ./data/storage:/root/.affine/storage
- ./config:/root/.affine/config
environment:
- REDIS_SERVER_HOST=redis
- DATABASE_URL=postgresql://affine:${DB_PASSWORD}@postgres:5432/affine
- AFFINE_INDEXER_ENABLED=false
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
redis:
image: redis:8-alpine
container_name: affine_redis
healthcheck:
test: ['CMD', 'redis-cli', '--raw', 'incr', 'ping']
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
postgres:
image: pgvector/pgvector:pg16
container_name: affine_postgres
volumes:
- ./data/postgres:/var/lib/postgresql/data
environment:
POSTGRES_USER: affine
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: affine
POSTGRES_INITDB_ARGS: '--data-checksums'
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'affine', '-d', 'affine']
interval: 10s
timeout: 5s
retries: 5
restart: unless-stoppedWystępują cztery różnice w stosunku do pliku dostarczanego przez twórców oprogramowania, z których każda ma swoje uzasadnienie.
127.0.0.1:3010:3010publikuje port tylko na adresie loopback, więc nic spoza serwera nie uzyska dostępu do AFFiNE, dopóki nie zostanie podjęta odpowiednia decyzja. Domyślne'3010:3010'wiąże się ze wszystkimi interfejsami, co w przypadku większości obrazów VPS obejmuje również interfejs publiczny.POSTGRES_HOST_AUTH_METHOD: trustzostało usunięte na rzecz ustawienia hasła. Uwierzytelnianie typu trust akceptuje każde połączenie z bazą danych jako użytkownikaffinebez hasła. Jest ono ograniczone do prywatnej sieci Compose, co jest bezpieczne do momentu podłączenia kolejnego kontenera do tej sieci lub opublikowania portu 5432 podczas debugowania.redis:8-alpinezastępuje suroweredis, które wskazuje nalatest. Według stanu na sierpień 2026 jest to Redis 8, więc przypięcie wersji utrzymuje przetestowaną wersję główną i zapobiega nieoczekiwanej aktualizacji do Redis 9 podczas niezwiązanej operacjidocker compose pull.pgvector/pgvector:pg16pozostaje dokładnie w takiej formie, w jakiej ustawili je twórcy, z powodów wymienionych powyżej.
POSTGRES_PASSWORD jest odczytywane tylko wtedy, gdy Postgres tworzy swój katalog danych po raz pierwszy. W przypadku istniejącej instancji należy ustawić hasło za pomocą docker compose exec postgres psql -U affine -c "ALTER USER affine WITH PASSWORD 'yourpassword'", a następnie zaktualizować DATABASE_URL, aby było z nim zgodne.
Konfiguracja znajduje się w config/config.json
AFFiNE odczytuje ustawienia z config/config.json, czyli katalogu zamontowanego w /root/.affine/config. Plik ten nie jest tworzony automatycznie, dlatego należy go utworzyć przed pierwszym uruchomieniem. Otwórz ~/affine/config/config.json w edytorze i wprowadź poniższą treść, zastępując przykładową domenę własną:
{
"$schema": "https://github.com/toeverything/affine/releases/latest/download/config.schema.json",
"server": {
"name": "Team workspace",
"externalUrl": "https://affine.example.com"
},
"copilot": {
"enabled": false,
"byok": {
"enabled": false
}
}
}server.externalUrl musi być adresem, który użytkownicy wpisują w przeglądarce. AFFiNE generuje na jego podstawie linki do udostępniania oraz zaproszenia do obszarów roboczych. Jeśli pozostawisz wartość http://localhost:3010, wysłane zaproszenie będzie kierować odbiorcę na jego własną maszynę, co uniemożliwi dostęp. Ustaw publiczny adres HTTPS przed pierwszym uruchomieniem, aby konfiguracja pliku była zgodna z panelem administratora. Ustawienia zmienione później w panelu administratora są zapisywane w bazie danych, a nie w tym pliku.
copilot steruje funkcjami AI. copilot.byok.enabled to przełącznik pozwalający właścicielowi obszaru roboczego na wprowadzenie własnego klucza dostawcy modelu w ustawieniach obszaru. Samodzielnie hostowana instancja AFFiNE nie zawiera subskrypcji AI. Pozostaw obie wartości false, jeśli nie chcesz korzystać z tych funkcji.
Uruchom stos:
docker compose up -d
docker compose ps --alldocker compose ps --all powinno wskazywać affine_postgres oraz affine_redis jako zdrowe (healthy), affine_server jako działające (running), a affine_migration_job ze stanem exited (0). Wartość --all jest istotna: bez niej docker compose ps wyświetla tylko działające kontenery, przez co zakończone zadanie migracji nie będzie widoczne. Każdy inny kod wyjścia zadania migracji wymaga analizy, a jego dziennik wskazuje krok, w którym wystąpił błąd:
docker compose logs affine_migrationPrzypnij obraz, zanim zapomnisz
stable to zmienny tag. Cykl wydawniczy AFFiNE wskazuje kilkoma tagami na każdą stabilną wersję, z czego dwa są tutaj istotne: stable, który jest aktualizowany przy każdym wydaniu, oraz stable- wraz z krótkim hashem git, który pozostaje niezmienny. Pozostawienie stable sprawi, że docker compose pull za sześć miesięcy pobierze inny obraz i uruchomi migracje bazy danych w nieoczekiwanym momencie. Przypnij dokładnie ten obraz, który został przetestowany:
docker compose pull
docker image inspect ghcr.io/toeverything/affine:stable --format '{{index .RepoDigests 0}}'Polecenie to wyświetli linię w formacie ghcr.io/toeverything/affine@sha256: wraz z długim hashem. Wklej cały ciąg znaków w linię image: w obu plikach: affine oraz affine_migration. Te dwie wartości muszą być zawsze identyczne, ponieważ jest to ten sam obraz pełniący dwie role; rozbieżność oznacza migrację bazy danych do jednego schematu przy jednoczesnej obsłudze przez inny. Aktualizacja staje się wtedy świadomą zmianą, a nie niespodzianką: zmień sumę kontrolną (digest), wykonaj kopię zapasową, docker compose pull, docker compose up -d.
Utwórz konto administratora, zanim zrobi to ktoś inny
Otwarcie /admin na nowej instancji powoduje przekierowanie do strony tworzenia konta, ponieważ serwer nie posiada jeszcze administratora. W tym procesie nie występuje kod zaproszenia ani token konfiguracyjny. Pierwsza osoba, która załaduje tę stronę, staje się administratorem serwera, dlatego port musi pozostać zamknięty do momentu zakończenia rejestracji.
Z tego powodu powyższy plik compose wiąże się z 127.0.0.1. Uzyskaj dostęp za pomocą tunelu SSH z własnej maszyny:
ssh -L 3010:127.0.0.1:3010 you@your-server-ipPozostaw to połączenie aktywne i otwórz http://127.0.0.1:3010/admin w lokalnej przeglądarce. Zarejestruj się i zaloguj, a następnie zamknij tunel. Dopiero teraz bezpiecznie można udostępnić instancję pod publiczną domeną. Ten sam wyścig występuje w innych aplikacjach self-hosted i jest bardziej ryzykowny tam, gdzie pierwsze logowanie tworzy klucz dostępu powiązany z nazwą hosta. Dlatego TLS oraz docelowa domena muszą zostać skonfigurowane przed utworzeniem pierwszego konta, gdy wdrażasz self-host openGym.
Gdzie AFFiNE przechowuje dane
Wszystkie dane znajdują się w trzech ścieżkach, zlokalizowanych wewnątrz utworzonego katalogu.
./data/postgresto katalog danych Postgres: dokumenty, użytkownicy, obszary robocze, uprawnienia oraz wszelkie ustawienia serwera zapisane z poziomu panelu administratora../data/storagejest montowany w/root/.affine/storagewewnątrz kontenera i przechowuje wszystkie przesłane pliki../configjest montowany w/root/.affine/configi przechowujeconfig.json.
Upstream stosuje tutaj bind mounts zamiast nazwanych wolumenów i jest to wybór celowy: ścieżki te można spakować do archiwum tar i skopiować za pomocą standardowych poleceń, bez konieczności sprawdzania w Docker, gdzie zostały umieszczone. Kosztem tego rozwiązania jest konieczność samodzielnego zarządzania uprawnieniami do plików na hoście, co jest kompromisem omówionym w bind mounts and named volumes.
Jak wykonać kopię zapasową AFFiNE
Wymagane jest zabezpieczenie dwóch elementów, z których każdy wymaga innej metody. Baza danych jest serwerem działającym w czasie rzeczywistym, więc kopiowanie jej plików w trakcie pracy spowoduje powstanie uszkodzonej kopii. Zamiast tego należy wykonać zrzut:
mkdir -p ~/affine/backup
cd ~/affine
docker compose exec -T postgres pg_dump --format c --username affine affine \
> backup/affine-$(date +%F).dump
ls -lh backup/Zrzut wykonywany jest wewnątrz kontenera przez lokalny socket, więc nie wymaga podania hasła. Należy sprawdzić rozmiar pliku w danych wyjściowych ls. Plik o rozmiarze kilkuset bajtów oznacza, że zrzut nie powiódł się, mimo że powłoka utworzyła plik – jest to błąd, który często wykrywa się dopiero po sześciu miesiącach. Flaga -T jest również istotna: bez niej Compose może przydzielić terminal i uszkodzić strumień binarny.
Przesłane pliki to zwykłe dane, więc należy je spakować za pomocą tar:
tar czf backup/storage-$(date +%F).tgz -C data storage
cp config/config.json backup/config-$(date +%F).jsonPlik config.json należy zachować w kopii zapasowej ręcznie, ponieważ jest to jedyny element konfiguracji, którego zrzut nie zawiera. Ustawienia zapisane w panelu administratora są przechowywane w bazie danych, więc powyższy zrzut już je zawiera. Według stanu na 12 października 2026, strona kopii zapasowych AFFiNE dodaje, że zmodyfikowane ustawienia serwera można również wyeksportować z poziomu panelu administratora. Wszystkie trzy pliki należy skopiować poza serwer. Kopia zapasowa znajdująca się na tym samym dysku, co chronione dane, nie jest kopią zapasową. Ten podział na zrzut bazy danych oraz archiwum tar katalogu z przesłanymi plikami to schemat, który należy powtarzać dla każdego kontenera przechowującego stan. Jest to również ta sama metoda, która zapewnia bezpieczeństwo historii rozmów i załączników, gdy wdrażasz Chatwoot jako własny system obsługi zgłoszeń.
Przywracanie danych i pułapka w opublikowanych instrukcjach
Należy zapoznać się z oficjalnymi instrukcjami przywracania danych przed wystąpieniem awarii i dokładnie je przeanalizować. W wersji opublikowanej w sierpniu 2026 r. kopiowano plik o nazwie affine.backup do kontenera, a następnie wykonywano przywracanie z ./pg.backup – są to dwie różne nazwy. Ponadto usuwano katalog ./postgres, podczas gdy bieżący plik compose przechowuje dane w ./data/postgres. Od 12 października 2026 r. strona konsekwentnie używa affine.backup i zaleca zachowanie starego katalogu Postgres do momentu zweryfikowania poprawności przywracania. Pułapka została zatem wyeliminowana w źródle, jednak kopie instrukcji zapisane przed tą datą nadal ją zawierają. W każdym przypadku należy stosować ścieżki faktycznie użyte w systemie, a nie te wskazane w wycinkach kodu. Poniżej przedstawiono sekwencję zgodną z układem opisanym w tym przewodniku:
cd ~/affine
docker compose down
sudo mv data/postgres data/postgres.old
docker compose up -d postgres
docker compose cp backup/affine-2026-08-08.dump postgres:/tmp/affine.dump
docker compose exec postgres pg_restore --format c --username affine \
--dbname affine --verbose /tmp/affine.dump
docker compose up -dNależy zwrócić uwagę na mv zamiast rm. Przywracanie danych na bazę, której kopia nie została zabezpieczona, to najprostsza droga do trwałej utraty danych wskutek jednego błędnego polecenia; przeniesienie starego katalogu w inne miejsce nie wiąże się z żadnym kosztem. Należy również przywrócić pliki przesłane (uploads) za pomocą tar xzf backup/storage-2026-08-08.tgz -C data, w przeciwnym razie każdy dokument będzie wyświetlany z uszkodzonymi załącznikami. Następnie należy zalogować się do systemu i otworzyć dokument zawierający obraz. Jest to test poprawności. Przywrócona kopia, która nie została sprawdzona w przeglądarce, pozostaje jedynie plikiem, a nie działającą kopią zapasową.
Umieszczanie AFFiNE za działającym proxy
AFFiNE korzysta z protokołu WebSocket i jest to wymóg konieczny. Dokumentacja jasno wskazuje, że WebSocket stanowi podstawę systemu synchronizacji i współpracy w AFFiNE. Proxy, które nie obsługuje aktualizacji (upgrade) tych połączeń, spowoduje, że edycja przestanie być synchronizowana. Strona się ładuje, logowanie działa, ale zmiany wprowadzone w jednej przeglądarce nie docierają do drugiej. W narzędziach deweloperskich przeglądarki należy otworzyć kartę Network i przefiltrować ruch do WS. Połączenie, które ciągle się otwiera i zamyka, oznacza, że proxy nie przekazuje nagłówka upgrade.
Jeśli używasz już Traefik dla innych kontenerów, AFFiNE można dodać jako standardową usługę. Usuń blok ports: z usługi affine, a następnie dodaj:
networks:
- default
- proxy
labels:
- 'traefik.enable=true'
- 'traefik.docker.network=proxy'
- 'traefik.http.routers.affine.rule=Host(`affine.example.com`)'
- 'traefik.http.routers.affine.entrypoints=websecure'
- 'traefik.http.routers.affine.tls.certresolver=letsencrypt'
- 'traefik.http.services.affine.loadbalancer.server.port=3010'oraz na końcu pliku, obok services::
networks:
proxy:
external: trueNazwa resolvera certyfikatów musi być zgodna z tą zdefiniowaną w konfiguracji Traefik, a loadbalancer.server.port to port kontenera 3010, nigdy port hosta. Traefik obsługuje połączenia WebSocket bez dodatkowej konfiguracji, więc nie trzeba nic więcej dodawać. Jeśli reszta Twojego stosu znajduje się już za Authentik dla logowania jednokrotnego, middleware typu forward auth na tym routerze ograniczy dostęp przeglądarki do AFFiNE. Wyłącz go jednak do czasu przetestowania aplikacji desktopowej, która nie posiada sesji przeglądarki i po prostu nie zsynchronizuje danych. Uruchamianie kilku aplikacji za jedną instancją zostało opisane w pojedynczy Traefik przed kilkoma aplikacjami.
W przypadku nginx należy jawnie wymusić aktualizację połączenia:
location / {
proxy_pass http://127.0.0.1:3010;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 100m;
}Wartość client_max_body_size domyślnie wynosi 1 MB w nginx, więc bez tej linii każdy przesył większy niż małe zdjęcie zakończy się błędem 413. W logach AFFiNE nic się nie pojawi, ponieważ żądanie nigdy nie dotarło do aplikacji. Caddy wymaga tylko jednej linii, reverse_proxy http://127.0.0.1:3010, i samodzielnie obsługuje certyfikaty oraz aktualizacje WebSocket.
Czego brakuje w samodzielnie hostowanej wersji
Przed przeniesieniem zespołu należy uczciwie ocenić ograniczenia tego rozwiązania.
Współpraca w czasie rzeczywistym jest dostępna i to właśnie ona determinuje wymagania sprzętowe, ponieważ dokumentacja AFFiNE przypisuje zużycie pamięci systemowi synchronizacji oraz scalaniu dokumentów. Edycja offline jest powodem, dla którego wiele osób wybiera narzędzia typu local-first, a aplikacja desktopowa pozwala dodać własny serwer do listy obszarów roboczych i zalogować się do niego. Przed podjęciem decyzji należy przetestować dokładnie to zachowanie offline, na którym polega zespół: przeprowadzić edycję w aplikacji desktopowej przy wyłączonej sieci, połączyć się ponownie, a następnie sprawdzić wynik na drugim urządzeniu. Listy funkcji nie stanowią dowodu działania, co dotyczy również niniejszego zestawienia.
Wyszukiwanie pełnotekstowe po stronie serwera jest wyłączone w dostarczonym pliku compose, gdzie AFFINE_INDEXER_ENABLED=false jest ustawione na serwerze oraz w zadaniu migracji. Włączenie tej funkcji wymaga dodania kontenera Manticore Search, co stanowi piątą usługę i zwiększa zapotrzebowanie na pamięć. Na maszynie z 2 GB RAM jest to zmiana, która wyczerpuje dostępne zasoby. Wyszukiwanie wewnątrz klienta nadal działa dla otwartego obszaru roboczego.
Przed zaproszeniem użytkowników warto znać dwa ograniczenia. Samodzielnie hostowany obszar roboczy jest ograniczony do maksymalnie 10 stanowisk, a przekroczenie tej liczby wymaga licencji Team od AFFiNE. Nielimitowana przestrzeń na obiekty blob oraz brak limitów ich rozmiaru dla instancji self-hosted są opisane w dokumentacji jako zamierzone, lecz jeszcze nie w pełni zaimplementowane (stan na październik 2026). Żadna z tych kwestii nie ma znaczenia dla użytku domowego lub małego zespołu. Obie stają się istotne w przypadku planów przeniesienia czterdziestu osób.
Aktualizacje
Przed przystąpieniem do prac należy zapoznać się z informacjami o wydaniu, szczególnie w przypadku zmiany wersji mniejszej, takiej jak 0.26 na 0.27, gdzie mogą wystąpić zmiany powodujące niekompatybilność. Przed wykonaniem jakichkolwiek działań należy wykonać kopię zapasową bazy danych oraz katalogu z danymi, ponieważ proces migracji zmienia schemat przy następnym uruchomieniu i nie ma możliwości jego cofnięcia. Następnie należy zmienić przypięty skrót (digest), wykonać docker compose pull, a po nim docker compose up -d, oraz monitorować docker compose logs -f affine_migration do momentu poprawnego zakończenia procesu. docker image prune usuwa następnie stare warstwy. Uwaga historyczna dla użytkowników bardzo starych instalacji: od wersji 0.23.0 nazwa obrazu zmieniła się z affine-graphql na affine, więc w plikach compose starszych niż ta wersja należy zaktualizować linie z nazwą obrazu, aby operacja pull mogła pobrać nowe dane. Od wersji 0.27.4 oficjalny szablon compose zmienił ścieżki dla Postgres, plików przesyłanych oraz konfiguracji, a także zmodyfikował domyślne połączenie z bazą danych. Z tego powodu należy porównać nowy plik z aktualnym, zamiast nadpisywać działającą konfigurację.
FAQ
Dlaczego kontener AFFiNE nie uruchamia się?
Usługa affine deklaruje condition: service_completed_successfully w zadaniu affine_migration, więc jeśli migracja zakończy się statusem innym niż 0, serwer nie zostanie uruchomiony i interfejs WWW nie będzie dostępny. Uruchom docker compose logs affine_migration, aby sprawdzić, na którym kroku wystąpiło zatrzymanie. Najczęstszą przyczyną w ręcznie edytowanym pliku compose jest użycie standardowego obrazu postgres zamiast pgvector/pgvector:pg16, ponieważ schemat AFFiNE wymaga rozszerzenia pgvector i tworzy tabele z kolumnami vector(1024), których standardowy Postgres nie obsługuje.
Ile pamięci RAM wymaga samodzielnie hostowany AFFiNE?
Strona wymagań AFFiNE wskazuje na co najmniej 4 rdzenie CPU i 2 GB pamięci RAM, z zapotrzebowaniem rosnącym do 4 GB przy dokumentach przekraczających 10 000 słów. Warto zauważyć, że scalanie dokumentu z 10 000 modyfikacji może wygenerować skokowe zużycie rzędu 1 GB. Na serwerze z 2 GB RAM to właśnie ten skok powoduje awarię, a nie obciążenie w stanie spoczynku: mechanizm OOM killer jądra systemu wybiera proces do zakończenia, najczęściej serwer AFFiNE jako największy, a restart: unless-stopped uruchamia go ponownie, przez co użytkownicy widzą odświeżenie strony zamiast błędu. Potwierdź to za pomocą docker inspect affine_server --format '{{.State.OOMKilled}}' oraz sudo dmesg -T | grep -i 'out of memory', a następnie dodaj plik wymiany (swap) o rozmiarze 2 GB jako bufor na krótkotrwałe skoki obciążenia. Jeśli procesy nadal są przerywane, zwiększ ilość pamięci RAM do 4 GB.
Gdzie AFFiNE przechowuje dane i co należy archiwizować?
Wszystkie dane znajdują się w trzech ścieżkach wewnątrz katalogu compose: ./data/postgres dla bazy danych, ./data/storage dla przesłanych plików oraz ./config dla config.json. Bazę danych należy archiwizować za pomocą docker compose exec -T postgres pg_dump --format c --username affine affine > affine.dump, zamiast kopiować pliki bezpośrednio, ponieważ nie można bezpiecznie skopiować działającej instancji Postgres. Zrzut ten zawiera również ustawienia zapisane w panelu administratora, ponieważ AFFiNE przechowuje je w bazie danych. Archiwizuj ./data/storage dla przesłanych plików i zachowaj kopię config.json ręcznie, ponieważ plik ten nie jest częścią bazy danych.
Czy współpraca w czasie rzeczywistym działa w samodzielnie hostowanym AFFiNE?
Tak, nie wymaga to żadnej dodatkowej konfiguracji. Jedynym wymogiem jest odpowiednia konfiguracja reverse proxy, ponieważ synchronizacja odbywa się przez połączenia WebSocket. W przypadku nginx oznacza to proxy_http_version 1.1 oraz nagłówki Upgrade i Connection: upgrade, podczas gdy Traefik i Caddy przekazują te połączenia bez dodatkowych ustawień. Objawem proxy, które nie obsługuje upgrade'u połączeń, jest sytuacja, w której obszar roboczy ładuje się i loguje poprawnie, ale edycje wykonane w jednej przeglądarce nie pojawiają się w drugiej.
Czy można uruchomić AFFiNE ze standardowym obrazem Postgres?
Nie. schema.prisma w AFFiNE deklaruje extensions = [pgvector(map: "vector")] i definiuje cztery tabele z kolumną embedding typu vector(1024), a zadanie migracji tworzy te tabele nawet wtedy, gdy funkcje AI są wyłączone. Użyj pgvector/pgvector:pg16, czyli Postgres 16 z wkompilowanym tym rozszerzeniem. Jeśli wskazujesz AFFiNE na zewnętrzny serwer Postgres, zainstaluj na nim pgvector i utwórz rozszerzenie w docelowej bazie danych przed uruchomieniem migracji.