plantpulse-server (Web Console + IIoT Engine)
Role
This is the core module that acts as the platform's face and brain. The web console that operators see, the REST APIs that external systems call, and the IIoT engine that actually processes industrial data all run inside this single module.
| Item | Value |
|---|---|
| Module name | plantpulse-server |
| Container | plantpulse-server-web (self image, self container) |
| Path inside container | /opt/kopens/plantpulse-platform/plantpulse-server/ |
| User access | 80 / 443 — plantpulse-proxy receives this and forwards to this container. The admin console is also at the same address |
| Host port | None — it only runs behind a proxy |
| Memory limit | DOCKER_SERVER_MEMORY default 14g / heap DOCKER_SERVER_HEAP default 8g |
| Runtime | Apache Tomcat 10.1 + Java 21 |
| Throughput | 40,000 messages per second (36-thread pipeline) |
User traffic always goes through plantpulse-proxy (80/443). So even if the backend moves or is renamed, user and equipment configurations do not need to be touched. Container status can be checked via bin/status.sh, and logs via bin/logs.sh plantpulse-server-web.
Areas of Responsibility
Directory Structure
plantpulse-server/
├── config/ # 서버 관리 설정 (외부화 — 배포해도 유지)
│ ├── plantpulse-engine.properties # IIoT 엔진 튜닝
│ ├── plantpulse-storage.properties # DB 연결
│ ├── plantpulse-mq.properties # 메시징 연결
│ └── log4j2.xml # 로깅 설정 (자가 시딩 — 최초 기동 시 생성)
├── bin/
│ ├── start.sh
│ ├── stop.sh
│ └── log-viewer.sh
├── logs/
│ └── system.log
├── path/ # 경로 매핑
└── server/ # 내장 Tomcat
├── conf/
│ ├── server.xml # 커넥터 / 포트
│ ├── context.xml # 컨텍스트 / 데이터소스
│ ├── catalina.properties
│ ├── logging.properties
│ ├── tomcat-users.xml
│ ├── web.xml
│ └── Catalina/localhost/
│ └── ROOT.xml # 컨텍스트 설정 (sessionCookieName 등)
└── webapps/
├── ROOT.war # 웹 애플리케이션 (WAR 통배포)
└── ROOT/ # Tomcat 이 unpack 한 디렉토리 (수정 금지)
Deployment method: The web app is deployed by replacing the
server/webapps/ROOT.war(WAR full deployment, migration in progress). The existingapp/plantpulse-server-webexploded directory method is scheduled for discontinuation, and the ContextdocBaseoverride fromserver.xmlhas been removed. If you directly modify files insideROOT/, they will disappear on the next deployment, so always make configuration changes inconfig/.
Key Configuration Files
Configuration files are located in the module's config/ directory, and the server searches in the following order: -Dpp.conf.dir → ${catalina.base}/../config → classpath (fallback). When an external configuration is loaded, Properties loaded from external conf: is printed in the startup log.
application.properties has been removed (2026.06). Console behavior settings such as theme/homepage are now managed in the System > Settings Management (
mm_configtable) in the console, andalarm.duplicate.check.minuteshave been migrated toplantpulse-engine.properties.
plantpulse-engine.properties
IIoT engine pipeline tuning.
| Item | Default Value | Description |
|---|---|---|
engine.pipeline.threads | 36 | Number of parallel processing threads |
engine.pipeline.ratelimit | 40000 | Maximum messages per second |
engine.pipeline.batch.size | 1000 | Batch size |
engine.pipeline.queue.size | 1200000 | Queue capacity |
engine.dedup.enabled | true | Enable deduplication |
plantpulse-storage.properties
Data store connection.
# Cassandra
cassandra.host=${PP_CASSANDRA_HOST}
cassandra.port=${PP_CASSANDRA_PORT}
cassandra.keyspace=${PP_KEYSPACE}
cassandra.user=${PP_CASSANDRA_USER}
cassandra.password=${PP_CASSANDRA_PASSWORD}
# PostgreSQL (Spring DataSource)
postgres.url=jdbc:postgresql://${PP_POSTGRES_HOST}:${PP_POSTGRES_PORT}/${PP_DB_NAME}
postgres.user=${PP_PG_USER}
postgres.password=${PP_PG_PASSWORD}
# Valkey
redis.host=${PP_REDIS_HOST}
redis.port=${PP_REDIS_PORT}
redis.password=${PP_REDIS_PASSWORD}
This app does not render settings. After being split into containers, the web server starts without the data lake's settings renderer, and reads values from three places — ① external file placed by the operator → ② default values embedded in the WAR → ③ environment variables passed by compose. Among the three, environment variables override the embedded defaults.
Therefore, the only authoritative source for what the app sees is
compose/docker-compose.yml. Names that only exist on the data lake side and are not in compose do not reach this app — the incident where app five continued using old names at a site where only identifiers were changed in the data lake is a case in point (2026-08-29).Do not modify files inside the web app (
server/webapps/ROOT/) — they will disappear on deployment.
server/conf/server.xml
Tomcat connector settings. Port changes / TLS / compression / thread pool.
<Connector port="80" protocol="HTTP/1.1"
connectionTimeout="20000"
maxThreads="500"
acceptCount="200"
URIEncoding="UTF-8"
compression="on"
compressibleMimeType="text/html,text/css,application/json,application/javascript" />
<Connector port="443" protocol="org.apache.coyote.http11.Http11Nio2Protocol"
SSLEnabled="true"
maxThreads="500"
sslEnabledProtocols="TLSv1.3,TLSv1.2">
<SSLHostConfig>
<Certificate certificateKeystoreFile="/var/security/plantpulse/server.jks"
certificateKeystorePassword="${PP_TLS_KEYSTORE_PASSWORD}" />
</SSLHostConfig>
</Connector>
Operational Commands
From the host — per container
Restarting the container is the equivalent of restarting this module. Infrastructure and other apps are unaffected.
cd /opt/kopens/plantpulse-platform-docker
# 서버만 재시작
docker compose -f compose/docker-compose.yml restart plantpulse-server-web
# 상태 · 로그
bin/status.sh
bin/logs.sh plantpulse-server-web -n 200
Entering the container
cd /opt/kopens/plantpulse-platform-docker/bin
./shell.sh plantpulse-server-web
restart-server.shThat script is a thin delegation that calls the module's launcher from within each app's own container. The data lake container does not have the web server executable, so it ends with a «module not found» error. Please use the per-container restart above.
Hot reload (apply settings without restart)
Some settings can be hot-reloaded from the System > Settings Management menu in the console. Boot-time settings such as config/ property files or server.xml require a restart.
Logs
| Log | Path | Content |
|---|---|---|
| Main log | logs/system.log | Application logic, errors |
| Tomcat catalina | server/logs/catalina.out | Tomcat standard output |
| Access log | server/logs/localhost_access_log.YYYY-MM-DD.txt | HTTP request log |
| Engine log | logs/engine.log | IIoT engine processing details |
| Slow query | logs/slow.log | Queries exceeding 200ms |
Changing log level — the logging configuration (log4j2.xml) is externalized to the module's config/ (self-seeding: on first start, the WAR defaults are copied to config/log4j2.xml, and Logging reconfigured from external conf: is printed in the startup log upon application):
# 웹 서버 컨테이너 안에서
cd /opt/kopens/plantpulse-platform-docker/bin
./shell.sh plantpulse-server-web
vi /opt/kopens/plantpulse-platform/plantpulse-server/config/log4j2.xml
# <Logger name="plantpulse" level="DEBUG"/> 등 수정 후 exit
# 호스트에서 그 컨테이너만 재시작
cd /opt/kopens/plantpulse-platform-docker
docker compose -f compose/docker-compose.yml restart plantpulse-server-web
Being an external file, it is permanently preserved even after WAR redeployment. Do not modify files inside the web app (
server/webapps/ROOT/WEB-INF/classes/log4j2.xml) — they will disappear on deployment.
Performance Tuning
JVM Heap
# server/bin/setenv.sh (없으면 생성)
export CATALINA_OPTS="-Xms16g -Xmx32g \
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/plantpulse/heap/ \
-Xlog:gc*:file=/var/log/plantpulse/gc.log:time,uptime:filecount=10,filesize=100M"
Tomcat Thread Pool
Adjust maxThreads / acceptCount in the Connector of server.xml. For large-scale concurrent access environments, the following is recommended.
| Concurrent Connections | maxThreads | acceptCount |
|---|---|---|
| ~ 100 | 200 | 100 |
| ~ 500 | 500 | 200 |
| 500+ | 1000 | 500 |
IIoT Engine Throughput
Set engine.pipeline.threads in plantpulse-engine.properties to 70~80% of the host CPU cores. If messages are accumulating, also increase engine.pipeline.queue.capacity.
For detailed tuning, please refer to the Performance Tuning page.
Where Do Uploads Go — RustFS
Equipment documents · equipment images · profile images are stored in object storage (RustFS). The s3://<버킷>/<key> is stored in the database instead of the file itself.
| Item | Value |
|---|---|
| Bucket | PP_S3_UPLOAD_BUCKET (default upload) — different from the plantpulse bucket used by the data lake |
| Connection | PP_S3_HOST · PP_S3_PORT · PP_S3_USER · PP_S3_PASSWORD |
| Where uploads land | Only on plantpulse-server-web. They do not go to workers |
The reason the buckets are separated is so that the retention rules of one do not affect the other.
2026.09 does not create buckets at boot time. Pointing to a non-existent bucket causes upload failures. Clean installations create the bucket during the migration process, but if you manually change the value to point to a different bucket, create that bucket first.
If connection values are empty, object storage cannot be created and the upload path does not appear. Container environments are fixed at creation time, so if you add values, you must recreate the container for them to take effect — not just docker restart.
Upload Limits — Cannot be summarized in one line
Limits exist at two layers, and the one that almost always triggers first is the lower layer (endpoint).
Endpoint-specific guards — these trigger first
| What you are uploading | Endpoint | Limit |
|---|---|---|
| Equipment documents | POST /asset/tree/document/upload | 50 MiB |
| Equipment documents (API v5) | POST /api/v5/assets/{asset_id}/documents | 50 MiB |
| Equipment images | POST /asset/image/upload | 10 MiB |
| Canvas SVG | POST /canvas/svgupload | 10 MiB |
Equipment images and Canvas SVGs are rejected at 10 MiB. Only documents are 50 MiB.
What you see when a limit is exceeded
The four endpoints use the same validation function, and the message only shows the byte number.
file_size: 12582912 > max 10485760
Example when uploading a 12 MiB image — 10485760 is 10 MiB, 52428800 is 50 MiB.
The message has no units, so operators have to do the mental math from bytes, and it doesn't say which file was rejected. If multiple files are uploaded at once and rejected, you have to measure their sizes to find the culprit.
Container limits — the ceiling above that
| What | Value | Where |
|---|---|---|
| Single file | 50 MiB | <max-file-size> of web.xml |
| Entire multipart request | 52 MiB | <max-request-size> of web.xml |
| Form parameters (excluding file parts) | 2 MiB | Tomcat default — neither connector declares maxPostSize |
maxPostSize is the limit on bytes that Tomcat turns into request parameters. x-www-form-urlencoded counts the entire body, multipart/form-data counts only the non-file parts. Raw bodies like JSON are not affected.
Measurement (2026-09-18): sending a 3 MiB multipart through the proxy causes the entire body to be transmitted and reaches the application authentication layer (it doesn't fail due to size, but to 401). There is no 2 MiB wall in the transmission path, and nginx is client_max_body_size 200m.
Before 2026-09-18, only the 443 connector had maxPostSize="-1", so the same server enforced different limits depending on the port. Now 80 and 443 are the same.
Health Check
# 표준 헬스체크 (익명, readiness — 엔진이 전 단계 기동을 완료(RUNNING)했을 때만 UP. 배포 자동검증 / 모니터링용)
curl -fsS http://127.0.0.1/api/health
# 준비 완료: 200 {"status":"UP","service":"plantpulse-server-web","ts":1765500000000,"checks":{"engine":"RUNNING"}}
# 기동 중: 503 {"status":"STARTING",...} / 기동 실패·중지: 503 {"status":"DEGRADED",...}
# 경로는 /api/health 다 — «/api/v5/health» 는 없다. 인증도 필요 없다(익명 허용 경로).
# 콘솔 ping
curl -fsS http://127.0.0.1/api/v5/ping
# 공개 상태 페이지의 데이터 (익명 — 24시간 가용성·p99·엔드포인트별 상태)
curl -fsS http://127.0.0.1/api/v5/public/status
# WebSocket 핸드셰이크 확인
curl -i -N \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" \
-H "Sec-WebSocket-Version: 13" \
http://127.0.0.1:8000/ws
Common Problems
| Symptom | Cause | Action |
|---|---|---|
OutOfMemoryError | Heap exhaustion | Increase -Xmx in setenv.sh + analyze GC logs |
Too many open files | File descriptor limit | nofile in /etc/security/limits.conf to 65535+ |
| Console slow response | DB slow queries | Check PostgreSQL slow query log, Valkey cache hit rate |
| Infinite login loop | Session/cookie issue | Delete browser cache, check sessionCookieName of server/conf/Catalina/localhost/ROOT.xml |
| 502 / 504 (reverse proxy) | Backend response timeout | Increase nginx proxy_read_timeout |
| WebSocket disconnection | Firewall idle timeout | Set firewall timeout > 60s, adjust tomcat.websocket.session.timeout |
For detailed diagnostics, please refer to the Troubleshooting page.
Security Checklist
- Change the default admin password (
admin / admin123!) - Remove or use a strong password for the manager account in
tomcat-users.xml - Block external access to
shutdown port (7000)ofserver.xml - Apply HTTPS certificate (materials from the
pp-securityvolume baked byplantpulse-certsor an external CA) - Verify X-Frame-Options / CSP / HSTS header settings
- Store API Bearer Tokens in the operational vault
- Disable debug options and restrict permissions for the
config/property file (chmod 600)
For detailed security settings, please refer to the Security Settings page.