Repository navigation
Server Configuration
Milo 1.2.0-RC1, Java 17. API baseline 69cb25087804.
This page explains the settings that decide where clients reach your server and who owns each resource it runs. Read it before you change the loopback endpoint from Your first server, expose a server on a network, or give the server your own components to start and stop. The server needs milo-sdk-server and the matching OPC TCP transport dependencies described in Getting started.
You describe a server once, before it starts. OpcUaServerConfig holds the application identity, the endpoints, user authentication, and limits and worker threads. OpcUaServer combines it with a transport factory, and you register the components the server should start and stop.
Decide the application URI, advertised hosts, security policies, and authentication rules before you expose a listener, because certificates and clients depend on them. The application URI is a persistent identity, and with security it must match the URI in the application certificate.
The tutorial's create(int port) method builds the endpoint, the configuration, and the server.
var endpoint =
EndpointConfig.newBuilder()
.setBindAddress("127.0.0.1")
.setHostname("127.0.0.1")
.setBindPort(port)
.setPath("/wiki")
.setSecurityPolicy(SecurityPolicy.None)
.setSecurityMode(MessageSecurityMode.None)
.addTokenPolicy(OpcUaServerConfig.USER_TOKEN_POLICY_ANONYMOUS)
.build();
var config =
OpcUaServerConfig.builder()
.setApplicationUri("urn:eclipse:milo:wiki:server")
.setApplicationName(LocalizedText.english("Wiki server"))
.setProductUri("urn:eclipse:milo:wiki")
.setCertificateManager(new DefaultCertificateManager())
.setEndpoints(Set.of(endpoint))
.build();
var server =
new OpcUaServer(
config,
profile -> new OpcTcpServerTransport(OpcTcpServerTransportConfig.newBuilder().build()));The endpoint binds 127.0.0.1 and advertises the same address, so with the program's default port of 12686 clients connect to opc.tcp://127.0.0.1:12686/wiki. SecurityPolicy None, MessageSecurityMode None, and the anonymous token policy leave the channel unencrypted and unauthenticated and the user anonymous, which is acceptable only on loopback.
The configuration names the application and gives it an empty DefaultCertificateManager, which is enough for a None endpoint because it uses no application certificate. The default identity validator accepts the anonymous token the endpoint offers. The transport factory is called once for each transport profile the endpoints use and returns an OPC TCP transport. The complete file, with imports, is on Your first server.
The rest of this page changes these choices one at a time.
An EndpointConfig describes one endpoint:
- transport profile
- bind address and port
- advertised hostname and path
- security policy and security mode
- a fixed certificate or a certificate selection request
- supported user-token policies
Pass the set to OpcUaServerConfig.builder().setEndpoints(...). Several endpoint descriptions can share one listening socket while offering different security modes or token policies.
The bind address selects a local network interface, and the advertised hostname is what clients discover and connect to. Defaults are bind address localhost, hostname localhost, and port 12685. The listener and the advertised URL share bindPort, and there is no separate advertised-port setting. NAT or container port translation therefore needs equal internal and external ports or a deliberate client URL override. See Deployment.
Never advertise an all-interface bind address. Advertise names that clients can resolve and route to, and include those names in the server certificate SANs. NAT, containers, DNS, and firewalls remain deployment responsibilities.
This guide uses OPC TCP. The repository's HTTP and WebSocket experiments are not a deployment alternative. See Capabilities and limitations.
After startup, two methods report endpoints, and they are not interchangeable. getBoundEndpoints() returns the configurations that actually bound. server.getApplicationContext().getEndpointDescriptions() returns what discovery advertises, which can change after certificate updates. resetEndpointDescriptionCache() recalculates those advertisements but binds no new listening socket.
A server on a network needs an application certificate, a secured policy and mode, and a user-token policy backed by a validator that checks real credentials. This fragment makes those changes and sets one operation limit, explained in the next section. It assumes:
-
builderis anOpcUaServerConfigBuilder. -
portis an available local port. -
applicationUri,certificate, andcertificateManagerare matching provisioned identities, as described in Server security. -
usernameValidatoris an application-suppliedUsernameIdentityValidatorthat checks credentials against your credential store.
EndpointConfig endpoint =
EndpointConfig.newBuilder()
.setBindAddress("127.0.0.1")
.setHostname("localhost")
.setBindPort(port)
.setPath("/wiki")
.setCertificate(certificate)
.setSecurityPolicy(SecurityPolicy.Basic256Sha256)
.setSecurityMode(MessageSecurityMode.SignAndEncrypt)
.addTokenPolicy(OpcUaServerConfig.USER_TOKEN_POLICY_USERNAME)
.build();
builder
.setApplicationUri(applicationUri)
.setCertificateManager(certificateManager)
.setEndpoints(Set.of(endpoint))
.setIdentityValidator(usernameValidator)
.setLimits(
new OpcUaServerConfigLimits() {
@Override
public UInteger getMaxNodesPerRead() {
return uint(1_000);
}
});Imports:
-
EndpointConfig,OpcUaServerConfig, andOpcUaServerConfigLimitsfromorg.eclipse.milo.opcua.sdk.server -
SecurityPolicyfromorg.eclipse.milo.opcua.stack.core.security -
MessageSecurityModefromorg.eclipse.milo.opcua.stack.core.types.enumerated -
UIntegerfromorg.eclipse.milo.opcua.stack.core.types.builtin.unsigned, and staticUnsigned.uint java.util.Set
Discovery now advertises localhost with the configured port and the username token policy. Once the client and server trust each other's application certificates, a valid credential on this encrypted Basic256Sha256 username-token path establishes the Session.
setCertificate(certificate) advertises that fixed certificate, so its identity and key pair must be in the certificate manager. To follow certificate rotation instead, omit it, and the secure endpoint selects a compatible identity from the DefaultApplicationGroup. To select a different registered group or request a certificate type, call EndpointConfig.Builder.setEndpointCertificateConfig(EndpointCertificateConfig) without a fixed certificate. See Server security.
The identity validator decides who the user is. AnonymousIdentityValidator, UsernameIdentityValidator, X509IdentityValidator, and CompositeValidator cover the standard token types. The default, AnonymousIdentityValidator.INSTANCE, accepts only anonymous tokens, so advertise only user-token policies the configured validator accepts. The application certificate authenticates the application, not the person or service account using a Session. Do not copy the demonstration passwords or the permissive X.509 predicate from ExampleServer into a deployment.
What an authenticated user may do is a separate decision, made by Access control. No role mapper is installed by default, so install one with setRoleMapper(...) when you need role checks.
Use setLimits(OpcUaServerConfigLimits) to bound operations per request, Sessions, subscriptions, MonitoredItems, and sampling and publishing rates. Base each limit on what your application can serve, because raising an operation limit adds no device throughput or memory, and test the revised values the server returns to clients.
With the fragment above, a Read of more than 1,000 nodes fails as a whole with Bad_TooManyOperations (the default limit is 10,000). A service-level limit like this rejects the entire request, while per-node errors stay in the result arrays. Clients can split large reads as shown in Recovery and limits.
Without explicit worker resources, the server uses the process-wide daemon pools Stack.sharedExecutor() and Stack.sharedScheduledExecutor(), and server.shutdown() does not close them. See Configuration and Deployment for coordinated cleanup at process exit. setExecutor(...) and setScheduledExecutor(...) accept your own executors instead. Close those after server work stops, and coordinate with all users of a shared executor before you close it.
Bound the time that I/O and Method execution can take, because a blocking device read or authorization filter holds shared workers that other clients need.
Register managed namespaces before startup so the server owns their lifecycle. In this fragment, server listens for inbound connections and namespace is an instance of your ManagedNamespaceWithLifecycle subclass. The same pattern applies to other Lifecycle participants. Import java.util.concurrent.TimeUnit.
server.addLifecycleParticipant(namespace);
try {
server.startup().get(10, TimeUnit.SECONDS);
if (server.getBoundEndpoints().isEmpty()) {
throw new IllegalStateException("server has no bound endpoint");
}
// Application work runs here while the server owns the namespace lifecycle.
} finally {
server.shutdown().get(10, TimeUnit.SECONDS);
}Your service does its work, or waits for its own shutdown signal, inside the try, as the tutorial's main method waits for Enter. Report readiness only after startup succeeds. Once the server owns the namespace, do not call namespace.startup() or namespace.shutdown() yourself.
Without Reverse Connect targets the bound-endpoint guard is redundant, because startup already fails when nothing binds. Omit it for a Reverse Connect-only server, which can start with no bound endpoint when at least one target is schedulable, and inspect its targets and connection progress as described in Reverse Connect.
If shutdown also fails, this fragment loses the startup exception. Production error reporting should keep both, for example by adding one as a suppressed exception of the other.
Participants start in registration order, after core facilities are ready and before the server listens. If startup fails, only participants whose startup completed normally get a rollback shutdown, in reverse order, so a participant that throws must clean up its own partial allocations. Registering a participant twice, or after startup begins, is an error. Before startup, removeLifecycleParticipant(...) transfers ownership back to the application.
Shutdown rejects new Sessions, stops Reverse Connect activity, unbinds endpoints, and closes Sessions. It then tears down application participants before the standard address space. Every started participant gets a cleanup attempt. A participant may request shutdown from its startup callback, but it must not wait for the returned future.
A server instance is terminal after failure or shutdown. Calling startup() again can return the original future, but it does not restart the listener. To restart, build a new server and new owned resources.
Startup does not require every configured endpoint, so a server can start with less than you asked for. Before reporting readiness for inbound connections, compare getBoundEndpoints() with the listening endpoints you require, especially secured ones.
| What you see | Cause | What to do |
|---|---|---|
| A startup warning, and a secure endpoint is missing from the bound and advertised endpoints | Its security provider is unusable, managed certificate selection found no compatible identity, or an enhanced policy rejected an incompatible fixed certificate | Fix the certificate or policy. A None endpoint can still bind while the secure endpoint is omitted. |
Failed to bind endpoint at WARN, but startup succeeds |
One listener could not bind, for example because its port is in use, while another bound | Compare getBoundEndpoints() with your requirements. Advertisements can still include the configuration that failed to bind. |
Startup fails with Bad_ConfigurationError
|
Nothing bound and no Reverse Connect target is schedulable, or Session-capable endpoint selection keys collide | Free the port or fix the bind address, or make the endpoint configurations distinct. |
Session activation fails with Bad_IdentityTokenInvalid
|
The validator rejected the credential on the encrypted Basic256Sha256 username-token path | Check the credential and the validator. |
Session activation fails with Bad_UserAccessDenied
|
Other username-token paths can report a rejected credential this way | Inspect the actual activation result instead of matching one status. |
A request fails with Bad_TooManyOperations
|
The request exceeds a service-level limit such as getMaxNodesPerRead()
|
Split the request on the client, or raise the limit if the application can serve it. |
| The shutdown future completes exceptionally | A participant's cleanup failed | The future carries the first cleanup failure, with later failures attached as suppressed exceptions. |
A wait on startup() or shutdown() times out |
The lifecycle work is slow or stalled | The work continues, because timing out or cancelling the returned future does not cancel it. |
startup() after shutdown returns but nothing listens |
The server instance is terminal | Build a new server. |
You know what each endpoint setting does and how the server owns its namespace. To see how that namespace builds the thermostat's nodes, continue with Address space.
Related: Server SDK · Server security · Configuration reference · Deployment
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