Repository navigation
Deployment
Applies to Milo 1.2.0-RC1. Source baseline 69cb25087, 2026-10-12.
When your application is ready to leave the development machine, this page takes it to a host that other machines connect to. It uses the thermostat server from Your first server as the example, and the ownership rules apply equally to a client application that embeds Milo. Make the server and client workflow pass locally before you start, so that any new failure belongs to the deployment.
The tutorial server makes three choices that only work on one machine. It binds and advertises 127.0.0.1, so only local clients reach it. Its endpoint uses SecurityPolicy None with anonymous access, so it has no application identity and checks no peer certificates. It runs in a terminal until you press Enter.
A deployment replaces each choice, in the order of the sections below. The server listens on the interfaces that carry client traffic and advertises names those clients can resolve. It keeps a persistent identity and trust store for its secured endpoints, and a service manager starts and stops it. Finally, you repeat the tutorial's operations from the real client network with deployment security enabled. Before you begin, decide which part of your application owns the network configuration, the persisted security material, and the runtime resources.
The bind address selects the local interfaces the listener accepts connections on. The advertised hostname goes into the endpoint URLs that discovery returns, so clients connect to it. The tutorial sets both to 127.0.0.1, and if you set neither, EndpointConfig binds and advertises localhost:12685. The advertised URL also uses bindPort, because there is no separate advertised-port setting.
This method builds the thermostat's deployment endpoints, one secured endpoint per advertised hostname, all listening on every IPv4 interface.
static Set<EndpointConfig> deploymentEndpoints(List<String> advertisedHostnames, int port) {
var endpoints = new LinkedHashSet<EndpointConfig>();
for (String hostname : advertisedHostnames) {
EndpointConfig endpoint =
EndpointConfig.newBuilder()
.setBindAddress("0.0.0.0")
.setHostname(hostname)
.setBindPort(port)
.setPath("/wiki")
.setSecurityPolicy(SecurityPolicy.Basic256Sha256)
.setSecurityMode(MessageSecurityMode.SignAndEncrypt)
.addTokenPolicy(OpcUaServerConfig.USER_TOKEN_POLICY_USERNAME)
.build();
endpoints.add(endpoint);
}
return endpoints;
}Bind a specific interface address instead of 0.0.0.0 when the server should accept traffic from only one network. The endpoints share one listening socket because they share the bind address and port. Pass the set to OpcUaServerConfig.builder().setEndpoints(...) in place of the tutorial's loopback endpoint, together with the certificate manager and username identity validator described in Server configuration and Server security. No endpoint calls setCertificate(...), so each one selects its identity from the DefaultApplicationGroup and follows certificate rotation.
The loop creates one EndpointConfig per hostname because GetEndpoints prefers endpoints whose advertised hostname matches the hostname in the discovery request URL. If none match, it returns every endpoint eligible for the requested transport profiles. Discovery does not substitute the request hostname into an endpoint URL, so each hostname clients should discover needs its own endpoint. Without a None endpoint for the path, a None channel still serves discovery, so clients can find these secured endpoints, but Milo rejects every other service on it with Bad_SecurityPolicyRejected.
A wildcard bind address such as 0.0.0.0 is not a destination to advertise. A container hostname, loopback address, or private DNS name can work inside one environment yet fail for a remote client. Keep client-resolvable names, routing, firewall rules, and the certificate's subject alternative names consistent, because the client must resolve the name, reach the port, and, with hostname validation, find the name in the server certificate.
If a container or NAT maps a different external port, the advertised port is wrong for clients using that mapping. Keep internal and external ports equal, or set up an explicit client URL override with EndpointUtil.updateUrl(...) as described in Client connecting. Rewriting an endpoint URL for a known NAT arrangement does not authenticate the peer, so keep the intended identity and trust checks.
When a remote connection fails, check both URLs the client uses, the initial discovery URL and the endpoint URL that discovery returns. If GetEndpoints succeeds and the connection then fails, the selected advertised endpoint is often unreachable.
Reverse Connect turns the connection around. Allow the server to reach the client's listener and configure routing selectors. The SecureChannel and Session that follow still need normal peer and user validation. See Reverse Connect.
The OPC TCP connection limits are enabled process-wide by default and apply to every non-loopback remote address. Loopback is exempt, so a loopback load test does not exercise them.
| Limit | Default | System property |
Stack.ConnectionLimits field |
|---|---|---|---|
| Connection attempts from one address | 4 per 1,000 ms |
milo.stack.rateLimit.maxAttempts, milo.stack.rateLimit.windowMs
|
RATE_LIMIT_MAX_ATTEMPTS, RATE_LIMIT_WINDOW_MS
|
| Concurrent connections from one address | 100 | milo.stack.rateLimit.maxConnectionsPerAddress |
RATE_LIMIT_MAX_CONNECTIONS_PER_ADDRESS |
| Concurrent connections in total | 10,000 | milo.stack.rateLimit.maxConnections |
RATE_LIMIT_MAX_CONNECTIONS |
Clients behind one NAT or proxy share one address budget. To tune the limits without code, pass the system properties to the JVM, for example -Dmilo.stack.rateLimit.maxConnectionsPerAddress=500. Integer values must be positive. milo.stack.rateLimit.enabled=false turns off the attempt limit and both connection caps, which is safer than raising the attempt limit. Invalid values are logged at WARN and the default is kept. When any of these properties is set, the server logs the effective limits at INFO.
Milo reads the properties when Stack.ConnectionLimits is first loaded, so pass them on the command line rather than setting them from code. Code can instead assign the Stack.ConnectionLimits fields, and an assignment wins over the property. Assign them before the first server transport binds, because the shared rate limiter captures the values at that point. The Hello deadline is set separately with OpcTcpServerTransportConfigBuilder.setHelloDeadline(...) and defaults to 10,000 ms. Stack.ConnectionLimits.HELLO_DEADLINE_MS is deprecated and has no effect.
A secured endpoint presents an application identity, and a new identity at every launch breaks the trust clients have already established. Generate or provision the identity once, store its private key securely, and reuse it across restarts. Server security walks through provisioning a persistent certificate store and trust list.
Also persist certificate chains, trusted and issuer certificates, CRLs where used, and enrollment state. Keep rejected certificates in quarantine distinct from certificates an administrator has approved, because only the approved set should grant trust.
These stores need an owner. A DefaultCertificateManager registers groups but does not own their backing resources, and removing a group from the manager does not close it. Initialize file stores and trust-list managers before use, coordinate updates, and close their watchers only after clients and servers stop using them. When several processes or store instances can update a keystore, the application must coordinate writers.
FileBasedTrustListManager logs write and delete failures at ERROR instead of returning them to the caller, and a reload that cannot list a directory keeps the previous snapshot and logs WARN. Alert on these logs. When persistence must be confirmed, read the backing directories after administrative updates, because the in-memory trust list can change even when the write to disk failed.
Test certificate renewal with existing and new connections, and confirm that an untrusted replacement stays rejected. GDS Pull does not schedule or install certificates automatically, so renewal through a GDS stays application work. See Server security, Client security, and GDS client.
The tutorial registers its namespace with server.addLifecycleParticipant(...) and lets the server drive it. Keep that pattern for every namespace and device owner in a deployment, and register them before startup. The server starts them in registration order and stops them in reverse, so do not also start or stop registered participants yourself.
What changes is how the process ends. A service manager stops the process with a signal such as SIGTERM instead of a key press, and the JVM then runs its shutdown hooks but not the finally block in main. So a deployed server shuts down and releases the stack's shared resources from a shutdown hook. Call this method from main with a server built the way FirstServer.create(...) builds one, but with the deployment endpoints above.
static void runUntilStopped(OpcUaServer server) throws Exception {
var stopped = new CountDownLatch(1);
Runnable stopOnExit =
() -> {
stopServer(server);
stopped.countDown();
};
Runtime.getRuntime().addShutdownHook(new Thread(stopOnExit, "opcua-server-shutdown"));
server.startup().get(10, TimeUnit.SECONDS);
Set<EndpointConfig> configured = server.getConfig().getEndpoints();
List<EndpointConfig> bound = server.getBoundEndpoints();
if (!bound.containsAll(configured)) {
throw new IllegalStateException("Some configured endpoints did not bind: " + bound);
}
logger.info("Server ready on {}", bound);
stopped.await();
}
static void stopServer(OpcUaServer server) {
try {
server.shutdown().get(10, TimeUnit.SECONDS);
} catch (Exception e) {
logger.warn("Server shutdown did not complete", e);
} finally {
Stack.releaseSharedResources();
}
}The stack's shared threads are daemon threads and do not keep the JVM alive, so main waits on the latch until the service manager stops the process and the hook finishes. The hook is registered before startup, so it also cleans up when startup fails. The startup exception ends main, the JVM exits, and the hook shuts the server down. After startup, the method compares the bound endpoints with the configured ones, because startup can succeed with some endpoints unbound. It treats every configured endpoint as required, so if some are optional, compare against the set you need.
An application that also runs clients, Reverse Connect, or its own stores extends the shutdown path in this order.
- Stop accepting new application work. Stop application-owned enrollment, registration, and device tasks that can create more work.
- Delete subscriptions and other resources whose immediate cleanup matters. Then disconnect clients with
disconnect()or awaitdisconnectAsync(). Include clients whose connection attempt failed, because they keep trying to connect, including configured endpoint-resolution work, until you disconnect them. Stop accepting or creating Reverse Connect work according to the listener and target ownership described in Reverse Connect. - Await
server.shutdown(). This also stops its registered lifecycle participants. - Close application-owned stores, trust-list watchers, listeners, and executors after their users have stopped. Keep these cleanup paths active after a partial startup failure.
- At final process shutdown, after every Milo component has stopped, call
Stack.releaseSharedResources(). Never call it while another client or server in the process still needs the shared resources.
Restarting means building new objects. An OpcUaServer instance is single-use. Later startup calls return the same future, and a stopped or failed instance is not restarted, so construct a new server to restart. An AbstractLifecycle whose startup throws becomes stopped, and a later shutdown does not call its shutdown hook, so the startup implementation must release the resources it acquired before failing.
Use bounded waits and record cleanup failures. Keep the total wait below the service manager's stop timeout, because the service manager kills the process when that timeout expires and the hook stops where it is. An interrupted or timed-out shutdown does not prove that resources closed, so the application needs its own policy for that case. Preserve the original failure when reporting additional cleanup failures.
First check what the server bound and advertised. Compare server.getBoundEndpoints() with the intended endpoint configurations, as the service method above does. When an endpoint fails to bind, the server logs WARN "Failed to bind endpoint" and skips it. Startup can still succeed if another endpoint binds or a Reverse Connect target can be scheduled. Advertisements come from resolved endpoints, not the bound set, so a failed bind can leave an advertised URL with no listener. Check the discovery results as well as the bound set. For endpoints excluded during certificate or policy resolution, look for WARN "Omitting endpoint advertisement".
Then move to a host on the client network and repeat the tutorial's operations with the deployed endpoint, its security policy, and the intended user identity. For the thermostat, that means four checks.
- Browse the Objects folder and find Thermostat with Temperature, Setpoint, and AdjustSetpoint, as in Browsing.
- Read Temperature and get Good 21.5, as in Reading.
- Write Setpoint, the designated writable test Node, as in Writing. Write only that Node.
- Subscribe to Setpoint and receive a data notification after the write, as in Subscriptions.
In your own server, use the expected namespace, a known value that reads Good, one designated writable test Node, and a Node whose changes you can trigger. Also check that a restricted user is denied an operation. The tutorial server has no access control, so that check needs the authorization described in Access control.
Repeat the checks after an application restart to confirm stable NodeIds, persisted identity and trust, and expected subscription recovery. Repeat them after a peer restart and a deliberate network interruption to see how the application handles gaps and uncertain operation outcomes.
Record the Milo and JDK versions, application revision, endpoint policy and mode, requested and revised limits, data quality, and shutdown outcome. A later failure report can then be compared with a known-good run.
Milo logs through SLF4J, so supply an SLF4J logging implementation suited to the hosting application. At server class initialization, INFO logs record the Java, Stack, and Server SDK versions. Endpoint startup logs "Binding endpoint" at INFO for each endpoint. For certificate-path rejection details, enable DEBUG on org.eclipse.milo.opcua.stack.core.security.DefaultServerCertificateValidator during the investigation. The connection limiter logs each rejected connection at DEBUG under org.eclipse.milo.opcua.stack.transport.server.tcp.RateLimitingHandler. See Diagnostics and Wireshark for log categories and capture handling.
Measure service latency, queue behavior, and resource use under the intended workload before tuning. This guide does not prescribe a container image, service manager, or universal JVM sizing rule.
Use Your first client for finite client work and Your first server for registered participants, awaited shutdown, and final shared-resource cleanup. ExampleServer shows a broader address space and the per-hostname endpoint loop, but starts its namespaces itself and stops them before server shutdown. Its main() neither calls its shutdown method nor releases shared stack resources. Treat the tutorials and the service method above as the lifecycle reference.
Your server now passes the tutorial's checks from the client network. When a check fails, or a client reports an error later, continue with Troubleshooting, which starts from the symptom and finds the boundary that failed.
Related: Server configuration · Server security · Client connecting · Reverse Connect · Diagnostics and Wireshark · Reference
User guide for Milo 1.2.0-RC1 at source 69cb25087, 2026-10-12. Home · Getting started · Examples · Troubleshooting · Release notes
- Home
- Start here
- Client guide
- Server guide
- Feature guides
- Reference
- Operations
- Examples
- Release notes
- Contributing