Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ The example manifests are in the [k8s/manifests](./k8s/manifests/) directory. It
| Variable | Default | Values | Description | Valid For |
| :------- | :------ | :----: | :---------- | :-------: |
| `_JAVA_OPTIONS` | _null_ | _any valid JVM args_ | Overrides the default JVM memory arguments (e.g. `-Xms128m -Xmx1024m`); takes priority over CLI args without modifying them; see [Low Resource Systems](#low-resource-systems) for more details | >= `3.2` |
| `APPLICATION_PROPERTIES` | _null_ | _any Spring Boot properties_ | Content written verbatim to `application.properties` on the classpath at startup, loaded by Spring Boot; see [Spring Boot Tuning](./spring-boot-tuning.md) for details and recommended settings | >= `5.x` |
| `EAP_MONGOD_URI` | _null_ | `mongodb://user:pass@1.2.3.4:27017/omada` | Used to specify the URI of MongoDB when running it external to the controller container | >= `5.x` |
| `JAVA_MAX_HEAP_SIZE` | _null_ | _any valid JVM heap size_ | Replaces the hardcoded `-Xmx` value in the default CMD (e.g. `512m`, `1g`); works with any JVM including HotSpot | >= `3.2` |
| `JAVA_MIN_HEAP_SIZE` | _null_ | _any valid JVM heap size_ | Replaces the hardcoded `-Xms` value in the default CMD (e.g. `64m`, `128m`); works with any JVM including HotSpot | >= `3.2` |
Expand Down Expand Up @@ -595,6 +596,8 @@ Treat this as a practical reference point rather than a universal recommendation

Changing these values would be necessary on these low resource systems to prevent the operating system from killing the container due to it thinking it can allocate more memory than it should. The controller process may still actually functionally require more memory so your mileage may vary in terms of the impact of running on such a low resource system.

For advanced tuning of Spring Boot internals (Tomcat thread pool, async executors, logging levels), see [Spring Boot Tuning](./spring-boot-tuning.md).

#### Mismatched Userland and Kernel

If a Raspberry Pi 4 is running a 32 bit version of Raspberry Pi OS, a [recent firmware update](https://github.com/raspberrypi/firmware/issues/1795) has intentionally made it so the default kernel the Pi will boot from has been switched from 32 bit kernel to a 64 bit kernel. This is a problem for the running container because the version of MongoDB that is present in the `armv7l` image (also known as `armhf`), will fail to start on a 64 bit kernel. Most software tends to run fine when switching kernels but in this case, it will prevent the controller from running due to MongoDB failing to start. Please also review the [Notes for armv7l](#notes-for-armv7l) to also understand the risks for running the `armv7l` based controller!
Expand Down
23 changes: 23 additions & 0 deletions entrypoint-unified.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ setup_environment() {
# JAVA HEAP OVERRIDES (replace hardcoded -Xmx/-Xms in the CMD)
JAVA_MAX_HEAP_SIZE="${JAVA_MAX_HEAP_SIZE:-}"
JAVA_MIN_HEAP_SIZE="${JAVA_MIN_HEAP_SIZE:-}"

# SPRING BOOT APPLICATION PROPERTIES (written to classpath as application.properties)
APPLICATION_PROPERTIES="${APPLICATION_PROPERTIES:-}"
}

restore_properties_files() {
Expand Down Expand Up @@ -677,6 +680,25 @@ EOF
chmod +x "${MONGOD_LINK}"
}

inject_application_properties() {
if [ -z "${APPLICATION_PROPERTIES}" ]
then
return
fi

PROPS_FILE="/opt/tplink/EAPController/properties/application.properties"
echo "INFO: APPLICATION_PROPERTIES set; writing Spring Boot properties to ${PROPS_FILE}"
printf '%s\n' "${APPLICATION_PROPERTIES}" > "${PROPS_FILE}"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In non-rootless mode, the script runs as root at this point and gosu only executes at the very end. Double check my logic but the file is created owned by root:root with mode 640. The omada controller process (launched via exec gosu "${PUSERNAME}") is not in the root group, so it cannot read the file. Spring Boot will silently finds no application.properties and the feature does nothing.

So if I am right, the fix that would be easiest would probably be to add the same chown in inject_application_properties(), guarded for rootless.

chmod 640 "${PROPS_FILE}"

# in non-rootless mode this function runs as root before gosu hands off to ${PUSERNAME};
# without chown the file is root:root 640 and Spring Boot silently can't read it
if [ "${ROOTLESS}" != "true" ]
then
chown "${PUSERNAME}:${PGROUP}" "${PROPS_FILE}"
fi
}

enable_tls_1_11() {
TLS_1_11_ENABLED="${TLS_1_11_ENABLED:-false}"

Expand Down Expand Up @@ -749,6 +771,7 @@ common_setup_and_validation() {
update_general_properties
fix_permissions
setup_mongodb_wrapper
inject_application_properties
import_ssl_certificate
enable_tls_1_11
check_old_version_files
Expand Down
191 changes: 191 additions & 0 deletions spring-boot-tuning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# Spring Boot Tuning
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of putting this in a separate docs/ directory, could we just move this to the root of the project? I know it's starting to get a bit unorganized but that might be a future me problem to address properly.


The Omada Controller (v5.x and above) is built on [Spring Boot](https://spring.io/projects/spring-boot).
Its classpath includes `/opt/tplink/EAPController/properties`, which means Spring Boot will automatically
load an `application.properties` file placed there at startup.

The `APPLICATION_PROPERTIES` environment variable allows you to inject arbitrary Spring Boot properties
into the controller without rebuilding the image or overriding the `CMD`.

> [!WARNING]
> This is an unofficial tuning mechanism. TP-Link does not document or guarantee which
> Spring Boot properties are honoured. Settings listed here are best-effort based on the Spring Boot
> defaults for an embedded Tomcat server. Test in a non-production environment first.

---

## How to Use

Set `APPLICATION_PROPERTIES` to a newline-separated list of `key=value` pairs. The entrypoint writes
them verbatim to `/opt/tplink/EAPController/properties/application.properties` before the JVM starts.

### Docker Compose

```yaml
services:
omada-controller:
image: mbentley/omada-controller:6.2
environment:
APPLICATION_PROPERTIES: |
server.tomcat.threads.max=50
server.tomcat.threads.min-spare=5
spring.task.execution.pool.max-size=10
```

### Docker CLI

```bash
docker run \
-e APPLICATION_PROPERTIES=$'server.tomcat.threads.max=50\nserver.tomcat.threads.min-spare=5' \
mbentley/omada-controller:6.2
```

### Kubernetes / Helm (extraEnvVars)

```yaml
extraEnvVars:
APPLICATION_PROPERTIES: |
server.tomcat.threads.max=50
server.tomcat.threads.min-spare=5
spring.task.execution.pool.core-size=5
spring.task.execution.pool.max-size=10
spring.task.scheduling.pool.size=3
```

The startup log will confirm the file was written:

```
INFO: APPLICATION_PROPERTIES set; writing Spring Boot properties to /opt/tplink/EAPController/properties/application.properties
```

---

## Available Settings

### Tomcat HTTP Thread Pool

Controls how many threads Tomcat uses to handle incoming HTTP/HTTPS requests.
The Omada web UI and API run through this pool.

| Property | Default | Description |
|---|---|---|
| `server.tomcat.threads.max` | `200` | Maximum number of worker threads. Reduce to limit memory usage. |
| `server.tomcat.threads.min-spare` | `10` | Minimum number of threads kept alive (idle). |
| `server.tomcat.accept-count` | `100` | Queue size for incoming connections when all threads are busy. |
| `server.tomcat.connection-timeout` | `20000` | Timeout (ms) for accepting a connection. |

**Typical constrained setup:**
```properties
server.tomcat.threads.max=50
server.tomcat.threads.min-spare=5
server.tomcat.accept-count=50
```

---

### Async Task Executor

Used for background tasks dispatched via Spring's `@Async` annotation — e.g. device
status polling, event processing.

| Property | Default | Description |
|---|---|---|
| `spring.task.execution.pool.core-size` | `8` | Threads always kept alive in the pool. |
| `spring.task.execution.pool.max-size` | `Integer.MAX_VALUE` | Maximum pool size. Set this explicitly. |
| `spring.task.execution.pool.queue-capacity` | `Integer.MAX_VALUE` | Task queue depth before new threads are spawned. |
| `spring.task.execution.pool.keep-alive` | `60s` | How long idle threads above core-size are kept. |
| `spring.task.execution.thread-name-prefix` | `task-` | Thread name prefix (useful for profiling). |

**Typical constrained setup:**
```properties
spring.task.execution.pool.core-size=5
spring.task.execution.pool.max-size=20
spring.task.execution.pool.queue-capacity=100
```

---

### Scheduled Task Pool

Controls the thread pool for `@Scheduled` tasks — periodic jobs like cleanup,
health checks, and device discovery heartbeats.

| Property | Default | Description |
|---|---|---|
| `spring.task.scheduling.pool.size` | `1` | Number of scheduling threads. Rarely needs to exceed 3. |
| `spring.task.scheduling.thread-name-prefix` | `scheduling-` | Thread name prefix. |

**Typical setup:**
```properties
spring.task.scheduling.pool.size=2
```

---

### Logging

Reducing log verbosity saves CPU and I/O, especially on slow storage (e.g. NFS-backed PVCs).

| Property | Default | Description |
|---|---|---|
| `logging.level.root` | `INFO` | Root log level. Set to `WARN` to reduce noise. |
| `logging.level.org.springframework` | `INFO` | Spring Framework log level. |
| `logging.level.org.mongodb` | `INFO` | MongoDB driver log level. |
| `logging.level.com.tplink` | `INFO` | Omada application log level. |

**Reduce verbosity:**
```properties
logging.level.root=WARN
logging.level.com.tplink=INFO
```

---

## Example: Memory-Constrained Setup (Kubernetes, ≤ 2 GB pod limit)

This example targets a pod with `limits.memory: 2048Mi` running OpenJ9.
Combined with `JAVA_MAX_HEAP_SIZE=512m` and `MONGOD_EXTRA_ARGS=--wiredTigerCacheSizeGB 0.25`,
the approximate memory budget is:

| Component | Budget |
|---|---|
| Java heap (`-Xmx`) | 512 MB |
| Java metaspace + JIT code | ~150 MB |
| MongoDB WiredTiger cache | 256 MB |
| Tomcat threads × ~1 MB stack | ~50 MB |
| OS + JVM overhead | ~300 MB |
| **Total** | **~1.3 GB** |

```yaml
extraEnvVars:
JAVA_MAX_HEAP_SIZE: "512m"
JAVA_MIN_HEAP_SIZE: "128m"
MONGOD_EXTRA_ARGS: "--wiredTigerCacheSizeGB 0.25"
APPLICATION_PROPERTIES: |
server.tomcat.threads.max=50
server.tomcat.threads.min-spare=5
server.tomcat.accept-count=50
spring.task.execution.pool.core-size=5
spring.task.execution.pool.max-size=20
spring.task.execution.pool.queue-capacity=100
spring.task.scheduling.pool.size=2
logging.level.root=WARN
logging.level.com.tplink=INFO
```

---

## Notes and Limitations

- **Version requirement**: `APPLICATION_PROPERTIES` only works with v5.x and above (Spring Boot base).
On v4.x and below, the properties directory is not on the classpath and the file will be ignored.
- **File is regenerated on every start**: The file is written fresh from the env var each time the
container starts, so changes to `APPLICATION_PROPERTIES` always take effect on the next restart.
- **No conflict with `omada.properties`**: Spring Boot reads `application.properties` for its own
framework settings; Omada's application config lives in `omada.properties` (a different file).
- **Unknown properties are ignored**: Spring Boot will log a warning for unrecognised keys but will
not fail to start.
- **Property precedence**: Environment variables set directly on the container (e.g.
`SERVER_TOMCAT_THREADS_MAX=50`) have higher precedence than `application.properties` in the Spring
Boot relaxed-binding hierarchy. Both approaches work; `APPLICATION_PROPERTIES` is more explicit and
easier to manage as a block.