# Image Build & Runtime Lifecycle

> How the Dockerfile extends featurehub/party-server:latest, how build-setup.sh temporarily starts and stops the embedded party-server during docker build to run the setup pipeline, and how entrypoint.sh launches the server at runtime with no re-initialization. Covers the build-time vs run-time env var contract and the server discovery fallback chain.

- Repository: vtex/dk-flags
- GitHub: https://github.com/vtex/dk-flags
- Human wiki: https://grok-wiki.com/public/wiki/vtex-dk-flags-0a8c140c3cfa
- Complete Markdown: https://grok-wiki.com/public/wiki/vtex-dk-flags-0a8c140c3cfa/llms-full.txt

## Source Files

- `docker/Dockerfile`
- `docker/build-setup.sh`
- `docker/entrypoint.sh`
- `docker/run-setup.sh`
- `docker/package.json`
- `specs/dk-flags-party-server-image.md`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [docker/Dockerfile](docker/Dockerfile)
- [docker/build-setup.sh](docker/build-setup.sh)
- [docker/entrypoint.sh](docker/entrypoint.sh)
- [docker/run-setup.sh](docker/run-setup.sh)
- [docker/package.json](docker/package.json)
- [docker/config.js](docker/config.js)
- [docker/setup.js](docker/setup.js)
- [specs/dk-flags-party-server-image.md](specs/dk-flags-party-server-image.md)
</details>

# Image Build & Runtime Lifecycle

The `dk-flags-party-server` Docker image extends `featurehub/party-server:latest` and adds a one-time initialization pipeline that runs **at image build time**. By baking the FeatureHub admin user, portfolio, application, and seed feature flags into an image layer during `docker build`, every container that starts from the image is immediately ready to serve requests — no startup hooks, no sidecar containers, no manual bootstrapping.

This page explains how the build-time and run-time phases divide responsibilities, how each shell script discovers and controls the embedded server, and what the environment variable contract means for operators who need to customize credentials or the server URL.

---

## Phase Separation: Build vs. Runtime

The image lifecycle is split into two fully independent phases:

```text
┌──────────────────────────────────────────────────────────────┐
│  docker build                                                 │
│                                                               │
│  1. Extend featurehub/party-server:latest                     │
│  2. apk add nodejs npm curl procps                            │
│  3. COPY docker/*.js + exampleFlags.json → /app/             │
│  4. npm install (zero dependencies; engines: node >=18)       │
│  5. RUN build-setup.sh                                        │
│       ├─ start party-server (background)                      │
│       ├─ wait up to 120 s for GET $APP_URL → 200             │
│       ├─ node /app/setup.js  (5-step init pipeline)           │
│       └─ kill party-server; finalize layer                    │
│                                                               │
│  Result: image layer contains fully-initialized H2 state      │
└──────────────────────────┬───────────────────────────────────┘
                           │ (image artifact)
┌──────────────────────────▼───────────────────────────────────┐
│  docker run                                                   │
│                                                               │
│  ENTRYPOINT /app/entrypoint.sh                                │
│       └─ exec party-server (foreground, no setup re-run)      │
│                                                               │
│  Container is ready when the server is ready.                 │
│  No initialization happens; no extra wait.                    │
└──────────────────────────────────────────────────────────────┘
```

The build-time server and run-time server are the **same binary** launched by the same discovery logic, but play different roles: the build-time instance is ephemeral (started in the background, stopped after setup), while the run-time instance is the container's process 1.

Sources: [docker/Dockerfile:44-53](), [docker/build-setup.sh:1-127](), [docker/entrypoint.sh:39-43]()

---

## Base Image and System Dependencies

```dockerfile
FROM featurehub/party-server:latest

RUN set -eux; \
    apk add --no-cache \
        nodejs \
        npm \
        curl \
        procps;
```

The base image is Alpine-based and ships `bathe.BatheBooter` plus the FeatureHub party-server JARs pre-installed under `/app/classpath/` and `/app/libs/`. The `RUN apk add` layer adds:

| Package | Purpose |
|---------|---------|
| `nodejs` / `npm` | Run the Node.js 18+ setup pipeline (`setup.js` and its modules) |
| `curl` | HTTP readiness probe in `build-setup.sh` and `run-setup.sh` |
| `procps` | Provides `kill -0` (process liveness test) used in `build-setup.sh` |

`package.json` declares `engines: { "node": ">=18.0.0" }` and lists **zero npm dependencies** — the setup pipeline uses Node's built-in `fetch` and CommonJS modules only.

Sources: [docker/Dockerfile:1-13](), [docker/package.json:8-9]()

---

## Build-Time: `build-setup.sh`

`build-setup.sh` is executed as a `RUN` layer in the Dockerfile. It orchestrates the temporary server lifecycle around the setup pipeline.

### Server Discovery Fallback Chain

Because the base image layout can vary across `featurehub/party-server` versions, the script tries four strategies in order:

```text
1. /app/classpath/party-server*.jar exists?
   → java -cp "/app/classpath/*:/app/libs/*" bathe.BatheBooter
         -Rio.featurehub.party.Application
         -P/etc/common-config/common.properties
         -P/etc/app-config/application.properties
         -P/etc/app-config/secrets.properties  (background &)

2. Any *.jar in /app (excluding libs/)?
   → java -jar <first-found>.jar  (background &)

3. featurehub-party-server on $PATH?
   → featurehub-party-server  (background &)

4. Executable file at one of:
     /app/party-server
     /usr/local/bin/party-server
     /opt/party-server/party-server
   → exec from that path  (background &)

5. None found → "Error: Could not find server executable" → exit 1
```

Strategy 1 (`bathe.BatheBooter`) is the primary path for the current base image. All strategies capture `stdout`/`stderr` to `/tmp/server.log`.

Sources: [docker/build-setup.sh:13-73]()

### Readiness Wait

After the server starts in the background, the script polls `$APP_URL` (default `http://localhost:8085`) with `curl -f -s` every 2 seconds, up to `MAX_WAIT=120` seconds. If the server does not respond within that window, the script prints the last 50 lines of `/tmp/server.log`, kills the server process, and exits 1 — failing the `docker build`.

```sh
while [ $WAIT_COUNT -lt $MAX_WAIT ]; do
  if curl -f -s "$APP_URL" > /dev/null 2>&1; then
    echo "✓ Server is ready!"
    break
  fi
  sleep 2
  WAIT_COUNT=$((WAIT_COUNT + 2))
done
```

Sources: [docker/build-setup.sh:82-102]()

### Running the Setup Pipeline

Once the server is healthy, the script hands off to the Node.js setup orchestrator:

```sh
if node /app/setup.js; then
  echo "✓ Setup completed successfully!"
else
  kill $SERVER_PID 2>/dev/null || true
  exit 1
fi
```

A non-zero exit from `setup.js` causes the script to kill the server and fail the build.

Sources: [docker/build-setup.sh:104-113]()

### Graceful Server Shutdown

After successful setup, the server is stopped so it does not persist as a running process inside the committed image layer:

```sh
kill $SERVER_PID 2>/dev/null || true
sleep 2
if kill -0 $SERVER_PID 2>/dev/null; then
  kill -9 $SERVER_PID 2>/dev/null || true
fi
wait $SERVER_PID 2>/dev/null || true
```

The graceful `SIGTERM` is followed by a 2-second wait; if the process is still alive, `SIGKILL` is sent. This ensures the image layer captures a clean stopped state.

Sources: [docker/build-setup.sh:115-127]()

---

## Node.js Setup Pipeline (`setup.js`)

The setup pipeline is a modular Node.js orchestrator invoked by `build-setup.sh`. It is composed of six CommonJS modules under `docker/`:

```text
docker/
├── setup.js        ← orchestrator (5 ordered steps)
├── config.js       ← env var + hard-coded constants
├── api.js          ← HTTP helpers + readiness probe
├── auth.js         ← initialize or login, returns bearer token
├── portfolio.js    ← portfolio / application / environment discovery
├── features.js     ← create, unlock, and set-value per flag
└── flags.js        ← load exampleFlags.json from candidate paths
```

### Orchestration Steps

```mermaid
sequenceDiagram
    participant B as build-setup.sh
    participant S as party-server (build-time, background)
    participant O as setup.js
    participant API as FeatureHub Mgmt API

    B->>S: start (bathe.BatheBooter or fallback)
    B->>S: poll GET $APP_URL every 2s (max 120s)
    B->>O: node /app/setup.js
    O->>API: waitForApp() — re-checks readiness (max 60s)
    O->>API: POST /mr-api/initialize  (create site admin)
    alt already initialized
        O->>API: POST /mr-api/login  (fallback)
    end
    API-->>O: bearer token
    O->>API: GET /mr-api/portfolio  (select first)
    O->>API: POST /mr-api/portfolio/{id}/application  (name: integrationTest)
    O->>API: GET /mr-api/application/{id}/environment  (select first)
    loop each flag in exampleFlags.json
        O->>API: POST /mr-api/application/{id}/features
        alt type == BOOLEAN
            O->>API: PUT /mr-api/application/{id}/feature-environments/{key}  (unlock)
        end
        O->>API: PUT /mr-api/features/{envId}/feature/{key}  (set typed value)
    end
    O-->>B: exit 0
    B->>S: kill (SIGTERM → SIGKILL after 2s)
```

`setup.js` exits 0 on success (including the "no flags" case when `exampleFlags.json` is missing), and exits 1 only when an unhandled error is thrown from the orchestrator.

Sources: [docker/setup.js:1-82]()

### Configuration (`config.js`)

All environment-injectable values and hard-coded constants are centralized in `config.js`:

```js
module.exports = {
    APP_URL:           process.env.APP_URL           || 'http://localhost:8085',
    ADMIN_USER:        process.env.ADMIN_USER         || 'admin',
    ADMIN_PASSWORD:    process.env.ADMIN_PASSWORD     || 'admin',
    ADMIN_EMAIL:       process.env.ADMIN_EMAIL        || 'admin@example.com',
    MAX_WAIT_TIME:     60000, // 60 s (not env-overridable)
    PORTFOLIO_NAME:    'integrationTest',
    ORGANIZATION_NAME: 'vtex',
    APPLICATION_NAME:  'integrationTest',
};
```

`PORTFOLIO_NAME`, `ORGANIZATION_NAME`, and `APPLICATION_NAME` are **not env-driven** — they are hard-coded and require a code change to rename.

Sources: [docker/config.js:1-10]()

---

## Runtime: `entrypoint.sh`

At `docker run` time, the entrypoint starts the server in the foreground without repeating any initialization:

```sh
# Setup was already done during build, so just start the server
echo "Starting FeatureHub party-server (setup already completed during build)..."
start_server "$@"
```

### Runtime Discovery Fallback Chain

`entrypoint.sh` uses the same binary discovery order as `build-setup.sh`, but critically uses `exec` to replace the shell process, making the server PID 1:

```text
1. /app/libs/bathe-booter*.jar AND /app/classpath/party-server*.jar both exist?
   → exec java -cp "/app/classpath/*:/app/libs/*" bathe.BatheBooter
         -Rio.featurehub.party.Application
         -P/etc/common-config/common.properties
         -P/etc/app-config/application.properties
         -P/etc/app-config/secrets.properties

2. featurehub-party-server on $PATH?
   → exec featurehub-party-server

3. /app/party-server?
   → exec /app/party-server

4. /usr/local/bin/party-server?
   → exec /usr/local/bin/party-server

5. None found → exit 1
```

If the `ENTRYPOINT` is called with arguments (e.g., a `CMD` override), `exec "$@"` is used instead, delegating to the base-image default command.

Sources: [docker/entrypoint.sh:5-36]()

### Key Difference from Build-Time Startup

| Aspect | `build-setup.sh` (build) | `entrypoint.sh` (runtime) |
|--------|--------------------------|--------------------------|
| Background vs foreground | Background (`&`) | Foreground (`exec`) |
| PID tracking | `$SERVER_PID` (to kill later) | PID 1 (container process) |
| Log capture | `> /tmp/server.log 2>&1` | Direct stdout/stderr |
| Setup invocation | Yes — runs `node setup.js` | No — setup already baked |
| Shutdown | Script-controlled after setup | Container stop signal |

---

## Environment Variable Contract

### Build-Time Variables

These are the only variables that affect the embedded state baked into the image layer. They must be set **before** or **during** `docker build`:

| Variable | Default | Effect |
|----------|---------|--------|
| `APP_URL` | `http://localhost:8085` | URL that `build-setup.sh` polls for readiness and that `setup.js` uses for all API calls |
| `ADMIN_USER` | `admin` | Display name of the site admin created by `POST /mr-api/initialize` |
| `ADMIN_PASSWORD` | `admin` | Password for the site admin |
| `ADMIN_EMAIL` | `admin@example.com` | Email (login identifier) for the site admin |

The Dockerfile sets these as `ENV` instructions, making them available to the `RUN build-setup.sh` layer:

```dockerfile
ENV APP_URL=http://localhost:8085
ENV ADMIN_USER=admin
ENV ADMIN_PASSWORD=admin
ENV ADMIN_EMAIL=admin@example.com
```

Sources: [docker/Dockerfile:39-43]()

### Run-Time Variables — What They Cannot Do

Passing `ADMIN_USER`, `ADMIN_PASSWORD`, or `ADMIN_EMAIL` at `docker run` time has **no effect** on the embedded FeatureHub state. The setup pipeline ran during the build; `entrypoint.sh` does not re-run it. The admin that was created at build time is the only admin in the database, regardless of any run-time env var values.

`APP_URL` at run time also has no effect: `setup.js` is never invoked at runtime, and the server binds to its configured port (`8085`) from its own configuration files, not from the `APP_URL` env var.

> **Operator note**: The defaults (`admin` / `admin`) are insecure and intended for development and CI only. For any non-development image, override all `ADMIN_*` env vars at `docker build` time via `--build-arg` or by setting `ENV` values in a downstream Dockerfile.

Sources: [specs/dk-flags-party-server-image.md:168-175]()

---

## `run-setup.sh` — Manual Path (Not Part of the Image Build)

`run-setup.sh` is a standalone convenience script that polls readiness and runs `node /app/setup.js` against a server that is already running externally. It is **not called** by the Dockerfile or `build-setup.sh`:

```sh
# Wait up to 60s for $APP_URL, then:
node /app/setup.js
```

It serves as an operator escape hatch — for example, running the setup pipeline against a live container that was started without the baked initialization, or during local development. Its existence does not affect the normal image lifecycle.

Sources: [docker/run-setup.sh:1-29]()

---

## Summary

The `dk-flags-party-server` image achieves a self-contained, ready-on-start FeatureHub instance by running a two-phase lifecycle: a **build-time phase** that temporarily starts the embedded `bathe.BatheBooter`-launched party-server, drives a five-step Node.js initialization pipeline against the FeatureHub Management REST API, and then stops the server before committing the initialized filesystem as an image layer; and a **run-time phase** in which `entrypoint.sh` simply re-launches the same server in the foreground via `exec`, with no setup logic whatsoever. Admin credentials and the seed flag set are permanently baked at build time — run-time environment variables cannot retroactively change the embedded state, which means any change to flags or credentials requires a rebuild.
