Tutorial

Spring Boot in a 512 MB Container: Making It Start, Fit and Stay Up on a Free Tier

SnapDeploy Team 2026-10-12 Updated 2026-10-12 7 min read
javaspring-bootdockerjvmfree-tier

Spring Boot has a reputation for being heavy, and on a 512 MB container with a quarter of a CPU the reputation is partly earned: with default settings a small Boot app can take half a minute to start and hold more memory than it needs. SnapDeploy itself is a Spring Boot application, so this is not theory. Here is what it takes to make a Boot 3 service start, fit and stay up on a free container tier.

Why 512 MB is tight for the JVM by default

Since JDK 10 the JVM reads the container's memory limit, but its default maximum heap is only 25% of that limit: 128 MB in a 512 MB container. Everything else (metaspace, thread stacks, the JIT's code cache, direct buffers) lives outside the heap. A Boot app with Spring Data and a web server usually wants 150 to 250 MB of heap and another 100 to 150 MB of non-heap. Left to its defaults it either runs out of heap or the JIT and GC threads fight the single quarter-core for CPU during start-up.

1. Tell the JVM how much it may use

JAVA_TOOL_OPTIONS=-XX:MaxRAMPercentage=65 -XX:+UseSerialGC -Xss512k -XX:ReservedCodeCacheSize=48m
  • MaxRAMPercentage=65 gives the heap about 330 MB and leaves the rest for non-heap memory. Do not go above 75 or the container will be killed on the first spike.
  • UseSerialGC is the right collector for one CPU: G1 spends threads you do not have.
  • Xss512k halves the default thread stack. Tomcat's request threads add up.
  • ReservedCodeCacheSize=48m caps the JIT's code cache; the default reserve is far larger than a small service needs.

Set it as an environment variable in the container settings, or bake it into the Dockerfile with ENV. The JVM prints "Picked up JAVA_TOOL_OPTIONS" on start, which is how you confirm it took effect.

2. Trim what Boot starts

# application.properties
server.tomcat.threads.max=20
server.tomcat.threads.min-spare=2
spring.jmx.enabled=false
spring.main.lazy-initialization=true
management.endpoints.web.exposure.include=health

Twenty request threads is plenty for a service that gets a few requests a second; the default 200 is sized for a real server. Lazy initialization defers bean creation until first use, which cuts start-up time noticeably on a slow CPU at the cost of a slower first request. If you use Spring Boot Actuator, keep only the health endpoint exposed; it is the one thing you want reachable at /actuator/health.

3. Build a layered image

SnapDeploy's generated Dockerfile builds a Maven or Gradle project on Java 17 and runs the jar on a JRE 17 image, which is fine for a first deploy. If your project needs Java 21, or you want faster rebuilds, bring this Dockerfile instead. It uses Boot's layer tools so that dependency layers are cached between builds and only your classes change:

FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /build
COPY . .
RUN ./mvnw -q -DskipTests package
RUN java -Djarmode=tools -jar target/*.jar extract --layers --destination extracted

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /build/extracted/dependencies/ ./
COPY --from=builder /build/extracted/spring-boot-loader/ ./
COPY --from=builder /build/extracted/snapshot-dependencies/ ./
COPY --from=builder /build/extracted/application/ ./
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=65 -XX:+UseSerialGC -Xss512k -XX:ReservedCodeCacheSize=48m"
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "application.jar"]

-Djarmode=tools is Spring Boot 3.3 and later; on 3.0 to 3.2 use -Djarmode=layertools with the same extract command. Gradle projects swap the build line for ./gradlew -q bootJar and the jar path for build/libs/*.jar.

4. Start-up time, honestly

On a quarter vCPU, expect a small Boot 3 service to start in roughly 15 to 40 seconds depending on how much auto-configuration it triggers. On a free SnapDeploy container that matters twice: on deploy, and on every wake after 15 minutes without traffic, where the wake itself takes about 60 seconds and the app's own start-up adds to it. Things that help, in order of payoff:

  • Lazy initialization (above).
  • Remove starters you do not use. Every starter on the classpath runs its auto-configuration.
  • Class data sharing: Boot 3.3+ can produce a CDS archive with a training run (-XX:ArchiveClassesAtExit), typically 20 to 30% off start-up. Worth it once the service is stable.
  • If the app must answer instantly at any hour, that is what Always-On is for ($12/month for the same 512 MB, $25 for 2 GB), not a keep-alive ping.

5. Database connections

HikariCP defaults to a pool of 10 connections. A SnapDeploy PostgreSQL Mini allows 50, so one small service is fine, but if you run three services against one Mini, set spring.datasource.hikari.maximum-pool-size=5 in each. When the container sleeps, the pool goes with it; that is normal, and the first request after a wake pays for a new connection.

Deploying it

  1. Push to GitHub with the Dockerfile above (or without one, to use the generated Java 17 build).
  2. Containers → Deploy from GitHub → pick the repository. The port is 8080 unless server.port says otherwise.
  3. Add environment variables: SPRING_PROFILES_ACTIVE=prod, the database URL, and JAVA_TOOL_OPTIONS if it is not in the Dockerfile.
  4. Deploy, then open the logs and look for two lines: "Picked up JAVA_TOOL_OPTIONS" and "Started ... in N seconds". If N is over 60, go back to section 2.

6. Health checks and the wake page

Expose one cheap endpoint that returns 200 as soon as the application context is up: Actuator's /actuator/health if you use it, or a two-line controller if you do not. Two reasons. First, it is what you curl after a deploy to know the app is alive rather than merely started. Second, on a free container the platform's wake page polls the container until it answers, then redirects the visitor; the sooner the app answers, the shorter the visible wait. A health endpoint that touches the database is fine, but keep the query trivial: on a sleeping database connection the first check can take longer than the request timeout, and a health check that fails during start-up looks like a crash.

7. Reading the logs: three failures and their fixes

What the log shows What happened Fix
The container exits with code 137, often with no Java stack traceThe kernel killed the process for exceeding 512 MB. Usually the heap percentage is too high or the code cache and threads pushed non-heap memory over the limit.Lower MaxRAMPercentage to 60, keep Xss512k and the code cache cap, cut Tomcat threads.
java.lang.OutOfMemoryError: Java heap spaceThe opposite problem: the heap is too small for what the app holds, often a large result set or an in-memory cache.Page the query, bound the cache, or move to Always-On Medium (2 GB).
"Started … in 12 seconds" but the URL times outBoot is listening on a port the platform is not routing to, or bound to localhost.Match server.port to the container's port setting (8080 by default) and do not set server.address.

One more line worth knowing: Picked up JAVA_TOOL_OPTIONS is printed by the JVM itself before any Boot logging. If it is missing, the environment variable never reached the process and every flag in section 1 is inactive.

Numbers to keep: free container 512 MB and 0.25 vCPU, sleeps after 15 minutes, wakes in about 60 seconds; Always-On Small $12/month (512 MB), Medium $25 (2 GB), Large $45 (4 GB); PostgreSQL Mini $29/month with 50 connections. All on the pricing page and the free tier docs.

Ready to Deploy?

Deploy free. 10 deploys a day, 100 hours a month, no credit card.

Run this yourself: Get a dedicated NVIDIA T4 (16 GB) for a flat $499/mo or A10G (24 GB) for $999/mo — never shared, never spot, no hourly metering. See dedicated GPU hosting →

One-click Ollama, vLLM, ComfyUI, Whisper, PyTorch & more — or deploy your own GitHub repo or Docker image. Compare plans.

FIRST OF ITS KIND

Sprint Pack — ₹99 for 24 hours

The first managed container PaaS to offer a one-time 24-hour Always-On pass under $5. Unlimited deploys + Always-On for one Small (512 MB) container — including WebSockets & real-time apps. No subscription, no auto-renewal. Stack two for 48 hours.

Need a managed database too? DB Sprint Pack — ₹99 / 12h for Postgres, MySQL, MariaDB, Mongo, Redis, or RabbitMQ.

Tip: Need 24 hours of Always-On for a demo, weekend, or quick test — or WebSockets & real-time apps? Sprint Pack is ₹99 one-time — no subscription, no auto-renewal.

Need a managed Postgres / MySQL / Mongo / Redis instead? DB Sprint Pack — ₹99 / 12h.

Take SnapDeploy with you

Deploy, monitor and wake your containers from your iPhone.

Download on the App Store

Get DevOps Tips & Updates

Container deployment guides, platform updates, and DevOps best practices. No spam.

Unsubscribe anytime. We respect your privacy.

More Articles