medOS ultra

Deploy via Remote Jumphost

Installing the on-premise stack onto a server reachable only through a remote jumphost.

8 min read diagramsUpdated 2026-06-11docs/DEPLOY_VIA_JUMPHOST.md

How to install the medOS on-premise stack onto a target server that is only reachable via VPN + jumphost (corporate intranet pattern). Wraps the existing air-gap install flow so the whole pipeline runs from your laptop.

Companion docs: DEPLOY_ONPREMISE_AIRGAPPED.md covers the underlying on-premise install. This doc only adds the VPN + jumphost layer.


Network Topology

 your laptop
     │ (GlobalProtect VPN, on outside network)
     ▼
 VPN gateway (vnet.example.org)
     │
     ▼
 jumphost                e.g. 172.26.x.x   (corporate bastion, SSH only)
     │ (ssh -J)
     ▼
 target server (NewHis)  e.g. 172.16.x.x   (intranet, runs the stack)

You cannot reach the target directly from your laptop. SSH ProxyJump tunnels through the jumphost in one command, so scp / rsync / ssh all behave as if you were connected directly.

Important: The hospital’s internal Wi-Fi cannot connect to the VPN. Run the install from an outside network — home Wi-Fi or a mobile hotspot.


One-Time Setup

1. Install GlobalProtect VPN

  1. Open a browser and go to your VPN portal (e.g. vnet.example.org).
  2. Log in with your VPN credentials.
  3. Download the matching installer (Windows 64-bit / 32-bit / Mac).
  4. Run the installer (GlobalProtect64.msi on Windows) — Next → Next → Finish.

2. Connect to the VPN

  1. Launch GlobalProtect from the Start Menu / system tray.
  2. Click Get Started.
  3. Portal: vnet.example.orgConnect.
  4. Enter VPN username + password → Connect.
  5. Wait for status Connected (GP-Ext-GW).

3. Install SSH Keys on Jumphost and Target

The install script does not take passwords. Set up key-based auth once:

# Copy your public key to the jumphost
ssh-copy-id <jumphost_user>@<jumphost_host>

# Copy your public key to the target, tunneling through the jumphost
ssh -J <jumphost_user>@<jumphost_host> <target_user>@<target_host> \
  "mkdir -p ~/.ssh"
ssh-copy-id -o ProxyJump=<jumphost_user>@<jumphost_host> \
  <target_user>@<target_host>

Test it:

ssh -J <jumphost_user>@<jumphost_host> <target_user>@<target_host> 'hostname; uname -a'

If that prints the target’s hostname without prompting for a password, you’re done.

4. Optional: SSH Config Shortcut

Add to ~/.ssh/config to drop the long -J flag:

Host newhis-jump
  HostName <jumphost_host>
  User <jumphost_user>

Host newhis
  HostName <target_host>
  User <target_user>
  ProxyJump newhis-jump

Then ssh newhis / scp file newhis:/tmp/ / rsync ... newhis:... all work.


The Install Script

infrastructure/scripts/install-via-jumphost.sh wraps the four phases of an on-prem install (probe, package, transfer, install) and runs each one through SSH ProxyJump.

Subcommands

Action What it does Destructive?
probe SSHs into the target through the jumphost. Reports OS, Docker availability, free disk, sudo state. No
package Runs package-airgap.sh locally to build a medos-airgap-<region>-<date>.tar.gz (3–6 GB) containing Docker images + compose file + market-pack seeds. No
transfer rsync --partial the tarball to /tmp/ on the target. Resumable. No
install Extracts the tarball into /opt/medos, docker loads the images, runs setup-onpremise.sh, then docker compose up -d. Asks for confirmation first. Yes — creates files on target and starts containers
all probe → confirm → packagetransferinstall. With --yes, skips all confirmations. Yes

Flags

Flag Default Purpose
--region <name> thailand japan, philippines, or thailand. Picks which market-pack seeds get bundled.
--jumphost <user@host> (set in script for this hospital) The bastion you tunnel through.
--target <user@host> (set in script for this hospital) The server you’re installing onto.
--hostname <name> medos.local Used in the self-signed SSL cert’s SAN. If users will browse by IP, set this to the IP.
--remote-dir <path> /opt/medos Where the install lives on the target.
--package-file <path> auto-detected from infrastructure/ Reuse an existing tarball instead of rebuilding.
--yes / -y off Skip confirmation prompts.

Typical Workflow

Step 1 — Probe the target (always do this first)

cd infrastructure
./scripts/install-via-jumphost.sh probe

You’ll get:

  • uname -a (Linux distro / kernel — or Windows version)
  • Contents of /etc/os-release
  • docker --version and compose plugin status
  • df -h on /, /tmp, /opt
  • Whether sudo is passwordless
  • Current user + shell

Decide based on the output:

Probe result Action
Linux + Docker + ≥10 GB free on /opt Proceed to all.
Linux, no Docker Install Docker on the target first (curl -fsSL https://get.docker.com | sh if internet is available, else use a vendored installer). Then proceed.
Windows Server Stop. This stack is Linux-only. Options: install WSL2 + Docker Desktop, run a Linux VM on the host, or pick a different target.
/opt has < 10 GB free Free space, change --remote-dir, or attach more storage before proceeding.

Step 2 — Run the full pipeline

./scripts/install-via-jumphost.sh all \
  --region thailand \
  --hostname medos.local

The script will:

  1. Re-run probe for the record.
  2. Ask for confirmation.
  3. Build the air-gap tarball locally (~5–15 min, mostly Docker build time).
  4. Transfer ~3–6 GB to /tmp/ on the target via rsync --partial.
  5. SSH in, extract into /opt/medos, docker load, run setup-onpremise.sh, then docker compose up -d.

Use --yes to skip all prompts (for unattended runs).

Step 3 — Verify

Wait ~60 seconds for containers to settle, then:

ssh -J <jumphost> <target> 'cd /opt/medos && docker compose -f docker-compose-onpremise.yml ps'

All services should be Up (healthy). From a workstation on the same intranet (or via VPN), browse to https://<hostname> (accept the self-signed cert warning) and log in.

Step 4 — Seed market data

ssh -J <jumphost> <target> 'cd /opt/medos && ./deploy.sh thailand seed'

Seeds are idempotent (ON CONFLICT DO NOTHING), so safe to re-run.


Running Individual Steps

The pipeline is broken into steps so you can re-run a single phase without redoing everything:

# Just rebuild the package (e.g. after a code change)
./scripts/install-via-jumphost.sh package --region thailand

# Just push the latest tarball
./scripts/install-via-jumphost.sh transfer

# Just run the remote install (assumes tarball is already on /tmp/ via transfer)
./scripts/install-via-jumphost.sh install --region thailand --yes

Switching Region on an Existing Install

# On the target (through jumphost):
ssh -J <jumphost> <target>
cd /opt/medos
docker compose -f docker-compose-onpremise.yml down

# From your laptop:
./scripts/install-via-jumphost.sh package  --region philippines
./scripts/install-via-jumphost.sh transfer
./scripts/install-via-jumphost.sh install  --region philippines --yes

Data volumes are preserved across region switches; new seeds are additive.


Troubleshooting

Symptom Cause / Fix
ssh: connect to host ... port 22: Connection timed out (to the jumphost) VPN not connected, or you’re on the hospital Wi-Fi. Reconnect GlobalProtect from an outside network.
Permission denied (publickey,password) to the jumphost Re-run ssh-copy-id <jumphost_user>@<jumphost_host>.
Permission denied to the target (after the jumphost hop works) Re-run the second ssh-copy-id from the setup section.
rsync repeatedly stalls The VPN link is unstable. --partial means re-running transfer resumes from where it stopped.
docker: command not found during install The probe missed it, or Docker was uninstalled. SSH in, install Docker, re-run install.
Browser shows “name mismatch” warning, not just “self-signed” The cert SAN doesn’t match the URL. Re-run with --hostname <the-name-you-browse-to> and re-run install. The cert gets regenerated by setup-onpremise.sh.
Stack starts but services keep restarting Likely env or compose issue, not jumphost-related. Check docker compose logs <service> over SSH.

Security Notes

  • Never commit credentials. This doc uses placeholders. Real VPN / jumphost / target passwords belong in your password manager.
  • Rotate exposed credentials. If a password was ever pasted into a chat transcript, email, or ticket, rotate it.
  • Prefer keys over passwords. The install script intentionally does not accept passwords — keys (with passphrase + agent) are safer and don’t end up in shell history.
  • Self-signed certs are fine for intranet but warn users they’ll see a browser warning on first visit. For a trusted cert, run an internal CA or use Let’s Encrypt DNS-01 against a real domain.

File Purpose
infrastructure/scripts/install-via-jumphost.sh This workflow’s entry point.
infrastructure/scripts/package-airgap.sh Builds the transferable tarball.
infrastructure/scripts/setup-onpremise.sh First-time setup on the target.
infrastructure/scripts/generate-ssl-cert.sh Self-signed cert generation.
infrastructure/docker-compose-onpremise.yml All-in-one stack definition.
infrastructure/deploy.sh Region-switching + seed runner.
docs/DEPLOY_ONPREMISE_AIRGAPPED.md Underlying on-prem install reference.

Consensus Layer via Outbound-Only Relay (Tier C)

When a hospital’s NAT is too strict even for the WireGuard mesh (tier A), the optional consensus node (consensus0, --profile consensus) can still join the consortium chain without any inbound firewall rule: devp2p connections are bidirectional once established, so an on-prem validator that dials out to the always-reachable cloud validators (Singapore / Seoul / Bangkok) fully participates in consensus.

Setup on the on-prem box:

  1. Set CONSENSUS_BOOTNODES to the cloud validators’ enode://…@<host>:30303 URLs (these boxes run infrastructure/consensus/docker-compose-cloud-validator.yml).
  2. Leave 30303 unpublished/closed inbound — outbound TCP to the cloud endpoints is the only requirement.
  3. Start with CONSENSUS_ENABLED=true docker compose ... --profile consensus up -d.

Full connectivity ladder (tiers A–D) and the hospital “ask sheet”: infrastructure/consensus/README.md.

Ask Anything