RE:NODE

Nasazení aplikace Spring Boot do produkce na Java serveru

Text: Tým RE:NODE
21 min čtení


Aplikace Spring Boot se nasazuje jako jediný soubor: fat jar, který obsahuje tvůj kód, jeho závislosti a vestavěný webový server. Server potřebuje Java runtime správné verze, jar, port, na kterém bude naslouchat, a heap, který se vejde do limitu paměti, jejž platíš. Právě poslední bod je místo, kde selhává téměř každé neúspěšné nasazení Spring Bootu: JVM si ráda řekne o víc paměti, než malý tarif má, a kontejner zastavený za překročení limitu vypadá zvenku jako pád bez jakéhokoli vysvětlení.

Tento článek pokrývá celou cestu pro Spring Boot a poznamenává, kde se Quarkus a Micronaut liší: build jaru pomocí Mavenu nebo Gradle, volbu verze Javy, navázání na správný port, dimenzování heapu podle tarifu, udržení konfigurace mimo jar, připojení databáze PostgreSQL nebo MySQL přes HikariCP, zpřístupnění health endpointu, provoz za proxy tak, aby adresy klientů přežily, a chyby, které uvidíš, když něco nesedí.

Co server potřebuje ke spuštění fat jaru#

Fat jar (říká se mu také uber jar nebo executable jar) je normální jar, do kterého jsou vnořeny jary závislostí, a spouštěcí třída, která je umí číst. Vyrábějí ho pluginy Spring Bootu pro Maven a Gradle a celý spouštěcí příkaz je java -jar app.jar. Žádný aplikační server k instalaci, žádný WAR, který bys nahrával do Tomcatu, žádný classpath, který bys skládal ručně.

Pro hosting z toho plyne pár věcí:

  • JRE, ne JDK. Kompiluješ na vlastním stroji nebo v CI. Server výsledek jen spouští, takže mu stačí runtime.
  • Správná hlavní verze. Spring Boot 3 vyžaduje Javu 17 nebo novější. Spring Boot 2.7 a starší běží na Javě 8 až 17, ale už nemají open-source podporu. Řada Java aplikací na RE:NODE spustí jakýkoli jar na Javě 17, 21 nebo 25, takže aplikace na Bootu 3 se vejde na všechny tři a aplikace na Bootu 2.x dává smysl jen na 17.
  • Jediný naslouchací port. Jeden vestavěný server, jeden port. Actuator lze přesunout na druhý port, ale u tarifu s jednou alokací ho necháš na hlavním a ochráníš jinak.
  • Paměť na víc než jen heap. Spotřeba JVM je heap plus metaspace, zásobníky vláken, code cache, direct buffery a struktury garbage collectoru. Heap 512 MB běžně znamená proces o velikosti 700 až 800 MB. Většina následující části o dimenzování existuje proto, aby tuhle větu zkonkrétnila.

Verzi Javy vybírej podle toho, pro co kompiluješ, ne podle toho, co je nejnovější. Kompiluj s --release 17 nebo odpovídajícím nastavením Mavenu a Gradle a jar sestavený pro 17 poběží beze změny na 21 i 25. Opačným směrem to selže s UnsupportedClassVersionError, o čemž je řeč v části o řešení potíží. Pokud máš aktuální závislosti, je rozumným výchozím bodem Java 21: jsou k dispozici virtuální vlákna, garbage collectory jsou lépe vyladěné pro kontejnery a má dlouhý horizont podpory. Java 25 je novější řada a stojí za vyzkoušení, pokud chceš být na nejnovějším dlouhodobě podporovaném vydání.

Build jaru pomocí Mavenu nebo Gradle#

Build dělej v CI nebo na vlastním stroji, testy spusť tam a nahrávej hotový artefakt. Build není nic, co by se mělo dělat na malém produkčním tarifu, ze stejného důvodu, proč se tam nebuildí Rust: Maven nebo Gradle s nahřátou cache závislostí si vedou dobře, ale studený build na kontejneru s 1 GB je pomalý a může narazit na limit paměti.

U Mavenu udělá jar spustitelným rodič Spring Bootu nebo spring-boot-maven-plugin:

bash
$ ./mvnw -B clean package -DskipTests=false$ ls target/*.jartarget/shop-api-1.4.0.jar  target/shop-api-1.4.0.jar.original

Soubor .original je tenký jar před přebalením. Nasazuj ten druhý. U Gradle:

bash
$ ./gradlew clean bootJar$ ls build/libs/shop-api-1.4.0.jar

bootJar je úloha, která vyrábí spustitelný archiv; obyčejná jar vyrobí tenký, který nenastartuje. Buildy Gradle s aplikovaným pluginem org.springframework.boot vyrábějí také -plain.jar, který také není to, co chceš.

Tři drobnosti ti později ušetří potíže při nasazení:

  1. Dej jaru stabilní název. V Mavenu nastav <finalName>app</finalName> uvnitř <build>; v Gradle nastav bootJar { archiveFileName = 'app.jar' }. Startovací příkaz, který míří na app.jar, pak není třeba při každém vydání upravovat.
  2. Build info pro health endpoint. spring-boot-maven-plugin má cíl build-info a plugin pro Gradle má springBoot { buildInfo() }. S kterýmkoli z nich může /actuator/info ohlásit verzi, která skutečně běží, což odpovídá na první otázku při každém incidentu.
  3. Zkontroluj manifest. unzip -p app.jar META-INF/MANIFEST.MF má ukázat Main-Class s hodnotou org.springframework.boot.loader... a Start-Class s tvou aplikací. Pokud ne, krok přebalení neproběhl.

Quarkus a Micronaut se liší v artefaktu. Výchozí rozložení quarkus-app u Quarkusu je adresář, ne jediný soubor, s quarkus-run.jar a složkou lib vedle něj. Pokud chceš jeden soubor, nastav quarkus.package.jar.type=uber-jar, sestav pomocí ./mvnw package a spusť java -jar target/*-runner.jar. Úloha shadowJar v Gradle u Micronautu a konfigurace Maven Shade vyrobí jediný spustitelný jar. Všechno níže o heapu, portech a databázích platí pro obojí, jen s jinými názvy vlastností.

Nahrání a spuštění#

Způsob, jakým se jar dostane na server, rozhoduje o tom, kolik z ostatního můžeš automatizovat. Cesty jsou tři: nahrát ho ručně přes správce souborů nebo SFTP; stáhnout repozitář z GitHubu a buildovat přímo na serveru; nebo buildovat v CI a nahrát jen artefakt.

Na RE:NODE má každý aplikační server správce souborů, který archivy rozbaluje na místě, a SFTP s přihlašovacími údaji pro každý server. Obojí je pro jar v pořádku. Panel také podporuje nasazení z GitHubu přes GitHub App, které funguje i pro soukromé repozitáře, se dvěma přepínači: stažení při každém startu a nasazení při pushi, které restartuje běžící server a spustí zastavený. To je cesta od zdrojáků na server, takže pomůže jen tehdy, když smí server sám buildovat; pro jar sestavený v CI je jednodušší nahrát artefakt a udržet build mimo produkční paměťový rozpočet. Pokud použiješ cestu přes Git, commitni hotový jar nebo použij větev, která obsahuje jen release artefakty, a nečekej, že tarif bude spouštět Maven.

Když se Java server vytvoří, na své adrese zpočátku zobrazuje malou pracovní stránku, dokud nenasadíš vlastní kód, a ta stránka nikdy nepřepíše tvoje soubory. Jakmile je tvůj jar na místě a startovací nastavení na něj míří, stránku nahradí tvoje aplikace. Na záložce Startup se nastavují proměnné prostředí a konzole ukazuje živý výstup s tlačítky start, stop, restart a kill.

nahráníjava -jarJDBCHTTPSHTTPTvůj buildmvn packageKlientiapp.jarfat jarProxy slotTLS, X-Forwarded-ForJVM-Xmx, MaxRAMPercentageDatabázepool HikariCP
Od buildu k běžící aplikaci Spring Boot

Porty: server.port a SERVER_PORT#

Naslouchací port je první věc, která se pokazí. Spring Boot má výchozí 8080. Na hostingovém tarifu dostaneš přidělený port a tvoje aplikace musí naslouchat na něm, ne na tom, který jsi používal lokálně.

Spring Boot čte stejnou vlastnost z více míst a na pořadí záleží. Argumenty příkazové řádky mají přednost před proměnnými prostředí OS, a ty mají přednost před application.properties uvnitř jaru. Proměnná prostředí SERVER_PORT je tvar server.port podle pravidla relaxed binding, takže její nastavení v prostředí nevyžaduje žádný kód:

environment
SERVER_PORT=25565SPRING_PROFILES_ACTIVE=prod

Uvedené číslo je zástupné; použij port zobrazený u svého serveru. Na panelech odvozených od Pterodactylu bývá alokace zpřístupněna jako SERVER_PORT, takže než cokoli předpokládáš, zkontroluj záložky Startup a Network, co tvůj server skutečně nabízí. Pokud proměnná už existuje a obsahuje alokaci, nic nenastavuješ: Spring ji převezme jako server.port hned při startu.

Pokud to raději držíš v souboru, dobrým vzorem je odkaz s výchozí hodnotou, protože funguje lokálně i v produkci:

application.properties
server.port=${SERVER_PORT:8080}

Druhou polovinou je adresa. Vestavěný server Springu ve výchozím stavu naslouchá na všech rozhraních, takže server.address nepotřebuješ. V produkci ho nenastavuj na 127.0.0.1 - aplikace by pak byla dosažitelná jen zevnitř vlastního kontejneru a proxy by hlásila bad gateway. Quarkus v některých režimech ve výchozím stavu váže na localhost a potřebuje quarkus.http.host=0.0.0.0 a quarkus.http.port=${SERVER_PORT:8080}. Micronaut čte micronaut.server.port a respektuje proměnnou prostředí MICRONAUT_SERVER_PORT.

Dimenzování JVM podle limitu paměti tarifu#

Tohle je nejdůležitější část. Kontejner má pevný limit paměti a JVM, pokud jí to dovolíš, si velikost heapu zvolí sama. Moderní JVM čtou limit kontejneru přes cgroups a maximální heap nastaví ve výchozím stavu na čtvrtinu. To je bezpečná výchozí hodnota, ale plýtvavá: na tarifu se 2 GB dostaneš heap 512 MB a 1,5 GB zůstane nevyužito.

Co se stane na limitu, je věc platformy. Na RE:NODE, když proces dosáhne limitu paměti, jádro kontejner zastaví a ten se čistě restartuje místo swapování. Původní Pterodactyl dělá opak a nechá stroj swapovat. Čisté zastavení je pro produkční službu lepší, protože swapující JVM způsobuje pauzy garbage collectoru v řádu sekund a pak pomalu umírá. Znamená to také, že když nastavíš heap příliš velký, příznakem je restart bez čehokoli v aplikačním logu: proces byl zabit zvenčí a JVM neměla šanci zapsat OutOfMemoryError.

Je třeba rozlišit dvě různá selhání:

  • `java.lang.OutOfMemoryError: Java heap space`. Heap je pro tvá data příliš malý. JVM to zapíše do logu a heap je třeba zvětšit, nebo aplikace uniká paměť.
  • Restart bez jakékoli chyby Javy. Celý proces překročil limit kontejneru. Heap je pro tarif příliš velký, nebo narostla paměť mimo heap. Heap je třeba zmenšit.

Tři způsoby, jak heap nastavit, seřazené podle preference:

bash
# 1. Procento limitu kontejneru (doporučeno)$ java -XX:MaxRAMPercentage=65.0 -XX:InitialRAMPercentage=40.0 -jar app.jar# 2. Absolutní strop$ java -Xms256m -Xmx768m -jar app.jar# 3. Jen prostředí, bez změny příkazuJAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=65.0 -XX:+ExitOnOutOfMemoryError"

MaxRAMPercentage se přizpůsobí, když změníš tarif, což je hlavní důvod, proč ho preferovat: přejdeš ze 2 GB na 4 GB a heap se přizpůsobí bez jakékoli úpravy. -Xmx je přesné a je správnou volbou, když jsi změřil a chceš pevnou hodnotu. Pokud jsou přítomny obě, vyhrává -Xmx. JAVA_TOOL_OPTIONS čte každá JVM, která se spustí, což se hodí, když nemůžeš upravit startovací příkaz; JVM vypíše na začátek konzole Picked up JAVA_TOOL_OPTIONS: ..., takže si můžeš ověřit, že se nastavení uplatnilo. Pamatuj, že platí i pro každý další proces Javy spuštěný ve stejném prostředí.

Procento, které zvolíš, závisí na tarifu, protože pevná režie je u malého tarifu větším podílem:

Paměť tarifuNastavení heapuDůvod
1 GB-Xmx384m až -Xmx512m500 MB a více jde na paměť mimo heap
2 GBMaxRAMPercentage=60Heap asi 1,2 GB, 800 MB na zbytek
4 GBMaxRAMPercentage=65Režie mimo heap je menším podílem
6 GB a víceMaxRAMPercentage=70Nech prostor pro direct buffery a špičky

Jsou to výchozí body, ne měření tvé aplikace. Služba s velkými cache nebo hodně NIO bude chtít větší rezervu. Malé API s nevelkým stromem závislostí si vystačí s méně. Sleduj graf paměti v konzoli proti limitu během jednoho dne skutečného provozu a uprav hodnoty.

Pár dalších příznaků stojí za znalost:

PříznakÚčinek
-XX:+ExitOnOutOfMemoryErrorUkončí proces při OOM na heapu, aby ho platforma čistě restartovala
-XX:MaxMetaspaceSize=256mOmezí metadata tříd, která mohou při opakovaném nasazení unikat
-Xss512kMenší zásobníky vláken, pokud běží stovky vláken
-XX:+UseSerialGCNa 1 CPU nebo pod 1,5 GB collector s nejnižší režií

ExitOnOutOfMemoryError je příznak, který většina lidí přeskakuje, a neměla by. JVM, která vyhodila OOM na heapu v jednom vlákně, často žije, ale je rozbitá: některé požadavky obsluhuje a jiné selhávají. Ukončení umožní, aby ti výše popsané restartování dalo čistý proces. Počítej s tím, že restarty se sledují: detekce pádů se každé dvě minuty dotazuje, zda uptime nešel zpět nebo zda server nepřešel offline, restarty, o které požádáš ty, se nepočítají, tři restarty za hodinu vyvolají varování a automatický tiket a šest znamená pozastavení. Služba, která se restartuje ve smyčce kvůli příliš velkému heapu, je přesně to, co tohle hledá, takže dimenzování oprav dřív, než ji necháš běžet.

Na malých tarifech záleží na collectoru. Java vybere G1 na strojích se dvěma nebo více CPU a nejméně 1792 MB, jinak se vrátí k sériovému collectoru. G1 je dobrá výchozí volba od 2 GB výš. Nelaď ho, dokud ti to nenaznačí graf. CPU vs RAM u herních serverů uvádí obecnou myšlenku, že se má měřit, který zdroj je limitující, než se dokoupí ten druhý, a platí to pro JVM beze změny.

CPU je tvrdě omezeno na podíl, který sis koupil. JVM dimenzuje své thread pooly podle počtu CPU, který vidí, a ten se odvozuje z kvóty kontejneru a zaokrouhluje nahoru. Služba na půl jádra může věřit, že má jedno, takže vlákna kompilátoru a GC soupeří s vlákny požadavků. Pokud je start pomalý nebo latence po dávkách vyskakuje, -XX:ActiveProcessorCount=1 zafixuje pohled JVM na stroj a na nejmenších tarifech stojí za zkoušku.

Konfigurace: application.properties a proměnné prostředí#

Jar je všude stejný. To, co se mezi tvým notebookem a produkcí mění, je konfigurace, a žádná z ní by neměla být uvnitř jaru. Externalizovaná konfigurace Spring Bootu má jasné pořadí, od nejnižší priority k nejvyšší:

  1. application.properties nebo application.yml zabalené uvnitř jaru.
  2. Stejné soubory vedle jaru, v pracovním adresáři nebo ve složce config/ vedle něj.
  3. Soubory specifické pro profil, například application-prod.properties.
  4. Proměnné prostředí OS.
  5. Systémové vlastnosti Javy (-Dkey=value) a argumenty příkazové řádky (--key=value).

Důsledek je užitečný: výchozí hodnoty dodej uvnitř jaru a přepiš jen to, co se v produkci liší, pomocí prostředí. Pravidlo vazby proměnných prostředí je mechanické - velká písmena, tečky a pomlčky se mění na podtržítka:

VlastnostProměnná prostředí
server.portSERVER_PORT
spring.datasource.urlSPRING_DATASOURCE_URL
spring.datasource.hikari.maximum-pool-sizeSPRING_DATASOURCE_HIKARI_MAXIMUMPOOLSIZE
management.endpoint.health.show-detailsMANAGEMENT_ENDPOINT_HEALTH_SHOWDETAILS

Všimni si řádku s velikostí poolu: pomlčka v maximum-pool-size se vypouští místo převodu a název se slije dohromady. Podtržítka uvnitř názvu klíče jsou obvyklou příčinou proměnné prostředí, která zřejmě nic nedělá. Když si nejsi jistý, zavolej /actuator/env v testovacím prostředí, nebo jednou nastav --debug a přečti si v konzoli condition report.

Tajné údaje drž mimo jar i mimo repozitář. Hesla k databázím, API klíče a podpisová tajemství patří do proměnných prostředí, které se na RE:NODE nastavují na záložce Startup. Proměnné prostředí a tajné údaje popisuje, kam který druh patří a co nelogovat. Konfigurační soubor vedle jaru je alternativa pro hodnoty, které chceš upravovat bez ohledu na cyklus restartu; editor správce souborů se zvýrazněním syntaxe to dělá pohodlným. Spring application.properties za běhu znovu nenačítá, takže změna vždy vyžaduje restart.

Vše spojí přepínač profilu. application.properties nech pro sdílené výchozí hodnoty a application-prod.properties pro to, co produkce mění, a aktivuj ho pomocí SPRING_PROFILES_ACTIVE=prod:

application-prod.properties
server.port=${SERVER_PORT:8080}server.forward-headers-strategy=nativeserver.shutdown=gracefulspring.lifecycle.timeout-per-shutdown-phase=20sspring.jpa.open-in-view=falsespring.jpa.hibernate.ddl-auto=validatespring.main.lazy-initialization=falselogging.level.root=INFO

spring.jpa.open-in-view=false zabrání Springu držet databázové spojení po celou dobu každého webového požadavku. Ve výchozím stavu je zapnuté a při startu loguje varování, protože je častým zdrojem vyčerpání poolu. ddl-auto=validate způsobí, že aplikace schéma kontroluje, místo aby ho měnila; produkční schémata patří nástroji pro migrace, jako je Flyway nebo Liquibase, ne odhadům Hibernate.

Připojení databáze PostgreSQL nebo MySQL#

Spring Boot automaticky nakonfiguruje DataSource ze tří vlastností a ovladače na classpathu. Ovladač je závislost: org.postgresql:postgresql pro PostgreSQL, com.mysql:mysql-connector-j pro MySQL. URL nese hostitele, port a název databáze:

environment
SPRING_DATASOURCE_URL=jdbc:postgresql://db-host:5432/shop?sslmode=preferSPRING_DATASOURCE_USERNAME=shop_appSPRING_DATASOURCE_PASSWORD=change-me

U MySQL URL začíná jdbc:mysql://host:3306/dbname a třída ovladače se najde automaticky podle URL. Hostitel, uživatel a heslo pocházejí z toho, kde databáze běží. Každý aplikační server má sloty pro databáze, vytvářené v panelu s vygenerovaným hostitelem, uživatelem a heslem. Pro samostatnou databázi existuje hosting databází s PostgreSQL a MySQL 8.4 LTS, jejichž přihlašovací údaje se generují pro každý server a které jsou dostupné na hostiteli a portu tvého tarifu. Hodnoty zkopíruj do tří výše uvedených proměnných, ne do jaru. Průvodce vzdáleným připojením k PostgreSQL a ten pro MySQL procházejí připojovací řetězce podrobněji.

Velikost poolu HikariCP

Spring Boot ve výchozím stavu používá HikariCP. Udržuje pool otevřených spojení a předává je požadavkům. Výchozí maximum-pool-size je 10 a minimum-idle má výchozí hodnotu stejnou, takže aplikace Boot při startu otevře deset spojení a drží je. To je pro malou databázi hodně.

application-prod.properties
spring.datasource.hikari.maximum-pool-size=6spring.datasource.hikari.minimum-idle=2spring.datasource.hikari.connection-timeout=10000spring.datasource.hikari.idle-timeout=300000spring.datasource.hikari.max-lifetime=1500000spring.datasource.hikari.keepalive-time=120000

Hodnoty, kterým je třeba rozumět:

  • `maximum-pool-size` je pevný strop spojení z tohoto procesu. Větší neznamená rychlejší. Pool o 6 spojeních obslouží službu na jednom nebo dvou jádrech mnohem víc provozu, než většina lidí čeká, protože dotaz trvající 5 ms nechá jedno spojení obsloužit 200 požadavků za sekundu. Pokud požadavky čekají ve frontě na spojení, hledej nejdřív pomalé dotazy, než číslo zvýšíš.
  • `connection-timeout` je doba v milisekundách, po kterou požadavek čeká na volné spojení, než selže. Výchozích 30 sekund je na webový požadavek příliš dlouho; 10 sekund nebo méně selže viditelně a rychle.
  • `max-lifetime` musí být kratší než jakýkoli časový limit spojení na databázi nebo na síťovém zařízení mezi vámi. Hikari spojení vyřadí dřív, než ho server stihne zavřít pod rukama. Výchozí hodnota je 30 minut a správná je hodnota o pár minut pod skutečným limitem.
  • `keepalive-time` pinguje nečinná spojení, což brání stavovým firewallům, aby je potichu zahazovaly.

Spočítej spojení napříč vším, co se připojuje. Pokud provozuješ dvě instance aplikace nebo vedle ní migrační úlohu, součty se sčítají proti vlastnímu limitu spojení databáze. Connection pooly a limity obsahuje tuto aritmetiku a článek o limitech spojení v MySQL pokrývá stranu serveru.

Přidej Flyway nebo Liquibase a migrace dej do src/main/resources/db/migration. Aplikace pak při startu, než začne obsluhovat provoz, sama dotáhne schéma na aktuální stav. Start se tím mírně prodlouží a nasazení jsou opakovatelná, a to je hlavní důvod, proč nastavit ddl-auto=validate.

Health checky s Actuatorem#

Přidej spring-boot-starter-actuator a zpřístupni jen to, co potřebuješ. Ve výchozím stavu je přes HTTP zpřístupněn jen health, což je správný instinkt, který si zachovej:

application-prod.properties
management.endpoints.web.exposure.include=health,infomanagement.endpoint.health.probes.enabled=truemanagement.endpoint.health.show-details=nevermanagement.health.db.enabled=true

Se zapnutými sondami Spring kromě /actuator/health zpřístupní také /actuator/health/liveness a /actuator/health/readiness. Liveness odpovídá na otázku, zda je proces ve stavu, ve kterém může pokračovat, a nikdy by neměla záviset na databázi. Readiness odpovídá na otázku, zda má tato instance dostávat provoz, a nedostupná databáze patří sem. Celkový /actuator/health agreguje registrované indikátory, včetně datasource, místa na disku a čehokoli, co přidáš, a vrací {"status":"UP"} s HTTP 200, nebo 503, když je něco dole.

Vyzkoušej to z konzole nebo z vlastního stroje:

bash
$ curl -s -o /dev/null -w "%{http_code}\n" https://api.example.com/actuator/health200

Dvě upozornění. Nikdy nezpřístupňuj env, heapdump, beans ani mappings na veřejné adrese: prozrazují konfiguraci a v případě heapdump obsah paměti včetně tajných údajů. Pokud je potřebuješ, dej je na samostatný management port pomocí management.server.port a nepřeposílej ho, nebo je chraň pomocí Spring Security. A udržuj odpověď health levnou: indikátor, který volá tři cizí služby, promění každý ping monitoru v zátěž. Graceful shutdown a health checky popisuje, co dobrá kontrola dělá a nedělá, a server.shutdown=graceful výše je část o vypínání téhož příběhu, která nechá rozpracované požadavky doběhnout, když stiskneš restart.

Za proxy: TLS a X-Forwarded-For#

TLS neukončuj uvnitř aplikace. Tarify pro aplikace zahrnují slot reverzní proxy: nasměruj záznam A na zobrazenou adresu a certifikát se vydá a obnovuje automaticky, v rámci 21denního okna. Tvoje aplikace obsluhuje prosté HTTP na přiděleném portu a proxy posílá skutečnou adresu klienta v X-Forwarded-For. Co dělá reverzní proxy vysvětluje celý řetězec.

Spring Boot těmto hlavičkám nevěří, dokud mu to neřekneš. Bez toho request.getRemoteAddr() vrací proxy, každé přesměrování se sestaví s http:// a request.isSecure() je false. Řešením je jedna vlastnost:

application-prod.properties
server.forward-headers-strategy=native

Dvě užitečné hodnoty jsou native a framework. native nechá vestavěný server (Tomcat, Jetty nebo Undertow) zpracovat X-Forwarded-* sám, což je správná odpověď pro běžnou servletovou aplikaci. framework používá vlastní ForwardedHeaderFilter Springu, což je volba pro WebFlux nebo pro případ, kdy chceš, aby práci udělal Spring. Nenastavuj obě. S kteroukoli z nich getRemoteAddr() vrací klienta, přesměrování a hlavičky Location používají https a generované absolutní URL v odkazech a OAuth callbacích jsou správné.

Tomcat hlavičku respektuje jen od důvěryhodné proxy. Příslušná vlastnost je server.tomcat.remoteip.internal-proxies, jejíž výchozí hodnotou je regulární výraz pokrývající privátní a loopback rozsahy. Pokud se proxy dostane ke tvému kontejneru z adresy mimo tento rozsah, hlavička se ignoruje a nic v logu ti neřekne proč. Otestuj to požadavkem a z ladicího endpointu vypiš X-Forwarded-For a zjištěnou vzdálenou adresu.

Pro Quarkus je ekvivalentem quarkus.http.proxy.proxy-address-forwarding=true spolu s quarkus.http.proxy.allow-x-forwarded=true v aktuálních verzích. U Micronautu zjišťuj klienta přes HttpClientAddressResolver, který X-Forwarded-For čte za tebe. WebSockety a server-sent events potřebují zvládnout upgrade a časový limit nečinnosti delší než tvůj heartbeat; WebSockety za reverzní proxy to pokrývá a platí to pro WebSocket ve Springu i SseEmitter beze změny.

Doba startu a jak ji zkrátit#

Aplikace Spring Boot s Hibernate, zabezpečením a tuctem starterů je na zlomku jádra běžně připravena za 8 až 25 sekund a po nasazení při prvním startu déle, protože JVM musí načíst a ověřit každou třídu. To je normální a záleží na tom ze dvou důvodů: monitory, které kontrolují příliš brzy, hlásí výpadek, a restarty jsou pomalé.

Co číslo skutečně posune:

  • Větší podíl CPU při startu. Start je závislý na CPU a převážně jednovláknový. Tarif s půl jádrem trvá znatelně déle než s celým, takže nejlevnější zrychlení je větší tarif, ne příznak.
  • Méně beanů a méně autokonfigurací. Odstraň startery, které nepoužíváš. --debug vypíše seznam autokonfigurací, které se uplatnily, a každá stojí čas.
  • `-XX:TieredStopAtLevel=1`. Zastaví JIT na rychlém kompilátoru. Start je rychlejší, propustnost v ustáleném stavu nižší. Vhodné pro malý interní nástroj, špatné pro vytížené API.
  • `-Xshare:auto` s AppCDS. Archivy sdílení dat tříd zrychlují start. Spring Boot 3.3 a novější umí takový archiv sestavit z jaru; aktuální postup je v dokumentaci Spring Bootu.
  • Líná inicializace. spring.main.lazy-initialization=true odkládá vytváření beanů až na první použití. Start je rychlejší, ale první požadavek to zaplatí a špatně nakonfigurované beany objeví uživatelé místo startu. V produkci to nech vypnuté.
  • Indexuj a migruj před startem. Pomalý start bývá migrace Flyway nebo sken repozitářů, ne JVM.

Pokud ti dominuje studený start, Quarkus a Micronaut jsou na něj stavěné: oba dělají analýzu injektování závislostí při buildu a na JVM startují za jednu až dvě sekundy. Nativní image to dostane pod 100 ms, za cenu dlouhých buildů, které potřebují několik gigabajtů paměti, takže je dělej v CI.

Řešení potíží#

`UnsupportedClassVersionError: ... has been compiled by a more recent version of the Java Runtime`. Jar byl zkompilován pro novější Javu, než jaká na serveru běží. Kompiluj s --release 17, nebo pro server zvol Javu 21 či 25.

`no main manifest attribute, in app.jar`. Nahrál jsi tenký jar, obvykle soubor .original z Mavenu nebo -plain.jar z Gradle. Nahraj přebalený.

`Web server failed to start. Port 8080 was already in use`. Aplikace se snaží navázat na port, který není tvůj, nebo běží dvě kopie. Nastav SERVER_PORT na přidělený port a ověř, že starý proces skončil.

Server se restartuje bez stack trace, pořád dokola. Proces překročil limit paměti kontejneru. Sniž heap, například MaxRAMPercentage=55, a porovnej graf paměti s limitem. Pamatuj, že tři restarty za hodinu spustí varování.

`java.lang.OutOfMemoryError: Java heap space`. Heap je příliš malý, nebo se něco hromadí. Zvětši heap, pokud na to tarif má prostor, nebo pořiď heap dump pomocí -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/container/dump.hprof a otevři ho v profileru. Dumpy potom smaž; jsou stejně velké jako heap.

`HikariPool-1 - Connection is not available, request timed out after 30000ms`. Pool je vyčerpaný. Než zvýšíš maximum-pool-size, hledej pomalé dotazy a open-in-view, které drží spojení.

`Connection refused` nebo `UnknownHostException` u databáze. Hostitel nebo port v SPRING_DATASOURCE_URL je špatně. Zkopíruj je ze stránky databáze a pamatuj, že porty MySQL a PostgreSQL se liší.

`FATAL: password authentication failed`. Přihlašovací údaje jsou špatné nebo obsahují zatoulané mezery z vložení. Proměnnou přepiš ručně.

Přesměrování vedou na `http://` a sessions se neudrží. Přeposlané hlavičky se ignorují. Nastav server.forward-headers-strategy=native a pokud to stále selhává, zkontroluj server.tomcat.remoteip.internal-proxies.

Bad gateway od proxy, zatímco konzole říká, že aplikace běží. Aplikace je navázaná na 127.0.0.1, naslouchá na špatném portu, nebo se ještě spouští. Než začneš soudit, počkej na řádek Started ... in N seconds.

Proměnná prostředí se zdá ignorovaná. Zkontroluj relaxed binding. SPRING_DATASOURCE_HIKARI_MAXIMUM_POOL_SIZE nefunguje; SPRING_DATASOURCE_HIKARI_MAXIMUMPOOLSIZE ano.

FAQ#

Kolik paměti potřebuje aplikace Spring Boot?

Malé API s JPA a zabezpečením typicky chce heap 256 až 384 MB a kontejner 600 až 800 MB, takže tarif s 1 GB stačí pro jednoduchou službu a 2 GB je pohodlných. Větší cache, velké výsledky dotazů a mnoho vláken hodnotu zvedají. Měř podle grafu paměti, nehádej.

Mám použít -Xmx, nebo MaxRAMPercentage?

MaxRAMPercentage použij, když čekáš změnu tarifu, protože sleduje limit. -Xmx použij, když máš změřené číslo, které chceš nechat pevné. Pokud jsou nastaveny obě, přednost má -Xmx.

Mohu stejně provozovat Quarkus nebo Micronaut?

Ano. Oba vyrábějí jar spustitelný pomocí java -jar, příznaky heapu jsou stejné a liší se jen názvy vlastností: quarkus.http.port a micronaut.server.port místo server.port. Jejich rychlejší start je na malých tarifech výhodou.

Jakou verzi Javy zvolit?

Nejnižší verze, kterou tvé závislosti podporují, je nejbezpečnější pro build a Java 21 je dobrá výchozí volba pro běh. Boot 3 potřebuje alespoň 17. Jar sestavený pro 17 běží na 21 i 25, ale ne naopak.

Potřebuji před aplikací reverzní proxy?

Potřebuješ něco, co ukončí TLS a obnovuje certifikáty, a to dělá slot proxy. Nastav server.forward-headers-strategy, aby tvoje aplikace viděla skutečnou adresu klienta a správné schéma.

Proč je moje aplikace první minutu pomalá?

JVM nejdřív spouští bytecode v interpretu a horké cesty kompiluje, jak se je učí, takže první požadavky jsou pomalejší a využití CPU je během zahřívání vysoké. Malý podíl CPU ho prodlužuje. Pokud v té době selhávají kontroly readiness, dej jim delší počáteční zpoždění.