Environment Variable Reference
This is the complete set of environment variables read by the Studio server. Unlike the values you change on the screen (Environment Settings), the values here are applied only after you edit the file and restart.
The canonical file is /etc/kopens/plantpulse-studio.env
/etc/kopens/plantpulse-studio.env
.env in the repository root is no longer readValues placed in the old path (.env in the installation directory) will not be applied. If the old file remains, the script will notify you of that fact. When changing values, use the canonical path above.
Set the permissions to chmod 600 — this file contains API keys.
Image and Version
| Variable | Default | Description |
|---|---|---|
REGISTRY | docker.kopens.io/ps | Registry from which to pull images. If blank, only local tags are used (when built directly) |
PP_IMAGE_TAG | (empty) | Calver tag for our image set (server · web) |
PS_IMAGE_TAG_SERVER | (none) | When only plantpulse-studio-server should run with a different tag |
PS_IMAGE_TAG_WEB | (none) | When only plantpulse-studio-web should run with a different tag |
PS_IMAGE_TAG_AGENT_SERVER | 0c5ac7fb | Agent sidecar. Not a calver; it uses a digest pin (OpenHands SDK v1.43.1), so it does not follow PP_IMAGE_TAG |
PP_IMAGE_TAG empty is recommendedIf left empty, bin/start.sh reads the calver from the VERSION file and automatically tracks each release.
If you manually write 'YYYY.MM' here, that value is frozen as-is — in fact, that's why there was an incident with a one-month-old image being deployed.
If you must pin to a specific point in time, use a snapshot tag (2026.09-20260911), not 'YYYY.MM' (2026.09).
Do not compute it with date +%Y.%m — between 00:00 and 04:00 KST on the 1st of each month, this points to a tag that does not yet exist (the monthly release is published at 04:00).
TAG · AGENT_SERVER_TAGWhat compose reads are only the names in the table above. If you write something like TAG=2026.08, nothing happens, and the stack runs with the calver from VERSION — you think you've pinned it, but you're actually tracking.
To pin to a specific release, use a snapshot tag — e.g., TAG=2026.08-20260810.
:::
Locale and Time Zone
Uses the same variable names as Platform and Edge.
| Variable | Default | Description |
|---|---|---|
PP_LANG | en | Default web language (ko / en). The value stored in the browser takes precedence |
PP_TZ | Asia/Seoul | IANA time zone ID. Basis for interpreting relative times like "today/yesterday" |
For a Korean field environment, set this to PP_LANG=ko.
Data Storage Location
| Variable | Default | Description |
|---|---|---|
DATA_ROOT | /var/lib/pp-studio | Workspaces, build artifacts, and state are all stored under this path |
This path is the backup target → Backup and Recovery.
Database — External PostgreSQL
Studio uses an external PostgreSQL. You can share the same PG instance that the Platform uses.
| Variable | Example |
|---|---|
DATABASE_URL | postgres://<user>:<password>@<db-host>:5432/ppstudio |
🔴 There is no such variable as PG_PASSWORD. The DB password is included in the same line as DATABASE_URL — we consolidated it into one line because a separate variable for just the password could diverge from the URL.
DATABASE_URL is requiredIf it's empty, the installation halts right there and tells you what's missing — it does not invent a plausible default.
The server only runs migrations on an existing database. It does not create the DB or role — bin/provision-platform-db.sh handles that.
See "Database Preparation" in Installation.
Platform Integration
| Variable | Description |
|---|---|
PP_API_URL | Platform address for real-data queries and delegating login authentication |
PP_API_TOKEN | The api_key for that platform |
In logs or inside the container, they appear as PLATFORM_API_TARGET · PLATFORM_API_KEY.
The names to write in .env are PP_API_URL · PP_API_TOKEN — see "The two layers of names" below.
Listener Ports
| Variable | Default | Description |
|---|---|---|
PORT | 5170 | API port for the Studio server (orchestrator). The web proxies it internally, so there's no need to expose it externally — for diagnostics (curl localhost:5170/health) |
APPS_PORT | 5171 | Separate listener port that serves previews and deployed apps |
- Direct-port access installation — you must also open this port in the firewall for previews, deployed apps, and QR access to work.
- Behind a reverse proxy — you don't need to open it. The proxy forwards
/container/preview·/appson the same domain to this listener → Domains and Reverse Proxy.
The client only uses relative paths. There's no need to separately provide the public URL for previews or deployed apps, so it works as-is even in a configuration that only opens 80/443.
AI Providers
The values here are initial values. Values entered on the screen (Environment Settings → AI) override these.
| Variable | Default | Description |
|---|---|---|
AI_PROVIDER | openai-compatible | If blank, it is always openai-compatible |
PI_LLM_URL | (empty — required) | Gateway address. No default is set |
AI_MODEL | gpt-4o | Model name |
PI_LLM_API_KEY | (empty — required) | OpenAI-compatible key |
PS_ANTHROPIC_API_KEY | (empty) | Only when using cloud Claude |
⚠ If PI_LLM_URL and PI_LLM_API_KEY are empty, the stack refuses to start.
PI_LLM_URLIf the gateway isn't on that host, the entire chat becomes fetch failed, and because the value looks plausible, locating the cause is expensive. Write the actual address. You can verify whether the entered value is correct via the Connection Test in the Environment Settings → AI tab before saving → Environment Settings (Admin).
The provider is explicitly specified with AI_PROVIDER. If you infer it from which key is present, you can't predict what will appear depending on the env configuration.
CORS
| Variable | Default | Description |
|---|---|---|
STUDIO_CORS_ORIGINS | (empty = same-origin) | Specify only when accessing from a separate domain |
Installation and Startup Flags
These are things to prefix before the command, not .env (SKIP_DOCKER=1 bash bin/install.sh).
| Variable | Default | Description |
|---|---|---|
SKIP_DOCKER | 0 | If 1, skips the Docker installation step (when it's already installed) |
SKIP_WAIT | 0 | If 1, the script finishes without waiting for startup to complete |
WAIT_READY_MIN | 10 | Time (minutes) to wait for startup to complete. Increase on slow connections or large images |
Agent Sidecar Resources
| Variable | Default | Description |
|---|---|---|
AGENT_SERVER_CPUS | 1 | Number of CPUs allocated to the builder sidecar |
AGENT_SERVER_MEMORY | 2g | Memory limit for the builder sidecar |
What is Not Carried via env
User accounts and roles are not defined via environment variables. When the server finds no accounts, it seeds an admin in the DB; after that, users are added/deleted and roles (admin / builder / viewer) are assigned from the User Management screen of the app.
→ Changing the Initial Password
Related Documents
- Installation — the procedure for filling in the initial values
- Secret Management — where to put keys
- Environment Settings (Admin) — values changed on the screen
- Domains and Reverse Proxy
Two Layers of Names — .env vs. inside the container
🔴 This is where mistakes are most common. The names seen in logs, docker inspect, and the product source differ from the names to write in .env. You write the left side.
Name to write in .env | Name visible inside the container |
|---|---|
PP_API_URL | PLATFORM_API_TARGET |
PP_API_TOKEN | PLATFORM_API_KEY |
PI_LLM_URL | AI_BASE_URL |
PI_LLM_API_KEY | AI_API_KEY |
PS_ANTHROPIC_API_KEY | ANTHROPIC_API_KEY |
The rest (DATABASE_URL · DATA_ROOT · APPS_PORT · AI_PROVIDER · AI_MODEL · STUDIO_CORS_ORIGINS) use the same name on both sides.
Multiple products (Platform · AI · Studio) are deployed together on a single server. By adding the product prefix (PP_ Platform · PI_ Common infrastructure · PS_ Studio) to the .env-side names, which product a value belongs to is clear from the name alone, and there's no confusion even when two products share a single value.