Skip to main content

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.

ItemValue
Module nameplantpulse-server
Containerplantpulse-server-web (self image, self container)
Path inside container/opt/kopens/plantpulse-platform/plantpulse-server/
User access80 / 443 — plantpulse-proxy receives this and forwards to this container. The admin console is also at the same address
Host portNone — it only runs behind a proxy
Memory limitDOCKER_SERVER_MEMORY default 14g / heap DOCKER_SERVER_HEAP default 8g
RuntimeApache Tomcat 10.1 + Java 21
Throughput40,000 messages per second (36-thread pipeline)
This container does not open any host ports

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 existing app/plantpulse-server-web exploded directory method is scheduled for discontinuation, and the Context docBase override from server.xml has been removed. If you directly modify files inside ROOT/, they will disappear on the next deployment, so always make configuration changes in config/.

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_config table) in the console, and alarm.duplicate.check.minutes have been migrated to plantpulse-engine.properties.

plantpulse-engine.properties​

IIoT engine pipeline tuning.

ItemDefault ValueDescription
engine.pipeline.threads36Number of parallel processing threads
engine.pipeline.ratelimit40000Maximum messages per second
engine.pipeline.batch.size1000Batch size
engine.pipeline.queue.size1200000Queue capacity
engine.dedup.enabledtrueEnable 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
Not accessible via the data lake's restart-server.sh

That 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​

LogPathContent
Main loglogs/system.logApplication logic, errors
Tomcat catalinaserver/logs/catalina.outTomcat standard output
Access logserver/logs/localhost_access_log.YYYY-MM-DD.txtHTTP request log
Engine loglogs/engine.logIIoT engine processing details
Slow querylogs/slow.logQueries 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 ConnectionsmaxThreadsacceptCount
~ 100200100
~ 500500200
500+1000500

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.

ItemValue
BucketPP_S3_UPLOAD_BUCKET (default upload) — different from the plantpulse bucket used by the data lake
ConnectionPP_S3_HOST · PP_S3_PORT · PP_S3_USER · PP_S3_PASSWORD
Where uploads landOnly 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.

The bucket must exist beforehand

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.

Requires environment configuration to start

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 uploadingEndpointLimit
Equipment documentsPOST /asset/tree/document/upload50 MiB
Equipment documents (API v5)POST /api/v5/assets/{asset_id}/documents50 MiB
Equipment imagesPOST /asset/image/upload10 MiB
Canvas SVGPOST /canvas/svgupload10 MiB
If you only mention an "upload limit of 50 MiB", you'll be wrong for images.

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.

No units, no filenames

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​

WhatValueWhere
Single file50 MiB<max-file-size> of web.xml
Entire multipart request52 MiB<max-request-size> of web.xml
Form parameters (excluding file parts)2 MiBTomcat default — neither connector declares maxPostSize
The 2 MiB is not a "file" limit — large files pass through the proxy

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​

SymptomCauseAction
OutOfMemoryErrorHeap exhaustionIncrease -Xmx in setenv.sh + analyze GC logs
Too many open filesFile descriptor limitnofile in /etc/security/limits.conf to 65535+
Console slow responseDB slow queriesCheck PostgreSQL slow query log, Valkey cache hit rate
Infinite login loopSession/cookie issueDelete browser cache, check sessionCookieName of server/conf/Catalina/localhost/ROOT.xml
502 / 504 (reverse proxy)Backend response timeoutIncrease nginx proxy_read_timeout
WebSocket disconnectionFirewall idle timeoutSet 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) of server.xml
  • Apply HTTPS certificate (materials from the pp-security volume baked by plantpulse-certs or 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.