Deploy a Java API with ECS Express Mode
Deploy a Java API with Docker, ECR, and Amazon ECS Express Mode.
Introduction
30 Second Summary
A small program can work perfectly on your laptop yet fail the moment other people try to reach it. Finding where that connection breaks turns deployment from guesswork into a repeatable skill.
In this project, you will build a Java 21 Mission Status API that returns live mission status plus health JSON. Amazon ECR feeds its Docker image into Amazon ECS Express Mode for a public HTTPS deployment.
What You'll Build
You open a public HTTPS link where live mission status JSON proves your container is running beyond your laptop.
By the end of this project, you'll have:
- A live Java API with an operational status plus a UTC timestamp at http://localhost:8080/. Its /health endpoint returns a response ready for a load-balancer health check.
- A Linux/AMD64 Docker image that serves the API through http://localhost:8080/ after you diagnose the container's loopback boundary.
- A public HTTPS deployment that serves the same JSON through Amazon ECS Express Mode. You can inspect its AWS Fargate tasks. You can trace requests through its Application Load Balancer. You can check its health status. You can inspect its auto scaling. You can read its Amazon CloudWatch logs.
- Secret Mission: Add a /version endpoint to a v2 image. Watch Amazon ECS Express Mode roll it out through a canary deployment.
Are there any prerequisites?
You'll need internet access plus a web browser. You also need permission to create AWS resources plus a payment method if you create a new AWS account.
You remove the usage-based AWS resources after testing through the guided cleanup section.
Before We Start
This is your moment to commit to the API you are building before the hands-on work begins. A clear goal gives every local test, container image, and cloud deployment a purpose.
Prepare Your AWS and Local Environment
A reproducible cloud deployment starts with a verified identity. It also needs a fixed Region for every AWS resource.
Your local tools need to match the environment that runs the API. An incorrect Java release or a stopped container engine can block the build before deployment begins.
You will protect your AWS account with multi-factor authentication. Then you will authenticate the AWS CLI in us-east-1.
In this step, get ready to:
- Protect your AWS account with MFA in us-east-1.
- Install the required AWS CLI version. Authenticate a temporary session.
- Verify the Java runtime. Verify the compiler. Confirm the Docker engine is running.
Secure your AWS account in one Region
AWS Regions control where your cloud resources live. Using US East (N. Virginia) keeps the future image repository and deployment in the same Region.
✔️ I have an active AWS account
Your existing account is ready for this project once its root user is protected. The console also needs to show the project Region.
- Sign in to the AWS Management Console.
- Confirm that the root user has MFA registered.
- Register MFA if it is missing by following the official root-user security guidance.
- Confirm that you have not created root-user access keys.
- Choose US East (N. Virginia) in the console Region control.
- Confirm that the Region code is us-east-1.
ⓧ I need to create an AWS account
Creating an AWS account requires billing information. This step creates no billable cloud resources.
- Complete the official AWS account sign-up guide.
Account activation can extend the total project time. A delay at this point does not indicate a problem with your setup.
- Wait for the account activation confirmation before continuing.
- Sign in to the AWS Management Console.
- Register root-user MFA by following the official root-user security guidance.
- Confirm that you have not created root-user access keys.
- Choose US East (N. Virginia) in the console Region control.
- Confirm that the Region code is us-east-1.
Your account now has the protection this project needs. Its console is also pointing at the Region that holds every cloud resource you create later.
Authenticate the AWS CLI
The AWS CLI lets your terminal make authenticated requests to AWS. This project requires version 2.32.0 or later for the account login workflow.
- Check your installed AWS CLI version by running this command:
aws --version
What does this check show?
The command prints the installed AWS CLI version. The result tells you whether the current installation supports this project's login workflow.
✔️ My AWS CLI meets the minimum
An AWS CLI version of 2.32.0 or later meets this project's requirement.
ⓧ I see an older version
Your current installation needs an update before you authenticate.
- Open the official AWS CLI installation guide.
- Choose the instructions for your operating system.
- Complete the AWS CLI update.
- Return to the Visual Studio Code terminal.
- Repeat the version check above.
ⓧ The command is not found
The AWS CLI needs to be installed before the terminal can start an authenticated session. The installer may request administrator approval.
- Open the official AWS CLI installation guide.
- Choose the instructions for your operating system.
- Complete the AWS CLI installation.
- Approve only the installer downloaded through the official AWS guide.
- Return to the Visual Studio Code terminal.
- Repeat the version check above.
AWS CLI still unavailable?
Close Visual Studio Code after the installation completes. Reopen it so the integrated terminal can detect the installed command.
Use the operating-system branch in the official installer guide if the command remains unavailable.
Help me troubleshoot my AWS CLI installation.
The login flow opens a browser for authentication. It stores temporary credentials for your terminal session.
- Choose us-east-1 when the login flow asks for a Region.
- Start the browser login flow by running this command:
aws login
Why use this login flow?
The browser flow authenticates the AWS CLI with temporary credentials. Your terminal can call AWS without a root-user access key.
The selected Region keeps future CLI operations aligned with the console.
Before you check, consider which AWS identity the temporary session should report.
- Verify the authenticated identity by running this command:
aws sts get-caller-identity
What should you see?
The response includes UserId. It also includes Account and Arn.
These fields confirm that the terminal has an active AWS identity.
Identity check failing?
Complete the browser sign-in before returning to the terminal. Repeat the login flow if the browser session was closed early.
Confirm that the login flow used us-east-1.
Help me fix my AWS CLI authentication.
Verify Java and Docker in Visual Studio Code
The Java Development Kit supplies the runtime that starts your API. It also supplies the compiler that builds the class used by the container.
You will run every local check inside Visual Studio Code. This keeps the same terminal ready for the next build step.
- Press Cmd+Space on macOS or the Windows key on Windows to open system search.
- Type Visual Studio Code into system search.
- Press Enter to open Visual Studio Code.
- Click Terminal in the top menu.
- Click New Terminal.
- Check the Java runtime and compiler by running these commands:
java --version
javac --version
Why check both commands?
- The first command confirms which Java runtime starts the API.
- The second command confirms which compiler builds the Java source.
- Both results need to report release 21.
✔️ Both commands show the required release
Java release 21 is ready for the source-file run and container build used later.
ⓧ One command shows an older release
Your terminal is still using an older Java runtime or compiler. Installing the required JDK gives both commands the same release.
- Open the official Amazon Corretto documentation for release 21.
- Choose the installation package for your operating system.
- Complete the JDK installation.
- Restart Visual Studio Code.
- Repeat both Java checks above.
ⓧ A Java command is not found
A complete JDK installation provides both terminal commands. Use the official package for your operating system.
- Open the official Amazon Corretto documentation for release 21.
- Choose the installation package for your operating system.
- Complete the JDK installation.
- Restart Visual Studio Code.
- Repeat both Java checks above.
Java checks still disagree?
A terminal opened before the JDK installation can keep an older command path. Restart Visual Studio Code before checking again.
Use the operating-system instructions in the Amazon Corretto documentation if only one Java command reports release 21.
Help me fix my Java runtime and compiler versions.
Before you run the final check, consider whether every command will succeed on the first pass.
- Verify your cloud identity and local tools together by running these commands:
aws sts get-caller-identity
java --version
javac --version
docker version
What confirms a ready environment?
- The AWS response shows an account and ARN for your temporary session.
- The Java runtime reports release 21.
- The Java compiler reports release 21.
- Docker shows both client and server details.
✔️ I see every expected result
That is the environment ready. Your AWS session works in the selected Region.
Your Java toolchain matches the project. Your Docker engine is also accepting requests.
ⓧ Docker server details are missing
Docker client output proves the command is installed. Server details appear once the container engine is running.
- Start Docker Desktop through system search if you use the desktop application.
- Start the existing Docker service through your operating system's service manager if you use Docker Engine.
- Wait for the container engine to finish starting.
- Repeat the final verification commands above.
You should now see Docker client details followed by server details.
Docker server still unavailable?
Check that Docker Desktop has finished starting. A newly launched engine can take a short time before it accepts terminal requests.
Help me connect the Docker client to the Docker server.
Your cloud identity is verified in us-east-1. Next, you will build the first working version of the Mission Status API.
Build the First Local API
Your AWS identity is verified. Your local tools are ready.
Now you'll build the first local API with Java's built-in HTTP server. A local response proves the application logic before Docker or cloud deployment adds extra layers.
This version listens on 127.0.0.1 at port 8080. That narrow binding gives you a clean local baseline for the container test that follows.
In this step, get ready to:
- Create the local project structure.
- Add JSON responses for the mission-status routes.
- Run the API locally and verify both endpoints.
Create the project foundation
A Visual Studio Code workspace keeps the source file inside one named project folder. You will create it on your Desktop so it stays easy to find.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder....
- Select your Desktop as the location.
- Select New Folder.
- Enter mission-status-api as the folder name.
- Confirm the folder with Select Folder on Windows or Open on macOS.
You will see mission-status-api at the top of the Explorer sidebar. This confirms that VS Code is working inside the correct folder.
- Click the New Folder button in the Explorer sidebar.
- Enter src and press Enter.
- Select the src folder in the Explorer sidebar.
- Click the New File... button.
- Enter MissionStatusApi.java and press Enter.
- Paste the runnable server foundation below into the editor:
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
public class MissionStatusApi {
private static final int PORT = 8080;
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", PORT), 0);
server.start();
System.out.println("Mission Status API listening on port " + PORT);
}
}
What does this code establish?
- The PORT constant keeps the listening port at 8080.
- The HttpServer.create() call binds the process to the loopback address at 127.0.0.1.
- The server.start() call begins listening for requests. The print statement gives you a visible startup signal.
- Save src/MissionStatusApi.java.
- Select View from the top menu bar.
- Select Terminal.
- Start the server foundation by running this command:
java --add-modules jdk.httpserver src/MissionStatusApi.java
What does this command do?
- The java command compiles the source file in source-file mode. It runs the resulting class immediately.
- The --add-modules jdk.httpserver option makes the built-in HTTP server module available.
- The src/MissionStatusApi.java path points Java to your source file.
You will see Mission Status API listening on port 8080 in the terminal. The terminal stays attached to the running Java process.
Server did not start?
- Confirm that the terminal is inside the mission-status-api folder.
- Check that the Explorer sidebar shows src/MissionStatusApi.java with matching capitalization.
Help me troubleshoot the local Java server.
Add the JSON responses
An HTTP context connects a URL path to code that handles its requests. Your two contexts will return mission status data and a focused health response.
- Return to the terminal panel.
- Press Ctrl+C to stop the server foundation.
The terminal prompt returns. You can now update the source before starting the complete API.
- Find this current method in src/MissionStatusApi.java:
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", PORT), 0);
server.start();
System.out.println("Mission Status API listening on port " + PORT);
}
What are you replacing?
This starter method creates the listener. It starts the server without registering any URL paths.
- Replace the current main() method with this expanded version:
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", PORT), 0);
server.createContext("/", exchange -> sendJson(
exchange,
200,
"{\"service\":\"mission-status-api\",\"status\":\"operational\",\"timestamp\":\""
+ Instant.now()
+ "\"}"
));
server.createContext("/health", exchange -> sendJson(
exchange,
200,
"{\"status\":\"healthy\"}"
));
server.start();
System.out.println("Mission Status API listening on port " + PORT);
}
How do the routes work?
- The / context returns the service name. It also returns the operational status.
- The Instant.now() call adds a live UTC timestamp to every root response.
- The /health context returns a small response that can support a load-balancer health check.
Both contexts call sendJson(). Add that helper before you run the file again.
- Add the sendJson() helper before the final class-closing brace by pasting this code:
private static void sendJson(HttpExchange exchange, int statusCode, String body) throws IOException {
byte[] response = body.getBytes(StandardCharsets.UTF_8);
Headers headers = exchange.getResponseHeaders();
headers.set("Content-Type", "application/json; charset=utf-8");
exchange.sendResponseHeaders(statusCode, response.length);
try (OutputStream output = exchange.getResponseBody()) {
output.write(response);
}
}
What does the response helper do?
- The response byte array converts each JSON string into UTF-8 data.
- The headers variable marks the response as JSON.
- The statusCode parameter controls the HTTP response code. The body parameter holds the JSON content.
- The output stream sends the response bytes to the browser. The try block closes the stream after writing.
- Save src/MissionStatusApi.java.
- Return to the integrated terminal.
- Start the complete API by running this command:
java --add-modules jdk.httpserver src/MissionStatusApi.java
What happens on this run?
Java recompiles the updated source file. The running server now includes both URL contexts and the shared response helper.
- Switch back to the browser from earlier.
- Enter http://localhost:8080/ in the address bar.
You will see JSON containing "service":"mission-status-api" and "status":"operational". The response also contains a live UTC timestamp.
You've got the first live response. Your Java logic now answers a real browser request.
Root response not loading?
- Confirm that the terminal still shows the running Java process.
- Check that the browser address uses port 8080.
- Compare the root context path with /.
Help me fix the local API response.
Verify the local API
A health check gives deployment infrastructure a small endpoint to test. The root route proves that the application can also return changing mission data.
Before you compare the two addresses, which response do you expect to carry the live timestamp?
- Refresh the browser tab at http://localhost:8080/.
- Replace the address with http://localhost:8080/health.
At the root address, you will see "service":"mission-status-api" with "status":"operational". The timestamp value reflects the current UTC time.
At /health, you will see {"status":"healthy"}. This focused response proves that the health route is ready for later deployment checks.
Health response missing?
- Check that the address ends with /health.
- Confirm that the health context calls sendJson() with the healthy response body.
- Restart the Java process after saving any correction.
Help me debug the health endpoint.
- Return to the integrated terminal.
- Press Ctrl+C to stop the local Java process.
The terminal prompt returns when the process stops. Strong work. Both routes responded locally before the server shut down.
✔️ Awesome, I've got everything!
Great. Double check that you saved src/MissionStatusApi.java after testing both local URLs.
ⓧ I'd like to double check the full code
Your src/MissionStatusApi.java file should match this version:
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
public class MissionStatusApi {
private static final int PORT = 8080;
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", PORT), 0);
server.createContext("/", exchange -> sendJson(
exchange,
200,
"{\"service\":\"mission-status-api\",\"status\":\"operational\",\"timestamp\":\""
+ Instant.now()
+ "\"}"
));
server.createContext("/health", exchange -> sendJson(
exchange,
200,
"{\"status\":\"healthy\"}"
));
server.start();
System.out.println("Mission Status API listening on port " + PORT);
}
private static void sendJson(HttpExchange exchange, int statusCode, String body) throws IOException {
byte[] response = body.getBytes(StandardCharsets.UTF_8);
Headers headers = exchange.getResponseHeaders();
headers.set("Content-Type", "application/json; charset=utf-8");
exchange.sendResponseHeaders(statusCode, response.length);
try (OutputStream output = exchange.getResponseBody()) {
output.write(response);
}
}
}
Your Java API answers both requests locally. Next, you'll package it into a Docker image and test the published port.
Containerize the API
Your Java API already responds on your computer. Packaging it as a container image creates the artifact that AWS can run later.
However, a container has its own network boundary. This step tests whether publishing port 8080 can reach a server that listens on loopback.
In this step, get ready to:
- Create the container build files.
- Build mission-status-api:latest for linux/amd64.
- Run the image through a published port.
Create the container files
A Dockerfile gives Docker the recipe for building your image. Amazon Corretto supplies the Java 21 environment inside it.
- Use the VS Code file sidebar to create Dockerfile inside the mission-status-api folder.
You should see a blank Dockerfile in the file sidebar.
- Define the image by pasting this content into Dockerfile:
FROM amazoncorretto:21
WORKDIR /app
COPY src/MissionStatusApi.java .
RUN javac --add-modules jdk.httpserver MissionStatusApi.java
EXPOSE 8080
CMD ["java", "--add-modules", "jdk.httpserver", "MissionStatusApi"]
What Does This File Do?
- The FROM instruction starts from the official amazoncorretto:21 image.
- The WORKDIR instruction makes /app the container's working folder.
- The COPY instruction places MissionStatusApi.java in that folder.
- The RUN instruction compiles the API with the jdk.httpserver module.
- The EXPOSE instruction documents port 8080 as the listening port.
- The CMD instruction starts MissionStatusApi when the container runs.
- Save Dockerfile.
- Confirm that Dockerfile remains visible inside the mission-status-api folder in the file sidebar.
Dockerfile Missing or Misnamed?
Confirm that the filename is exactly Dockerfile. Remove any added file extension.
Make sure the file sits beside the src folder inside mission-status-api.
Help me check my container file location.
Docker sends files from the project folder into its build context. A .dockerignore file keeps repository metadata plus generated files out of that context.
- Use the VS Code file sidebar to create .dockerignore inside the mission-status-api folder.
You should see a blank .dockerignore beside Dockerfile.
- Add the build exclusions by pasting this content into .dockerignore:
.git
.vscode
out
*.class
What Gets Excluded?
- The .git entry excludes repository metadata.
- The .vscode entry excludes editor settings.
- The out entry excludes generated output.
- The *.class entry excludes compiled Java classes from your computer.
- Save .dockerignore.
- Confirm that Dockerfile plus .dockerignore appear beside the src folder.
Exclusions Not Saving?
Check that the filename begins with a dot. Confirm that it does not have an added file extension.
Help me check my Docker ignore file.
✔️ Awesome, I've got everything!
Great. Double check that you saved Dockerfile plus .dockerignore.
Your MissionStatusApi.java file should still contain the loopback binding from the previous step.
ⓧ I'd like to double check the full code
Your .dockerignore file should look like this:
.git
.vscode
out
*.class
Your Dockerfile should look like this:
FROM amazoncorretto:21
WORKDIR /app
COPY src/MissionStatusApi.java .
RUN javac --add-modules jdk.httpserver MissionStatusApi.java
EXPOSE 8080
CMD ["java", "--add-modules", "jdk.httpserver", "MissionStatusApi"]
Your unchanged src/MissionStatusApi.java file should look like this:
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
public class MissionStatusApi {
private static final int PORT = 8080;
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", PORT), 0);
server.createContext("/", exchange -> sendJson(
exchange,
200,
"{\"service\":\"mission-status-api\",\"status\":\"operational\",\"timestamp\":\""
+ Instant.now()
+ "\"}"
));
server.createContext("/health", exchange -> sendJson(
exchange,
200,
"{\"status\":\"healthy\"}"
));
server.start();
System.out.println("Mission Status API listening on port " + PORT);
}
private static void sendJson(HttpExchange exchange, int statusCode, String body) throws IOException {
byte[] response = body.getBytes(StandardCharsets.UTF_8);
Headers headers = exchange.getResponseHeaders();
headers.set("Content-Type", "application/json; charset=utf-8");
exchange.sendResponseHeaders(statusCode, response.length);
try (OutputStream output = exchange.getResponseBody()) {
output.write(response);
}
}
}
Build the Linux image
A container image targets a specific CPU architecture. Building for linux/amd64 matches the default X86_64 architecture used by ECS Express Mode.
The first build may take a few minutes while Docker downloads the Corretto image. A quiet pause during that download is normal.
- Build mission-status-api:latest for the target platform by running this command in the VS Code terminal:
docker build --platform linux/amd64 -t mission-status-api:latest .
What Does This Build Command Do?
- The docker build command reads your Dockerfile.
- The --platform linux/amd64 option selects the deployment architecture.
- The -t mission-status-api:latest option assigns the image name plus tag.
- The final . uses the current mission-status-api folder as the build context.
- Wait for the image build to finish.
You should see the build complete without an error. The final output should report the mission-status-api:latest image tag.
Good work. Your API now has a Linux image that matches the architecture expected by ECS Express Mode.
Image Build Failing?
Confirm that the terminal is inside the mission-status-api folder. Docker must find Dockerfile in the current folder.
Check that Docker is still running. The build needs access to the Docker engine.
Help me troubleshoot this image build.
Run the container and test the boundary
Port publishing forwards traffic from a port on your computer to a port inside the container. The API listener still decides whether that forwarded traffic can reach the process.
Before you run the container, decide whether the browser can reach the API through the published port.
- Start the container with port 8080 published by running this command:
docker run --name mission-status-api --rm -p 8080:8080 mission-status-api:latest
What Does This Run Command Do?
- The --name mission-status-api option gives the container a predictable name.
- The --rm option removes the container after it stops.
- The -p 8080:8080 option publishes the container port through the matching host port.
- The mission-status-api:latest value selects the image you just built.
The terminal should display Mission Status API listening on port 8080. This confirms that the Java process started inside the container.
- Leave the container running in that terminal.
- Open http://localhost:8080/ in your browser.
Your browser cannot connect to the API. This shortfall is the intended result.
The server listens on 127.0.0.1 inside the container. That address belongs to the container's own loopback interface.
Docker publishes port 8080 successfully. The loopback-only listener cannot accept the forwarded traffic.
Seeing a Different Result?
If the startup message is missing, confirm that the image build completed. Check that Docker is still running.
If the page loads, confirm that src/MissionStatusApi.java still contains new InetSocketAddress("127.0.0.1", PORT).
Help me reproduce the container networking wall.
- Return to the terminal running the container.
- Press Ctrl+C to stop it.
Your terminal returns to its prompt. Docker removes the test container because the run command included --rm.
You have proved that the image starts while its loopback listener blocks published traffic. Next up, you will change the network binding so the container can serve the API.
Fix the Container Network Binding
Your Docker image already starts successfully. The failed browser request proves the Java process listens only on the container's loopback interface.
A wildcard binding accepts traffic arriving through Docker's published port. This small socket change makes both JSON endpoints reachable from your computer.
In this step, get ready to:
- Change the server from a loopback binding to a wildcard binding.
- Rebuild the Linux/AMD64 Docker image.
- Prove both JSON endpoints work through the published port.
Bind the server to every local address
- In src/MissionStatusApi.java, find the line where HttpServer.create() uses new InetSocketAddress("127.0.0.1", PORT).
- Replace new InetSocketAddress("127.0.0.1", PORT) with new InetSocketAddress(PORT).
- Save src/MissionStatusApi.java.
What changed?
The two-argument socket address pinned the server to the container's own loopback interface. The one-argument new InetSocketAddress(PORT) constructor uses the wildcard local address for binding.
The server can now accept traffic forwarded into the container through port 8080.
✔️ Awesome, I've got everything!
Your Java file now uses the wildcard socket address. Double check that you saved src/MissionStatusApi.java.
ⓧ I'd like to double check the full code
Your complete src/MissionStatusApi.java file should look like this:
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
public class MissionStatusApi {
private static final int PORT = 8080;
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(PORT), 0);
server.createContext("/", exchange -> sendJson(
exchange,
200,
"{\"service\":\"mission-status-api\",\"status\":\"operational\",\"timestamp\":\""
+ Instant.now()
+ "\"}"
));
server.createContext("/health", exchange -> sendJson(
exchange,
200,
"{\"status\":\"healthy\"}"
));
server.start();
System.out.println("Mission Status API listening on port " + PORT);
}
private static void sendJson(HttpExchange exchange, int statusCode, String body) throws IOException {
byte[] response = body.getBytes(StandardCharsets.UTF_8);
Headers headers = exchange.getResponseHeaders();
headers.set("Content-Type", "application/json; charset=utf-8");
exchange.sendResponseHeaders(statusCode, response.length);
try (OutputStream output = exchange.getResponseBody()) {
output.write(response);
}
}
}
Rebuild the image
The existing image still contains the loopback-bound version of the compiled Java class. Rebuilding packages your saved wildcard binding into the image.
- Rebuild the local image for Linux/AMD64 by running this command:
docker build --platform linux/amd64 -t mission-status-api:latest .
What does this build do?
- The --platform linux/amd64 option creates an image for the X86_64 target used later by ECS Express Mode.
- The -t mission-status-api:latest option assigns the existing project name and tag to the rebuilt image.
- The final . uses the current mission-status-api folder as the build context.
- Wait for the image build to finish.
- Confirm the command returns to the terminal prompt without reporting a failed build.
The local mission-status-api:latest tag now points to an image containing the wildcard binding.
Build not completing?
- Confirm Docker is still running on your computer.
- Check that your terminal is inside the mission-status-api folder containing the Dockerfile.
- Confirm that src/MissionStatusApi.java is saved before rebuilding.
Help me troubleshoot the Docker image build.
Prove the fixed container works
The container stays attached to this terminal while the API runs. Its startup message confirms that the updated Java process launched inside the rebuilt image.
- Predict whether the browser can reach the API through the published port this time.
- Start the rebuilt container by running this command:
docker run --name mission-status-api --rm -p 8080:8080 mission-status-api:latest
What does this command do?
- The --name mission-status-api option gives the running container a predictable name.
- The --rm option removes the container after it stops.
- The -p 8080:8080 option forwards host port 8080 to container port 8080.
You'll see Mission Status API listening on port 8080 in the terminal. The container keeps running while you test it in the browser.
- Predict whether both browser requests will pass through the published port.
- Open http://localhost:8080/ in your browser.
- Open http://localhost:8080/health in your browser.
At the root URL, you'll see JSON containing "service":"mission-status-api". The same response contains "status":"operational" plus a live UTC timestamp.
At the health URL, you'll see {"status":"healthy"} from inside the container.
That is the container boundary cleared. Both JSON endpoints now respond through Docker's published port.
Browser still cannot connect?
- Stop the current container by pressing Ctrl+C before rebuilding.
- Confirm src/MissionStatusApi.java contains new InetSocketAddress(PORT).
- Run the build command from the previous substep again before starting a new container.
Help me troubleshoot the published Docker port.
- Stop the test container by pressing Ctrl+C in its terminal.
The --rm option removes the stopped test container automatically.
Your rebuilt container now serves both JSON endpoints through Docker's published port. Next, you'll store the image in Amazon ECR before deploying it through Amazon ECS Express Mode.
Publish and Deploy the API
Your Docker container now serves the mission-status JSON through its published port. The health endpoint works too.
A local image exists only on your computer. Amazon ECR gives AWS a private registry for that image.
Amazon ECS Express Mode turns the stored image into a public HTTPS service. It also manages the infrastructure around your container.
In this step, get ready to:
- Publish the existing image to a private Amazon ECR repository.
- Deploy the stored image digest with Amazon ECS Express Mode.
- Verify the public HTTPS endpoint.
Publish the image to Amazon ECR
Amazon ECR stores your container image inside your AWS account. It assigns an immutable image digest that identifies the exact image contents.
- Switch back to the AWS Management Console from earlier.
- Confirm that the console shows US East (N. Virginia), us-east-1.
- Use the console search bar to open Amazon ECR.
- Choose Private repositories.
- Choose Create repository.
- Enter mission-status-api in Repository name.
- Keep the default AES-256 encryption.
- Choose Create.
You will see mission-status-api in Private repositories. Your private image destination is ready.
- Select the mission-status-api repository.
- Choose View push commands.
- Run the generated authentication command in the Visual Studio Code terminal from earlier.
Docker will confirm that it authenticated with your private registry. The generated workflow now has permission to push the image.
Why use the generated commands?
The console builds registry commands with your account-specific URI. It also uses your selected Region.
The authentication token is temporary. It remains valid for 12 hours.
- Run the generated tag command in the Visual Studio Code terminal.
- Run the generated push command in the Visual Studio Code terminal.
You will see Docker report the push progress. When the upload finishes, Amazon ECR contains the image you tested locally.
- Return to the Images view.
- Confirm that the image shows the latest tag.
- Confirm that the image shows a SHA digest.
Push not completing?
Confirm that your AWS CLI session remains active in us-east-1. Run the unedited commands from View push commands again if the session has expired.
Make sure the generated tag command starts from mission-status-api:latest. A different local image name points the push at the wrong artifact.
Help me troubleshoot my Amazon ECR authentication or image push.
Deploy with Amazon ECS Express Mode
Express Mode uses the exact digest you select from Amazon ECR. This keeps the deployment attached to the image you verified.
Why Express Mode?
Amazon ECS Express Mode creates an AWS Fargate service for your container.
It places an Application Load Balancer in front of that service.
It also manages the supporting infrastructure for health checks.
A standard ECS setup requires you to connect each resource yourself. Express Mode keeps this project focused on the deployment workflow.
- Switch from Amazon ECR to Amazon ECS in the AWS Management Console.
- Choose Express mode.
- Choose Browse ECR images.
- Choose Private repository.
- Select mission-status-api as the Image.
- Set Select image by to Image digest.
- Select the newest mission-status-api image digest.
The deployment form now points to the immutable image you uploaded. Express Mode uses that exact artifact for its tasks.
- Keep the existing Task execution role when the field already contains one.
- Choose Create new role under Task execution role when no role is available.
- Keep the existing Infrastructure role when the field already contains one.
- Choose Create new role under Infrastructure role when no role is available.
Both required role fields now contain a role selection. These roles let Express Mode pull your image and manage the deployment resources.
- Expand Additional configurations - optional.
- Enter mission-status-api in Name.
- Enter 8080 in Container port.
- Enter /health in Health check path.
- Keep the CPU architecture set to X86_64.
Charges start with deployment
Creating the service starts usage-based charges for AWS Fargate and the Application Load Balancer.
You stay in control because the cleanup section removes the service after testing.
- Start the managed deployment by choosing Create.
The deployment can remain in progress for several minutes while AWS creates the managed resources. The Resources tab shows that work as it progresses.
Service creation paused?
The first Express Mode service in an account can show Invalid Parameter Exception: Unable to assume the service linked role. Wait a few seconds before choosing Create again.
Help me troubleshoot my ECS Express Mode deployment.
Verify the public API and managed resources
The Application URL is your public HTTPS endpoint. The same API that worked inside Docker should now respond through AWS.
- Wait until the deployment completes.
- Record the generated Application URL here: your ECS Express Mode Application URL.
- Open the Application URL from the deployment page.
You will see JSON containing "service":"mission-status-api". You will also see "status":"operational".
Before you check the health route, do you expect the load balancer to reach the same endpoint that worked inside Docker?
- Open the Application URL followed by /health in your browser.
You will see {"status":"healthy"}. This confirms that public traffic reaches a healthy container task.
- Return to the ECS Express Mode service page from earlier.
- Choose the Resources tab.
- Confirm that the managed resource list contains every component described below.
What did Express Mode create?
- AWS Fargate tasks run your container.
- The Application Load Balancer accepts public HTTPS traffic.
- The target group sends health checks to /health.
- Security groups control network access.
- Auto scaling adjusts task capacity.
- The certificate secures the HTTPS listener.
- Amazon CloudWatch stores application logs.
- The task definition records the container settings.
- IAM roles grant the required permissions.
That is the full deployment path working. Your local Java API now runs behind a public AWS HTTPS endpoint.
Secret mission
Ship a Versioned Release
Your first image is live. Add a /version endpoint, publish mission-status-api:v2 to Amazon ECR, then release it through an Amazon ECS Express Mode canary deployment.
Clean Up Your Resources
Clean Up Your Resources
Your public Amazon ECS Express Mode deployment can continue generating AWS charges while it remains active. Choose below whether to keep the cloud resources, remove the deployment, or delete every AWS resource from this project.
Cost warning
Amazon ECS Express Mode has no additional service charge. Underlying AWS Fargate usage is billed separately.
The Application Load Balancer bills each hour or partial hour.
Amazon ECR storage can incur charges. Amazon CloudWatch usage can incur charges.
Public IPv4 addresses can incur charges. Data transfer can incur charges.
Resources you used:
- Amazon ECS Express Mode service mission-status-api with its managed Fargate tasks, load balancing, scaling, logging, certificate, networking, security, task definition, and role resources.
- Private Amazon ECR repository mission-status-api containing the latest and v2 image tags.
Keep everything running
No action needed. Choose this if you are still using the public API or want to demonstrate its versioned response.
- Keep the mission-status-api Express Mode service available.
- Check your AWS charges while the service remains active.
- Keep the ECR repository to preserve both immutable image digests.
- Keep your local project artifacts and Docker image tags at no AWS cost.
Pause - I'll come back to this later
Shut down the active cloud deployment to reduce ongoing charges. Your ECR images and local project artifacts remain available.
- Return to the Amazon ECS console from earlier.
- Choose Clusters.
- Select the cluster that contains mission-status-api.
- Choose Services.
- Select mission-status-api.
- Choose Delete.
- Enter delete in the confirmation field.
- Confirm the deletion request.
AWS may show the service as draining while it removes the managed resources. That status is expected.
- Stay on the deletion page until AWS finishes the cleanup report.
- Return to Services after the cleanup completes.
You should no longer see the mission-status-api service. The deletion report shows which managed resources AWS removed or retained.
Delete - I don't want to use this again
This permanently removes the public deployment and its private image repository. Your local project artifacts remain on your computer at no AWS cost.
- Return to the Amazon ECS console from earlier.
- Choose Clusters.
- Select the cluster that contains mission-status-api.
- Choose Services.
- Select mission-status-api.
- Choose Delete.
- Enter delete in the confirmation field.
- Confirm the deletion request.
AWS may show the service as draining while it removes the managed resources. That status is expected.
- Stay on the deletion page until AWS finishes the cleanup report.
The deletion report shows which managed resources AWS removed or retained. The ECR repository remains until you delete it separately.
- Delete the non-empty mission-status-api repository in us-east-1 by running this command:
aws ecr delete-repository --repository-name mission-status-api --force --region us-east-1
What does this command do?
The --force flag lets AWS delete the non-empty repository. This permanently removes the latest and v2 images from ECR.
- Return to Private repositories in Amazon ECR.
You should no longer see mission-status-api in the private repository list.
Can't delete the repository?
- Refresh your AWS CLI browser session if its temporary authentication has expired.
- Confirm that your AWS CLI session targets us-east-1.
- Confirm that your AWS identity has permission to delete the repository.
- Help me diagnose my ECR repository deletion.
Nice Work!
Nice Work!
You made it. Your Java 21 Mission Status API is live at a public HTTPS address after a versioned cloud release.
You've learned how to:
- Built a Java 21 Mission Status API that returns live mission data. Added JSON health responses for automated checks.
- Traced an unreachable published port to a container loopback binding. Switched to a wildcard binding so Docker could route traffic into the API.
- Published a Linux/AMD64 image to private Amazon ECR with an immutable digest. Deployed that digest through Amazon ECS Express Mode. Inspected the managed AWS Fargate runtime. Followed public requests through the Application Load Balancer.
- Secret Mission: Added the /version endpoint. Published a v2 image with a new digest. Rolled it out through a managed canary deployment.
Ready to quiz yourself?