Deploy via Remote Jumphost
Installing the on-premise stack onto a server reachable only through a remote jumphost.
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.mdcovers 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
- Open a browser and go to your VPN portal (e.g.
vnet.example.org). - Log in with your VPN credentials.
- Download the matching installer (Windows 64-bit / 32-bit / Mac).
- Run the installer (
GlobalProtect64.msion Windows) — Next → Next → Finish.
2. Connect to the VPN
- Launch GlobalProtect from the Start Menu / system tray.
- Click Get Started.
- Portal:
vnet.example.org→ Connect. - Enter VPN username + password → Connect.
- 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 → package → transfer → install. 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 --versionand compose plugin statusdf -hon/,/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:
- Re-run
probefor the record. - Ask for confirmation.
- Build the air-gap tarball locally (~5–15 min, mostly Docker build time).
- Transfer ~3–6 GB to
/tmp/on the target viarsync --partial. - SSH in, extract into
/opt/medos,docker load, runsetup-onpremise.sh, thendocker 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.
Related Files
| 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:
- Set
CONSENSUS_BOOTNODESto the cloud validators’enode://…@<host>:30303URLs (these boxes runinfrastructure/consensus/docker-compose-cloud-validator.yml). - Leave 30303 unpublished/closed inbound — outbound TCP to the cloud endpoints is the only requirement.
- 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.