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 existingdocker-compose.yml(hot-reload dev environment, bind-mounted sourcenpm run dev/pnpm run dev) is UNCHANGED and still works viadocker compose up -d.docker-compose.prod.ymlis 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, nevernpm run dev) anddashboard/Dockerfile.prod(multi-stage:pnpm install+pnpm run build→ static files served bynginx,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) viahost.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.exampleif missing):IQV_BACKEND_PORT(default3001),IQV_FRONTEND_PORT(default8080),IQV_BIND_HOST(default0.0.0.0),VITE_API_BASE_URL. docker-compose.prod.ymlis generic: it contains no credentials, no customer/server-specific network, IP, port or hostname, and it does not overrideMONGODB_URIfrombackend/.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:
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(compiledbackend/dist/server.js). - Dashboard:
corepackenablespnpm@9.15.9, thenpnpm install --frozen-lockfile+pnpm run build(staticdashboard/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 servesdashboard/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 passwordlesssudois 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.envUNTOUCHED ([OK] ... already exists). - In Docker mode,
docker compose up -donly recreates containers when actually needed. - In native mode,
pm2 startOrReloadupdates existing processes idempotently (never spawns duplicate processes).
Update flow¶
A single command in production:
- Verify the repository root (
docker-compose.prod.yml,backend/,dashboard/). - Detect install mode — from
.iqv-install/state.json(or, if absent, best-effort detection from running containers/PM2 processes). - Verify
.envandbackend/.env: existing files are left untouched, only a missing one is bootstrapped from its.example..env,backend/.env,dashboard/.envanddocker-compose.server.ymlare then backed up under.iqv-install/backups/<timestamp>/(contents are never logged). - 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 rungit reset --hard/git clean -fd/git checkout ./git merge/git rebase. 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 safegit checkoutis performed because the working tree is already known to be clean (no merge/rebase).- "Current version"/"Target version" and the old→new commit SHA are
logged from the
VERSIONfile. If new.env.examplekeys arrived, only the missing key NAMES are warned about; existing values are never read or changed. git diff --name-only <old-sha> <new-sha>inspects what changed and acts accordingly:backend/package.json/package-lock.jsonchanged →npm cibackend/src|scriptschanged → backend is rebuiltbackend/Dockerfile*/docker-compose*.ymlchanged → the Docker image is rebuiltdashboard/package.json/pnpm-lock.yamlchanged →pnpm install --frozen-lockfiledashboard/src|vite.config.ts|...changed → dashboard is rebuilt- migration-like files (
backend/scripts/*migrat*|*rename*) changed → NOT run automatically (data safety) — only a[WARN]reminder to review them manually. - Docker: the resolved compose stack (with
docker-compose.server.ymladded automatically when present) is validated withconfig, then only the changed service(s) are rebuilt andup -dis run. Native:pm2 startOrReload/pm2 restart+pm2 save. - Health check —
http://127.0.0.1:${IQV_BACKEND_PORT}/healthandhttp://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 BEFOREup -d, so the current version keeps serving). .iqv-install/state.jsonis 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):
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.