Files
servicedesk/install.md
Kacper 0d116dfd98 v1.2.0
- E-mail intake (IMAP), optional and off by default: clients can create a
  ticket or reply to an existing one just by sending/replying to an e-mail.
  Configure any number of mailboxes in the new Admin > Poczta page (SMTP +
  IMAP together, replacing the old "E-MAIL" tab), each routed to a specific
  subcategory or a whole category (new tickets.category_id column). Replies
  are matched to their ticket via the number/checksum already in every
  notification subject; autoresponders/bounces are detected and rejected;
  "restrict tickets to LDAP" is enforced for e-mail like the guest web form.
  Manual "Pobierz teraz" per-mailbox fetch button; dedicated
  storage/logs/imap-*.log regardless of the app's log level; mail-icon badges
  on e-mail-originated tickets/messages in the operator queue and ticket view.
- Operator queue: "select all" checkbox in the table header for every
  currently visible ticket under the active filter/tab.
- Fixed: scheduled commands (SLA breach check, automation rules, and now IMAP
  fetch) always sent notifications through .env's default mailer instead of
  the configured SMTP server, because AppServiceProvider's Settings override
  used to skip itself for any console command, not just migrate.
- Fixed: visiting a ticket that no longer exists (deleted mid-session, or a
  stale background refresh) showed a raw 404 instead of redirecting back to
  the operator queue / client dashboard.
- Docs: README/ARCHITECTURE/CLAUDE/install/wiki updated for all of the above,
  including the previously-missing host crontab entry for schedule:run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 12:44:30 +02:00

526 lines
19 KiB
Markdown

# Instalacja / wdrożenie
Dwa sposoby uruchomienia Servicedesk: przez **Docker Compose** (tak jak działa ten
projekt obecnie) albo **bezpośrednio na serwerze** z Apache/Nginx + PHP-FPM, bez
kontenerów.
Zanim zaczniesz, ważne rozróżnienie — w projekcie są **dwa różne pliki `.env`**:
| Plik | Do czego służy |
|---|---|
| `.env` (w katalogu głównym repo, obok `compose.yaml`) | Zmienne dla **Docker Compose** — hasła do MariaDB, nazwa hosta dla Traefika. Czyta go `compose.yaml`, **nie** czyta go Laravel. |
| `src/.env` (w katalogu aplikacji Laravel) | Cała konfiguracja **aplikacji** — połączenie z bazą, adres URL, klucz szyfrowania, LDAP, SMTP, itd. Czyta go wyłącznie Laravel. |
Oba istnieją niezależnie od siebie (nawet w wersji Docker) — wartości `DB_*` w
`src/.env` muszą **odpowiadać** wartościom `MYSQL_*` z głównego `.env`, ale to dwa
osobne pliki, w dwóch różnych miejscach.
---
## 1. Wdrożenie przez Docker Compose
### Wymagania
- Docker + wtyczka `docker compose`.
- Zewnętrzna sieć Docker `traefik_public`, jeśli używasz Traefika tak jak w
`compose.yaml` (`docker network create traefik_public`, jeśli jeszcze nie
istnieje). Bez Traefika trzeba samodzielnie zmapować porty serwisu `servicedesk`
na hosta (`ports: ["8080:80"]`) i obsłużyć TLS inaczej (patrz sekcja 2 niżej, w
razie potrzeby reverse-proxy przed kontenerem).
### 1.1. Pliki `compose.yaml` i `.env` w katalogu głównym
Oba są celowo `.gitignore`'owane (podobnie jak `src/.env`) — kopiujesz je z
szablonów przy pierwszym wdrożeniu, a potem edytujesz lokalnie:
```bash
cp compose.yaml.example compose.yaml
cp .env.example .env
```
`.env`:
```env
HOSTNAME=servicedesk.twoja-domena.pl
MYSQL_ROOT_PASSWORD=wygeneruj-silne-haslo
MYSQL_DATABASE=servicedesk
MYSQL_USER=servicedesk
MYSQL_PASSWORD=wygeneruj-inne-silne-haslo
# IMAGE_TAG=latest
```
- `HOSTNAME` — domena, pod którą Traefik wystawi aplikację (trafia do reguły
`Host(...)` w etykietach `servicedesk`).
- `MYSQL_*` — dane bazy dla kontenera `mariadb`; `MYSQL_USER`/`MYSQL_PASSWORD` to
**te same** wartości, które za chwilę wpiszesz do `src/.env` jako `DB_USERNAME`/
`DB_PASSWORD`.
- `IMAGE_TAG` — który tag obrazu `servicedesk` wdrożyć (patrz sekcja 1.3a); domyślnie
`latest`, jeśli zmienna nie jest ustawiona.
### 1.2. Plik `src/.env` (Laravel)
```bash
cp src/.env.example src/.env
```
Kluczowe wartości do zmiany względem `.env.example`:
```env
APP_NAME=Servicedesk
APP_ENV=production
APP_DEBUG=false
APP_URL=https://servicedesk.twoja-domena.pl # to samo co HOSTNAME wyżej, z https://
APP_LOCALE=pl
APP_FALLBACK_LOCALE=pl
AUTHOR_CONTACT=helpdesk@twoja-domena.pl # widoczne w Admin > O aplikacji
VERSION=1.1.3 # widoczne w Admin > O aplikacji
DB_CONNECTION=mysql
DB_HOST=mariadb # nazwa serwisu z compose.yaml, NIE 127.0.0.1
DB_PORT=3306
DB_DATABASE=servicedesk # = MYSQL_DATABASE z głównego .env
DB_USERNAME=servicedesk # = MYSQL_USER
DB_PASSWORD=wygeneruj-inne-silne-haslo # = MYSQL_PASSWORD
SESSION_DRIVER=database
QUEUE_CONNECTION=database
```
`APP_KEY` wygenerujesz komendą artisan (krok 1.4) — zostaw puste w pliku.
`LDAP_*` i `MAIL_*` w `src/.env` są tylko **wartościami startowymi/awaryjnymi**.
Docelowo LDAP i SMTP konfiguruje się wygodniej z poziomu **Admin > Konfiguracja**
w samej aplikacji (patrz ramka ostrzegawcza w kroku 1.6) — ale jeśli chcesz mieć
sensowny fallback zanim ktokolwiek się zaloguje do panelu admina, warto je od razu
uzupełnić:
```env
LDAP_HOST=ldap.twoja-domena.pl
LDAP_USERNAME=cn=admin,dc=twoja-domena,dc=pl
LDAP_PASSWORD=haslo-bind-ldap
LDAP_BASE_DN=dc=twoja-domena,dc=pl
LDAP_PORT=389
MAIL_MAILER=smtp
MAIL_HOST=smtp.twoja-domena.pl
MAIL_PORT=587
MAIL_USERNAME=noreply@twoja-domena.pl
MAIL_PASSWORD=haslo-smtp
MAIL_FROM_ADDRESS=noreply@twoja-domena.pl
MAIL_FROM_NAME="${APP_NAME}"
```
### 1.3. Start kontenerów
Obraz `servicedesk` **nie jest budowany lokalnie**`compose.yaml` odwołuje się
do obrazu zbudowanego przez CI i wypchniętego do rejestru kontenerów Gitea (patrz
1.3a). Uruchomienie stacka to więc zawsze:
```bash
docker compose pull
docker compose up -d
```
Przy zwykłych zmianach w kodzie PHP/Blade nic więcej nie trzeba robić — źródło
jest zamontowane z `./src`, Laravel odświeża się natychmiast. `docker compose
pull` uruchamiaj ponownie tylko wtedy, gdy chcesz podnieść nowszy tag obrazu
(np. po zmianie w `Dockerfile` i przebudowie przez CI).
### 1.3a. Automatyczne budowanie obrazu (CI, Gitea Actions)
`.gitea/workflows/build.yml` buduje i wypycha obraz do wbudowanego rejestru
kontenerów Gitea (`gitea.kzbikowski.pl/kzbkowski/servicedesk`) po każdym pushu na
`main`, który zmienia `Dockerfile` (celowo nie odpala się na zwykłe zmiany w
`src/` — obraz nie zawiera kodu aplikacji, tylko PHP/Apache/rozszerzenia, więc
przebudowa dla samego kodu byłaby marnowaniem czasu CI). Wypycha dwa tagi:
`latest` i `<sha commita>`.
Zanim to zadziała, workflow potrzebuje sekretu `REGISTRY_TOKEN` — Gitei **nie**
ufaj domyślnemu `secrets.GITHUB_TOKEN` do logowania w jej własnym rejestrze
kontenerów, w praktyce kończy się to błędem `unauthorized` przy
`docker login`. Zamiast tego:
1. Wygeneruj token: **Ustawienia użytkownika > Aplikacje > Generate New Token**,
z uprawnieniami co najmniej `write:package` i `read:package`.
2. Dodaj go jako sekret repo: **Ustawienia repo > Actions > Secrets**
nazwa `REGISTRY_TOKEN`, wartość = wygenerowany token.
Jeśli ten sekret nie jest ustawiony, workflow celowo przerywa się **przed**
próbą logowania z czytelnym komunikatem błędu (`::error::`), zamiast wysyłać
puste/nieautoryzowane dane do rejestru i kończyć na niejasnym `unauthorized` z
demona Dockera.
To **tylko build + push** — świadomie bez auto-deployu na produkcję. Po tym jak
CI skończy, wdrożenie nowego obrazu na serwerze wciąż jest ręcznym krokiem:
```bash
cd /ścieżka/do/repo
docker compose pull
docker compose up -d
```
Zanim to zadziała po raz pierwszy, potrzebne jest jednorazowe zalogowanie hosta
produkcyjnego do rejestru Gitea (żeby `docker compose pull` miał czym
autoryzować pobranie obrazu, jeśli repozytorium/paczka nie są publiczne):
```bash
docker login gitea.kzbikowski.pl -u <twoja-nazwa-uzytkownika>
```
Jeśli to zupełnie pierwsze wdrożenie (rejestr jeszcze nie ma żadnego wypchniętego
obrazu `servicedesk`) — poczekaj, aż workflow CI przejdzie choć raz (np. przez
push/PR zmieniający `Dockerfile`, albo ręczne odpalenie z zakładki Actions w
Gitea), zanim spróbujesz `docker compose pull` na serwerze.
### 1.3b. Real-time (Laravel Reverb)
Realtime (kolejka operatora, czat na żywo) wymaga trzeciej usługi w
`compose.yaml`, `reverb` — tego samego obrazu `servicedesk`, tylko z innym
`command: php artisan reverb:start --host=0.0.0.0 --port=8080`. Obraz musi
mieć rozszerzenia PHP `pcntl`/`posix` (potrzebne serwerowi Reverb do obsługi
sygnałów) — jeśli `Dockerfile` ich nie instaluje, `reverb` będzie się zapętlać
w restartach z błędem `Undefined constant "...SIGINT"`; dodaj `pcntl posix` do
listy w `docker-php-ext-install` i poczekaj na przebudowanie obrazu przez CI.
Traefik musi kierować ścieżkę websocketu (`/app*`) do `reverb`, a resztę do
`servicedesk` — na tej samej domenie, więc bez dodatkowego wpisu DNS/certyfikatu:
```yaml
reverb:
image: gitea.kzbikowski.pl/kzbkowski/servicedesk:${IMAGE_TAG:-latest}
command: php artisan reverb:start --host=0.0.0.0 --port=8080
volumes:
- ./src:/var/www/html
restart: unless-stopped
depends_on:
mariadb:
condition: service_healthy
networks:
- internal
- traefik_public
labels:
- traefik.enable=true
- traefik.docker.network=traefik_public
- traefik.http.routers.reverb.rule=Host(`${HOSTNAME}`) && PathPrefix(`/app`)
- traefik.http.routers.reverb.priority=1000
- traefik.http.routers.reverb.entrypoints=websecure
- traefik.http.routers.reverb.tls=true
- traefik.http.services.reverb.loadbalancer.server.port=8080
```
`priority=1000` jest ważne — Traefik domyślnie liczy priorytet reguły na
podstawie długości jej zapisu, co może dać samemu `Host(...)` z `servicedesk`
wyższy priorytet niż oczekiwano, przez co ścieżkowa reguła `reverb` przegrywa i
nic nie działa mimo poprawnej konfiguracji.
W `src/.env``BROADCAST_CONNECTION=reverb` plus:
```env
REVERB_APP_ID=wygeneruj-losowy-id
REVERB_APP_KEY=wygeneruj-losowy-klucz
REVERB_APP_SECRET=wygeneruj-losowy-sekret
REVERB_HOST=reverb # nazwa usługi compose — ruch serwer-serwer po sieci internal
REVERB_PORT=8080
REVERB_SCHEME=http
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST=servicedesk.twoja-domena.pl # publiczna domena — to, z czym łączy się przeglądarka
VITE_REVERB_PORT=443
VITE_REVERB_SCHEME=https
```
`REVERB_HOST` (serwer→serwer, wewnętrzna sieć Docker) i `VITE_REVERB_HOST`
(przeglądarka→Traefik, publiczna domena) to celowo dwie różne wartości —
pomylenie ich to najczęstszy błąd przy pierwszym wdrożeniu tej funkcji.
Po zmianie zmiennych `VITE_REVERB_*` trzeba przebudować front-end (krok 1.5) —
te wartości są wypiekane w zbudowany bundle JS, nie czytane w runtime.
### 1.4. Instalacja aplikacji wewnątrz kontenera
```bash
docker compose exec servicedesk composer install --no-dev --optimize-autoloader
docker compose exec servicedesk php artisan key:generate
docker compose exec servicedesk php artisan migrate --seed
docker compose exec servicedesk php artisan storage:link
```
`migrate --seed` (bez `--fresh`) na pustej bazie utworzy wszystkie tabele i
wypełni dane referencyjne: kategorie/podkategorie, pola dodatkowe, statusy,
priorytety/SLA, zespoły, szybkie akcje, szablony odpowiedzi/e-mail, branding oraz
**jedno konto lokalne `admin@example.com` / `admin`** — zmień jego hasło od razu
po pierwszym zalogowaniu (Admin > Użytkownicy).
### 1.5. Zbudowanie zasobów front-endowych (CSS/Tailwind)
Ani host, ani kontener `servicedesk` nie mają zainstalowanego Node.js — buduj
przez jednorazowy kontener `node:22` zamiast dorzucać Node do obrazu aplikacji:
```bash
cd /ścieżka/do/repo
docker run --rm -v "$(pwd)/src":/app -w /app node:22 npm ci
docker run --rm -v "$(pwd)/src":/app -w /app node:22 npm run build
```
Powtarzaj drugi krok po każdej zmianie w `resources/css/` lub `resources/js/`.
### 1.6. Zadanie cykliczne (SLA, automatyzacje, poczta IMAP) i kolejka
`routes/console.php` planuje `tickets:check-sla-breaches` i `automation:run-rules`
co 15 minut oraz `emails:fetch-imap` (odbieranie zgłoszeń/odpowiedzi e-mailem —
patrz Admin > Poczta) co 5 minut, ale **obraz Dockera nie ma wbudowanego
cron/supervisora** — bez dodatkowego kroku żadne z tych zadań nigdy się nie
uruchomi (poczta IMAP nadal da się sprawdzić ręcznie przyciskiem „Pobierz teraz”,
ale bez crona nic nie dzieje się samo). Najprościej dodać wpis crona **na hoście**:
```cron
* * * * * cd /ścieżka/do/repo && docker compose exec -T servicedesk php artisan schedule:run >> /dev/null 2>&1
```
Powiadomienia e-mail wysyłają się synchronicznie (nie trafiają do kolejki), więc
`php artisan queue:work` nie jest obowiązkowy — `QUEUE_CONNECTION=database` w
`.env` wystarcza jako bezpieczny domyślny driver, gdyby coś w przyszłości zaczęło
kolejkować zadania.
### ⚠️ Ważne: LDAP/SMTP z panelu Admina nadpisują `.env` w locie
`AppServiceProvider` na starcie żądania sprawdza tabelę `settings` — jeśli w
Admin > Konfiguracja pole **host LDAP** albo **SMTP włączony + host** jest
ustawione, **te wartości wygrywają z `.env`**, bez potrzeby restartu czy redeployu.
Po świeżym `migrate --seed` te pola zawierają **przykładowe placeholdery**
(`ldap.example.com`, `smtp.example.com`, `changeme-*-password`) — to znaczy, że
zaraz po instalacji LDAP/SMTP **faktycznie próbują łączyć się z tymi fałszywymi
adresami**, nawet jeśli w `.env` wpisałeś prawdziwe dane! Zanim oddasz system do
użytku:
1. Zaloguj się lokalnym kontem `admin@example.com` / `admin`.
2. Wejdź w **Admin > Konfiguracja** i wpisz prawdziwe dane LDAP/SMTP (albo wyczyść
pole hosta LDAP, żeby wrócić do wartości z `.env`).
3. Użyj przycisków **„Testuj połączenie”** przy obu sekcjach, zanim zaczniesz
polegać na logowaniu przez katalog.
### Integracje opcjonalne (BookStack)
Podpowiedzi artykułów z bazy wiedzy BookStack (przy tworzeniu zgłoszenia i w
panelu operatora) są **domyślnie wyłączone** i nie wymagają żadnej zmiennej w
`.env` — całość konfiguruje się w **Admin > Konfiguracja**: adres instancji,
Token ID/Secret (rola/użytkownik właściciela tokenu musi mieć w BookStacku
uprawnienie „Access System API”), oraz osobne listy dozwolonych półek dla
podpowiedzi przy tworzeniu zgłoszenia i dla panelu operatora — dopóki żadna
półka nie jest zaznaczona, wyszukiwanie nic nie zwraca.
---
## 2. Wdrożenie bezpośrednio na serwerze (Apache/Nginx, bez Dockera)
### Wymagania
- **PHP 8.3+** z rozszerzeniami: `curl`, `mysqli`, `pdo_mysql`, `ldap`, `zip`,
`mbstring`, `openssl`, `tokenizer`, `xml`, `ctype`, `bcmath`, `fileinfo`.
- **Composer 2**.
- **MariaDB/MySQL** (osobny serwer lub lokalny).
- **Node.js 20+** — tylko do jednorazowej (lub przy update'ach) budowy zasobów
Tailwind/Vite; niepotrzebny w runtime.
- **Apache** z `mod_rewrite` (+ `mod_ssl`) **lub** **Nginx + PHP-FPM**.
### 2.1. Pobranie kodu i zależności
```bash
cd /var/www
git clone <adres-repo> servicedesk
cd servicedesk/src
composer install --no-dev --optimize-autoloader
```
### 2.2. Konfiguracja `.env`
```bash
cp .env.example .env
```
Te same zmienne co w sekcji 1.2, z jedną różnicą — `DB_HOST` wskazuje na
prawdziwy adres serwera bazy, nie na nazwę serwisu Compose:
```env
APP_NAME=Servicedesk
APP_ENV=production
APP_DEBUG=false
APP_URL=https://servicedesk.twoja-domena.pl
APP_LOCALE=pl
APP_FALLBACK_LOCALE=pl
AUTHOR_CONTACT=helpdesk@twoja-domena.pl
VERSION=1.1.3
DB_CONNECTION=mysql
DB_HOST=127.0.0.1 # albo adres IP/hostname prawdziwego serwera DB
DB_PORT=3306
DB_DATABASE=servicedesk
DB_USERNAME=servicedesk
DB_PASSWORD=wygeneruj-silne-haslo
SESSION_DRIVER=database
QUEUE_CONNECTION=database
```
Uzupełnij też `LDAP_*`/`MAIL_*` jak w sekcji 1.2 (to samo ostrzeżenie o
Admin > Konfiguracja nadpisującym te wartości w locie dotyczy tu identycznie).
```bash
php artisan key:generate
php artisan migrate --seed
php artisan storage:link
```
### 2.3. Budowa zasobów
```bash
npm ci
npm run build
```
(Na maszynie bez Node.js zbuduj `public/build/` gdzie indziej i skopiuj cały
katalog — to jedyne, co Vite generuje poza źródłem.)
### 2.4. Uprawnienia plików
```bash
chown -R www-data:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cache
```
(Zmień `www-data` na użytkownika, pod którym faktycznie działa Apache/PHP-FPM w
Twojej dystrybucji, np. `apache` na RHEL/CentOS.)
### 2.5. Konfiguracja serwera WWW
**DocumentRoot musi wskazywać na `public/`**, nigdy na katalog główny aplikacji.
**Apache** (`/etc/apache2/sites-available/servicedesk.conf`):
```apache
<VirtualHost *:443>
ServerName servicedesk.twoja-domena.pl
DocumentRoot /var/www/servicedesk/src/public
<Directory /var/www/servicedesk/src/public>
AllowOverride All
Require all granted
</Directory>
SSLEngine on
SSLCertificateFile /etc/ssl/certs/servicedesk.crt
SSLCertificateKeyFile /etc/ssl/private/servicedesk.key
ErrorLog ${APACHE_LOG_DIR}/servicedesk-error.log
CustomLog ${APACHE_LOG_DIR}/servicedesk-access.log combined
</VirtualHost>
```
```bash
a2enmod rewrite ssl
a2ensite servicedesk
systemctl reload apache2
```
> Jeśli kiedykolwiek dodasz własny katalog `public/icons/` — nie rób tego. Stockowy
> moduł `mod_alias` Apache definiuje `Alias /icons/ "/usr/share/apache2/icons/"`
> (FancyIndexing), który po cichu przechwytuje `/icons/*` przed dotarciem do
> Laravela. Ten projekt celowo używa `public/pwa-icons/` z tego właśnie powodu.
**Nginx** (+ PHP-FPM):
```nginx
server {
listen 443 ssl http2;
server_name servicedesk.twoja-domena.pl;
root /var/www/servicedesk/src/public;
ssl_certificate /etc/ssl/certs/servicedesk.crt;
ssl_certificate_key /etc/ssl/private/servicedesk.key;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
```
### 2.6. Zadanie cykliczne (SLA, automatyzacje, poczta IMAP) i kolejka
Crontab użytkownika, pod którym stoi aplikacja (np. `www-data`) — obsługuje też
`automation:run-rules` i `emails:fetch-imap` (patrz 1.6 wyżej):
```cron
* * * * * cd /var/www/servicedesk/src && php artisan schedule:run >> /dev/null 2>&1
```
Jak w wersji Docker — kolejka (`php artisan queue:work`) nie jest obowiązkowa,
skoro powiadomienia wysyłają się synchronicznie; zostaw `QUEUE_CONNECTION=database`
jako bezpieczny domyślny driver na przyszłość.
Realtime (patrz 1.3b) potrzebuje tu **długo działającego procesu**
`php artisan reverb:start` — PHP musi mieć rozszerzenia `pcntl`/`posix`
(standardowo dostępne, ale sprawdź `php -m`). Najprościej pod systemd:
```ini
# /etc/systemd/system/servicedesk-reverb.service
[Unit]
Description=Servicedesk Reverb websocket server
After=network.target
[Service]
User=www-data
WorkingDirectory=/var/www/servicedesk/src
ExecStart=/usr/bin/php artisan reverb:start --host=0.0.0.0 --port=8080
Restart=always
[Install]
WantedBy=multi-user.target
```
```bash
systemctl enable --now servicedesk-reverb
```
Ustaw też te same zmienne `.env` co w 1.3b (`BROADCAST_CONNECTION=reverb`,
`REVERB_*`, `VITE_REVERB_*` — tu `REVERB_HOST` to po prostu `127.0.0.1`, nie
nazwa usługi compose) i skieruj serwer WWW tak, by ścieżka `/app*` trafiała do
portu 8080 zamiast do PHP-FPM/Apache (osobny `location`/`VirtualHost` dla tej
jednej ścieżki, analogicznie do reguły Traefika w 1.3b).
### 2.7. Pierwsze logowanie i dalsza konfiguracja
Identycznie jak w kroku 1.6 — zaloguj się `admin@example.com` / `admin`, zmień
hasło, uzupełnij prawdziwe LDAP/SMTP w Admin > Konfiguracja (placeholdery z seeda
inaczej realnie próbują łączyć się z fałszywymi adresami), przetestuj oba
połączenia przyciskiem „Testuj połączenie”.
### 2.8. Aktualizacje (bez przestoju)
```bash
cd /var/www/servicedesk/src
git pull
composer install --no-dev --optimize-autoloader
php artisan migrate --force
npm ci && npm run build
php artisan config:clear # tylko jeśli wcześniej użyto config:cache
```
Nie ma potrzeby restartu Apache/PHP-FPM dla samych zmian w PHP/Blade — restart
przyda się tylko po zmianie w `.env` lub konfiguracji samego serwera WWW.