Legendary Arena Lab

Deploy Server

wiki

Deploy Server

Migration in progress. api.legendary-arena.com and its PostgreSQL are moving off Render onto a self-hosted DigitalOcean Ubuntu droplet fronted by Cloudflare, per docs/PLAN.md . Until decommission, Render stays warm as the rollback target.

Summary

Clones the app, builds it with the pinned Node toolchain, optionally runs migrations, then installs and starts the la-server systemd unit behind Nginx. This is the step that turns a bare droplet into a running apps/server on 127.0.0.1:3000.

Mechanics

The owning artifact infra/scripts/40-app-deploy.ps1 requires root and requires REPO_URL. It runs in five stages:

  1. Inputs / clone — ensures the operator user exists, installs git, then clones REPO_URL into APP_DIR (default /opt/legendary-arena) or, if already present, fetches and checks out DEPLOY_REF (default main) with git pull --ff-only — all as the operator user.

  2. Build — builds as the operator via fnm, mirroring the Render buildCommand:

    /opt/fnm/fnm exec 24.18.0 pnpm install --frozen-lockfile
    # falls back to a non-frozen install on lockfile mismatch
    /opt/fnm/fnm exec 24.18.0 pnpm -r build
    
  3. Optional migrations — runs node scripts/migrate.mjs only when RUN_MIGRATIONS=true (default false).

  4. Install unit — copies infra/systemd/la-server.service to /etc/systemd/system/la-server.service, then systemctl daemon-reload + systemctl enable la-server.

  5. Restart — restarts la-server and shows its status.

The installed unit sets ExecStart=/opt/fnm/fnm exec 24.18.0 pnpm --filter @legendary-arena/server start, WorkingDirectory=/opt/legendary-arena, EnvironmentFile=/etc/la/.env, User=operator, Restart=on-failure, and After/Wants postgresql.service. The env file is infra/.env.example copied to /etc/la/.env mode 600. Environment overrides: OPERATOR_USER, NODE_VERSION (24.18.0), APP_DIR (/opt/legendary-arena), REPO_URL (required), DEPLOY_REF (main), RUN_MIGRATIONS (false), SYSTEMD_SRC. The build mirrors the plan’s deploy build (pnpm install --frozen-lockfile && pnpm -r build) and is heavy on the 8 GB box (docs/PLAN.md ).

Interactions

  • Node Runtime — provides fnm at /opt/fnm, Node 24.18.0, and pnpm; must run before this deploy or the build has no toolchain.
  • Systemd Service — the la-server unit this script installs, enables, and restarts.
  • Secrets and Env — supplies /etc/la/.env (mode 600), read via the unit’s EnvironmentFile.
  • PostgreSQL Setup — provides the DATABASE_URL target the app and migrations connect to.
  • Deploy Pipeline — the CI workflow that re-runs this flow on push.

Edge Cases

  • REPO_URL is required — the script cannot clone or fetch without it.
  • /etc/la/.env (mode 600) must exist before start or the unit’s EnvironmentFile fails; see Secrets and Env .
  • RUN_MIGRATIONS defaults false, so migrations are usually driven by the deploy pipeline instead of this script.
  • The build runs as the operator via fnm at /opt/fnm, so Node Runtime must have run first.
  • A --frozen-lockfile mismatch falls back to a plain install, which risks silent dependency drift; health is exposed at /health and /health/legends-publisher.

Execute

# Clone, build, install and start la-server (root required; REPO_URL mandatory)
sudo REPO_URL='https://github.com/legendary-arena/legendary-arena' \
     pwsh -File infra/scripts/40-app-deploy.ps1

# Deploy a specific ref and run migrations in the same pass
sudo REPO_URL='https://github.com/legendary-arena/legendary-arena' \
     DEPLOY_REF='release-2026-07' RUN_MIGRATIONS=true \
     pwsh -File infra/scripts/40-app-deploy.ps1

Verify

# Unit is active
systemctl status la-server --no-pager
# Expected: Active: active (running)

# App answers on the loopback health endpoint
curl -s localhost:3000/health
# Expected: a 200 health payload (e.g. {"status":"ok"})

# Recent unit logs show a clean start
journalctl -u la-server -n 50
# Expected: startup lines, no repeated Restart=on-failure loops

References