From d5dc96a6481be7bcd5a0b4cf738b5a384b661cf3 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:00:19 +0700 Subject: [PATCH 1/3] feat: deploy beta on mainnet GCP VM --- .github/workflows/deploy-config.yml | 75 +++++-- docker-compose/Caddyfile-beta | 64 ++---- docker-compose/Caddyfile-gcp-edge | 5 + docker-compose/docker-compose-beta.yml | 193 +++++++++--------- .../envs/common-blockscout-beta.env | 23 ++- docker-compose/envs/common-frontend-beta.env | 13 +- docs/BETA-GCP-DEPLOYMENT.md | 30 +++ scripts/apply-beta-on-mainnet-vm.sh | 71 +++++++ 8 files changed, 297 insertions(+), 177 deletions(-) create mode 100644 docs/BETA-GCP-DEPLOYMENT.md create mode 100755 scripts/apply-beta-on-mainnet-vm.sh diff --git a/.github/workflows/deploy-config.yml b/.github/workflows/deploy-config.yml index e41172ffd74e..8bf0e61d1881 100644 --- a/.github/workflows/deploy-config.yml +++ b/.github/workflows/deploy-config.yml @@ -1021,26 +1021,73 @@ jobs: github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' && inputs.environment == 'beta' - runs-on: [self-hosted, beta] + runs-on: ubuntu-latest environment: beta permissions: contents: read - env: - BETA_BLOCKSCOUT_DATABASE_URL: ${{ secrets.BETA_BLOCKSCOUT_DATABASE_URL }} - BETA_STATS_DATABASE_URL: ${{ secrets.BETA_STATS_DATABASE_URL }} - BETA_USER_OPS_DATABASE_URL: ${{ secrets.BETA_USER_OPS_DATABASE_URL }} + id-token: write steps: - - name: Pull latest and restart services + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Validate beta configuration run: | set -euo pipefail - REPO="$HOME/Projects/DOScan" - DEPLOY="$REPO/docker-compose" + secrets_file="$(mktemp)" + trap 'rm -f "${secrets_file}"' EXIT + DOSCAN_BLOCKSCOUT_SECRETS_ENV="${secrets_file}" \ + DOSCAN_BASE_ENV_DIR="${PWD}/docker-compose/envs" \ + docker compose -f docker-compose/docker-compose-beta.yml config -q + docker run --rm \ + -v "${PWD}/docker-compose/Caddyfile-beta:/etc/caddy/Caddyfile:ro" \ + caddy@sha256:844f60b64e4724a5aa8245e019dace0d3f199f7433ce6c57676cb30a920dbad9 \ + caddy validate --config /etc/caddy/Caddyfile - cd "$REPO" - git pull --ff-only origin main + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 + with: + workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ env.GCP_SERVICE_ACCOUNT }} + + - name: Set up Google Cloud CLI + uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 + with: + project_id: ${{ env.GCP_PROJECT_ID }} - cd "$DEPLOY" - docker compose -f docker-compose-beta.yml -p doscan pull - docker compose -f docker-compose-beta.yml -p doscan up -d --remove-orphans + - name: Package and upload beta configuration + run: | + set -euo pipefail + archive="/tmp/doscan-beta-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.tgz" + tar -czf "${archive}" \ + docker-compose/docker-compose-beta.yml \ + docker-compose/Caddyfile-beta \ + docker-compose/envs/common-blockscout.env \ + docker-compose/envs/common-blockscout-beta.env \ + docker-compose/envs/common-frontend-beta.env \ + scripts/apply-beta-on-mainnet-vm.sh + gcloud compute scp \ + --project="${GCP_PROJECT_ID}" \ + --zone="${GCP_ZONE}" \ + --tunnel-through-iap \ + "${archive}" \ + "${GCP_INSTANCE}:${archive}" + rm -f "${archive}" - echo "Beta deployment completed" + - name: Apply beta configuration and verify services + run: | + set -euo pipefail + gcloud compute ssh "${GCP_INSTANCE}" \ + --project="${GCP_PROJECT_ID}" \ + --zone="${GCP_ZONE}" \ + --tunnel-through-iap \ + --command=" + set -euo pipefail + deploy_id='${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}' + archive='/tmp/doscan-beta-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.tgz' + src=\"\$(mktemp -d /tmp/doscan-beta-deploy.XXXXXX)\" + trap 'rm -rf \"\${src}\" \"\${archive}\"' EXIT + tar -xzf \"\${archive}\" -C \"\${src}\" + sudo bash \"\${src}/scripts/apply-beta-on-mainnet-vm.sh\" \"\${src}\" \"\${deploy_id}\" + " + + curl --fail --silent --show-error --retry 12 --retry-delay 5 \ + https://beta.doscan.io/api/v2/stats >/dev/null diff --git a/docker-compose/Caddyfile-beta b/docker-compose/Caddyfile-beta index 071a462f95c5..3633b64c116d 100644 --- a/docker-compose/Caddyfile-beta +++ b/docker-compose/Caddyfile-beta @@ -1,75 +1,45 @@ -# DOS Chain Beta - Reverse Proxy -# HTTP only - Cloudflare Tunnel handles HTTPS -# Runs locally on WSL2, exposed via cloudflared Windows Service +# HTTP-only origin for beta.doscan.io. +# The VM edge Caddy terminates TLS and proxies to 127.0.0.1:14080. -# Stats + Viz subdomains (port 8080 — tunnel routes here) -:8080 { - header Access-Control-Allow-Origin "https://beta.doscan.io" - header Access-Control-Allow-Methods "GET, POST, OPTIONS" - header Access-Control-Allow-Headers "Content-Type" - - @options method OPTIONS - respond @options 204 - - @stats host beta-stats.doscan.io - reverse_proxy @stats stats:8050 - - @viz host beta-viz.doscan.io - reverse_proxy @viz visualizer:8050 +:80 { + encode zstd gzip - # Fallback - reverse_proxy stats:8050 -} + header >Cross-Origin-Opener-Policy "same-origin-allow-popups" -# Main site (port 80 — beta.doscan.io + beta-api.doscan.io) -:80 { - # Backend API + handle /api/v1/counters { + reverse_proxy stats:8050 + } + handle /api/v1/lines { + reverse_proxy stats:8050 + } + handle /api/v1/pages/main { + reverse_proxy stats:8050 + } handle /api/* { reverse_proxy backend:4000 } - - # WebSocket handle /socket/* { reverse_proxy backend:4000 } - - # Sitemap handle /sitemap.xml { reverse_proxy backend:4000 } - - # Auth handle /auth/* { reverse_proxy backend:4000 } - - # Metrics handle /metrics { reverse_proxy backend:4000 } - - # Stats API (path-based fallback) + handle /public-metrics { + reverse_proxy backend:4000 + } handle /stats-api/* { uri strip_prefix /stats-api - header Access-Control-Allow-Origin "https://beta.doscan.io" - header Access-Control-Allow-Methods "GET, OPTIONS" - header Access-Control-Allow-Headers "Content-Type" - @statsoptions method OPTIONS - respond @statsoptions 204 reverse_proxy stats:8050 } - - # Visualizer API (path-based fallback) handle /visualize/* { - header Access-Control-Allow-Origin "https://beta.doscan.io" - header Access-Control-Allow-Methods "GET, POST, OPTIONS" - header Access-Control-Allow-Headers "Content-Type" - @vizpathoptions method OPTIONS - respond @vizpathoptions 204 reverse_proxy visualizer:8050 } - - # Frontend (default) handle { reverse_proxy frontend:3000 } diff --git a/docker-compose/Caddyfile-gcp-edge b/docker-compose/Caddyfile-gcp-edge index 2fdb9c15b5d5..9e8f5c8574b9 100644 --- a/docker-compose/Caddyfile-gcp-edge +++ b/docker-compose/Caddyfile-gcp-edge @@ -1,3 +1,8 @@ +beta.doscan.io { + tls /etc/caddy/certs/origin.pem /etc/caddy/certs/origin-key.pem + reverse_proxy host.docker.internal:14080 +} + doscan.io, www.doscan.io { tls /etc/caddy/certs/origin.pem /etc/caddy/certs/origin-key.pem reverse_proxy doscan-l1-caddy-1:80 diff --git a/docker-compose/docker-compose-beta.yml b/docker-compose/docker-compose-beta.yml index e381259744d7..7d351b36ef2c 100644 --- a/docker-compose/docker-compose-beta.yml +++ b/docker-compose/docker-compose-beta.yml @@ -1,18 +1,13 @@ -# DOS Chain Mainnet - Block Explorer Beta (Blockscout) -# beta.doscan.io -# Runs locally on WSL2/WRX90, uses external PostgreSQL + AvalancheGo -# Cloudflare Tunnel (Windows Service) handles HTTPS +name: doscan-beta -name: doscan +# Beta runs on the mainnet GCP VM with an isolated database and Compose project. +# It shares only the local AvalancheGo archive RPC with production. services: - # ========================================================================== - # Cache - # ========================================================================== redis-db: - container_name: doscan-redis image: redis:alpine - restart: always + pull_policy: always + restart: unless-stopped command: redis-server volumes: - redis_data:/data @@ -22,171 +17,165 @@ services: timeout: 5s retries: 3 - # ========================================================================== - # Backend - Blockscout Latest - # ========================================================================== - backend: - container_name: doscan-backend + db-init: + image: postgres:15 + pull_policy: always + volumes: + - postgres_data:/var/lib/postgresql/data + entrypoint: ["sh", "-c", "chown -R 2000:2000 /var/lib/postgresql/data"] + + db: + image: postgres:15 + pull_policy: always + restart: unless-stopped depends_on: - redis-db: - condition: service_healthy + db-init: + condition: service_completed_successfully + user: 2000:2000 + shm_size: 256m + command: postgres -c 'max_connections=120' -c 'client_connection_check_interval=60000' + environment: + POSTGRES_USER: postgres + POSTGRES_HOST_AUTH_METHOD: trust + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 15s + timeout: 5s + retries: 5 + + backend: image: ghcr.io/dos/doscan:11.2.3.commit.86fd0dd5@sha256:423bab078a679d3290cc6e276774a8ed201686636933e0d066ce9859270f700d pull_policy: always - restart: always + restart: unless-stopped stop_grace_period: 5m - sysctls: - - net.ipv4.ip_local_port_range=1024 65535 + depends_on: + db: + condition: service_healthy + redis-db: + condition: service_healthy command: sh -c 'bin/blockscout eval "Elixir.Explorer.ReleaseTasks.create_and_migrate()" && bin/blockscout start' extra_hosts: - - 'host.docker.internal:host-gateway' + - host.docker.internal:host-gateway env_file: - ./envs/common-blockscout.env + - ${DOSCAN_BLOCKSCOUT_SECRETS_ENV:-/opt/doscan/envs/common-blockscout-secrets.env} - ./envs/common-blockscout-beta.env - ports: - - "127.0.0.1:14000:4000" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:4000/api/v2/stats"] interval: 30s timeout: 10s retries: 5 - start_period: 60s + start_period: 90s - # ========================================================================== - # Frontend - DOScan Latest - # ========================================================================== frontend: - container_name: doscan-frontend + image: metados/blockscout-frontend:2.9.5-dos.2@sha256:7d676950e8beb40e947b098f3fc39b9394f6ffac4ec0eccd58c25acac05eba19 + pull_policy: always + restart: unless-stopped depends_on: backend: condition: service_healthy - image: metados/blockscout-frontend:2.9.5-dos.2@sha256:7d676950e8beb40e947b098f3fc39b9394f6ffac4ec0eccd58c25acac05eba19 - pull_policy: always - restart: always - environment: - - HOSTNAME=0.0.0.0 env_file: - - ./envs/common-frontend.env + - ${DOSCAN_BASE_ENV_DIR:-/opt/doscan/envs}/common-frontend.env - ./envs/common-frontend-beta.env - ports: - - "127.0.0.1:13000:3000" + environment: + HOSTNAME: 0.0.0.0 healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000"] interval: 30s timeout: 10s - retries: 3 - start_period: 30s + retries: 5 + start_period: 60s - # ========================================================================== - # Microservices - # ========================================================================== smart-contract-verifier: - container_name: doscan-verifier image: ghcr.io/blockscout/smart-contract-verifier:latest pull_policy: always platform: linux/amd64 - restart: always + restart: unless-stopped env_file: - - ./envs/common-smart-contract-verifier.env - ports: - - "127.0.0.1:18043:8050" + - ${DOSCAN_BASE_ENV_DIR:-/opt/doscan/envs}/common-smart-contract-verifier.env visualizer: - container_name: doscan-visualizer image: ghcr.io/blockscout/visualizer:latest pull_policy: always platform: linux/amd64 - restart: always + restart: unless-stopped env_file: - - ./envs/common-visualizer.env - ports: - - "127.0.0.1:18044:8050" + - ${DOSCAN_BASE_ENV_DIR:-/opt/doscan/envs}/common-visualizer.env sig-provider: - container_name: doscan-sig-provider image: ghcr.io/blockscout/sig-provider:latest pull_policy: always platform: linux/amd64 - restart: always - ports: - - "127.0.0.1:18045:8050" + restart: unless-stopped - # ========================================================================== - # User Ops Indexer (ERC-4337 Account Abstraction) - # ========================================================================== user-ops-indexer: - container_name: doscan-user-ops - depends_on: - - backend image: ghcr.io/blockscout/user-ops-indexer:latest pull_policy: always platform: linux/amd64 - restart: always + restart: unless-stopped + depends_on: + db: + condition: service_healthy + backend: + condition: service_healthy extra_hosts: - - "host.docker.internal:host-gateway" + - host.docker.internal:host-gateway environment: - USER_OPS_INDEXER__INDEXER__RPC_URL: "http://20.195.24.239:9650/ext/bc/22v7AG7h6qaVxd4bLvAsSsg2LZ4RCn5iVYgFn7a2Fj1LCuYwjv/rpc" + USER_OPS_INDEXER__INDEXER__RPC_URL: http://host.docker.internal:9650/ext/bc/2ewKoUrSjnviEgGmeTiELHBmNjxVTVczBPowST471rYUZvA9bk/rpc + USER_OPS_INDEXER__INDEXER__CONCURRENCY: "2" + USER_OPS_INDEXER__INDEXER__ENTRYPOINTS__V06: "true" + USER_OPS_INDEXER__INDEXER__ENTRYPOINTS__V06_ENTRY_POINT: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" + USER_OPS_INDEXER__INDEXER__ENTRYPOINTS__V07: "true" + USER_OPS_INDEXER__INDEXER__ENTRYPOINTS__V07_ENTRY_POINT: "0x0000000071727De22E5E9d8BAf0edAc6f37da032" USER_OPS_INDEXER__INDEXER__ENTRYPOINTS__V08: "true" - USER_OPS_INDEXER__INDEXER__ENTRYPOINTS__V08_ENTRY_POINT: "0x433709009B8330FDa32311DF1C2AFA402eD8D009" - USER_OPS_INDEXER__INDEXER__ENTRYPOINTS__V06: "false" - USER_OPS_INDEXER__INDEXER__ENTRYPOINTS__V07: "false" - USER_OPS_INDEXER__DATABASE__CONNECT__URL: "${BETA_USER_OPS_DATABASE_URL:?}" + USER_OPS_INDEXER__INDEXER__ENTRYPOINTS__V08_ENTRY_POINT: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108" + USER_OPS_INDEXER__INDEXER__REALTIME__ENABLED: "true" + USER_OPS_INDEXER__INDEXER__PAST_RPC_LOGS_INDEXER__ENABLED: "true" + USER_OPS_INDEXER__INDEXER__PAST_RPC_LOGS_INDEXER__BLOCK_RANGE: "1000" + USER_OPS_INDEXER__INDEXER__PAST_DB_LOGS_INDEXER__ENABLED: "true" + USER_OPS_INDEXER__INDEXER__PAST_DB_LOGS_INDEXER__START_BLOCK: "0" + USER_OPS_INDEXER__INDEXER__PAST_DB_LOGS_INDEXER__END_BLOCK: "0" + USER_OPS_INDEXER__DATABASE__CONNECT__URL: postgresql://postgres:@db:5432/blockscout USER_OPS_INDEXER__DATABASE__CREATE_DATABASE: "false" USER_OPS_INDEXER__DATABASE__RUN_MIGRATIONS: "true" USER_OPS_INDEXER__SERVER__HTTP__ADDR: "0.0.0.0:8050" - ports: - - "127.0.0.1:18090:8050" - # ========================================================================== - # Stats Service - # ========================================================================== stats: - container_name: doscan-stats image: ghcr.io/blockscout/stats:latest pull_policy: always platform: linux/amd64 - restart: always - extra_hosts: - - "host.docker.internal:host-gateway" + restart: unless-stopped + depends_on: + db: + condition: service_healthy environment: - STATS__DB_URL: "${BETA_STATS_DATABASE_URL:?}" - STATS__BLOCKSCOUT_DB_URL: "${BETA_BLOCKSCOUT_DATABASE_URL:?}" + STATS__DB_URL: postgresql://postgres:@db:5432/stats + STATS__BLOCKSCOUT_DB_URL: postgresql://postgres:@db:5432/blockscout STATS__CREATE_DATABASE: "true" STATS__RUN_MIGRATIONS: "true" - STATS__SERVER__HTTP__ADDR: "0.0.0.0:8050" - STATS__SERVER__HTTP__CORS__ALLOWED_ORIGIN: "https://beta.doscan.io" + STATS__SERVER__HTTP__ADDR: 0.0.0.0:8050 + STATS__SERVER__HTTP__CORS__ALLOWED_ORIGIN: https://beta.doscan.io STATS__IGNORE_BLOCKSCOUT_API_ABSENCE: "true" - ports: - - "127.0.0.1:18052:8050" - # ========================================================================== - # Caddy - Reverse Proxy (HTTP only, Cloudflare Tunnel handles HTTPS) - # ========================================================================== caddy: - container_name: doscan-caddy + image: caddy@sha256:844f60b64e4724a5aa8245e019dace0d3f199f7433ce6c57676cb30a920dbad9 + restart: unless-stopped depends_on: backend: condition: service_healthy - image: caddy:latest - pull_policy: always - restart: always + frontend: + condition: service_healthy volumes: - - ./Caddyfile-beta:/etc/caddy/Caddyfile + - ./Caddyfile-beta:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config ports: - - "127.0.0.1:10080:80" - - "127.0.0.1:10081:8080" - healthcheck: - test: ["CMD", "caddy", "version"] - interval: 30s - timeout: 5s - retries: 3 - start_period: 5s + - 127.0.0.1:14080:80 volumes: + postgres_data: redis_data: caddy_data: caddy_config: - -networks: - default: diff --git a/docker-compose/envs/common-blockscout-beta.env b/docker-compose/envs/common-blockscout-beta.env index 820529ee87e2..40d897f69f3f 100644 --- a/docker-compose/envs/common-blockscout-beta.env +++ b/docker-compose/envs/common-blockscout-beta.env @@ -1,24 +1,27 @@ # ============================================================================= # DOS Chain Beta - Backend Environment OVERRIDES -# Base: common-blockscout.env (mainnet) +# Base: common-blockscout.env plus the VM-managed secret env file. # Only values that differ from base are listed here. # Docker Compose loads base first, then this file overrides. # ============================================================================= -# RPC - archive node (20.195.24.239) - same node as mainnet doscan.io -ETHEREUM_JSONRPC_HTTP_URL=http://20.195.24.239:9650/ext/bc/22v7AG7h6qaVxd4bLvAsSsg2LZ4RCn5iVYgFn7a2Fj1LCuYwjv/rpc -ETHEREUM_JSONRPC_TRACE_URL=http://20.195.24.239:9650/ext/bc/22v7AG7h6qaVxd4bLvAsSsg2LZ4RCn5iVYgFn7a2Fj1LCuYwjv/rpc -ETHEREUM_JSONRPC_WS_URL=ws://20.195.24.239:9650/ext/bc/22v7AG7h6qaVxd4bLvAsSsg2LZ4RCn5iVYgFn7a2Fj1LCuYwjv/ws +# Share the production VM's local AvalancheGo archive node. +ETHEREUM_JSONRPC_HTTP_URL=http://host.docker.internal:9650/ext/bc/2ewKoUrSjnviEgGmeTiELHBmNjxVTVczBPowST471rYUZvA9bk/rpc +ETHEREUM_JSONRPC_TRACE_URL=http://host.docker.internal:9650/ext/bc/2ewKoUrSjnviEgGmeTiELHBmNjxVTVczBPowST471rYUZvA9bk/rpc +ETHEREUM_JSONRPC_WS_URL=ws://host.docker.internal:9650/ext/bc/2ewKoUrSjnviEgGmeTiELHBmNjxVTVczBPowST471rYUZvA9bk/ws ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES=false +ETHEREUM_JSONRPC_PENDING_TRANSACTIONS_TYPE=geth -# Database - local PostgreSQL -DATABASE_URL=${BETA_BLOCKSCOUT_DATABASE_URL:?} +# The hostname resolves to the isolated database in the doscan-beta network. +DATABASE_URL=postgresql://postgres:@db:5432/blockscout?ssl=false -# Host BLOCKSCOUT_HOST=beta.doscan.io -SECRET_KEY_BASE=beta-local-dev-secret-key-base-do-not-use-in-production-1234567890abcdef +CHAIN_ID=7979 +SUBNETWORK=Mainnet +TOKEN_LIST_URL=https://raw.githubusercontent.com/DOS/DOScan-Frontend-Configs/main/configs/token-lists/mainnet.json +NFT_MEDIA_HANDLER_ENABLED=false -# Indexer - lower concurrency for local dev +# Keep beta indexing bounded on the shared four-vCPU VM. INDEXER_CATCHUP_BLOCKS_CONCURRENCY=5 INDEXER_COIN_BALANCES_CONCURRENCY=3 INDEXER_RECEIPTS_CONCURRENCY=5 diff --git a/docker-compose/envs/common-frontend-beta.env b/docker-compose/envs/common-frontend-beta.env index 1fc991c73ede..8155b979c84b 100644 --- a/docker-compose/envs/common-frontend-beta.env +++ b/docker-compose/envs/common-frontend-beta.env @@ -1,15 +1,20 @@ # ============================================================================= # DOS Chain Beta - Frontend Environment OVERRIDES -# Base: common-frontend.env (mainnet) +# Base: common-frontend.env managed on the GCP VM. # Only values that differ from base are listed here. # Docker Compose loads base first, then this file overrides. # ============================================================================= -# RPC — use main2 (archive node) instead of main (validator) +# Public browser RPC remains the production archive endpoint. NEXT_PUBLIC_NETWORK_RPC_URL=https://main2.doschain.com NEXT_PUBLIC_APP_HOST=beta.doscan.io NEXT_PUBLIC_APP_ENV=staging NEXT_PUBLIC_API_HOST=beta.doscan.io -NEXT_PUBLIC_STATS_API_HOST=https://beta-stats.doscan.io -NEXT_PUBLIC_VISUALIZE_API_HOST=https://beta-viz.doscan.io +NEXT_PUBLIC_STATS_API_HOST=https://beta.doscan.io +NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-api +NEXT_PUBLIC_VISUALIZE_API_HOST=https://beta.doscan.io/visualize +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://doscan.io/metadata-api +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://beta.doscan.io +NEXT_PUBLIC_INTERCHAIN_INDEXER_API_HOST=https://doscan.io +NEXT_PUBLIC_CROSS_CHAIN_TXS_ENABLED=true diff --git a/docs/BETA-GCP-DEPLOYMENT.md b/docs/BETA-GCP-DEPLOYMENT.md new file mode 100644 index 000000000000..ca497e17aa7c --- /dev/null +++ b/docs/BETA-GCP-DEPLOYMENT.md @@ -0,0 +1,30 @@ +# DOScan Beta on the Mainnet GCP VM + +`beta.doscan.io` runs on the `doscan-mainnet` VM in `asia-southeast1-b`. + +## Isolation boundary + +- Beta shares the host AvalancheGo archive RPC for chain ID 7979. +- Beta runs as the dedicated `doscan-beta` Docker Compose project. +- PostgreSQL, Redis, Caddy data, and Caddy config use beta-owned volumes. +- The production Blockscout database and beta Blockscout database are separate. +- Beta Caddy binds only to `127.0.0.1:14080`. +- The VM edge Caddy terminates TLS and proxies `beta.doscan.io` to that loopback port. +- NFT media processing is disabled in beta to avoid duplicate production bucket work. + +## Deployment + +The `deploy-beta` job in `.github/workflows/deploy-config.yml` runs on a GitHub-hosted runner. It authenticates with GCP Workload Identity Federation, uploads the checked-in configuration through IAP, and applies it under `/opt/doscan-beta`. + +The deployment verifies: + +- Docker Compose configuration +- Beta Caddy configuration +- Local explorer statistics and public metrics endpoints +- Shared AvalancheGo RPC chain ID +- Runtime frontend host and API configuration +- Public `https://beta.doscan.io/api/v2/stats` + +Secrets are not stored in the beta compose file. The deployment reads the VM-managed Blockscout secret env file under `/opt/doscan/envs`. + +The deployment stops only confirmed orphan containers from the retired `/opt/doscan` explorer stack. It does not stop the edge Caddy container and does not delete legacy volumes. diff --git a/scripts/apply-beta-on-mainnet-vm.sh b/scripts/apply-beta-on-mainnet-vm.sh new file mode 100755 index 000000000000..0d50d9fddeea --- /dev/null +++ b/scripts/apply-beta-on-mainnet-vm.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +src="${1:?source directory is required}" +deploy_id="${2:?deployment id is required}" +beta_path="/opt/doscan-beta" +shared_env_path="/opt/doscan/envs" +blockscout_secrets="${shared_env_path}/common-blockscout-secrets.env" +backup="${beta_path}/backups/github-${deploy_id}" + +test -s "${blockscout_secrets}" +test -s "${shared_env_path}/common-frontend.env" +test -s "${shared_env_path}/common-smart-contract-verifier.env" +test -s "${shared_env_path}/common-visualizer.env" + +mkdir -p "${backup}" "${beta_path}/envs" +cp -a "${beta_path}/compose.yml" "${backup}/compose.yml" 2>/dev/null || true +cp -a "${beta_path}/Caddyfile-beta" "${backup}/Caddyfile-beta" 2>/dev/null || true +cp -a "${beta_path}/envs" "${backup}/envs" 2>/dev/null || true + +install -m 0644 "${src}/docker-compose/docker-compose-beta.yml" "${beta_path}/compose.yml" +install -m 0644 "${src}/docker-compose/Caddyfile-beta" "${beta_path}/Caddyfile-beta" +install -m 0644 "${src}/docker-compose/envs/common-blockscout.env" "${beta_path}/envs/common-blockscout.env" +install -m 0644 "${src}/docker-compose/envs/common-blockscout-beta.env" "${beta_path}/envs/common-blockscout-beta.env" +install -m 0644 "${src}/docker-compose/envs/common-frontend-beta.env" "${beta_path}/envs/common-frontend-beta.env" + +cd "${beta_path}" +export DOSCAN_BLOCKSCOUT_SECRETS_ENV="${blockscout_secrets}" +export DOSCAN_BASE_ENV_DIR="${shared_env_path}" + +docker compose config -q +docker compose pull + +# These containers are confirmed orphans from the retired /opt/doscan explorer stack. +# Keep the edge Caddy container running and preserve all legacy volumes. +for legacy_container in \ + frontend backend redis-db db user-ops-indexer stats \ + smart-contract-verifier visualizer sig-provider; do + if [ "$(docker inspect -f '{{ index .Config.Labels "com.docker.compose.project" }}' "${legacy_container}" 2>/dev/null || true)" = "doscan" ]; then + docker stop "${legacy_container}" >/dev/null + fi +done + +docker compose up -d --remove-orphans + +for attempt in $(seq 1 48); do + if curl -fsS http://127.0.0.1:14080/api/v2/stats >/dev/null && + curl -fsS http://127.0.0.1:14080/public-metrics >/dev/null; then + break + fi + if [ "${attempt}" -eq 48 ]; then + echo "DOScan beta did not become healthy in time" >&2 + docker compose ps + exit 1 + fi + sleep 5 +done + +rpc_body='{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' +rpc_response="$(curl -fsS -H 'Content-Type: application/json' --data "${rpc_body}" \ + http://127.0.0.1:9650/ext/bc/2ewKoUrSjnviEgGmeTiELHBmNjxVTVczBPowST471rYUZvA9bk/rpc)" +grep -q '"result":"0x1f2b"' <<<"${rpc_response}" + +frontend_env="$(docker compose exec -T frontend curl -fsS http://localhost:3000/assets/envs.js)" +grep -Fq 'NEXT_PUBLIC_APP_HOST: "beta.doscan.io"' <<<"${frontend_env}" +grep -Fq 'NEXT_PUBLIC_API_HOST: "beta.doscan.io"' <<<"${frontend_env}" +grep -Fq 'NEXT_PUBLIC_STATS_API_BASE_PATH: "/stats-api"' <<<"${frontend_env}" + +docker compose exec -T caddy caddy validate --config /etc/caddy/Caddyfile +docker compose ps +echo "GCP beta deployment completed" From 55d0a1232dca1e98892ddaa1088019b16a951a39 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:13:13 +0700 Subject: [PATCH 2/3] fix: connect and verify beta services --- .github/workflows/deploy-config.yml | 2 ++ docker-compose/Caddyfile-gcp-edge | 2 +- docker-compose/docker-compose-beta.yml | 9 +++++++ docker-compose/docker-compose-gcp-edge.yml | 4 +++ docs/BETA-GCP-DEPLOYMENT.md | 30 ---------------------- scripts/apply-beta-on-mainnet-vm.sh | 27 ++++++++++++++++--- 6 files changed, 40 insertions(+), 34 deletions(-) delete mode 100644 docs/BETA-GCP-DEPLOYMENT.md diff --git a/.github/workflows/deploy-config.yml b/.github/workflows/deploy-config.yml index 8bf0e61d1881..724838b7d46d 100644 --- a/.github/workflows/deploy-config.yml +++ b/.github/workflows/deploy-config.yml @@ -488,6 +488,8 @@ jobs: } verify_nft_media_runtime + sudo docker network inspect doscan-beta >/dev/null 2>&1 || + sudo docker network create doscan-beta >/dev/null cd "${EDGE_PATH}" sudo docker compose config -q if [ "${EDGE_CONFIG_CHANGED}" -eq 1 ] || [ "${EDGE_CADDY_CONFIG_CHANGED}" -eq 1 ]; then diff --git a/docker-compose/Caddyfile-gcp-edge b/docker-compose/Caddyfile-gcp-edge index 9e8f5c8574b9..0ff173ffcfbc 100644 --- a/docker-compose/Caddyfile-gcp-edge +++ b/docker-compose/Caddyfile-gcp-edge @@ -1,6 +1,6 @@ beta.doscan.io { tls /etc/caddy/certs/origin.pem /etc/caddy/certs/origin-key.pem - reverse_proxy host.docker.internal:14080 + reverse_proxy beta-origin:80 } doscan.io, www.doscan.io { diff --git a/docker-compose/docker-compose-beta.yml b/docker-compose/docker-compose-beta.yml index 7d351b36ef2c..f92db0df6e99 100644 --- a/docker-compose/docker-compose-beta.yml +++ b/docker-compose/docker-compose-beta.yml @@ -173,9 +173,18 @@ services: - caddy_config:/config ports: - 127.0.0.1:14080:80 + networks: + default: + aliases: + - beta-origin volumes: postgres_data: redis_data: caddy_data: caddy_config: + +networks: + default: + external: true + name: doscan-beta diff --git a/docker-compose/docker-compose-gcp-edge.yml b/docker-compose/docker-compose-gcp-edge.yml index bb8bd8a518f0..181c850c8221 100644 --- a/docker-compose/docker-compose-gcp-edge.yml +++ b/docker-compose/docker-compose-gcp-edge.yml @@ -7,6 +7,7 @@ services: - "host.docker.internal:host-gateway" networks: - doscan_l1 + - doscan_beta volumes: - ./Caddyfile:/etc/caddy/Caddyfile - ./certs:/etc/caddy/certs:ro @@ -24,3 +25,6 @@ networks: doscan_l1: external: true name: doscan-l1_default + doscan_beta: + external: true + name: doscan-beta diff --git a/docs/BETA-GCP-DEPLOYMENT.md b/docs/BETA-GCP-DEPLOYMENT.md deleted file mode 100644 index ca497e17aa7c..000000000000 --- a/docs/BETA-GCP-DEPLOYMENT.md +++ /dev/null @@ -1,30 +0,0 @@ -# DOScan Beta on the Mainnet GCP VM - -`beta.doscan.io` runs on the `doscan-mainnet` VM in `asia-southeast1-b`. - -## Isolation boundary - -- Beta shares the host AvalancheGo archive RPC for chain ID 7979. -- Beta runs as the dedicated `doscan-beta` Docker Compose project. -- PostgreSQL, Redis, Caddy data, and Caddy config use beta-owned volumes. -- The production Blockscout database and beta Blockscout database are separate. -- Beta Caddy binds only to `127.0.0.1:14080`. -- The VM edge Caddy terminates TLS and proxies `beta.doscan.io` to that loopback port. -- NFT media processing is disabled in beta to avoid duplicate production bucket work. - -## Deployment - -The `deploy-beta` job in `.github/workflows/deploy-config.yml` runs on a GitHub-hosted runner. It authenticates with GCP Workload Identity Federation, uploads the checked-in configuration through IAP, and applies it under `/opt/doscan-beta`. - -The deployment verifies: - -- Docker Compose configuration -- Beta Caddy configuration -- Local explorer statistics and public metrics endpoints -- Shared AvalancheGo RPC chain ID -- Runtime frontend host and API configuration -- Public `https://beta.doscan.io/api/v2/stats` - -Secrets are not stored in the beta compose file. The deployment reads the VM-managed Blockscout secret env file under `/opt/doscan/envs`. - -The deployment stops only confirmed orphan containers from the retired `/opt/doscan` explorer stack. It does not stop the edge Caddy container and does not delete legacy volumes. diff --git a/scripts/apply-beta-on-mainnet-vm.sh b/scripts/apply-beta-on-mainnet-vm.sh index 0d50d9fddeea..c6b4019e0741 100755 --- a/scripts/apply-beta-on-mainnet-vm.sh +++ b/scripts/apply-beta-on-mainnet-vm.sh @@ -28,6 +28,7 @@ cd "${beta_path}" export DOSCAN_BLOCKSCOUT_SECRETS_ENV="${blockscout_secrets}" export DOSCAN_BASE_ENV_DIR="${shared_env_path}" +docker network inspect doscan-beta >/dev/null 2>&1 || docker network create doscan-beta >/dev/null docker compose config -q docker compose pull @@ -43,12 +44,14 @@ done docker compose up -d --remove-orphans -for attempt in $(seq 1 48); do +for attempt in $(seq 1 72); do if curl -fsS http://127.0.0.1:14080/api/v2/stats >/dev/null && - curl -fsS http://127.0.0.1:14080/public-metrics >/dev/null; then + curl -fsS http://127.0.0.1:14080/public-metrics >/dev/null && + curl -fsS http://127.0.0.1:14080/api/v1/lines >/dev/null && + curl -fsS http://127.0.0.1:14080/api/v2/proxy/account-abstraction/status >/dev/null; then break fi - if [ "${attempt}" -eq 48 ]; then + if [ "${attempt}" -eq 72 ]; then echo "DOScan beta did not become healthy in time" >&2 docker compose ps exit 1 @@ -56,6 +59,24 @@ for attempt in $(seq 1 48); do sleep 5 done +for service_url in \ + http://smart-contract-verifier:8050/health \ + http://visualizer:8050/health \ + http://sig-provider:8050/health; do + docker compose exec -T backend curl -fsS "${service_url}" >/dev/null +done + +running_services="$(docker compose ps --status running --services)" +for required_service in \ + redis-db db backend frontend smart-contract-verifier visualizer \ + sig-provider user-ops-indexer stats caddy; do + if ! grep -qx "${required_service}" <<<"${running_services}"; then + echo "Required beta service is not running: ${required_service}" >&2 + docker compose ps + exit 1 + fi +done + rpc_body='{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' rpc_response="$(curl -fsS -H 'Content-Type: application/json' --data "${rpc_body}" \ http://127.0.0.1:9650/ext/bc/2ewKoUrSjnviEgGmeTiELHBmNjxVTVczBPowST471rYUZvA9bk/rpc)" From 3455eb170bcadd5e6794cc169050b117c83b4be7 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:18:46 +0700 Subject: [PATCH 3/3] docs: correct beta edge route comment --- docker-compose/Caddyfile-beta | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose/Caddyfile-beta b/docker-compose/Caddyfile-beta index 3633b64c116d..69ddd3db2d06 100644 --- a/docker-compose/Caddyfile-beta +++ b/docker-compose/Caddyfile-beta @@ -1,5 +1,5 @@ # HTTP-only origin for beta.doscan.io. -# The VM edge Caddy terminates TLS and proxies to 127.0.0.1:14080. +# The VM edge Caddy terminates TLS and proxies over the shared doscan-beta network. :80 { encode zstd gzip