ACE-U field notes
From docker compose up to a public URL: ports, health and scale-to-zero
What changes when a local demo needs a real address, and how to keep it close to free when nobody is looking at it.
Problem
A local demo is easy. A public one raises new questions: which port is exposed, how the platform knows the app is healthy, what the first request does after a quiet night, and how to be sure a traffic spike does not turn into a surprise bill. You do not need a cluster for this. A managed container platform with scale-to-zero gives you a URL that costs almost nothing when idle.
Options and trade-offs
- Run a VM and keep it on: simple, but you pay for the idle hours.
- Run a cluster: powerful, and far more than a demo needs.
- Use a managed container platform with scale-to-zero: you accept a cold start in return for near-zero idle cost.
My recommendation
Make the app platform-friendly first: it listens on a configurable port, has a health endpoint, and redirects the root path somewhere useful. Then deploy a digest-pinned image with a minimum of zero replicas, managed TLS, and budget alerts set well below the ceiling.
Worked example
A tiny Java server with no dependencies. It reads the port from the environment, answers a health check, and redirects the root path to the docs.
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;
public class Main {
public static void main(String[] args) throws IOException {
int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8080"));
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext("/health", ex -> respond(ex, 200, "application/json", "{\"status\":\"ok\"}"));
server.createContext("/docs", ex -> respond(ex, 200, "text/html", "<h1>Docs</h1>"));
server.createContext("/", ex -> {
if ("/".equals(ex.getRequestURI().getPath())) {
ex.getResponseHeaders().add("Location", "/docs");
ex.sendResponseHeaders(302, -1);
ex.close();
} else {
respond(ex, 404, "text/plain", "not found");
}
});
server.start();
}
private static void respond(HttpExchange ex, int status, String type, String body) throws IOException {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
ex.getResponseHeaders().add("Content-Type", type);
ex.sendResponseHeaders(status, bytes.length);
try (OutputStream out = ex.getResponseBody()) {
out.write(bytes);
}
}
}
The deployment description is illustrative, since every platform has its own field names, but the shape is the same everywhere:
{
"name": "demo-app",
"image": "registry.example.com/team/app@sha256:<digest>",
"containerPort": 8080,
"healthCheckPath": "/health",
"ingress": { "tls": true },
"autoscale": { "minReplicas": 0, "maxReplicas": 3 }
}
docker compose up --build
curl -i http://127.0.0.1:8080/
curl -s http://127.0.0.1:8080/health
Checklist
- Read the port from the environment, and expose the one the platform expects.
- Add a cheap health endpoint, and point the platform at it.
- Redirect the root path to something useful.
- Deploy by digest, with managed TLS.
- Set minimum replicas to zero, and test the first request after idle.
- Set a budget alert well under your ceiling, and send it somewhere you will see it.
Where this doesn't apply
If you need steady low latency, scale-to-zero is the wrong choice, because the first request after a quiet period will be slow.
One honest limitation
A cold start on a JVM service can take seconds. You can soften it with a smaller image and class-data sharing, but you cannot remove it, so decide whether a slow first visit is acceptable for the demo.
Sources
- Docker Docs: Compose file reference (ports)
- Docker Docs: Dockerfile HEALTHCHECK
- Oracle Java Documentation: com.sun.net.httpserver package