From 53addadd3d53701b4c39974b0a345f4230ec1906 Mon Sep 17 00:00:00 2001 From: Weston Clark Date: Thu, 1 Oct 2026 16:56:28 -0400 Subject: [PATCH 1/2] feat: build the web client on the CI runner instead of on EC2 The EC2 instance (908 MB RAM) runs out of heap during `vite build`. The deploy action now builds on the GitHub runner and copies dist/ over, so the instance only unpacks it. A failed build stops the deploy before EC2 is touched. The optional WEB_API_BASE_URL repository variable sets the API URL baked into the bundle; when unset, web-client/.env.production applies as before. Co-Authored-By: Claude Opus 5.5 --- .github/actions/deploy-to-ec2/action.yml | 45 ++++++++++++++++++++++-- .github/workflows/deploy.yml | 1 + 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/.github/actions/deploy-to-ec2/action.yml b/.github/actions/deploy-to-ec2/action.yml index 5322ace..02fffad 100644 --- a/.github/actions/deploy-to-ec2/action.yml +++ b/.github/actions/deploy-to-ec2/action.yml @@ -1,5 +1,5 @@ name: 'Deploy to EC2' -description: 'Deploy application to EC2 using SSH with venv support' +description: 'Build the web client on the runner, then deploy it and cloud-services to EC2 over SSH' inputs: ec2_pem_key: @@ -19,10 +19,42 @@ inputs: description: 'Git branch to deploy' required: false default: 'main' + web_api_base_url: + description: 'API URL baked into the web-client build; empty keeps the value in web-client/.env.production' + required: false + default: '' runs: using: 'composite' steps: + # The EC2 instance has too little memory for `vite build`, so the runner builds the bundle. + - name: Check out web-client + uses: actions/checkout@v5 + with: + repository: hyperloop-cornell/web-client + ref: ${{ inputs.branch }} + path: .deploy/web-client + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: '.deploy/web-client/package-lock.json' + + - name: Build web-client + shell: bash + working-directory: .deploy/web-client + env: + WEB_API_BASE_URL: ${{ inputs.web_api_base_url }} + run: | + if [ -n "$WEB_API_BASE_URL" ]; then + export VITE_API_BASE_URL="$WEB_API_BASE_URL" + fi + npm ci + npm run build + tar -czf "$RUNNER_TEMP/web-client-dist.tgz" -C dist . + - name: Configure SSH shell: bash run: | @@ -37,6 +69,10 @@ runs: StrictHostKeyChecking no EOF + - name: Upload web-client build + shell: bash + run: scp "$RUNNER_TEMP/web-client-dist.tgz" ec2-deploy:/tmp/web-client-dist.tgz + - name: Deploy to EC2 shell: bash run: | @@ -55,9 +91,12 @@ runs: git checkout "$BRANCH" git pull origin "$BRANCH" - # Build web client + # `vite preview` serves dist/ from this checkout, so it still needs node_modules npm ci - npm run build + rm -rf dist + mkdir dist + tar -xzf /tmp/web-client-dist.tgz -C dist + rm -f /tmp/web-client-dist.tgz # Restart frontend service echo "Restarting frontend service..." diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 98e6ff2..ac2972c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,3 +41,4 @@ jobs: ec2_user: ${{ secrets.EC2_USER }} repo_base_dir: ${{ inputs.repo_base_dir }} branch: ${{ inputs.branch }} + web_api_base_url: ${{ vars.WEB_API_BASE_URL }} From 783b1c0c93987b01082fc2f8793b9a6e1309da09 Mon Sep 17 00:00:00 2001 From: Weston Clark Date: Thu, 1 Oct 2026 16:58:50 -0400 Subject: [PATCH 2/2] feat: serve the web client with nginx and proxy the API on the same origin `vite preview` is not meant for production and kept a ~90 MB Node process plus a full web-client checkout and node_modules on the instance. nginx now serves the CI-built files on port 4173 and proxies /auth, /api, /ws, /hub and /health to cloud-services, so the browser makes same-origin requests: no CORS, and port 8080 no longer has to be open to the internet. The bundle is now built with an empty API URL (the page origin) unless the WEB_API_BASE_URL repository variable is set. The first deploy installs nginx if it is missing and disables the old `frontend` service. Co-Authored-By: Claude Opus 5.5 --- .github/actions/deploy-to-ec2/action.yml | 59 ++++++++++++++---------- .github/workflows/deploy.yml | 2 +- deploy/nginx/hyperloop-gui.conf | 50 ++++++++++++++++++++ 3 files changed, 85 insertions(+), 26 deletions(-) create mode 100644 deploy/nginx/hyperloop-gui.conf diff --git a/.github/actions/deploy-to-ec2/action.yml b/.github/actions/deploy-to-ec2/action.yml index 02fffad..b73b4a8 100644 --- a/.github/actions/deploy-to-ec2/action.yml +++ b/.github/actions/deploy-to-ec2/action.yml @@ -12,7 +12,7 @@ inputs: description: 'EC2 SSH user' required: true repo_base_dir: - description: 'Base directory on EC2 where cloud-services and web-client are located' + description: 'Base directory on EC2 where cloud-services is located' required: false default: '/home/ubuntu' branch: @@ -20,7 +20,7 @@ inputs: required: false default: 'main' web_api_base_url: - description: 'API URL baked into the web-client build; empty keeps the value in web-client/.env.production' + description: 'API URL baked into the web-client build; empty means the page origin, which nginx proxies to cloud-services' required: false default: '' @@ -48,9 +48,8 @@ runs: env: WEB_API_BASE_URL: ${{ inputs.web_api_base_url }} run: | - if [ -n "$WEB_API_BASE_URL" ]; then - export VITE_API_BASE_URL="$WEB_API_BASE_URL" - fi + # Set even when empty so it overrides web-client/.env.production + export VITE_API_BASE_URL="$WEB_API_BASE_URL" npm ci npm run build tar -czf "$RUNNER_TEMP/web-client-dist.tgz" -C dist . @@ -69,9 +68,9 @@ runs: StrictHostKeyChecking no EOF - - name: Upload web-client build + - name: Upload web-client build and nginx config shell: bash - run: scp "$RUNNER_TEMP/web-client-dist.tgz" ec2-deploy:/tmp/web-client-dist.tgz + run: scp "$RUNNER_TEMP/web-client-dist.tgz" "$GITHUB_WORKSPACE/deploy/nginx/hyperloop-gui.conf" ec2-deploy:/tmp/ - name: Deploy to EC2 shell: bash @@ -82,26 +81,36 @@ runs: BASE_DIR="${{ inputs.repo_base_dir }}" BRANCH="${{ inputs.branch }}" - # Deploy Web Client + # Deploy Web Client: nginx serves the build and proxies the API (deploy/nginx/hyperloop-gui.conf) echo "Deploying web-client..." - cd "$BASE_DIR/web-client" - - # Update repository - git fetch origin - git checkout "$BRANCH" - git pull origin "$BRANCH" - - # `vite preview` serves dist/ from this checkout, so it still needs node_modules - npm ci - rm -rf dist - mkdir dist - tar -xzf /tmp/web-client-dist.tgz -C dist + if ! command -v nginx >/dev/null; then + sudo apt-get update -q + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -q nginx + # The stock site listens on :80, which this deployment does not use + sudo rm -f /etc/nginx/sites-enabled/default + fi + + WEB_ROOT=/var/www/hyperloop-gui + sudo rm -rf "$WEB_ROOT.new" "$WEB_ROOT.old" + sudo mkdir -p "$WEB_ROOT.new" + sudo tar -xzf /tmp/web-client-dist.tgz --no-same-owner -C "$WEB_ROOT.new" + if [ -d "$WEB_ROOT" ]; then sudo mv "$WEB_ROOT" "$WEB_ROOT.old"; fi + sudo mv "$WEB_ROOT.new" "$WEB_ROOT" + sudo rm -rf "$WEB_ROOT.old" rm -f /tmp/web-client-dist.tgz - - # Restart frontend service - echo "Restarting frontend service..." - sudo systemctl restart frontend - + + sudo install -m 644 /tmp/hyperloop-gui.conf /etc/nginx/sites-available/hyperloop-gui + sudo ln -sfn /etc/nginx/sites-available/hyperloop-gui /etc/nginx/sites-enabled/hyperloop-gui + rm -f /tmp/hyperloop-gui.conf + sudo nginx -t + + # nginx takes over port 4173 from the old `vite preview` service + if systemctl cat frontend >/dev/null 2>&1; then + sudo systemctl disable --now frontend + fi + sudo systemctl enable nginx + sudo systemctl reload-or-restart nginx + # Deploy Cloud Services API with venv echo "Deploying cloud-services..." cd "$BASE_DIR/cloud-services" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ac2972c..963dada 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -9,7 +9,7 @@ on: type: string default: 'main' repo_base_dir: - description: 'Base directory on EC2 where cloud-services and web-client are located' + description: 'Base directory on EC2 where cloud-services is located' required: false type: string default: '/home/ubuntu' diff --git a/deploy/nginx/hyperloop-gui.conf b/deploy/nginx/hyperloop-gui.conf new file mode 100644 index 0000000..49aecce --- /dev/null +++ b/deploy/nginx/hyperloop-gui.conf @@ -0,0 +1,50 @@ +# Serves the web-client build and proxies cloud-services on the same origin, so the +# browser needs no CORS and port 8080 does not have to be reachable from the internet. +# The deploy action installs this as /etc/nginx/sites-available/hyperloop-gui. + +map $http_upgrade $hyperloop_connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 4173; + server_name _; + + root /var/www/hyperloop-gui; + index index.html; + + gzip on; + gzip_types text/css application/javascript application/json image/svg+xml; + + # Flash requests carry the firmware base64-encoded in the JSON body + client_max_body_size 25m; + + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $hyperloop_connection_upgrade; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # Hub and browser websockets can sit idle between messages + proxy_read_timeout 1h; + proxy_send_timeout 1h; + + location /auth/ { proxy_pass http://127.0.0.1:8080; } + location /api/ { proxy_pass http://127.0.0.1:8080; } + location /ws/ { proxy_pass http://127.0.0.1:8080; } + location = /hub { proxy_pass http://127.0.0.1:8080; } + location = /health { proxy_pass http://127.0.0.1:8080; } + + # File names under /assets/ change with their content on every build + location /assets/ { + add_header Cache-Control "public, max-age=31536000, immutable"; + try_files $uri =404; + } + + # Client-side routes (/devices, /telemetry, ...) load the app shell + location / { + add_header Cache-Control "no-cache"; + try_files $uri $uri/ /index.html; + } +}