Instalacja edycji PHP
Edycję PHP wgrywa się taką, jaka jest, na hosting współdzielony: bez Composera, bez menedżera pakietów, bez usługi, którą trzeba utrzymywać przy życiu. Jeden katalog, jeden plik konfiguracyjny, jedno zadanie cykliczne. Ten przewodnik prowadzi od pustego katalogu do instancji produkcyjnej, łącznie z tym, co większość poradników pomija: uprawnienia, TLS, zadanie cykliczne, kopie zapasowe i aktualizacja.
Czego potrzeba#
| Element | Wersja | Dlaczego |
|---|---|---|
| PHP | 8.1 lub nowszy | Typy sumy, enum, readonly, never: kod ich używa. |
pdo_sqlite | — | Cała baza. Bez tego nic nie ruszy. |
mbstring | — | Poprawna obsługa napisów w szesnastu językach. |
openssl | zalecane | Szyfrowanie sekretów w bazie (hasła SMTP, tokeny wysyłki na zewnątrz). |
curl | zalecane | Webhooki wychodzące i składanie kopii u podmiotu zewnętrznego. |
intl | zalecane | Daty zapisywane jak w edycji Node. Bez tego nazwy miesięcy pochodzą ze słowników. |
Kreator instalacji sprawdza to wszystko i pokazuje na ekranie, zanim pozwoli iść dalej: to, co blokuje, zatrzymuje instalację; to, co opcjonalne, zostaje zgłoszone, ale nie blokuje.
Nie ma czego instalować composerem: czytnik IMAP, archiwum
tar.gz, PDF, CSV, kod QR i klient HTTP są napisane wewnątrz produktu. To
właśnie pozwala wgrać go na hosting, na którym nie masz wiersza poleceń.
1. Wgrać pliki#
Pobierz archiwum edycji PHP i wgraj je. Katalog wygląda tak:
toutadmin/
├── app/ kod (serwer WWW nigdy go nie udostępnia)
│ ├── Core/ jądro: trasowanie, baza, sesje, bezpieczeństwo
│ ├── Modules/ dziedzina: kadry, finanse, projekty, jakość…
│ ├── Controllers/
│ ├── views/ szablony
│ ├── locales/ szesnaście słowników
│ └── schema.sql schemat, identyczny jak w edycji Node
├── public/ ← katalog główny serwera wskazuje tutaj i nigdzie indziej
│ ├── index.php kontroler wejściowy
│ ├── css/ js/
├── data/ baza, sejf, teczka podpisów, otrzymane dokumenty, kopie
├── tools/ cron.php, check-keys.php
├── tests/
├── config.sample.php
└── VERSION
public/, a nie na cały katalog
To najważniejszy punkt tej strony. Jeśli serwer udostępnia cały katalog,
data/app.sqlite staje się do pobrania dla każdego: cała baza, wraz ze skrótami
haseł i sejfem. Katalog data/ leży powyżej public/ celowo.
Na hostingu współdzielonym#
Bywa dwojako. Jeśli hostingodawca pozwala wybrać katalog główny domeny (cPanel, Plesk i
większość hostingów w trybie „katalog główny”), wgraj katalog poza www/ i skieruj
domenę na toutadmin/public.
Jeśli katalog główny jest narzucony — często www/ albo
public_html/ — wgraj tam zawartość public/, a resztę
powyżej, po czym popraw ścieżkę na początku
public/index.php:
www/ ← narzucony katalog główny
├── index.php
├── css/ js/
toutadmin/ ← reszta, poza zasięgiem sieci
├── app/
├── data/
└── config.php
// www/index.php — wiersz do poprawienia
require_once __DIR__ . '/../toutadmin/app/bootstrap.php';
W ostateczności, gdy nic nie da się umieścić powyżej katalogu głównego, dostarczony plik
.htaccess w data/ już odmawia dostępu. To pas bezpieczeństwa, a nie
rozwiązanie: zawsze wybieraj katalog poza katalogiem głównym.
2. Zapisać konfigurację#
cp config.sample.php config.php
Potem otwórz config.php:
<?php
return [
// Baza SQLite i katalog danych: poza katalogiem głównym serwera.
'db_path' => __DIR__ . '/data/app.sqlite',
'data_dir' => __DIR__ . '/data',
// Klucz właściwy tej instancji. Wygenerować raz:
// php -r "echo bin2hex(random_bytes(32));"
'session_secret' => '…',
// Token instalacyjny: patrz następny krok.
'install_token' => '',
// Adres publiczny, dla odsyłaczy wysyłanych pocztą.
'base_url' => 'https://intranet.przyklad.pl',
// Włączać tylko za zaufanym serwerem pośredniczącym.
'trust_proxy' => false,
// Pułapy: próby logowania na kwadrans i na adres,
// żądania na minutę i na adres.
'login_rate_limit' => 10,
'global_rate_limit' => 300,
// Wygasanie: bezczynność, a potem czas bezwzględny, którego nic nie przedłuża.
'session_idle_minutes' => 60,
'session_max_hours' => 12,
];
Pieczętuje podpisy w teczce podpisów i szyfruje sekrety w bazie. Jego utrata unieważnia
podpisy. Jeśli zostawisz wartość przykładową, produkt sam wygeneruje klucz w
data/.instance-key (uprawnienia 0600) — pamiętaj wtedy, by objąć
ten plik kopiami zapasowymi.
Konfigurację można też trzymać gdzie indziej: zmienna środowiskowa
TOUTADMIN_CONFIG wskazuje inny plik.
3. Ustawić token instalacyjny#
Między wgraniem plików a Twoim wejściem do kreatora instancja należy do tego, kto ją znajdzie: pierwszy przybysz zakłada konto administratora. Okno jest krótkie, ale wystarczy. Ustaw więc losową wartość przed publikacją:
php -r "echo bin2hex(random_bytes(16));"
# a potem w config.php:
'install_token' => 'c3f1…',
Kreator o niego prosi, porównuje go w stałym czasie — zwykłe porównanie pozwoliłoby zgadywać token znak po znaku — i zapisuje każdą odmowę. Bez tokenu nic nie powstaje. Po zainstalowaniu instancji kreator zamyka się sam: token może zostać, już do niczego nie służy.
4. Uprawnienia#
| Ścieżka | Prawa | Kto zapisuje |
|---|---|---|
data/ | 0770 | Serwer WWW i tylko on |
data/app.sqlite | 0660 | Tworzone przez instalację |
config.php | 0640 | Nikt — dla serwera tylko do odczytu |
app/, public/ | 0755 | Nikt — tylko do odczytu |
# przykład na serwerze, gdzie PHP działa jako www-data
sudo chown -R ty:www-data /var/www/toutadmin
sudo find /var/www/toutadmin -type d -exec chmod 750 {} \;
sudo find /var/www/toutadmin -type f -exec chmod 640 {} \;
sudo chmod 770 /var/www/toutadmin/data
sudo chmod 750 /var/www/toutadmin/public
SQLite zapisuje też pliki sąsiadujące (-wal, -shm): zapisywalny musi
być katalog, nie tylko sama baza.
5. Serwer WWW#
Nginx#
server {
listen 443 ssl http2;
server_name intranet.przyklad.pl;
root /var/www/toutadmin/public; # i nic innego
index index.php;
ssl_certificate /etc/letsencrypt/live/intranet.przyklad.pl/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/intranet.przyklad.pl/privkey.pem;
client_max_body_size 20M; # wrzuty do sejfu, otrzymane dokumenty
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# Wgrane pliki nigdy nie są udostępniane wprost: idą przez trasę,
# która sprawdza sesję i skrót.
location ~ ^/(data|app|tools|tests)/ { deny all; }
}
server {
listen 80;
server_name intranet.przyklad.pl;
return 301 https://$host$request_uri;
}
Apache#
<VirtualHost *:443>
ServerName intranet.przyklad.pl
DocumentRoot /var/www/toutadmin/public
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/intranet.przyklad.pl/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/intranet.przyklad.pl/privkey.pem
<Directory /var/www/toutadmin/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
Dostarczony plik public/.htaccess już kieruje wszystkie adresy do
index.php. Na hostingu współdzielonym z Apache to wszystko, czego trzeba.
TLS#
Produkt ustawia nagłówek Strict-Transport-Security, gdy tylko połączenie jest
szyfrowane, i nigdy otwartym tekstem: ogłoszony ze strony nieszyfrowanej nie zostałby
przeczytany, a lokalną próbę zamknąłby w https na pół roku. Na serwerze, którym
sam zarządzasz:
sudo certbot --nginx -d intranet.przyklad.pl
Za serwerem pośredniczącym albo rozdzielaczem obciążenia, który kończy TLS, ustaw
'trust_proxy' => true — i tylko tam: inaczej każdy mógłby ogłosić w nagłówku
dowolny adres IP i dowolny protokół.
6. Zadanie cykliczne#
Witryna PHP żyje tylko w chwili żądania. To, co edycja Node robi w swoim cogodzinnym przebiegu — osiem operacji — tutaj robi cron hostingu:
* * * * * /usr/bin/php /var/www/toutadmin/tools/cron.php >> /var/log/toutadmin-cron.log 2>&1
| Co robi zadanie | W jakim rytmie |
|---|---|
| Zamyka konta, którym skończyła się umowa | przy każdym przebiegu |
| Wystawia faktury abonamentów, którym nadszedł termin | przy każdym przebiegu, nigdy dwa razy |
| Zamienia terminy w powiadomienia | przy każdym przebiegu, bez powtórzeń |
| Czyści przeczytane powiadomienia i dziennik audytu | zgodnie z wybranym okresem przechowywania |
| Pobiera księgową skrzynkę pocztową (IMAP) | jeśli pobieranie jest skonfigurowane |
| Opróżnia kolejkę webhooków wraz z ponowieniami | przy każdym przebiegu |
| Tworzy kopię automatyczną i wysyła ją na zewnątrz | gdy minie ustalony odstęp |
Wywoływanie skryptu częściej niż wynosi ustalony odstęp nie tworzy kopii częściej: nic nie
ruszy przed terminem. Na hostingu, który ogranicza cron do jednego przebiegu na kwadrans,
zamień * * * * * na */15 * * * * — webhooki wyruszą po prostu z tym
opóźnieniem.
Niektóre pakiety najniższej półki go nie mają. Wywołanie HTTP z usługi zewnętrznej
(monitora dostępności i podobnych) nie wystarczy: cron.php odmawia
uruchomienia poza wierszem poleceń, właśnie po to, by publiczny adres nie mógł wyzwolić
kopii zapasowej. W takim razie kopię uruchamia się ręcznie z ekranu
Kopie zapasowe.
7. Kreator#
Otwórz swoją domenę. Dopóki nie istnieje żadne konto, każdy adres przekierowuje do
/installation. Kreator mieści się na jednej stronie:
- Wymagania, sprawdzone i pokazane — to, co blokuje, jest tak oznaczone.
- Token instalacyjny, jeśli został ustawiony.
- Firma: nazwa wyświetlana wszędzie oraz liczba dni urlopu rocznego przyznawana każdemu nowemu pracownikowi na etacie.
- Język domyślny instancji, spośród 16.
- Konto administratora: adres i hasło o długości co najmniej dwunastu znaków.
Wszystko zapisuje się w jednej transakcji. Gdy tylko konto istnieje,
/installation odsyła do strony logowania: kreator zamknął się sam, nie ma
żadnego pliku do usunięcia ręcznie.
8. Sprawdzić, że wszystko działa#
# testy, na maszynie (nie zapisują niczego w Twojej bazie)
php tests/run.php
# tłumaczenia: żaden ekran nie używa klucza, którego brak w słownikach
php tools/check-keys.php
Potem, w interfejsie: utwórz pracownika, zaloguj się na niego, wgraj załącznik, uruchom kopię ręczną i sprawdź ją na ekranie Kopie zapasowe. Te cztery gesty dotykają bazy, plików, uprawnień i archiwum — czyli wszystkiego, co hosting musi umieć.
9. Aktualizacja#
# 1. najpierw kopia zapasowa, zawsze
php tools/cron.php # albo ekran Kopie zapasowe
# 2. wymienić kod, nie dane
# app/ public/ tools/ tests/ VERSION ← wymieniane
# data/ config.php ← zachowywane bez zmian
# 3. baza aktualizuje się sama przy pierwszym żądaniu
Schemat zmienia się przez migracje idempotentne: powtórzenie aktualizacji niczego nie psuje. Instancja edycji Node i instancja edycji PHP mają ten sam schemat — 142 tabel, 1311 kolumn — i te same hasła: baza przechodzi z jednej do drugiej bez konwersji.
10. Robić kopie naprawdę#
Archiwum tar.gz niesie bazę (skopiowaną przez VACUUM INTO, więc
spójną nawet w trakcie zapisu) oraz pięć katalogów plików: zdjęcia profilowe, otrzymane
życiorysy, sejf, teczkę podpisów, otrzymane dokumenty. Każdy plik niesie tam swój skrót
SHA-256, sprawdzany przy odtwarzaniu.
Archiwum, które zostaje na serwerze, który chroni, nie chroni niczego: skonfiguruj cel zdalny (FTPS albo Google Drive) na ekranie Kopie zapasowe. Szczegóły są na stronie Kopie zapasowe i odtwarzanie.
Najpierw spróbować lokalnie#
cp config.sample.php config.php
php -S localhost:8000 -t public public/index.php
Wbudowany serwer PHP wystarczy, by poznać produkt. Do produkcji się nie nadaje: jeden proces, brak TLS, brak ograniczania obciążenia.
Dokumentacja Toutadmin — zbudowana 2026-09-13. Witryna samodzielna, niezależna od oprogramowania.