Deploying Spring Boot on EC2
The most direct path from a Spring Boot JAR to a running production instance — launching EC2, installing a JDK, running the JAR as a systemd service, and fronting it with a load balancer.
Want a visual for this topic?
Generate a diagram tailored to Deploying Spring Boot on EC2 — the AI picks whichever visual (architecture, flowchart, ER diagram, etc.) best fits this specific AWS concept.
Sign in to generate a visual →🎓 Learning objectives
- •Package a Spring Boot application as an executable JAR and understand what `java -jar` actually runs
- •Run a Spring Boot JAR as a proper systemd service instead of a foreground terminal process
- •Explain why a bare EC2 deployment needs an Application Load Balancer in front of it for anything production-facing
- •Identify what EC2 deployment leaves entirely manual compared to ECS/EKS/Beanstalk
What is it?
Deploying Spring Boot on EC2 means running the application's executable JAR directly on a virtual machine you provision and manage yourself — installing a JDK, transferring the JAR, and configuring it to run as a persistent background service, with an Application Load Balancer in front for a stable, TLS-terminated, health-checked entry point. This is the most manual of the AWS deployment options for a Spring Boot application, sitting underneath ECS, EKS, Lambda, and Elastic Beanstalk in terms of how much AWS automates on your behalf.
Why it exists
Running an application directly on EC2 remains the most fundamental deployment model on AWS — every higher-level service (ECS, EKS, Elastic Beanstalk) ultimately runs on EC2 (or Fargate) underneath, and understanding the bare-EC2 path is what makes clear exactly what those higher-level services are automating on your behalf. It also remains a legitimate choice for teams with existing configuration-management tooling (Ansible/Chef/Puppet) already built around directly-managed instances.
Problem it solves
It solves the most basic deployment need — getting a Spring Boot JAR running reliably, restart-on-crash, and reachable over a stable HTTPS endpoint — using nothing beyond EC2 and an ALB, with no container runtime, orchestrator, or platform-specific concepts required.
Intuition
The key mental shift from a traditional Java deployment: there's no separate Tomcat installation step, because Spring Boot's executable JAR already contains an embedded servlet container — the JAR itself is a complete, self-contained runnable application, and java -jar is the entire 'start the server' command. Everything else (process supervision, restart-on-crash, traffic routing) is infrastructure you're assembling around that one command yourself.
Analogy
This is the equivalent of renting an empty apartment and moving every piece of furniture in yourself — full control over exactly where everything goes, but you're also the one who has to carry the couch up the stairs, and if you want the process automated for next time, you write your own moving checklist rather than hiring movers (ECS/Beanstalk).
Technical explanation
Spring Boot's Maven/Gradle plugin repackages the application into a 'fat JAR' with a custom Main-Class manifest entry (JarLauncher) that knows how to unpack and load both the application's own classes and its bundled dependencies (including the embedded Tomcat/Jetty/Undertow server) from nested JARs within the single outer JAR file — this nested-JAR-loading mechanism is exactly why java -jar app.jar alone is sufficient to start a fully functional web server with no external application server installation. A systemd unit file's Restart=on-failure directive causes systemd to automatically restart the process if it exits with a non-zero status, and WantedBy=multi-user.target combined with systemctl enable ensures the unit starts automatically during normal multi-user boot, without requiring a login session at all.
Architecture
A typical setup: an ALB in public subnets terminates TLS and forwards HTTP traffic to a target group of EC2 instances in private subnets, each instance running the Spring Boot JAR as a systemd-managed process on a fixed port. The instance's security group only allows inbound traffic from the ALB's security group, not directly from the internet. An Auto Scaling group can be layered underneath the same setup to add/remove instances based on CloudWatch metrics, registering/deregistering them from the ALB target group automatically as they launch or terminate — at which point this looks structurally like a hand-assembled version of what Elastic Beanstalk would set up automatically.
Workflow
- Launch an EC2 instance with an appropriate instance type and a security group allowing inbound traffic only from the ALB. 2) Install a JDK (Amazon Corretto is AWS's recommended, no-cost OpenJDK build). 3) Build the Spring Boot application as an executable JAR (
mvn package/./gradlew bootJar) and transfer it to the instance. 4) Write a systemd unit file specifying thejava -jarstart command,Restart=on-failure, andWantedBy=multi-user.targetso it starts on boot. 5)systemctl enable --nowthe service. 6) Create an Application Load Balancer with a target group pointing at the instance's application port, using Spring Boot Actuator's/actuator/healthendpoint as the health check path. 7) Point a Route 53 record at the ALB's DNS name.
Example
A team launches an m5.large EC2 instance running Amazon Linux, installs Amazon Corretto (AWS's OpenJDK distribution), copies a Spring Boot fat JAR built by their CI pipeline via scp, creates a systemd unit file for it, and starts the service — then places an Application Load Balancer with an ACM-issued TLS certificate in front, pointing at that single instance's target group, so the application is reachable over HTTPS at a stable custom domain regardless of the instance's actual IP.
Real-world usage
Bare EC2 deployment remains common in organizations with existing configuration-management investment (Ansible playbooks, Chef recipes) built around directly-managed instances, and in teams intentionally learning AWS fundamentals before adopting a higher-level service. Most production Spring Boot teams eventually layer Auto Scaling and a deployment automation tool (CodeDeploy, or a full migration to ECS) on top of this base pattern rather than staying with fully manual single-instance deployment long-term.
Trade-offs
Deploying on bare EC2 gives full, unfiltered control over the OS, JDK version, and exact process configuration — useful when an application has unusual requirements ECS/Beanstalk's assumptions don't fit. In exchange, every operational concern ECS/Beanstalk would otherwise automate (rolling deployments, auto-scaling triggers, health-based instance replacement, OS patching) has to be built by hand, usually via scripting or a configuration management tool (Ansible, Systems Manager) layered on top.
Visual explanation
Picture a single door (the ALB) that always looks the same to visitors, no matter which room behind it (which EC2 instance) is currently answering. Inside each room, a dedicated attendant (systemd) makes sure the one occupant (the Spring Boot JAR) gets back up immediately if they collapse, and greets them again automatically whenever the room itself reopens (instance reboot).
Advantages
- —
Full control over OS, JDK version/vendor, and exact process configuration with nothing abstracted away
- —
No additional service-specific concepts to learn beyond EC2 itself — a good foundation for understanding what ECS/Beanstalk automate
- —
Works identically for any JVM application, not just Spring Boot, and integrates naturally with existing configuration-management tooling
- —
No container-runtime overhead or image-build step required — just a JAR and a JVM
Disadvantages
- —
Deployments (rolling out a new JAR version) are entirely manual or require custom scripting/tooling — no built-in rolling-update mechanism the way ECS/CodeDeploy provide
- —
OS and JDK patching is the team's ongoing responsibility, unlike Fargate (no OS to patch) or managed platforms
- —
No built-in image/artifact versioning the way a container registry (ECR) provides — JAR versions on disk need their own naming/rollback convention
- —
Auto-scaling requires manually wiring CloudWatch alarms and Auto Scaling policies, rather than a managed service's built-in scaling behavior
Common mistakes
- —
Running the JAR as a foreground SSH-session process instead of a systemd service, so it silently dies the moment the SSH connection drops
- —
Pointing the ALB health check at
/instead of Spring Boot Actuator's/actuator/health, missing real application-level health signals (e.g., a broken database connection) that a bare HTTP-200-on-root check wouldn't catch - —
Allowing the EC2 instance's security group to accept inbound traffic directly from the internet instead of only from the ALB's security group, bypassing the load balancer entirely for anyone who discovers the instance's IP
- —
Not automating the deployment step at all, meaning every release involves someone manually SSH-ing in, stopping the service, replacing the JAR, and restarting it — slow and error-prone at any real team size
In the AWS Console
- 1
EC2 → Instances → Launch instances
Launch an EC2 instance, choosing Amazon Linux 2023 and an instance type sized to the application's expected load.
Amazon Corretto can be installed via the package manager immediately after launch — no separate download/license step needed.
- 2
EC2 → Target Groups → Create target group, then Load Balancers → Create Application Load Balancer
Create a target group for the application port and an Application Load Balancer pointing at it, with a health check path of `/actuator/health`.
- 3
EC2 → Load Balancers → [ALB] → Listeners → Add listener (HTTPS, port 443)
Attach an ACM-issued TLS certificate to the ALB's HTTPS listener.
Request the certificate in ACM first (Certificate Manager → Request certificate) — it needs DNS validation before it can be attached.
🎤 Interview questions
What does java -jar app.jar actually do for a Spring Boot application, and why doesn't it need a separate application server like Tomcat installed? (Listen for: Spring Boot's executable JAR embeds Tomcat/Jetty/Undertow directly inside the JAR via Spring Boot's repackaging — the JAR IS the server, java -jar starts an embedded servlet container in the same JVM process, unlike a traditional WAR deployed into an externally-installed app server)
Why would you run a Spring Boot JAR as a systemd service rather than just running java -jar app.jar in a terminal? (Listen for: a foreground terminal process dies when the SSH session disconnects and never restarts on crash or reboot; a systemd unit file runs it as a proper background service, restarts it automatically on crash (Restart=on-failure), and starts it automatically on instance boot)
Why put an Application Load Balancer in front of a single EC2 instance running Spring Boot, even before you need to scale to multiple instances? (Listen for: ALB provides a stable DNS endpoint independent of the instance's IP (which changes if the instance is replaced), TLS termination so the application itself doesn't need to handle certificates, and health-check-based routing that stops sending traffic to an instance if the app becomes unhealthy)
What's the main operational gap between deploying Spring Boot directly on EC2 versus using ECS or Elastic Beanstalk? (Listen for: EC2 deployment leaves patching the OS/JDK, process supervision, deployment orchestration (stopping the old JAR, copying the new one, restarting) and scaling entirely manual or hand-scripted; ECS/Beanstalk automate the deployment and scaling orchestration on top of the same underlying EC2 capacity)
How would you perform a zero-downtime deployment of a new JAR version to a fleet of EC2 instances behind an ALB, without ECS/CodeDeploy? (Listen for: rolling manually instance-by-instance — deregister one instance from the ALB target group, wait for in-flight connections to drain, stop the old JAR/copy the new one/restart via systemd, wait for the health check to pass, re-register it, then repeat for the next instance — essentially hand-rolling what CodeDeploy or an Auto Scaling rolling update would automate)