Docker Installation
Overview
This page covers manual (step-by-step) installation of the PlantPulse Platform as a Docker Compose stack, as well as air-gapped network installation.
For typical environments, we recommend the one-line installation. The one-line installation handles everything at once — package download, environment auto-detection, installation, and boot verification. Use the manual procedure on this page only in the following cases:
- When you need to review and approve each installation step yourself (security review, change management procedures, etc.)
- When you need to explicitly specify environment variables (
bin/env.sh) rather than relying on auto-detected values - When installing in an air-gapped network environment with no internet access → Air-gapped network installation
What Gets Installed — Nine Containers
The Platform runs as a single docker compose stack. The source of truth is compose/docker-compose.yml, and all operational scripts go through this file to manage containers.
| Container | Layer | Role |
|---|---|---|
plantpulse-certs | Certificates | A one-shot that bakes TLS material and then exits. A normal healthy state is Exited (0) |
plantpulse-datalake | Infrastructure | Storage, messaging, analytics, CEP, SQL, monitoring |
plantpulse-server-web | Application | Web console |
plantpulse-batch-web | Application | Batch |
plantpulse-warehouse | Application | Data warehouse |
plantpulse-plugin-opcua-server | Application | OPC-UA server plugin (11004 / 11005) |
plantpulse-plugin-aasx-server | Application | AASX server plugin |
plantpulse-ha | Application | Redundancy recovery daemon (10210) |
plantpulse-proxy | Edge | The only entry point users touch (80 / 443 / 1883 / 1884) |
plantpulse-certs is a one-shot that creates certificates and then exits on its own, so Exited (0) is normal for it. In docker ps, it's normal for the remaining eight to show as (healthy). Don't mistake the one-shot for a "dead container" — status.sh and ops-check.sh judge only this container by its exit code.
There is also a plantpulse-mirror-maker for mirroring defined in the compose file, but it's tied to the mirror profile and does not start by default.
Before 2026-08-29, you could choose a "monolith" configuration where all components ran together in a single plantpulse-platform container. This configuration has been discontinued, and the selector variable (PP_TOPOLOGY) and its compose file have been removed along with it. There's nothing left to choose — bin/up.sh brings up the stack shown above.
Commands like docker logs plantpulse-platform that remain in old runbooks will not work, since no container by that name exists. Replacement commands can be found in the Operations Command Summary.
The primary purpose of splitting into per-app containers is OOM isolation. Even if one app exhausts its memory, other apps and the infrastructure stay alive, and you can restart or roll back at the individual app level.
Prerequisites
Before starting the installation, please verify the following:
| Item | Requirement |
|---|---|
| Account privileges | root (or sudo privileges). The installation script configures OS settings and the Docker daemon |
| Operating System | RHEL/Rocky/Oracle Linux 8·9, Ubuntu 20.04+, Amazon Linux 2/2023 |
| Docker | Compose v2 is required (docker compose — the hyphen-less form). If install.sh is missing, it will be installed |
| Data disk | We recommend mounting a large-capacity disk at /data1 — Docker data (/data1/docker-data) and platform data volumes use this path |
| Registry access | HTTPS access must be available to docker.kopens.io (image registry) and product.kopens.io (installation packages). If access is blocked, please use the air-gapped network installation |
| Registry credentials | docker.kopens.io login credentials (issued by the KOPENS operations team) |
Disk path note: If there is no separate data disk, creating a
/data1directory on the root disk will still work, but for production environments we recommend mounting a dedicated disk at/data1.
Manual Installation Procedure
Step 1: Download the Installation Package
Download the installation package (tar.gz) from the download server and extract it to the standard path. There is no need to clone via Git or install any separate tools.
sudo -i
# 표준 설치 경로 생성 후 패키지 다운로드 + 압축 해제
mkdir -p /opt/kopens/plantpulse-platform-docker
curl -fsSL "https://product.kopens.io/plantpulse-platform/plantpulse-platform-docker.tar.gz" \
| tar -xz -C /opt/kopens/plantpulse-platform-docker --strip-components=1
cd /opt/kopens/plantpulse-platform-docker/bin
chmod +x *.sh tools/*.sh
Within the package, there are two locations that are actually used.
| Location | Contents |
|---|---|
bin/ | All installation/operation scripts |
compose/ | The stack source of truth docker-compose.yml and the cluster worker overlay |
Step 2: Preflight Check
preflight.sh checks whether installation is possible without making any changes to the system at all.
./preflight.sh
Items checked:
- Whether Docker is installed and the daemon is running (fine even if not installed —
install.shwill install it) - Whether the parent path of the Docker data-root (
/data1) exists - Whether the secrets sidecar (
/etc/kopens/plantpulse-platform.env) exists - Whether the main operational ports (80, 443, 7443, 4949, 4950) are in use
If only OK appears, proceed to the next step. If there's a ERROR, refer to Troubleshooting. (WARN is informational only and does not block installation.)
Step 3: Review Environment Variables (env.sh)
bin/env.sh is the source of truth for host-side configuration. The values the containers actually see are determined by compose/docker-compose.yml — see the Environment Variable Reference for details on the relationship between the two.
vi env.sh
env.sh examines the host and determines CPU, memory, and disk allocations on its own. These are not fixed defaults, so small boxes get small values even without any manual intervention. A value specified per-box always wins.
| Variable | How It's Determined | When to Change It |
|---|---|---|
DOCKER_PP_CPUS | nproc (falls back to 8 if the read fails) | When you want to give the container fewer cores |
DOCKER_PP_CLUSTER_CORES | DOCKER_PP_CPUS - 2, minimum 4 · maximum 30 | Usually left as-is |
DOCKER_PP_MEMORY | 90% of host RAM, minimum 8G | When you want to leave more headroom for the OS |
DOCKER_DATALAKE_MEMORY | 80G. If the host has less than that, 90% of RAM | Adjusting the data lake ceiling |
DOCKER_PP_DATA_DISK_NAME | Automatically traces back to the actual disk backing / (falls back to sda on failure) | When auto-detection is wrong |
PP_LANG | en | Set ko for Korean-language operations |
PP_TZ | Asia/Seoul | Overseas boxes |
DOCKER_PP_EXTERNAL_IP | Empty by default | Only needed when you must advertise an external public IP in a NAT environment |
DOCKER_PP_EXTERNAL_IPThis value flows through PP_SERVICE_IP into the SAN list of the TLS certificate. If even one improperly formatted IP is mixed in, openssl will reject the entire extension file, resulting in no certificate being generated at all, and the stack will fail to start. If you're not behind NAT, leave it empty — that's the default.
You can check the current server's values with the following command.
hostname -I | awk '{print $1}' # 서버 IP
free -g | awk '/^Mem:/{print $2"G"}' # 전체 메모리
nproc # CPU 코어 수
lsblk # 디스크 이름 (sda, sdb, nvme0n1 …)
On Korean-language operational boxes, usually only two lines need to be changed.
export PP_LANG=ko
export PP_TZ=Asia/Seoul
PP_LANG / PP_TZ are fixed at JVM boot time. To change them on an already-running stack, ./restart.sh is required. Also, since Cassandra time series data is ingested using KST epoch, please keep Asia/Seoul for Korean operations.
Step 4: Run the Installation (install.sh)
sudo ./install.sh
A single install.sh automatically performs the entire pre-installation process.
- OS Detection and System Configuration — file limits, kernel parameters (sysctl), time synchronization (chrony), switching SELinux to permissive, disabling swap
- Docker Engine Installation — installed via the OS-specific package manager + configures data-root as
/data1/docker-data(Compose v2 is also installed if missing) - Firewall Configuration — opens public ports (80/443/7443/4949/4950) and restricts internal ports to only be accessible from private network ranges
- Service Credential Generation — creates the
/etc/kopens/plantpulse-platform.envsecrets sidecar (permission 0600, does not overwrite existing values on re-run) - Registry Login — enters
docker.kopens.iocredentials (automatically skipped if already logged in) - Stack Startup — create network/volumes → pull images → seed configuration templates →
docker compose up -d
One-time options for installation (specify via environment variable only if needed):
| Option | Effect |
|---|---|
SKIP_OS=1 | Skip OS configuration and Docker installation (when Docker is already installed and running) |
SKIP_LOGIN=1 | Skip registry login (when already logged in or images are already local) |
SKIP_FW=1 | Skip firewall configuration (when the firewall is managed separately) |
DOCKER_DATA_DIR=<path> | Change the Docker data-root (default /data1/docker-data) |
# 예: Docker가 이미 설치된 서버
SKIP_OS=1 sudo -E ./install.sh
Because the secrets sidecar (/etc/kopens/plantpulse-platform.env) is an unconditional assignment in the form export VAR=값, it overrides shell exports. On a node where installation has already completed, PP_PG_PASSWORD=... ./up.sh is silently ignored. For pre-delivery replacement, do it via export PP_*_PASSWORD=... before installation; after installation completes, use passwd.sh from Password Rotation instead.
Step 5: Startup and Boot Verification
install.sh brings up the stack and returns immediately. You need to separately verify that boot has completed.
./up.sh
up.sh is idempotent, so it's safe even if the stack is already up, and it waits until the stack is ready. An exit code of 0 does not mean "the command succeeded" — it means "it's now usable."
| Environment Variable | Default | Meaning |
|---|---|---|
PP_READY_TIMEOUT | 900 | Readiness wait ceiling (seconds) |
PP_READY_INTERVAL | 15 | Check interval (seconds) |
PP_WAIT=0 | — | Do not wait. In this case, 0 does not mean readiness is complete |
Starting the processes themselves takes only 3–5 minutes (JVM warm-up), but it takes 15–18 minutes for all components to stabilize. Cassandra schema migration and stabilization is the slowest part. Restarts are much faster since the schema already exists.
Measured figures (2026-08-31, 32 vCPU / 128GiB): data lake 217 seconds, web server 316 seconds.
You can also run the readiness check yourself directly.
./stack-verify-boot.sh
This script looks at the entire stack, not just a single container — it checks whether the one-shot exited normally, whether the data lake and apps are running·healthy, and even whether the proxy is serving on 443.
Step 6: Status and Operational Check
./status.sh # 0 = 정상 / 2 = 비정상
./ops-check.sh
status.sh summarizes the service list, container status, health, and volumes. The exit code is a contract — you can use it directly in automation.
| Judgment | What It Considers Abnormal |
|---|---|
| Cannot read the service list | compose resolution failed or docker is inaccessible |
| No container exists for a service declared by compose | Not started |
An always-on container is not running | One-shots (plantpulse-certs) are excluded |
Health of an always-on container is unhealthy | starting / none hold judgment |
| Health API is not OK | Probed from inside the data lake |
Volumes and conf are reported only and not factored into the exit code — because in a 2-node split installation (PP_TIER=APP), it's normal for some of them to be missing.
ops-check.sh additionally scans recent critical logs (OOM, FATAL, SSL errors, etc.) on top of this.
Health Check
The monitor API is served by the plantpulse-datalake container. The command differs depending on where you're checking from.
# 컨테이너 안에서 — 어떤 구성에서도 동작하는 방법
docker exec plantpulse-datalake curl -kfsS https://127.0.0.1:4950/api/health | jq
# 호스트/외부에서 — 4950 이 publish 되어 있습니다
curl -kfsS https://<server-ip>:4950/api/health | jq
If "status" is OK or WARN, it's within normal range; if it's FAIL, that's a failure.
The console and health API are served on both ports — 4950 (HTTPS) and 4949 (plaintext HTTP). It's the same console, same API, differing only in scheme. 4949 no longer redirects to 4950.
4949 is plaintext — login passwords and session cookies travel unencrypted. On untrusted networks, use 4950. 4949 is an option for boxes where the self-signed certificate warning actually stops operators in their tracks.
Installation Artifacts
Networks and Volumes
| Resource | Name | Purpose |
|---|---|---|
| Network | pp-net | A dedicated Docker network for the Platform (default 10.99.0.0/24, gateway 10.99.0.1) |
| Volume | pp-data | Persistent data storage (Cassandra, PostgreSQL, Kafka, etc.) |
| Volume | pp-temp | Temporary data (Spark, Hive workspace) |
| Volume | pp-backup | Backup storage |
| Volume | pp-security | TLS certificates/keystores. Written to by plantpulse-certs, treated as read-only by the rest |
| Volume | pp-proxy-certs | Certificate the proxy uses on 443 |
Volumes are always preserved even if containers are stopped or removed, so data persists across updates and reinstallations.
Host Directories
| Path | Purpose |
|---|---|
/opt/kopens/plantpulse-platform-docker | Installation/operation scripts and the compose source of truth |
/etc/kopens/conf | Platform configuration templates (host bind mount). Automatically seeded from the image on first run; operators can edit them directly on the host afterward, and they persist across reinstalls |
/etc/kopens/plantpulse-platform.env | Service credential secrets sidecar (permission 0600) |
/etc/kopens/platform.node.env | Per-node identity (PP_TIER, etc.). Do not copy between nodes |
/etc/kopens/ca | Shared cluster CA (used in 2-node split installations) |
Connection Addresses
| Purpose | Address |
|---|---|
| Web console | http://<server-ip>/ · https://<server-ip>/ — served by plantpulse-proxy |
| Admin UI | https://<server-ip>:7443 |
| Monitor UI · Health | https://<server-ip>:4950/api/health |
| MQTT | <server-ip>:1883 (plaintext) · <server-ip>:1884 (TLS) |
| OPC-UA | <server-ip>:11004 · <server-ip>:11005 |
Security note: After the first login to the web console, be sure to change the default admin password. → Initial Password
Operations Command Summary
Commands used for day-to-day operations. All are run from /opt/kopens/plantpulse-platform-docker/bin/. All verbs support --help.
| Task | Command | Notes |
|---|---|---|
| Status check | ./status.sh | Summary of services / health / volumes. 0=normal / 2=abnormal |
| Operational check | ./ops-check.sh | Container health + health API + recent critical logs |
| Start | ./up.sh | Idempotent. 0 = ready |
| Stop | ./down.sh | Preserves state |
| Restart | ./restart.sh | graceful drain → stop → start → wait for readiness |
| View logs | ./logs.sh [서비스] | Prints the last 200 lines and exits. To follow, use -f. If no argument, targets all containers. See --list for the list |
| Enter a container | ./shell.sh [서비스] | Defaults to the data lake if no argument |
| Update images | ./update.sh | pull + recreate. Automatically rolls back to the previous image on failure |
| Full removal | ./remove.sh | Removes containers only (volumes/config preserved). RM_IMAGE=1 / RM_NETWORK=1 |
| Backup | ./backup.sh [볼륨 …] | Default pp-data · pp-security |
| Boot verification | ./stack-verify-boot.sh | Overall stack readiness judgment |
| Diagnostic bundle | ./doctor.sh | tarball for support requests. Secret values are masked |
| Change password | ./passwd.sh --list | List of keys that can be changed |
# 운영 중 빠른 점검 루틴
cd /opt/kopens/plantpulse-platform-docker/bin
./status.sh
./ops-check.sh
# 문제가 의심되면 진단 번들 생성
./doctor.sh
stack-run.sh · stack-stop.sh · stack-bash.sh · stack-update.sh · stack-remove.sh have not been removed. Running them directly simply prints the new name in one line, but the behavior is the same. You don't need to update existing customer runbooks all at once.
Caution — Deletes All Volumes:
./tools/remove-all-volumes.shpermanently deletes all data. Please use it only after a backup has been completed.
Air-gapped Network Installation
For environments without internet access, install in three steps: obtain the bundle → transfer via media → load on the air-gapped network server.
Step 1: Obtain the Bundle
The recommended path is to download the bundle published by KOPENS.
https://product.kopens.io/plantpulse-platform/plantpulse-platform-images-<version>.tar.gz
If you need to build it yourself on a server with internet access, use the following.
cd /opt/kopens/plantpulse-platform-docker/bin
./airgap-bundle.sh
# 옵션
INCLUDE_DATALAKE=1 ./airgap-bundle.sh # datalake 이미지까지 포함
OS_TARGET=both ./airgap-bundle.sh # 대상 서버가 Ubuntu인 경우 deb 패키지도 포함 (기본은 RHEL rpm)
The output is a single plantpulse-platform-images-<version>.tar.gz file, which includes all of the following:
- Docker Engine offline installation package (rpm / deb)
- Platform images (output of
docker save) repo/— all installation/operation scripts andcompose/
Step 2: Transfer to the Air-gapped Network Server
Deliver the .tar.gz file to the target server via an approved medium such as USB, an internal file server, or scp.
Step 3: Load and Install on the Air-gapped Network Server
sudo -i
mkdir -p /opt/kopens/plantpulse-platform-docker
tar -xzf plantpulse-platform-images-*.tar.gz -C /opt/kopens/plantpulse-platform-docker
cd /opt/kopens/plantpulse-platform-docker/repo/bin
vi env.sh # 3단계 환경 변수 검토와 동일하게 수정
./airgap-load.sh
airgap-load.sh automatically performs the following.
- Offline Docker Engine installation (
rpm -ivh/dpkg -i) — skipped if already installed - Loading the platform images
docker load - Invoking
install.sh(SKIP_LOGIN=1— no registry login needed since images are already local)
After installation, perform boot verification (./up.sh or ./stack-verify-boot.sh) and the operational check (./ops-check.sh) the same way as with manual installation.
Air-gapped Network Updates
Build a bundle containing the new version's images using the same procedure, transfer and load it, then recreate the existing containers with ./update.sh.
Troubleshooting
When the preflight Check Fails
| Message | Action |
|---|---|
docker is installed but daemon is not ready while SKIP_OS=1 | Start the Docker daemon first: systemctl start docker. Alternatively, run without SKIP_OS so that install.sh configures Docker for you |
| Port already in use (WARN) | Another service is using that port. Check the process with ss -tlnp | grep :<port> and clear it before installing the Platform |
| Docker data-root parent path missing (WARN) | The /data1 directory does not exist. Mount the data disk at /data1 or create the directory |
When the Stack Fails to Start
Since docker compose up -d waits for all depends_on conditions, if even one container fails its health check, the entire command fails, leaving behind just a single line.
dependency failed to start: container plantpulse-server-web is unhealthy
This single line tells you nothing beyond the container name. The operations script automatically prints the container list, status, health probe output, and each log's tail together at this point, so read that output first. To check manually:
cd /opt/kopens/plantpulse-platform-docker/bin
# 어떤 컨테이너가 어떤 상태인가
./status.sh
# 문제가 있는 컨테이너의 로그
./logs.sh --list # 볼 수 있는 서비스 목록
./logs.sh plantpulse-server-web -n 200
# 자원 상황
df -h
docker stats --no-stream
Frequently observed causes:
| Cause | Check |
|---|---|
| Out of memory (OOM) | docker inspect <컨테이너> --format '{{.State.OOMKilled}}'. For per-app mem_limit, refer to the Environment Variable Reference |
| Disk full | /data1 free space |
| Missing required credentials | compose requires passwords via :?. If empty, the stack does not come up halfway — it fails to come up at all — run install.sh first to create the sidecar |
| DB initialization delay | The first installation takes a long time due to schema creation. If it's progressing without errors in the logs, please wait a bit longer |
If you can't identify the cause, generate a diagnostic bundle with ./doctor.sh and attach it to a technical support request. The diagnostic bundle includes the status and logs of all containers.
When TLS Handshakes Only Show Up as TimeoutException
If the name an app uses to connect to the backend isn't in the certificate's SAN list, the error message won't mention the certificate at all — it will just look like a timeout. Suspect this case if you've renamed a container or specified the backend host directly. See Security Management for how to rebake certificates.
Registry Authentication Failure
# unauthorized 오류 시 재로그인
docker login docker.kopens.io
- If login credentials are missing or expired, request issuance from the KOPENS operations team.
- An internal firewall may be blocking access to the registry. Ask your network administrator to allow HTTPS access to the
docker.kopens.ioandproduct.kopens.iodomains. - If external access is completely blocked, please use the air-gapped network installation.
Technical Support
If you need assistance during installation or operation, please feel free to reach out anytime: webmaster@kopens.com