Spring Boot cannot run on Netlify, and this is a known limit rather than a configuration problem: Netlify serves static files and runs short-lived, stateless functions on a Lambda-style model, while a Spring Boot application is a long-running JVM process with an embedded server, connection pools, and scheduled tasks that expect to stay warm between requests. The pattern that does work is a split deploy: the frontend, built as static assets, on Netlify, and the Spring Boot backend on a host built to run an always-on process, with the two connected over CORS and an API base URL, not bundled into one deployment.
Searching for how to deploy Spring Boot on Netlify eventually leads to Netlify’s own support answer, which is a direct no, and that is the correct starting point rather than a workaround to find.
This post explains why, then walks through the split-deploy pattern end to end: the frontend side, the backend side, and a Dockerfile for the part Netlify was never going to run.
Table of contents
- Why Spring Boot cannot run on Netlify
- The split-deploy pattern
- CORS between frontend and backend
- API base URL as an environment variable
- A health check endpoint
- The container path: a real Dockerfile
- The database checklist
- How this fits the rest of the stack
- FAQ
Why Spring Boot cannot run on Netlify
Netlify’s compute model is functions: small units of code, each one starting cold or warm for a single request and then stopping, with an execution time limit measured in seconds. Spring Boot’s embedded Tomcat or Netty server is built around the opposite assumption, a process that starts once and keeps running, holding a JDBC connection pool, a thread pool, and any scheduled tasks alive between requests.
Packaging a Spring Boot app to run inside a function model is possible in principle with Spring Cloud Function and a lot of rework, but a standard Spring Boot jar built with spring-boot-starter-web is simply the wrong shape for that environment, which is why Netlify’s own answer is not to try.
Netlify’s own function execution limit is measured in seconds, ten by default and higher on some plans, which is nowhere near what a JVM needs just to finish class loading and Spring’s component scan on a cold start, before the first request is even handled.
The split-deploy pattern
The pattern that works separates the two concerns: a frontend, built to static files, deployed on Netlify, and a Spring Boot backend deployed as an always-on service somewhere built to run one, whether that is a container platform, a VM, or a managed Java service. The frontend calls the backend over HTTP like it would call any external API.
This is not a compromise forced by Netlify specifically; it is roughly how a static frontend and a stateful backend are deployed together regardless of host, once the backend is doing anything more than serving files.
A team migrating an existing monolith usually hits this the other way around: the Spring Boot app already exists and ran on a VM, and the new piece is a modern frontend built with a framework whose ecosystem defaults to Netlify or something like it. The split-deploy pattern is the same regardless of which half came first. The frontend’s build output goes wherever static hosting lives, and the backend keeps running wherever a JVM process can stay up.
CORS between frontend and backend
The frontend and backend now live on different origins, so the browser enforces CORS on every request between them. Spring configures this centrally rather than per endpoint.
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true);
}
}
allowedOrigins should name the exact frontend domain, not a wildcard, especially once allowCredentials is true, since browsers reject the combination of a wildcard origin and credentials outright.
Get this wrong and the browser console shows a message like has been blocked by CORS policy: No Access-Control-Allow-Origin header is present, which is a browser-enforced block, not a network failure. The request often succeeds on the server, visible in the backend’s own logs, while the browser refuses to hand the response to the frontend code at all, which is the detail that confuses people testing the same endpoint with curl and seeing it work fine.
API base URL as an environment variable
The frontend needs to know the backend’s URL, and that URL differs between local development, a preview deploy, and production. Set it as a build-time environment variable in Netlify’s site settings, injected into the frontend framework’s usual convention, VITE_API_URL or NEXT_PUBLIC_API_URL depending on the tool, rather than hardcoded.
Netlify’s deploy contexts, production, deploy previews, and branch deploys, can each carry a different value for that variable, which is what lets a preview deploy talk to a staging backend instead of production.
A missing value here fails quietly rather than loudly. The frontend falls back to a relative path like /api/users, which resolves against Netlify’s own domain instead of the backend, and returns Netlify’s own 404 page instead of a JSON error. That response still parses as valid HTML to some HTTP clients, which is worse than an obvious connection refused, because it can slip past error handling that only checks for a non-200 status.
A health check endpoint
Spring Boot Actuator exposes /actuator/health once added as a dependency, and it is what a host’s own health check should poll to know the container is actually ready, not just that the process started. A JVM app can take several seconds to finish starting the embedded server and connecting to the database, and a naive check on the process existing rather than the endpoint responding will route traffic in too early.
management:
endpoints:
web:
exposure:
include: health
endpoint:
health:
probes:
enabled: true
Actuator’s health endpoint composes from individual indicators: the database connection pool, disk space, and any custom indicator added to the app, and it returns 503 rather than 200 if any required one is down, not a flat 200 as soon as the process is merely listening. A host polling anything less specific, a plain TCP connect to the port for instance, will mark the container ready before the database connection pool has actually finished initializing.
The container path: a real Dockerfile
Running Spring Boot anywhere that is not Netlify almost always means a container, and a multi-stage Dockerfile keeps the final image to a JRE rather than a full JDK.
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./mvnw clean package -DskipTests
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
The build stage needs the full JDK and Maven wrapper; the runtime stage only needs a JRE to execute the already-built jar, which keeps the shipped image smaller than building and running from the same base.
A JAVA_OPTS or JDK_JAVA_OPTIONS environment variable is worth setting explicitly for the heap size, -XX:MaxRAMPercentage=75.0 rather than a fixed -Xmx value. A fixed number set for one host’s memory silently becomes wrong the moment the container moves to a plan with a different memory limit, and the JVM either wastes most of the available memory or gets killed for exceeding it.
The database checklist
The backend’s database connection also does not travel with a Netlify deploy, since Netlify has nowhere to run one. spring.datasource.url, username, and password should come from environment variables set on wherever the backend actually runs, not from application.properties committed to the repository.
- Size the HikariCP pool to the host’s memory and CPU, not to a default meant for a large server
- Run Flyway or Liquibase migrations as part of startup or as a separate step before the app starts, not manually against production
- Confirm the managed database requires TLS and that the JDBC URL requests it
A schema mismatch after a failed migration is the most common cause of a service that starts, passes its health check, and then fails on the first real request. Flyway and Liquibase both refuse to run a lower migration than what is already recorded, which stops a rollback deploy from silently corrupting a schema that a newer version already migrated forward.
How this fits the rest of the stack
None of this changes because the frontend happens to be on Netlify; the backend just needs somewhere built to keep a JVM process running. RunxBuild deploys Java services and Docker images from a repository with a live route and a health check, and the RunxBuild hosting calculator adds that up alongside a managed database for the Spring Boot half of a split deploy.
Useful related references:
- Django on Netlify: Why It Does Not Work, and What To Do Instead
- Deploy a Java Backend for Free on RunxBuild
- Docker on Netlify: Why It Does Not Run Containers, and Where to Put One Instead
- Java services on RunxBuild
FAQ
Can you deploy Spring Boot on Netlify?
No. Netlify runs static sites and short-lived functions with an execution time limit, while Spring Boot is a long-running JVM process with an embedded server and connection pools that expect to stay alive between requests. Netlify’s own support documentation states this directly rather than leaving it to be discovered through a failed deploy.
Where should a Spring Boot backend be deployed if the frontend is on Netlify?
Anywhere built to run an always-on process: a container platform, a managed Java hosting service, or a VM. The common pattern is a static frontend on Netlify and the Spring Boot backend deployed separately, with the two connected over HTTPS using an API base URL set as an environment variable and CORS configured on the backend.
How do I fix CORS errors between a Netlify frontend and a Spring Boot backend?
Configure CORS in Spring with a WebMvcConfigurer bean, naming the frontend’s exact origin in allowedOrigins rather than a wildcard, especially if allowCredentials is true, since browsers reject a wildcard origin combined with credentials. The allowed methods should match what the frontend actually calls.
Can Spring Boot run as a serverless function instead of a full deploy?
Only with significant rework using Spring Cloud Function, which restructures the app around a function-per-endpoint model rather than the usual embedded server. A standard Spring Boot jar built with spring-boot-starter-web is not compatible with a function runtime as-is, which is why the split-deploy pattern is the practical default.