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; + } +}