Skip to main content

Installation

This document describes the procedure for installing PlantPulse Studio on a single customer server. It consists of four Docker containers (web · server · builder sidecar · docker proxy), and the installation script handles everything from Docker installation to startup automatically.

PostgreSQL is not part of the stack

We use an external PG — you can share the same PG used by the platform. There used to be a mode that spun up a bundled container, but that has been removed (2026-08-28) — there was an actual incident where two instances fought over port 5432 on one server.

If you are on an air-gapped network without internet access, see Air-gapped installation instead of this document.


1. Prerequisites​

Server Specifications​

ItemMinimumRecommendedNotes
CPU4 cores8 cores or moreA build runs for each app session
Memory8 GB16 GB or moreSee the table below
Disk50 GB free100 GB or moreContainer images alone are about 6 GB
OSLinux (x86_64)RHEL 9 / Rocky 9 / Ubuntu 22.04 or higherAny with systemd + Docker will work

Memory calculation basis — these are the fixed upper limits set by the stack.

ContainerMemory Limit
studio-server (orchestrator)2 GB
agent-server (builder sidecar)2 GB (adjustable via AGENT_SERVER_MEMORY)
studio-web (nginx)256 MB
docker-proxy (Docker gateway)128 MB
Per app session container2 GB

One app session container is created each time a user opens a project, and it is automatically reclaimed after 30 minutes of inactivity. Add 2 GB for the number of concurrent workers.

If your target is 3~5 concurrent users

Plan for 16 GB memory · 8 cores · 100 GB disk as a comfortable baseline.

Software​

  • Docker Engine + Docker Compose v2 (v2.22 or higher) If not already installed, the one-line installation script will install them automatically.
  • curl, tar (included by default in most distributions)

Ports and Firewall​

The stack operates on the host network. This means container ports are exposed directly on the host, so access control is handled solely by the server firewall.

PortPurposeFirewall
80Studio UI (nginx)Open
5171App listener — preview · deployed apps · QR accessOpen (required)
5170Studio APIWeb serves as an internal proxy — no need to open (for diagnostics)
8000Builder sidecar (agent-server)Loopback only — never open
5172MCP tool bridgeLoopback only — never open
2375Docker daemon gateway (docker-proxy)Bind only to 127.0.0.1 — the only container in the stack that doesn't use the host network
If you block 5171, preview and deployed apps won't open

The preview (/preview/…) and deployed apps (/apps/…) are served from a different origin (default port 5171) than the studio for security boundary reasons. This is a mechanism to prevent JavaScript in chat-created apps from accessing the studio's login token. If port 5171 is not open, the screen will appear blank.

8000 · 5172 have no authentication

These two ports are for inter-component communication and have no authentication procedure. If exposed externally, the entire user workspace can be read and written. The stack binds them to loopback by default, but never open them in the firewall either.

Registry Access​

Images are pulled from the KOPENS registry (docker.kopens.io). Log in once before installation. Accounts are issued by the KOPENS operations team.

docker login docker.kopens.io

2. Database Preparation​

The studio uses an external PostgreSQL. You can share the same PG used by the platform.

DATABASE_URL is required

If left empty, the installation will stop right there and tell you what's missing — it does not invent plausible defaults. This is because a plausible default once pointed to the wrong place and caused an incident.

DATABASE_URL=postgres://`<user>`:`<password>`@`<db-host>`:5432/`<db-name>`

Create the database and account first​

🔴 The server only runs migrations on an existing database. It does not create the DB or roles. There is a separate script that creates them.

# .env 의 DATABASE_URL 이 가리키는 DB · 역할을 만든다
bin/provision-platform-db.sh

# 관리자 계정을 따로 줘야 하는 경우
ADMIN_URL=postgres://`<관리자>`:`<password>`@`<호스트>`:5432/postgres bin/provision-platform-db.sh

# 무엇을 할지 보기만 한다
DRYRUN=1 bin/provision-platform-db.sh

This follows the same rules as for the AI side.


Log in as root on the server and run a single line. The script performs Docker check/installation → package download → .env creation → stack startup.

curl -fsSL https://product.kopens.io/plantpulse-studio/install.sh | sudo bash

The installation location is /opt/kopens/plantpulse-studio-docker, and all subsequent operational commands are executed from this directory.

Pre-set values for unattended installation​

Environment variables must be placed before bash to be passed through the pipe.

curl -fsSL https://product.kopens.io/plantpulse-studio/install.sh \
| sudo PLATFORM_API_TARGET=https://192.168.0.41 \
DATA_ROOT=/data1/pp-studio \
PS_ANTHROPIC_API_KEY=sk-ant-... \
bash

Being prompted for values​

curl -fsSL https://product.kopens.io/plantpulse-studio/install.sh | sudo INTERACTIVE=1 bash
⌨️ Install on a new box — from start to «Ready» in 2 minutes 6 seconds. Recorded from an actual run, including delays (the static section of image download is collapsed)
This screen only appears on a «new» box

If you run it again on an already-installed server, the sidecar already has values, so it skips the prompts entirely. The screen above is a first installation on a clean box.

There are only five prompt questions (as of 2026.09). The remaining seven use default values, and these fixed values are printed as a list at the end of installation — they are shown, not hidden.

OrderPromptVariable
1Platform API server (https://…)PP_API_URL
2Platform API token (X-API-Key)PP_API_TOKEN
3AI gateway address — including /v1PI_LLM_URL
4AI gateway key (LiteLLM master key)PI_LLM_API_KEY
5AI model name — name registered with the gatewayAI_MODEL
Have these values ready — installation will stop if they are missing

If a required value is empty, the installation halts. This is an intentional behavior to prevent a "seems like installation finished, but the stack doesn't start" state. DATABASE_URL is not asked — PostgreSQL comes with the stack and the installation fills it in itself.

The seven items that use defaults without asking — DATA_ROOT(/var/lib/pp-studio) · AI_PROVIDER(openai-compatible) · PP_LANG(en) · PP_TZ(Asia/Seoul) · APPS_PORT(5171) · REGISTRY(docker.kopens.io/ps) · PP_IMAGE_TAG(current calendar version).

Why we ask for the model name

If you specify a model name that the gateway does not serve, it fails just like a wrong key. So we excluded it from the defaults and made it a prompt.

To change them, provide them as environment variables or run with PP_ADVANCED=1 to be prompted for all values.

PS_ANTHROPIC_API_KEY is no longer asked

It was only needed when using Claude in the cloud, and since this product assumes a gateway as a prerequisite, it was effectively a question everyone just pressed Enter on. The functionality remains, and there are three ways to set it — as an environment variable during installation, later via bin/passwd.sh PS_ANTHROPIC_API_KEY (if you omit the value, it will prompt you so it doesn't remain on the command line), or via the Settings > AI screen. PP_ADVANCED=1 also does not ask it.

Safe to run again

If you re-run the installation script on the same server, only the code is updated, and .env and the local backup (dist/) are preserved. You can use it as-is for upgrades as well.

If the installation script gives a 404 or an old version

There might be a CDN cache remaining. Append ?v=$(date +%s) to the URL and download it again.

If you provide wrong values, it stops before installation​

Before touching the machine — before even installing Docker — it makes one real call with the provided credentials. For the platform it's GET /api/v5/site?limit=1, and for the gateway it's GET /v1/models.

⌨️ Run with a wrong gateway key — the platform token is rejected with 401 and installation halts without proceeding. The later «503» remains as a warning only

It only stops on rejection. "Could not ask" does not stop it.

ResponseJudgmentInstallation
2xxAcceptedContinues
401 · 403Credentials are wrongStops
400The installation script's request is malformed — a product defect, please report itStops
5xx · UnreachableCannot judgeWarning only, continues

It is intentional that 5xx is not placed in the same column as 401 — there was a case where a loaded platform returned 503 and then the same token returned 401 three minutes later. Mislabeling a gateway that is being inspected or comes up later than the studio as a "wrong key" would reject a valid installation.

The model name is also checked. It reads the gateway's /v1/models response and confirms whether AI_MODEL is in that list. If not, it prints the serving list and stops. If the list cannot be read, it does not judge — saying "it's not there" when the answer is empty would be the same falsehood as a test that cannot fail.

To skip the check — PP_SKIP_CRED_CHECK=1

This is for when you only want to test the installation itself with placeholder credentials (the product's release gate uses this path). Enabling this value in a production installation only postpones the inevitable first-use failure to later.


4. Installation (Manual)​

This is for when you have already downloaded the packages or want to write .env yourself.

cd /opt/kopens/plantpulse-studio-docker
sudo cp .env.example /etc/kopens/plantpulse-studio.env
sudo vi /etc/kopens/plantpulse-studio.env # 아래 표를 보고 값 채우기
sudo chmod 600 /etc/kopens/plantpulse-studio.env
bash bin/up.sh
The single source of truth for settings is /etc/kopens/plantpulse-studio.env

The environment variable file lives outside the repo tree, not in the installation directory, at /etc/kopens/plantpulse-studio.env. Its permissions are 0600. The platform, AI, and studio — all three products — use /etc/kopens/plantpulse-<product>.env under the same convention.

The .env in the installation directory root is the old path. Neither the compose file nor the other scripts read that file anymore, so editing it won't change the stack. The .env of an existing installation is moved to the new path by bin/install.sh (it is not created anew — if it were created anew, the DB would have the old password while the env has a new default, and the next restart would fail).

Key Environment Variables​

ItemDescriptionDefault
REGISTRYImage registry. If empty, only local images are used without pulling (air-gapped)docker.kopens.io/ps
PP_IMAGE_TAGImage tag. If empty, follows the calendar version of VERSION. To pin, a snapshot tag (2026.09-20260911) is recommended(empty)
DATA_ROOTPath where all data (workspace · builds · state · DB) is stored/var/lib/pp-studio
PLATFORM_API_TARGETPlantPulse platform address to delegate live data and authentication forhttps://192.168.0.41
APPS_PORTApp listener port (preview · deployed apps)5171
PP_LANGWeb default language ko / enen
PP_TZTimezone (IANA ID). Basis for interpreting "today · yesterday · last week"Asia/Seoul
STUDIO_CORS_ORIGINSOnly specify when calling the API from a different domain. Blocked by defaultempty
DATABASE_URLExternal PostgreSQL connection address — see section 2 aboveempty (required)
PI_LLM_API_KEY · PP_API_TOKEN etc.AI · platform keys — see Secrets managementempty (required)
Accounts and roles are not carried via environment variables

Accounts are not carried via environment variables. When the server has no accounts, it seeds a bootstrap administrator into the DB. After that, add or delete accounts and assign roles (administrator / builder / viewer) in the app's Settings → User Management. See 7. First Login below.

Korean field sites should add PP_LANG=ko manually

The .env created by the one-line installation does not have the PP_LANG · PP_TZ lines, so the defaults (en / Asia/Seoul) apply. To display in Korean, add PP_LANG=ko to .env and bash bin/restart.sh.

After editing .env, you must bin/restart.sh

docker restart does not re-read .env. This is a classic trap where you spend a long time wondering "why isn't it changing" before realizing the value wasn't reflected. Details are in Secrets management.


5. Checking Stack Composition​

Once installation is complete, the container network should be up. There is no PostgreSQL here — we use an external PG.

ContainerRole
plantpulse-studio-webStudio UI (nginx :80) — static screens + API proxy
plantpulse-studio-serverOrchestrator (:5170) — agents · sessions · deployments · app listener (:5171)
plantpulse-studio-agent-serverBuilder sidecar — the coding engine that writes app code
plantpulse-studio-docker-proxyDocker daemon gateway. Only accessible on 127.0.0.1
The orchestrator does not directly grab the Docker socket

It does not mount docker.sock on the server; it goes through the Docker proxy. Creating and removing session containers requires the daemon, and giving the socket directly is equivalent to having full host permissions.

Only the proxy does not use the host network and publishes to 127.0.0.1:2375. If it were the host network, 2375 would be an open daemon with nothing protecting it but the firewall.

The Docker proxy is not a boundary to prevent "escape"

To create session containers, POST /containers/create is required, and anyone who can call it can also create a container that mounts /. The proxy's control unit is endpoints, not parameters.

What this proxy actually does is two things — cut off unused API surfaces (volume · secret · config · swarm · node · plugin · system events), and consolidate daemon access to a single point to make it auditable. The allowed scope is narrowed to exactly what studio-server actually uses (container · image · build · exec · network · registry login).

So place this stack inside your trust boundary. Operate it assuming that a person building an app has permission over the host itself.

In addition, when a user opens a project, an app session container is created for each project. These are not stack containers; the server creates them as needed and reclaims them after 30 minutes of idle time.


6. Startup Verification​

cd /opt/kopens/plantpulse-studio-docker

bash bin/status.sh # 컨테이너 상태 + 헬스 + 세션 컨테이너 수
curl -s localhost:5170/health

Immediately after installation, it is recommended to run the comprehensive inspection script once. It checks health · web response · actual login · session runtime images · container status, and if any fail, it exits with an error.

bash bin/smoke-install.sh
✓ health
✓ client-config → {"lang":"ko",...}
✓ web 200
✓ 로그인(부트스트랩 계정)
✓ 세션 런타임 이미지
✓ 컨테이너 running(...)
✅ 설치 스모크 통과
Session runtime image

App session containers use the plantpulse-studio-runtime image. If bin/up.sh is not available locally, it downloads and tags it, but if that fails, a ⚠️ 세션 런타임 이미지 없음 warning appears and opening a project fails. In that case, check the registry login state and re-run bash bin/up.sh.

If it's already available locally, up.sh will not touch it — to change to a new version, see bin/update.sh.


7. First Login​

Access via http://<server-ip>/ in your browser.

When the server first starts with no accounts at all, it seeds a bootstrap administrator into the DB.

ItemValue
IDadmin
Passwordadmin123!

It is also printed in the boot logs — 부트스트랩 관리자 생성: admin — 로그인 후 비밀번호를 바꿀 것.

Change this password first

It's the same value for every installation. As soon as you log in, change it in Settings → User Management (Password reset, at least 6 characters). Editing the environment variable file doesn't change it — the server checks for login in the order of ①DB → ②environment variables, and if it's set in the DB, it stops there.

What to do after login:

  1. Settings → User Management — change the bootstrap administrator's password, create field user accounts, assign roles (administrator / builder / viewer)
  2. Change passwords · API keys — rotate DB accounts and AI keys at once via bin/passwd.sh. This only needs to be done once during commissioning
  3. Settings → AI — confirm the AI provider and key connection status
  4. Settings → Platform — confirm the platform address/key connection (required for live data queries)

Detailed screen descriptions are in Getting started and Settings (Administrator).​

8. Daily Operations Commands​

All commands are executed from the installation directory (/opt/kopens/plantpulse-studio-docker).

bash bin/up.sh # 시작(레지스트리에서 최신 이미지 pull 포함). 종료 코드 0 = «준비됨»
bash bin/down.sh # 중지 — 데이터는 그대로 보존, 세션 컨테이너도 정리
bash bin/restart.sh # 재기동(.env 변경 반영)
bash bin/status.sh # 상태 요약
bash bin/logs.sh # 서버 로그 팔로우(기본 studio-server)
bash bin/logs.sh studio-web # 특정 서비스 로그
bash bin/update.sh # 이미지 갱신 + 재기동 → [업데이트](#9-updates)
bash bin/remove.sh # 컨테이너·네트워크 제거. **데이터는 보존이 기본**
The old names start.sh · stop.sh still work too

The three products (Platform · AI · Studio) have been organized to use common verbs with the same names and meanings — up · down · restart · status · logs · update · remove.

The old names have not been removed. bin/start.sh and bin/stop.sh still do the same things (CI calls them directly on remote hosts). You don't have to rewrite existing runbooks all at once.

Why down.sh doesn't only call compose

The session containers that the Studio server creates when opening apps (labeled plantpulse-studio=1) are not managed by compose. If you only call docker compose down, those keep running — that's the point where "I brought it down, but the containers are still alive" happens. down.sh cleans those up too.

remove.sh does not delete data — you must explicitly request it
bash bin/remove.sh # 컨테이너·네트워크만. 데이터 그대로
bash bin/remove.sh --volumes # 데이터까지 (확인을 묻습니다)
bash bin/remove.sh --volumes --yes # 자동화용 — 묻지 않습니다

The real data of this stack is not in named volumes but in the DATA_ROOT bind mount (default /var/lib/pp-studio) — PG data · workspace · build artifacts · state. It's not deleted by docker compose down -v, but when an operator says "remove," they often mean that too. Since it cannot be reversed, only touch it when explicitly requested.

To view only the session containers separately,

docker ps --filter label=plantpulse-studio=1

9. Updates​

bin/update.sh re-pulls images and restarts. Re-running the installation script also updates the operational scripts (.env · backups are preserved).

=== "Image-only update"

cd /opt/kopens/plantpulse-studio-docker
bash bin/update.sh
up.sh (old start.sh) alone leaves the «session runtime» on the old image

up.sh pulls images for compose services. However, the session runtime image where user apps actually run is not a compose service so it isn't caught by compose pull, and up.sh only pulls it when it's not already available locally — if it's there, it leaves it alone.

So if you only run up.sh, everything else gets the new image but only the session runtime silently stays on the old version. The difference from up.sh is that update.sh forcefully re-pulls that one.

=== "Package update too"

curl -fsSL https://product.kopens.io/plantpulse-studio/install.sh | sudo bash

Pinning Versions​

The version format is YYYY.MM (e.g., 2026.08). In production environments where you don't want automatic updates, pin the tag in .env.

TAG=2026.08
Back up before updating

Run bash bin/backup.sh first. The procedure is in Backup and Recovery.

Image Signature Verification (Optional)​

You can verify that the distributed images are signed by KOPENS. cosign must be installed.

cd /opt/kopens/plantpulse-studio-docker
bash bin/verify-images.sh
[verify] ✅ docker.kopens.io/ps/plantpulse-studio-server:2026.08
[verify] ✅ docker.kopens.io/ps/plantpulse-studio-web:2026.08
[verify] ✅ docker.kopens.io/ps/plantpulse-studio-runtime:2026.08

Next Steps​