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.
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
| Item | Minimum | Recommended | Notes |
|---|---|---|---|
| CPU | 4 cores | 8 cores or more | A build runs for each app session |
| Memory | 8 GB | 16 GB or more | See the table below |
| Disk | 50 GB free | 100 GB or more | Container images alone are about 6 GB |
| OS | Linux (x86_64) | RHEL 9 / Rocky 9 / Ubuntu 22.04 or higher | Any with systemd + Docker will work |
Memory calculation basis — these are the fixed upper limits set by the stack.
| Container | Memory 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 container | 2 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.
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.
| Port | Purpose | Firewall |
|---|---|---|
| 80 | Studio UI (nginx) | Open |
| 5171 | App listener — preview · deployed apps · QR access | Open (required) |
| 5170 | Studio API | Web serves as an internal proxy — no need to open (for diagnostics) |
| 8000 | Builder sidecar (agent-server) | Loopback only — never open |
| 5172 | MCP tool bridge | Loopback only — never open |
| 2375 | Docker daemon gateway (docker-proxy) | Bind only to 127.0.0.1 — the only container in the stack that doesn't use the host network |
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.
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 requiredIf 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.
3. Installation (One-line · Recommended)
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
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.
| Order | Prompt | Variable |
|---|---|---|
| 1 | Platform API server (https://…) | PP_API_URL |
| 2 | Platform API token (X-API-Key) | PP_API_TOKEN |
| 3 | AI gateway address — including /v1 | PI_LLM_URL |
| 4 | AI gateway key (LiteLLM master key) | PI_LLM_API_KEY |
| 5 | AI model name — name registered with the gateway | AI_MODEL |
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).
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 askedIt 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.
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.
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.
It only stops on rejection. "Could not ask" does not stop it.
| Response | Judgment | Installation |
|---|---|---|
2xx | Accepted | Continues |
401 · 403 | Credentials are wrong | Stops |
400 | The installation script's request is malformed — a product defect, please report it | Stops |
5xx · Unreachable | Cannot judge | Warning 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.
PP_SKIP_CRED_CHECK=1This 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
/etc/kopens/plantpulse-studio.envThe 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
| Item | Description | Default |
|---|---|---|
REGISTRY | Image registry. If empty, only local images are used without pulling (air-gapped) | docker.kopens.io/ps |
PP_IMAGE_TAG | Image tag. If empty, follows the calendar version of VERSION. To pin, a snapshot tag (2026.09-20260911) is recommended | (empty) |
DATA_ROOT | Path where all data (workspace · builds · state · DB) is stored | /var/lib/pp-studio |
PLATFORM_API_TARGET | PlantPulse platform address to delegate live data and authentication for | https://192.168.0.41 |
APPS_PORT | App listener port (preview · deployed apps) | 5171 |
PP_LANG | Web default language ko / en | en |
PP_TZ | Timezone (IANA ID). Basis for interpreting "today · yesterday · last week" | Asia/Seoul |
STUDIO_CORS_ORIGINS | Only specify when calling the API from a different domain. Blocked by default | empty |
DATABASE_URL | External PostgreSQL connection address — see section 2 above | empty (required) |
PI_LLM_API_KEY · PP_API_TOKEN etc. | AI · platform keys — see Secrets management | empty (required) |
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.
PP_LANG=ko manuallyThe .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.
.env, you must bin/restart.shdocker 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.
| Container | Role |
|---|---|
plantpulse-studio-web | Studio UI (nginx :80) — static screens + API proxy |
plantpulse-studio-server | Orchestrator (:5170) — agents · sessions · deployments · app listener (:5171) |
plantpulse-studio-agent-server | Builder sidecar — the coding engine that writes app code |
plantpulse-studio-docker-proxy | Docker daemon gateway. Only accessible on 127.0.0.1 |
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.
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(...)
✅ 설치 스모크 통과
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.
| Item | Value |
|---|---|
| ID | admin |
| Password | admin123! |
It is also printed in the boot logs — 부트스트랩 관리자 생성: admin — 로그인 후 비밀번호를 바꿀 것.
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:
- Settings → User Management — change the bootstrap administrator's password, create field user accounts, assign roles (administrator / builder / viewer)
- 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 - Settings → AI — confirm the AI provider and key connection status
- 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 # 컨테이너·네트워크 제거. **데이터는 보존이 기본**
start.sh · stop.sh still work tooThe 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.
down.sh doesn't only call composeThe 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 itbash 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 imageup.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
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
- Secrets management — how to securely store AI · platform keys
- Domain and Reverse Proxy — serving via domain/HTTPS instead of IP
- Backup and Recovery — automated backup installation and recovery rehearsal
- Troubleshooting — symptom-based causes and actions table