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ą w formie czterech kontenerów: aplikacji, jednorazowego zadania migracji, Postgres oraz Redis. Współpraca w czasie rzeczywistym jest dostępna dla maksymalnie 10 użytkowników, co stanowi domyślny limit dla samodzielnie hostowanej przestrzeni roboczej. 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 łączy edytor dokumentów i nieskończone płótno w jednej przestrzeni roboczej, dzięki czemu tę samą stronę można przeglądać jako dokument lub rozplanować jako tablicę. Jeśli nadal rozważasz wybór oprogramowania, przeczytaj najpierw porównanie alternatyw dla Notion do samodzielnego hostowania. Niniejszy przewodnik zakłada, że decyzja została już podjęta i skupia się na prawidłowym uruchomieniu AFFiNE, a nie na ponownym porównywaniu rozwiązań.
Wszystkie informacje zostały zweryfikowane pod kątem dokumentacji self-host AFFiNE oraz plików wydania opublikowanych 8 sierpnia 2026. Najnowszą stabilną wersją w tym dniu była 0.27.3, opublikowana 23 lipca 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 typu 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 utworzenie kolumn przez migrację i sprawia, że serwer oczekuje 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 czegokolwiek należy zapoznać się ze stroną aktualizacji w dokumentacji self-host AFFiNE.
Dodatkowa uwaga dotycząca tego tagu. pg16 oznacza Postgres 16, a głównej wersji Postgres nie można zmienić poprzez zwykłą 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 wykonania zrzutu bazy danych (dump) i przywrócenia go do świeżego katalogu danych.
Ile procesora i pamięci RAM wymaga samodzielnie hostowany AFFiNE
Strona z wymaganiami AFFiNE wskazuje na co najmniej 4 rdzenie procesora i 2 GB pamięci RAM, zwiększając zapotrzebowanie do 4 GB po przekroczeniu 10 000 słów w dokumentach. 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ć w szczycie 1 GB pamięci.
Należy zestawić to z planem 2 GB pamięci przy dwóch użytkownikach. Średnie zużycie jest akceptowalne. Procesy Postgres oraz Node mieszczą się w limicie z pewnym zapasem. Problemem są wartości szczytowe. Pojedyncze, duże scalanie może wymagać 1 GB pamięci ponad to, co jest już zajęte. Na serwerze z 2 GB pamięci i bez partycji swap, mechanizm OOM (out-of-memory) killer jądra systemu reaguje na takie żądanie, zabijając największy proces, czyli serwer AFFiNE.
Współpracownik nie zobaczy błędu. Zauważy jedynie przeładowanie strony, ponieważ restart: unless-stopped przywraca kontener w ciągu kilku sekund. Nie należy zgadywać, lecz potwierdzić przyczynę:
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 na node w drugim przypadku oznacza, że zabrakło pamięci, a nie wystąpił błąd w oprogramowaniu. Należy rozwiązać ten problem dwutorowo. Najpierw należy dodać swap, aby nagły skok zapotrzebowania spowodował spowolnienie, a nie awarię:
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 wskazywać łącznie 2.0Gi swap. Swap nie przyspiesza działania AFFiNE i nie do tego służy. Zamienia on sekundowy skok zapotrzebowania na sekundę spowolnienia, zamiast doprowadzić do wyłączenia kontenera. Drugim elementem rozwiązania jest ograniczenie Postgresowi możliwości rozrastania pamięci podręcznej kosztem przestrzeni potrzebnej aplikacji podczas scalania, do czego służą limity pamięci dla usługi w Compose.
Zapotrzebowanie na pamięć masową jest znacznie łatwiejsze do przewidzenia. Oto wartości publikowane przez AFFiNE na tej samej stronie:
The data behind this chart
[
{
"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ą bliską zeru. Tysiąc przesłanych plików dodaje 10 GB, co wyczerpuje temat. Są to wartości planistyczne, a nie pomiary z działającej instancji, więc należy traktować je jako szacunek, a nie gwarancję. Istotny jest sam schemat: 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. Przed jego zastosowaniem warto jednak znać jeden szczegół: na dzień 8 sierpnia 2026 r. plik dołączony do wydania 0.27.3 nadal odczytuje ścieżki z pliku .env, używając ${UPLOAD_LOCATION}, ${CONFIG_LOCATION} oraz ${DB_DATA_LOCATION}, podczas gdy strona dokumentacji prezentuje nowszy układ, który przechowuje wszystko w ./data i nie wymaga .env. Oba rozwiązania są poprawne. Samodzielne napisanie pliku rozwiązuje ten problem, a edycja i tak jest konieczna w celu przypięcia wersji obrazów oraz ustawienia hasła 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. Ten nawyk warto stosować w każdym uruchamianym stosie, a uzasadnienie znajduje się w utrzymywanie sekretów poza plikiem compose.
Teraz należy utworzyć ~/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-stoppedWprowadzono cztery różnice względem pliku dostarczanego przez twórców oprogramowania, z których każda ma swoje uzasadnienie.
127.0.0.1:3010:3010publikuje port wyłącznie na adresie loopback, dzięki czemu nic spoza serwera nie uzyska dostępu do AFFiNE, dopóki nie zostanie podjęta decyzja o sposobie udostępnienia. 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 ustawionego 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 akceptowalne do momentu podłączenia kolejnego kontenera do tej sieci lub opublikowania portu 5432 podczas debugowania.redis:8-alpinezastępuje ogólneredis, które wskazuje nalatest. Na sierpień 2026 r. jest to Redis 8, więc przypięcie wersji zachowuje przetestowaną wersję główną i zapobiega nieoczekiwanej aktualizacji do Redis 9 podczas niezwiązanego z tymdocker compose pull.pgvector/pgvector:pg16pozostaje w formie zdefiniowanej przez twórców, zgodnie z powyższym uzasadnieniem.
POSTGRES_PASSWORD jest odczytywane tylko wtedy, gdy Postgres tworzy 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 linki do udostępniania oraz zaproszenia do obszarów roboczych na podstawie tej wartości. Jeśli pozostanie ona ustawiona na http://localhost:3010, wysłane zaproszenie będzie kierować odbiorcę na jego własną maszynę, co zakończy się błędem. Ustaw publiczny adres HTTPS przed pierwszym uruchomieniem, aby zapewnić spójność między plikiem konfiguracyjnym a panelem administratora.
copilot steruje funkcjami AI. copilot.byok.enabled to przełącznik umożliwiający korzystanie z własnego klucza API, który pozwala właścicielowi obszaru roboczego na wklejenie klucza dostawcy modelu w ustawieniach obszaru. Samodzielna instalacja AFFiNE nie obejmuje subskrypcji AI. Pozostaw obie wartości false, jeśli nie chcesz korzystać z tych funkcji.
Uruchom stos:
docker compose up -d
docker compose psdocker compose ps powinno wykazać, że affine_postgres oraz affine_redis są w stanie healthy, affine_server działa, a affine_migration_job znajduje się w stanie exited (0). Każdy inny kod wyjścia zadania migracji wymaga analizy; dziennik tego zadania wskaże krok, który spowodował zatrzymanie:
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 plik compose powyżej 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 działający tunel 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ą nazwą domenową.
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 oraz uprawnienia../data/storagejest montowany w/root/.affine/storagewewnątrz kontenera i przechowuje wszystkie przesłane pliki../configjest montowany w/root/.affine/configi przechowujeconfig.json.
Autorzy oprogramowania stosują tutaj bind mounts zamiast nazwanych wolumenów, co jest wyborem celowym: ścieżki te można archiwizować za pomocą tar i kopiować zwykłymi poleceniami, bez konieczności sprawdzania w Docker, gdzie zostały umieszczone. Ceną tego rozwiązania jest konieczność samodzielnego zarządzania uprawnieniami do plików na hoście, co jest kompromisem omówionym w bind mounts a nazwane wolumeny.
Jak wykonać kopię zapasową AFFiNE
Należy zabezpieczyć dwa elementy, a proces ich archiwizacji różni się. Baza danych jest aktywnym serwerem, 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 za pośrednictwem lokalnego gniazda, więc nie wymaga podania hasła. Należy sprawdzić rozmiar 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 użytkownicy odkrywają 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 pliki, 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 zabezpieczyć w kopii zapasowej ręcznie. Dokumentacja AFFiNE w sierpniu 2026 roku nadal wskazuje, że eksport konfiguracji z panelu administratora nie jest zaimplementowany, więc plik na dysku jest jedyną kopią ustawień. Wszystkie trzy pliki należy skopiować poza serwer. Kopia zapasowa znajdująca się na tym samym dysku co chronione dane nie jest kopią zapasową.
Przywracanie danych i pułapka w opublikowanej instrukcji
Należy zapoznać się z oficjalną procedurą przywracania danych przed wystąpieniem awarii i dokładnie ją przeanalizować. W wersji opublikowanej w sierpniu 2026 r. instrukcja kopiuje plik o nazwie affine.backup do kontenera, a następnie wykonuje przywracanie z ./pg.backup – są to dwie różne nazwy. Ponadto instrukcja usuwa katalog ./postgres, podczas gdy bieżący plik compose przechowuje dane w ./data/postgres. Należy stosować ścieżki faktycznie użyte w danej konfiguracji, a nie te podane w przykładzie. Poniżej znajduje się sekwencja operacji zgodna z układem plików 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 całkowitej utraty danych w wyniku jednego błędnego polecenia; przeniesienie starego katalogu w inne miejsce nic nie kosztuje. Należy również przywrócić pliki przesłane przez użytkowników 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. To jest test poprawności. Przywrócona kopia, która nie została sprawdzona w przeglądarce, pozostaje jedynie plikiem, a nie działającym backupem.
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 wersji self-hosted
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ć zachowanie w trybie offline, od którego zależy zespół: edytować dokument 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ż tego zestawienia.
Wyszukiwanie pełnotekstowe po stronie serwera jest wyłączone w dostarczonym pliku compose, gdzie AFFINE_INDEXER_ENABLED=false jest ustawione zarówno dla serwera, jak i zadania 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 powoduje przekroczenie dostępnych zasobów. Wyszukiwanie wewnątrz klienta nadal działa w ramach otwartego obszaru roboczego.
Przed zaproszeniem użytkowników warto znać dwa ograniczenia. Obszar roboczy w wersji self-hosted jest ograniczony do maksymalnie 10 stanowisk, a przekroczenie tej liczby wymaga licencji Team od AFFiNE. Nielimitowana pamięć na obiekty (blob storage) oraz brak limitów rozmiaru obiektów dla instancji self-hosted są opisane w dokumentacji jako zamierzone, lecz w sierpniu 2026 roku nie były jeszcze w pełni zaimplementowane. Ż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 czterdziestoosobowej grupy.
Aktualizacje
Przed przystąpieniem do prac należy zapoznać się z informacjami o wydaniu, szczególnie w przypadku aktualizacji wersji mniejszej, takiej jak 0.26 do 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 modyfikuje schemat przy następnym uruchomieniu i nie posiada funkcji cofania zmian. Następnie należy zmienić przypięty skrót (digest), wykonać docker compose pull, a po nim docker compose up -d, monitorując docker compose logs -f affine_migration do momentu poprawnego zakończenia procesu. Polecenie docker image prune służy do usunięcia starych warstw obrazu po zakończeniu aktualizacji. Uwaga historyczna dla użytkowników bardzo starych instalacji: od wersji 0.23.0 nazwa obrazu uległa zmianie z affine-graphql na affine, dlatego w plikach compose starszych niż ta wersja należy zaktualizować linie definiujące obraz, aby operacja pull mogła pobrać nową wersję.
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 w ogóle się nie pojawi. Uruchom docker compose logs affine_migration, aby sprawdzić, na którym kroku nastą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 deklaruje rozszerzenie pgvector i tworzy tabele z kolumnami vector(1024), których zwykły 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, gdy dokumenty przekraczają 10 000 słów. Zauważono, że scalanie dokumentu z 10 000 modyfikacji może wymagać nawet 1 GB pamięci. Na serwerze z 2 GB RAM to właśnie ten skok zużycia powoduje problemy, a nie obciążenie w stanie spoczynku: mechanizm out-of-memory killer jądra systemu zatrzymuje proces AFFiNE, a restart: unless-stopped uruchamia go ponownie, przez co użytkownicy widzą przeładowanie strony zamiast błędu. Potwierdź to za pomocą docker inspect affine_server --format '{{.State.OOMKilled}}' i sudo dmesg -T | grep -i 'out of memory', a następnie dodaj plik wymiany (swap) o rozmiarze 2 GB, aby nagłe skoki zużycia powodowały spowolnienie, a nie awarię.
Gdzie AFFiNE przechowuje dane i co należy archiwizować?
Wszystkie dane znajdują się w trzech ścieżkach w katalogu compose: ./data/postgres dla bazy danych, ./data/storage dla przesłanych plików oraz ./config dla config.json. Wykonuj kopię zapasową bazy danych za pomocą docker compose exec -T postgres pg_dump --format c --username affine affine > affine.dump, zamiast kopiować pliki bezpośrednio, ponieważ działającego procesu Postgres nie można bezpiecznie skopiować. Archiwizuj ./data/storage dla przesłanych plików i przechowuj kopię config.json ręcznie, ponieważ eksport konfiguracji z panelu administratora nie został jeszcze zaimplementowany (stan na sierpień 2026).
Czy współpraca w czasie rzeczywistym działa w samodzielnie hostowanym AFFiNE?
Tak, nie wymaga to żadnej dodatkowej konfiguracji. Jedynym wymogiem jest odpowiedni 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 dodatkowej konfiguracji. Objawem proxy, które nie obsługuje upgrade'u połączeń, jest sytuacja, w której obszar roboczy ładuje się i loguje poprawnie, ale zmiany wprowadzone w jednej przeglądarce nie pojawiają się w drugiej.
Czy mogę 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 ze skompilowanym 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.