# KodeMed on Ubuntu with Docker — Install Runbook Install the KodeMed Deployment-Wizard package (the ZIP the portal generates) on **Ubuntu Server** (22.04 / 24.04 LTS) with **Docker Engine installed from docker.com's official repository**. Written to be copy-paste simple: run the six steps as **root** (or with `sudo`) and you are done — the wizard ZIP is ready to run. > **Validated end-to-end with Docker Compose v2** (the `docker compose` plugin): full > stack up, classification data imported, browser login working. **ZIPs generated from > Aug 2026 onward** are ready to run as-is. Ubuntu uses **AppArmor**, not SELinux, so > the SELinux relabel steps from the Podman runbooks do not apply here (any `:z`/`:Z` > flags in the compose are harmless no-ops under Docker on Ubuntu). > **Install Docker from docker.com, not Ubuntu's `docker.io`.** The distro package is > older and ships a different compose. This runbook uses Docker's **official apt > repository** so you get current **Docker Engine + the `docker compose` v2 plugin**. > **Run everything as `root`** (or `sudo`): ports 443/8443/… are < 1024, and the user > that runs `docker login` / `oras login` must be the **same user that runs the pull > and the compose**. Log in as root, pull as root. (Mixing "login as your user, pull > as root" is the #1 cause of a silent `unauthorized`.) --- ## Before you start - An **Ubuntu Server 22.04 or 24.04 LTS** host with **root**/`sudo` access. - **At least 8 GB RAM** (the stack is ~11 mostly-JVM containers). 4 GB is not enough. - **At least ~30 GB free disk.** Docker storage (`/var/lib/docker`) holds the images + DB/Lucene-index volumes (~20 GB), and `/opt/kodemed` holds the deployment files + `import-data` (~10 GB). On hosts with small partitioned volumes, point **both** the Docker data-root (`/etc/docker/daemon.json` → `"data-root"`) and `/opt/kodemed` at a larger disk before deploying, or the pull/import fails with `no space left on device`. - **Harbor credentials** from KodeMed AG: a robot account (`robot$…`) + its secret. - The **Wizard ZIP** for your host. For a single server reached by IP with no DNS, pick the **IP + self-signed (ip-caddy)** preset in the portal. Copy the ZIP to the server, e.g. `scp deployment.zip root@:/opt/`. --- ## 1. Install Docker (from docker.com) + tools (once per host) Add Docker's **official apt repository** and install Docker Engine + the Compose v2 plugin from it: ```bash # 1a. Remove any old distro Docker packages (safe if none are installed) apt-get remove -y docker docker-engine docker.io containerd runc 2>/dev/null || true # 1b. Add Docker's official GPG key + repository apt-get update apt-get install -y ca-certificates curl gnupg unzip openssl install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ > /etc/apt/sources.list.d/docker.list # 1c. Install Engine + CLI + Compose v2 plugin apt-get update apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # ORAS CLI (pulls the classification-data + grouper artifacts in step 4) curl -fsSL "https://github.com/oras-project/oras/releases/download/v1.2.0/oras_1.2.0_linux_amd64.tar.gz" \ | tar -xz -C /usr/local/bin oras # Symlink into /usr/bin so `oras` is on PATH even on hardened hosts whose PATH omits /usr/local/bin ln -sf /usr/local/bin/oras /usr/bin/oras docker --version && docker compose version && oras version ``` > **Prefer the one-liner?** Docker's convenience script does 1a–1c in one go: > `curl -fsSL https://get.docker.com | sh` (also from docker.com). The explicit apt > repo above is the production-recommended path. > > **Air-gapped / proxy?** The apt repo and `oras` download need network access. On an > isolated host, mirror Docker's apt repo internally (or install the `.deb`s by hand); > everything else in this runbook is offline once the images are pulled. ## 2. Open the ports (only if the host runs a local firewall) ```bash # Cloud VMs usually gate at the provider's security group — open the ports THERE. # If ufw is active locally, open them here (ip-caddy preset ports): ufw status | grep -q "Status: active" && { for p in 443 8443 8444 8445 9443; do ufw allow ${p}/tcp; done } ``` ## 3. Unpack + log in to Harbor (as root) ```bash mkdir -p /opt/kodemed && cd /opt/kodemed unzip -o /opt/deployment.zip chmod -R a+rX . # make the mounted files readable (safe on any umask) # Robot usernames contain '$' → SINGLE quotes. Log in as root (same user that pulls). docker login harbor.mieresit.com -u 'robot$YOUR_ROBOT' oras login harbor.mieresit.com -u 'robot$YOUR_ROBOT' # oras has its own login ``` ## 4. Download the classification data + grouper specs ```bash cd /opt/kodemed # Classification data → import-data/2026/ oras pull harbor.mieresit.com/kodemed/data:2026 -o import-data/ cd import-data && mkdir -p 2026 unzip -o *.zip -d 2026/ 2>/dev/null || tar -xzf *.tar.gz -C 2026/ rm -f *.zip *.tar.gz && cd .. # Grouper specs (.sgs) + catalogues (.csv) oras pull harbor.mieresit.com/kodemed/grouper:2026 -o ./ unzip -o kodemed-grouper-*.zip 2>/dev/null || tar -xzf kodemed-grouper-*.tar.gz rm -f kodemed-grouper-*.zip kodemed-grouper-*.tar.gz # The DataServer imports as UID 1001 and moves files during the scan → give it ownership: chown -R 1001:1001 import-data/ ``` ## 5. Start the stack ```bash cd /opt/kodemed docker compose up -d docker compose logs -f kodemed-dataserver # watch the first-time import (~3-5 min) ``` ## 6. Verify ```bash docker ps --format '{{.Names}}\t{{.Status}}' # all healthy curl -ks https://:8443/actuator/health # Server → UP curl -ks https://:8444/actuator/health # DataServer → UP curl -ks https://:8445/api/v1/grouper/versions # loaded:true ``` Open the CodingUI at `https:///` (accept the self-signed warning once per port), log in with a bundled demo user (`demo-admin` / `KodeMed2026!`). Ports: **443** UI · **8443** Server (+WebSocket) · **8444** DataServer · **8445** Grouper · **9443** Keycloak. --- ## Troubleshooting **`oras pull` says `unauthorized to access repository … action: pull`, yet the robot has pull rights.** Almost always: you ran `oras login` as one user but the pull runs as another (e.g. logged in as your account, then `sudo` to root). `oras` reads the credential from the *current* user's store, so it goes anonymous. Fix: run **both** `oras login` and `oras pull` as the **same user** (root). Copy the robot name exactly from Harbor. **CodingUI shows a blank/`runtime-config.js` MIME error, or config falls back to localhost.** The UI container can't read the mounted `runtime-config.js` — file permissions. Re-run `chmod -R a+rX /opt/kodemed` then `docker compose up -d --force-recreate kodemed-ui`. **CodingClient / DLL: `Unexpected character <` on `/api/v1/config`, or WebSocket `200 vs 101`.** The `serverUrl` points at the UI (`:443`) instead of the API. Use the **API port**: `https://:8443` (Server + WebSocket), `:8444`/`:8445` for DataServer/Grouper, `:9443` for Keycloak. **Login shows `invalid_grant` / "CORS Missing Allow Origin".** Current ZIPs bake the correct Keycloak CORS origins into the realm, so a **fresh** deploy is fine. If it happens on a **reused** deploy, the Keycloak DB volume is stale — recreate it: `docker compose down && docker volume rm _kodemed-keycloak-db && docker compose up -d`. **A SearchServer stays `starting`/`unhealthy`.** Usually not enough RAM (see the 8 GB prerequisite); they also stay `starting` until the import finishes and they build their Lucene index. **`docker compose` says "command not found" / `docker-compose: not found`.** You have the old distro Docker or only the v1 binary. Reinstall from docker.com's repo (step 1) — the `docker-compose-plugin` package provides the `docker compose` v2 subcommand.