Skip to content

Installation / Update / Uninstall

This page documents what the install/update/uninstall scripts under scripts/windows/ and scripts/linux/ ACTUALLY do — the same commands as the "Quick Start" section of the repository root README.md, explained here in more detail.

Supported matrix

Platform Docker Native (no Docker)
Windows ✅ install.ps1 -Mode docker ✅ install.ps1 -Mode native
Linux ✅ install.sh --mode docker ✅ install.sh --mode native

If -Mode/--mode is omitted (auto), the script checks whether Docker is actually usable (docker info + docker compose version) and picks a mode accordingly, always logging its decision ([INFO] Installation mode: docker|native).

What Docker mode installs

  • docker-compose.prod.yml (repo root) — the existing docker-compose.yml (hot-reload dev environment, bind-mounted source
  • npm run dev/pnpm run dev) is UNCHANGED and still works via docker compose up -d. docker-compose.prod.yml is a separate Compose project (iqv-dictionary-prod) that uses fully production images: backend/Dockerfile.prod (multi-stage: npm ci + npm run build → node dist/server.js, never npm run dev) and dashboard/Dockerfile.prod (multi-stage: pnpm install + pnpm run build → static files served by nginx, dashboard/nginx.conf, never the Vite dev server).
  • MongoDB is NOT containerized — same as docker-compose.yml, the app connects to a MongoDB that already runs externally (host/another server) via host.docker.internal.
  • Ports, the published bind address and the frontend's build-time API address are read from the repo root .env (auto-created from .env.example if missing): IQV_BACKEND_PORT (default 3001), IQV_FRONTEND_PORT (default 8080), IQV_BIND_HOST (default 0.0.0.0), VITE_API_BASE_URL.
  • docker-compose.prod.yml is generic: it contains no credentials, no customer/server-specific network, IP, port or hostname, and it does not override MONGODB_URI from backend/.env.

Configuration layers

Layer Git Contents
.env gitignored (.env.example tracked) IQV_FRONTEND_PORT, IQV_BACKEND_PORT, IQV_BIND_HOST, VITE_API_BASE_URL
backend/.env gitignored (backend/.env.example tracked) MONGODB_URI, MONGODB_DB, collections, JWT_SECRET, JWT_EXPIRES_IN, CORS_ORIGIN
docker-compose.prod.yml tracked Generic production Docker definition
docker-compose.server.yml gitignored (docker-compose.server.example.yml tracked) Optional server/customer Docker override

backend/.env is the single source of truth for the MongoDB connection. Compose only makes the name resolvable via extra_hosts: host.docker.internal:host-gateway; the real URI (with credentials when needed) lives in backend/.env and never enters the repository.

Server override (docker-compose.server.yml)

Server-specific Docker settings never require editing a tracked file. Once, on the server:

cp docker-compose.server.example.yml docker-compose.server.yml

When the file exists, the install/update/uninstall scripts and the npm run docker:* commands detect it automatically:

# without an override
docker compose -f docker-compose.prod.yml --env-file .env <command>

# with an override
docker compose -f docker-compose.prod.yml -f docker-compose.server.yml \
  --env-file .env <command>

This resolution lives in exactly one helper — iqv_compose in scripts/linux/lib.sh (Windows: Invoke-IqvCompose, npm: scripts/common/compose.mjs) — and every install/update/rebuild/status/ healthcheck/uninstall path uses it; no script builds its own compose arguments.

Typical contents (the external Docker network of a reverse proxy such as Caddy):

services:
  dictionary-backend:
    networks: [default, iqv_proxy]
  dictionary-frontend:
    networks: [default, iqv_proxy]

networks:
  iqv_proxy:
    external: true
    name: iqv_proxy

Binding the published ports to loopback needs no override at all — IQV_BIND_HOST=127.0.0.1 in .env is enough. To replace the port mappings entirely, use the !override example (Compose v2.24+) shown in docker-compose.server.example.yml.

What native mode installs

  • Backend: npm ci + npm run build (compiled backend/dist/server.js).
  • Dashboard: corepack enables pnpm@9.15.9, then pnpm install --frozen-lockfile + pnpm run build (static dashboard/dist).
  • Process management: PM2 on both platforms (scripts/common/ecosystem.config.js) — identical logic on Windows/Linux:
  • Backend: node dist/server.js (under PM2, autorestart).
  • Frontend: a dependency-free, project-specific static file server (scripts/common/static-server.mjs) that serves dashboard/dist — the native counterpart of the Docker image's nginx; no separate nginx install needed on Windows.
  • Start-on-boot:
  • Windows: pm2-windows-startup (pm2-startup install) — needs no admin rights, restores PM2's saved process list on login.
  • Linux: pm2 startup systemd — generates a systemd unit; the script installs it automatically if passwordless sudo is available, otherwise it prints the exact command to run (the script never blocks waiting for a password).

Idempotency

Running install.ps1/install.sh a second time:

  • Leaves an existing backend/.env / dashboard/.env / root .env UNTOUCHED ([OK] ... already exists).
  • In Docker mode, docker compose up -d only recreates containers when actually needed.
  • In native mode, pm2 startOrReload updates existing processes idempotently (never spawns duplicate processes).

Update flow

A single command in production:

cd /opt/iqv/apps/iqv-dictionary
bash ./scripts/linux/update.sh --branch main
  1. Verify the repository root (docker-compose.prod.yml, backend/, dashboard/).
  2. Detect install mode — from .iqv-install/state.json (or, if absent, best-effort detection from running containers/PM2 processes).
  3. Verify .env and backend/.env: existing files are left untouched, only a missing one is bootstrapped from its .example. .env, backend/.env, dashboard/.env and docker-compose.server.yml are then backed up under .iqv-install/backups/<timestamp>/ (contents are never logged).
  4. Dirty-tree check — TRACKED source only (git status --porcelain --untracked-files=no). A real source modification SAFELY ABORTS the update and the dirty files are listed. Untracked/gitignored server configuration (.env, backend/.env, docker-compose.server.yml) does not block it. None of the scripts ever run git reset --hard / git clean -fd / git checkout . / git merge / git rebase.
  5. git fetch origin <branch> + target-branch verification + git pull --ff-only (fails safely on divergence, never overwrites anything). With --branch <name>, if a different branch is checked out, a safe git checkout is performed because the working tree is already known to be clean (no merge/rebase).
  6. "Current version"/"Target version" and the old→new commit SHA are logged from the VERSION file. If new .env.example keys arrived, only the missing key NAMES are warned about; existing values are never read or changed.
  7. git diff --name-only <old-sha> <new-sha> inspects what changed and acts accordingly:
  8. backend/package.json/package-lock.json changed → npm ci
  9. backend/src|scripts changed → backend is rebuilt
  10. backend/Dockerfile* / docker-compose*.yml changed → the Docker image is rebuilt
  11. dashboard/package.json/pnpm-lock.yaml changed → pnpm install --frozen-lockfile
  12. dashboard/src|vite.config.ts|... changed → dashboard is rebuilt
  13. migration-like files (backend/scripts/*migrat*|*rename*) changed → NOT run automatically (data safety) — only a [WARN] reminder to review them manually.
  14. Docker: the resolved compose stack (with docker-compose.server.yml added automatically when present) is validated with config, then only the changed service(s) are rebuilt and up -d is run. Native: pm2 startOrReload/pm2 restart + pm2 save.
  15. Health check — http://127.0.0.1:${IQV_BACKEND_PORT}/health and http://127.0.0.1:${IQV_FRONTEND_PORT}/; if either fails, the script exits with an error code. A failed update never tears down the running containers (a build failure stops BEFORE up -d, so the current version keeps serving).
  16. .iqv-install/state.json is updated (updatedAt, version).

Validation tests

These guarantees are covered by automated tests (no Docker, MongoDB or network required; they run in the CI scripts-lint job):

bash scripts/linux/tests/deployment-config.test.sh

Uninstall / Purge / Purge-Data

Command What it does
uninstall.ps1 / uninstall.sh Stops/removes containers or PM2 processes. Source code, node_modules, dist, .env files are left UNTOUCHED.
-Purge / --purge In addition: removes node_modules, dist, generated .env files, production Docker images, the .iqv-install/ state directory.
-Purge -RemoveSource / --purge --remove-source In addition: deletes the entire repository. Since the script cannot synchronously delete the directory it's running from, it schedules a separate cleanup script (in $TEMP//tmp) that deletes the folder a few seconds later. Requires an extra confirmation (typing yes or passing -Yes/--yes).
-PurgeData / --purge-data Since MongoDB was never managed by this install (it's external), this deletes no data — it only logs that fact explicitly.

The default (flagless) uninstall never deletes the production database — it never creates a DB container/volume in the first place.

Version mechanism

Single source of truth: the repo root VERSION file (plain text, e.g. 1.1.0). backend/package.json (1.0.0) and dashboard/package.json (1.1.0) are each sub-project's own independent module version and are UNCHANGED — install/update scripts only read VERSION for "Current version"/"Target version"; no second version file was invented.

Install state file

.iqv-install/state.json (not tracked by Git — see .gitignore):

{
  "mode": "docker",
  "version": "1.1.0",
  "installPath": "/path/to/Dictionary",
  "installedAt": "2026-08-31T06:00:00Z",
  "updatedAt": "2026-08-31T06:00:00Z",
  "services": { "backend": "iqv-dictionary-backend-prod", "frontend": "iqv-dictionary-frontend-prod" },
  "ports": { "backend": 3001, "frontend": 8080 }
}

No secret/token/password is ever written to this file.