2. Instalace

Tato kapitola je technická — určená pro osobu, která systém nasazuje (IT administrátor, hostingový tým). Běžný uživatel ji může přeskočit.

Nabízíme dvě cesty: Docker (nejrychlejší, doporučeno pro nové instalace) nebo nativní install (PHP + MariaDB + web server, tradiční hosting).

2.1 Docker (3 minuty)

Předpoklady: Docker Desktop (Windows / macOS) nebo Docker Engine + compose-plugin (Linux).

Klon repa je společný krok pro obě varianty:

git clone https://github.com/radekhulan/myinvoice.git myinvoice
cd myinvoice

Pak si vyber variantu podle toho, jestli chceš stavět image lokálně, nebo si stáhnout pre-built z GHCR.

2.1.1 Varianta A — pre-built image z GHCR (rychlejší, bez local buildu)

Stáhne hotový multi-arch image (ghcr.io/radekhulan/myinvoice:latest, linux/amd64 + linux/arm64). Nepotřebuješ na hostu pnpm/composer ani několikaminutový build.

# Linux / macOS
cmd/docker-ghcr.sh

# Windows PowerShell
.\cmd\docker-ghcr.ps1

Skript docker-ghcr postupně:

  1. Vygeneruje .env s náhodnými DB hesly (28 znaků base64)
  2. Vygeneruje cfg.docker.php z cfg.sample.php (host=db / redis, randomized app.pepper + secret_encryption_key, dev-friendly cookies)
  3. docker compose pull — stáhne image z GHCR
  4. up -d + počká na DB healthy + spustí migrace

Používá docker-compose.production.yml (image-only, žádný build: block), takže další compose příkazy vyžadují flag -f docker-compose.production.yml (viz 2.1.6 Daily ops).

💡 V produkci pinuj konkrétní verzi — uprav docker-compose.production.yml a změň :latest na konkrétní tag (např. :1.7.0). Update pak přes cmd/docker-update.{sh,ps1} (auto-detekuje registry mode = pull + up -d + migrace).

Aktualizace na novou verzi:

# Linux / macOS
cmd/docker-update.sh

# Windows PowerShell
.\cmd\docker-update.ps1

Skript v registry módu sám zavolá docker compose pull app (stáhne nový image z GHCR), restartuje stack a doběhne pending migrace. Volumes (DB data) zůstávají zachovány. Mód detekuje automaticky — pokud nemáš .git/ nebo build: blok v compose, jede přes pull.

Nový image se publikuje automaticky při každém release tagu v*.*.*, takže aktualizace je otázkou jednoho příkazu.

🔔 Upgrade přímo z UI: od v3.0.0 vidí admin v Systém → Aktualizace stav verze + tlačítko *Aktualizovat*, které pull image + restart spustí přes host-side watcher. Detaily včetně instalace watcheru jako systemd unit / Scheduled Task → § 2.1.9 nebo § 19.4. Pro denní kontrolu nové verze nezapomeň naplánovat php api/bin/cron-version-check.php (1× denně, viz § 19.2).

WSL2 / Linux po klonu: pokud ./cmd/docker-ghcr.sh hlásí Permission denied nebo /usr/bin/env: 'bash\r': No such file…, má tvůj git zapnutý core.autocrlf=true, který na checkoutu konvertuje LF → CRLF. Oprav jednorázově existující soubory a vypni autocrlf globálně (na Linuxu nikdy nemá být zapnutý): ``bash sed -i 's/\r$//' cmd/*.sh chmod +x cmd/*.sh git config --global core.autocrlf input ` Repo má .gitattributes s *.sh text eol=lf, takže příští git clone` bude LF i bez tohoto kroku.

2.1.2 Varianta B — build z source

Postaví image lokálně z repa — vhodné pro vývoj a vlastní úpravy.

# Linux / macOS
cmd/docker-install.sh

# Windows PowerShell
.\cmd\docker-install.ps1

Skript docker-install postupně:

  1. Vygeneruje .env s náhodnými DB hesly (28 znaků base64)
  2. Vygeneruje cfg.docker.php z cfg.sample.php (host=db / redis, randomized app.pepper + secret_encryption_key, dev-friendly cookies pro HTTP loopback)
  3. Postaví image myinvoice:latest (multi-stage: Vue build → composer → PHP 8.5 + Apache)
  4. Spustí stack: app (Apache:80 → host:8080) + db (MariaDB 11)
  5. Počká, až bude DB healthy, a spustí migrace

2.1.3 Varianta C — bez klonování repa (jen Docker)

Pokud nechceš mít na hostiteli klon repa (typicky produkční Linux server, jen Docker daemon), GHCR image obsahuje veškerý PHP/JS kód i migrace — z repa potřebuješ jen 3 malé soubory.

Varianta C1 — one-click přes docker-ghcr.sh (doporučeno)

Stáhne si i instalační skript a chová se stejně jako Varianta A (random hesla, vygenerovaný cfg.docker.php, pull image, migrace):

mkdir myinvoice && cd myinvoice
curl -O https://raw.githubusercontent.com/radekhulan/myinvoice/master/docker-compose.production.yml
curl -O https://raw.githubusercontent.com/radekhulan/myinvoice/master/cfg.sample.php
curl -O https://raw.githubusercontent.com/radekhulan/myinvoice/master/cmd/docker-ghcr.sh
chmod +x docker-ghcr.sh
./docker-ghcr.sh

Skript najde docker-compose.production.yml v aktuálním adresáři, takže nemusíš nic přejmenovávat. Update na novou verzi:

docker compose -f docker-compose.production.yml pull
docker compose -f docker-compose.production.yml up -d

Varianta C2 — manuálně, bez skriptu

Když chceš plnou kontrolu nad cfg.docker.php a .env:

mkdir myinvoice && cd myinvoice
curl -O https://raw.githubusercontent.com/radekhulan/myinvoice/master/docker-compose.production.yml
curl -O https://raw.githubusercontent.com/radekhulan/myinvoice/master/cfg.sample.php
mv docker-compose.production.yml docker-compose.yml
cp cfg.sample.php cfg.docker.php
# uprav cfg.docker.php — minimálně:
#   db.host => 'db', db.user => 'myinvoice', db.pass => '<heslo z .env níže>'
#   app.pepper a secret_encryption_key (oboje: openssl rand -base64 32)

cat > .env <<EOF
DB_PASSWORD=$(openssl rand -base64 28)
DB_ROOT_PASSWORD=$(openssl rand -base64 28)
EOF

docker compose up -d
docker compose exec app php api/bin/migrate.php

🛈 Od image v3.1.0 se v Dockeru migrace spouští automaticky při startu kontejneru (docker-entrypoint.sh). Ruční php api/bin/migrate.php zůstává bezpečný idempotentní fallback.

⚠️ Varianta C2 NEgeneruje hesla a secrets automaticky — musíš je do cfg.docker.php doplnit ručně. Pro one-click bez klonu repa použij C1.

📖 Manuál na /manual: GHCR image má od v2.1.5 vygenerovaný HTML manuál a od v2.3.0 i PDF (tools/generateManualHtml.php + tools/exportManualToPdf.php se volají build-time v Dockerfile), takže http://localhost:8080/manual funguje bez dalších kroků a v sidebaru je button „Stáhnout PDF". Update na nový obsah = cmd/docker-update.{sh,ps1} (pull novějšího image z GHCR stáhne i nové vygenerované kapitoly). Kdyby /manual vrátil 503 *„Manuál není zatím vygenerovaný“*: pokud jedeš na starém image před v2.1.5, cmd/docker-update.{sh,ps1} (pull nového GHCR image) je řešení — staré image neměly manual/*.md uvnitř vůbec. Na v2.1.5+ image regeneruješ manuál ručně bez rebuildu: ``bash docker compose -f docker-compose.production.yml exec app \ php tools/generateManualHtml.php docker compose -f docker-compose.production.yml exec app \ php tools/exportManualToPdf.php ``

2.1.4 Po dokončení (všechny varianty)

Otevři: 👉 http://localhost:8080

V prohlížeči naskočí setup wizard — viz 3. První spuštění.

⚠️ Použij http://, ne https://, a explicitní port :8080. Docker stack běží na plain HTTP — pokud zadáš https://... nebo defaultní port, dostaneš SSL_ERROR_RX_RECORD_TOO_LONG / ERR_SSL_PROTOCOL_ERROR. Pro HTTPS na LAN/produkčním serveru viz 2.1.8 HTTPS / TLS terminace.

🌐 Přístup z jiného stroje (LAN IP, hostname)? Setup wizard funguje z libovolného hostu (např. http://10.0.0.8:8080) a app.url se automaticky uloží podle URL, kterou v wizardu použiješ. Pokud potřebuješ URL znát už před setupem (např. produkční doména + reverzní proxy), spusť kontejner s -e MYINVOICE_APP_URL=https://invoice.example.com. 🛈 Přístup z LAN přes IP (např. http://192.168.1.50:8080) — od v2.1.1 automaticky funguje. RFC1918 privátní IP (10.*, 172.16-31.*, 192.168.*), 127.*, localhost a *.local jsou vyjmuty z HTTPS redirectu v .htaccess a web.config. Také požadavek s hlavičkou X-Forwarded-Proto: https (reverse proxy s TLS terminací) redirect přeskočí.

2.1.5 Změna portu

Edituj .env (vznikl po prvním spuštění):

APP_PORT=9000          # místo 8080
DB_PORT=3308           # místo 3307 (vázán jen na 127.0.0.1)

a docker compose up -d. URL pak http://localhost:9000.

2.1.5.1 Runtime env pro auto-migrace (Docker)

Vstupní skript image podporuje tyto proměnné:

MYINVOICE_SKIP_MIGRATIONS=1     # vypne auto-migraci při startu
MYINVOICE_MIGRATE_ATTEMPTS=20   # počet retry pokusů migrace
MYINVOICE_MIGRATE_DELAY=3       # pauza mezi pokusy (sekundy)
MYINVOICE_DATA_DIR=/data        # od v3.2.0 — opt-in single-volume mód
                                # (default unset → 3-volume layout, 3.1.x kompatibilní)
MYINVOICE_AUTH_REQUIRE_TOTP=true # od v3.3.0 — vynutit 2FA pro všechny uživatele
                                # (default false; viz § 18.2.4)

Default je 20 pokusů s pauzou 3 sekundy. Pokud proměnné nenastavíš, použije se výchozí chování.

MYINVOICE_DATA_DIR je od v3.2.1 čistě opt-in. Default je 3-volume layout (app-log, app-storage, app-private) kompatibilní s 3.1.x; pro sjednocení všech stateful adresářů pod jediný persistent volume app-data:/data viz dedikovanou sekci 2.1.5.3 Single-volume úložiště níže.

cfg.docker.php mount je nově volitelný — image obsahuje stub cfg.php (<?php return [];) a vše lze předat přes ENV (12-factor). Pro full-ENV deploy (Railway, Heroku, Fly.io) bind-mount ./cfg.docker.php:/var/www/html/cfg.php:ro v docker-compose.yml zakomentuj nebo odstraň.

2.1.5.2 Railway / PaaS specifika

Některé PaaS (typicky Railway) injectují nevyřešené placeholdery jako ${VAR}, pokud proměnná není definovaná. Od v3.1.0 je MyInvoice v env overridech ignoruje, takže nepřepíší validní hodnoty z cfg.php/cfg.docker.php. Pokud chybí secret_encryption_key, aplikace fallbackuje na HKDF z app.pepper.

2.1.5.3 Single-volume úložiště (volitelně)

🛈 TL;DR: chceš zálohovat všechna data MyInvoice jedním tarem a držet jen jeden persistent volume místo tří? Použij single-volume mód.

Default vs. single-volume. Docker stack umí dva ekvivalentní layouty:

VlastnostDefault (3-volume)Single-volume
Volumesapp-log + app-storage + app-privateapp-data (jediný)
Mount points/var/www/html/{log,storage,private}/data
EnvMYINVOICE_DATA_DIR unsetMYINVOICE_DATA_DIR=/data
Composedocker compose up -d… -f docker-compose.single-volume.yml …
Kompatibilita3.1.x → 3.x bez migraceod 3.2.0; vyžaduje opt-in migraci
Backupdocker run --rm -v ... tar czf × 3jeden tar nad app-data
Vhodné proself-hosted VPS, NAS, vlastní hardwarePaaS (Railway, Heroku, Fly.io), read-only FS, jednoduché zálohování

Obě varianty jsou trvale podporované — *single-volume není „lepší"*, je to alternativní layout pro specifické deploy scénáře.

Co se pod /data přesune. Pokud nastavíš MYINVOICE_DATA_DIR=/data, aplikace přepíše (přes Config::applyDataDirOverrides()):

Žádné jiné cesty se nemění (kód, vendor, web/dist zůstávají uvnitř /var/www/html, čistě read-only).

Pro novou instalaci (od nuly)

Nejjednodušší cesta — přejmenuj override na docker-compose.override.yml, compose ho pak natáhne automaticky:

# Linux / macOS
git clone https://github.com/radekhulan/myinvoice.git && cd myinvoice
cp docker-compose.single-volume.yml docker-compose.override.yml
bash cmd/docker-install.sh
# Windows
git clone https://github.com/radekhulan/myinvoice.git
cd myinvoice
Copy-Item docker-compose.single-volume.yml docker-compose.override.yml
.\cmd\docker-install.ps1

Pro GHCR install (Varianta C bez klonování repa) si stáhni oba compose soubory a override přejmenuj stejně:

mkdir myinvoice && cd myinvoice
curl -O https://raw.githubusercontent.com/radekhulan/myinvoice/master/docker-compose.production.yml
curl -O https://raw.githubusercontent.com/radekhulan/myinvoice/master/docker-compose.single-volume.yml
curl -O https://raw.githubusercontent.com/radekhulan/myinvoice/master/cfg.sample.php
mv docker-compose.production.yml docker-compose.yml
mv docker-compose.single-volume.yml docker-compose.override.yml
cp cfg.sample.php cfg.docker.php  # edituj — secrets viz Varianta C2
docker compose up -d

Ověření, že běží single-volume layout:

docker compose exec app sh -c 'echo $MYINVOICE_DATA_DIR'   # → /data
docker compose exec app ls /data                            # → log  storage  private
docker volume ls | grep myinvoice                           # vidíš pouze app-data + db-data

Pro existující 3-volume instalaci (migrace)

Nikdy nepřepínej layout bez migrace — aplikace by nahlížela do prázdného /data a tvářila se, že data zmizela. Postup je popsaný v § 19.5 Upgrade na 3.2.x — volitelná migrace na single-volume layout.

Shrnutí: cmd/docker-migrate-volumes.{sh,ps1} zkopíruje data ze starých volumes do nového app-data přes dočasný alpine sidecar, staré volumes nesmaže (musíš ručně po ověření, že vše funguje), je idempotentní.

Backup single-volume layoutu

docker run --rm \
  -v myinvoice_app-data:/data:ro \
  -v "$PWD":/backup \
  alpine tar czf /backup/myinvoice-data-$(date +%F).tar.gz -C /data .

Plus dump MariaDB (viz § 19.7 Záloha a obnova) — to jsou dohromady dvě entity k zálohování (db + app-data) místo čtyř.

2.1.6 Daily ops

docker compose up -d                                 # start
docker compose down                                  # stop (data v named volumes přežijí)
docker compose down -v                               # stop + WIPE volumes (ZNIČÍ DB!)
docker compose logs -f app                           # live logs
docker compose exec app bash                         # shell do kontejneru
docker compose exec app php api/bin/migrate.php      # CLI uvnitř kontejneru
cmd/docker-build.sh --no-cache                       # rebuild image (po PHP/JS změnách, jen Varianta B)

💡 Pokud jsi instaloval přes Variantu A (docker-ghcr), všechny docker compose příkazy potřebují flag -f docker-compose.production.yml, např. docker compose -f docker-compose.production.yml logs -f app.

2.1.7 Volitelný Redis

docker compose --profile redis up -d

a v cfg.docker.php nastav redis.enabled => true. Restart appky.

2.1.8 HTTPS / TLS terminace

Docker stack sám TLS nedělá — Apache uvnitř kontejneru poslouchá na portu 80 (HTTP) a mapuje se na host port 8080. Pokud potřebuješ HTTPS (LAN server, produkce, doménové jméno), postav před stack reverse proxy s TLS terminací.

Symptom špatné konfigurace: prohlížeč hodí SSL_ERROR_RX_RECORD_TOO_LONG (Firefox) nebo ERR_SSL_PROTOCOL_ERROR (Chrome) — znamená to, že browser mluví TLS, ale server odpovídá plain HTTP.

Tři rozumné cesty:

  1. Caddy (nejjednodušší) — automatický Let's Encrypt pro doménu nebo self-signed pro IP, jeden Caddyfile řádek:
   vase-domena.cz {
       reverse_proxy localhost:8080
   }
  1. Nginx + self-signed cert (mkcert nebo openssl) — pro intranet bez veřejného doménového jména.
  1. Cloudflare Tunnel / Tailscale Funnel — pokud chceš veřejný přístup bez otevírání portů na firewallu.

Konkrétní recept — Caddy jako další container vedle stacku:

V kořeni repa (vedle docker-compose.production.yml) vytvoř Caddyfile:

faktury.tvojefirma.cz {
    reverse_proxy localhost:8080
}

Pak Caddy spusť na host síti, aby viděl port 8080:

docker run -d --name caddy --restart unless-stopped \
  --network host \
  -v "$PWD/Caddyfile:/etc/caddy/Caddyfile:ro" \
  -v caddy_data:/data \
  -v caddy_config:/config \
  caddy:2

Caddy si vyžádá Let's Encrypt cert sám (potřebuje veřejně dostupné porty 80/443 a A/AAAA záznam pro doménu). Auto-renewuje. X-Forwarded-Proto: https posílá automaticky — to je důležité, protože .htaccess v repu bez tohoto hlavičky vynucuje HTTP→HTTPS redirect a vzniká redirect loop.

A v cfg.docker.php přepni production nastavení:

'app' => [
    'url' => 'https://faktury.tvojefirma.cz',  // doslova to, co user vidí v adresáku
    ...
],
'session' => [
    'cookie_secure'   => true,
    'cookie_name'     => '__Host-myinvoice_session',
    'cookie_samesite' => 'Lax',
],

app.url se používá v emailových odkazech (faktury, reset hesla, upomínky) — musí přesně odpovídat veřejné URL, jinak budou linky vést na špatnou doménu nebo localhost:8080. __Host- cookie prefix vyžaduje HTTPS — pokud jsi po této změně zkusil load přes http://, login se rozbije (cookie se neuloží).

Restart stacku: docker compose -f docker-compose.production.yml restart app (nebo bez -f flagu pro Variantu B).

2.1.9 Update watcher — jednoclick upgrade z UI (volitelné)

Od v3.0.0 vidí admin v Systém → Aktualizace stav verze + tlačítko *Aktualizovat*, které zařadí upgrade do fronty. Aby ho někdo aplikoval, musí na hostu běžet watcher — proces, který přes docker compose exec poslouchá flag soubor uvnitř kontejneru a spouští cmd/docker-update.(sh/ps1). Bez watcheru tlačítko *Aktualizovat* nikam nedojede (UI zůstane věčně ve stavu „Upgrade probíhá…") a musíš upgrade aplikovat ručně přes shell.

Test režim (foreground)

Než ho udělej daemon, otestuj ho v běžícím okně:

# Linux / macOS
cd /opt/myinvoice
bash cmd/docker-update-watcher.sh
# Windows PowerShell
cd C:\inetpub\myinvoice
powershell -NoProfile -ExecutionPolicy Bypass -File cmd\docker-update-watcher.ps1

Vidíš [watcher] start, polling storage/upgrade-requested.json inside container every 30s — od té chvíle hlídá flag. Klikni v UI „Aktualizovat" a do 30 s zachytí flag, spustí cmd/docker-update.(sh/ps1), výsledek napíše zpátky. Watcher zastav Ctrl+C.

Linux — systemd unit (produkce)

sudo tee /etc/systemd/system/myinvoice-update-watcher.service <<'EOF'
[Unit]
Description=MyInvoice update watcher
After=docker.service

[Service]
Type=simple
WorkingDirectory=/opt/myinvoice
ExecStart=/opt/myinvoice/cmd/docker-update-watcher.sh
Restart=always

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now myinvoice-update-watcher

Logy: journalctl -u myinvoice-update-watcher -f.

Windows — Scheduled Task (produkce)

schtasks /create /tn "MyInvoice Update Watcher" `
  /tr "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\inetpub\myinvoice\cmd\docker-update-watcher.ps1" `
  /sc onstart /ru SYSTEM /rl HIGHEST
schtasks /run /tn "MyInvoice Update Watcher"

Stav úlohy: schtasks /query /tn "MyInvoice Update Watcher" /v /fo list.

Daily check pro detekci nové verze

Watcher jen reaguje na *kliknutí*. Aby admin viděl, že je dostupná nová verze (badge v patičce + status na /admin/update), musí běžet denní cron cmd/cron-version-check.(sh/cmd) — viz § 19.2.

Plné detaily

Recovery při zaseknutém upgradu, test workflow z master, externí monitoring přes /api/version → § 19 Aktualizace.

2.2 Nativní install (5 minut)

Předpoklady:

2.2.1 Klon a konfigurace

git clone https://github.com/radekhulan/myinvoice.git myinvoice
cd myinvoice
cp cfg.sample.php cfg.php

Otevři cfg.php a vyplň:

2.2.2 Vytvoř databázi

mysql -u root -p -e "CREATE DATABASE myinvoice CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"

2.2.3 Backend + migrace

cd api && composer install && cd ..
php api/bin/migrate.php
php tools/generateManualHtml.php   # vyrenderuje manual/generated/ → /manual route
php tools/exportManualToPdf.php    # vygeneruje manual/manual.pdf (Stáhnout PDF v sidebaru)

generateManualHtml.php je self-contained (nepotřebuje composer/vendor), generuje HTML kapitoly + search index. exportManualToPdf.php vyžaduje api/vendor/ (mPDF). Spouštět obojí znovu po každém pull repa, aby /manual ukazoval aktuální obsah. (V Docker variantě se volá build-time uvnitř Dockerfile — viz § 2.1.)

2.2.4 Frontend build

cd web
pnpm install
pnpm build       # produkční build do web/dist/

2.2.5 Web server

2.3 Po instalaci

Otevři aplikaci v prohlížeči — pokračuj na 3. První spuštění.

2.4 CLI nástroje

php api/bin/migrate.php              # spustí pending migrace
php api/bin/migrate.php --status     # vypíše stav migrací
php api/bin/setup.php                # interaktivní úvodní zřízení
php api/bin/sample.php               # vygeneruje testovací data (po setupu)
php api/bin/reset.php                # smaže všechna user-data (vyžaduje "ANO")
php api/bin/recompute-stats.php      # přepočítá agregované statistiky

2.4.1 Cron skripty

V cmd/ jsou připravené .cmd (Windows Task Scheduler) i .sh (Linux cron) wrappery:

SkriptDoporučená frekvence
cron-cleanup1× denně 03:00
cron-backup1× denně 02:00
cron-bank-scankaždých 30 min
cron-send-reminders1× denně 09:00, Po–Pá

Detaily v cmd/README.md.