Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

DevLake — Architecture and Operations

DevLake provides DORA metrics and per-squad PR cycle time for the HungryHub team (~25 devs).


Architecture

Internet
   │
   ▼
Cloudflare Tunnel (cloudflared)
   │
   └──► config-ui  (port 4000) — DevLake admin UI
              │  proxies /api/* → devlake :8080
              ▼
        devlake (port 8080) — pipeline engine + REST API
              │
              ▼
       Aiven MySQL (managed)
       hh-development project
       devlake-mysql
       google-europe-north1 (Finland)

Host: Hetzner CPX42 VPS (devlake-prod), Helsinki — 62.238.41.27

No local MySQL or Grafana containers — both are replaced by Aiven managed services.


Infrastructure details

ResourceValue
Hetzner serverdevlake-prod, CPX42 (8 vCPU / 16 GB / 240 GB NVMe), hel1
IP62.238.41.27
Firewalldevlake-firewall (id: 11074343) — SSH inbound only, all outbound open
Aiven MySQLdevlake-mysql, startup-4, google-europe-north1, project hh-development
Aiven MySQL hostdevlake-mysql-hh-development.f.aivencloud.com:15939
Aiven Grafanapublic-grafana-hh-production.f.aivencloud.com, project hh-production
Cloudflare Tunneldevlake-prod, id 371e5df5-0fab-4b37-9e39-23670ed00577
CF CNAMEdevlake.hungryhub.com → 371e5df5...cfargotunnel.com (proxied)
CF account id6bacbc90386bf7634898c165b7dbc611
CF zone id104b9a2a601c57619abee0706a54b28a

Key components

ComponentDescription
devlake containerApache DevLake v1.0.3-beta12 — pipeline engine, REST API on :8080
config-ui containerWeb UI on :4000 — proxies /api/* → devlake :8080
cloudflared containerCloudflare Tunnel client — exposes config-ui externally without open ports
Aiven MySQLManaged startup-4 plan, databases lake and lake_test
Aiven GrafanaExternal Grafana with 11 DORA dashboards in DevLake DORA folder

Secrets (AWS SSM — prod account 202255947274, ap-southeast-1)

SSM pathPurpose
/devlake/encryption-secretDevLake DB encryption key — never regenerate on a live instance
/devlake/cloudflare-tunnel-tokenCloudflare Tunnel auth token
/devlake/db-urlAiven MySQL connection string (with tls=skip-verify)
/devlake/e2e-db-urlE2E test DB connection string
/devlake/cloudflare-api-tokenCF account-level API token
/devlake/github-apps/hh-devlake-1GitHub App creds (app_id, installation_id, private_key) for rate-limit distribution
/devlake/github-apps/hh-devlake-2GitHub App creds — see DevLake GitHub Apps Rate-Limit Distribution
/devlake/github-apps/hh-devlake-3GitHub App creds
/devlake/github-apps/hh-devlake-4GitHub App creds
/devlake/github-apps/hh-devlake-5GitHub App creds

Pull secrets to the VPS before deploying:

./deploy/pull-secrets.sh

DB URL format: mysql://user:pass@host:port/db?charset=utf8mb4&parseTime=True&loc=UTC&tls=skip-verify Always write the DB URL via Python re.sub — never sed, as & in the value causes shell expansion corruption.


IAM

IAM user devlake-vps (key AKIAS6F3E2YFCI6SBIPK) in the prod account has a narrow inline policy:

  • ssm:GetParameter + kms:Decrypt scoped to arn:aws:ssm:ap-southeast-1:202255947274:parameter/devlake/*

Credentials live at /root/.aws/credentials on the VPS.


GitHub connector configuration

ItemValue
Connection namehungryhub-team (id: 1)
Endpointhttps://api.github.com/
Auth methodAccessToken — 5 comma-separated installation tokens (round-robin, 25,000 req/hr)
Enable GraphQLtrue
Rate limit20,000 req/hr
Scope confighungryhub-dora (id: 1)

Tokens are minted from 5 org-owned GitHub Apps by a cron on the VPS every 30 min. See DevLake GitHub Apps Rate-Limit Distribution for the full setup.

Scope config hungryhub-dora:

  • deploymentPattern: (?i)(deploy|release|prod)
  • productionPattern: (?i)(prod|main|master|cloudflare_zone_env) — includes Terraform CF deploys
  • prBodyClosePattern: (?i)(fix|close|resolve)(\s*)(?:#)(\d+)

15 tracked repos (all with scopeConfigId: 1):

RepoGitHub ID
hungryhub-team/hh-server199970072
hungryhub-team/hh-android219383404
hungryhub-team/hh-menu494021211
hungryhub-team/hh-nuxt496500431
hungryhub-team/hh-menu-frontend-new510972874
hungryhub-team/book-bite539788675
hungryhub-team/hh-pegasus733067847
hungryhub-team/hh-felidae1006944319
hungryhub-team/hh-vendors-hub1006956323
hungryhub-team/hungryhub-terraform1028203938
hungryhub-team/hh-lion1077453560
hungryhub-team/eagle-eye1156890442
hungryhub-team/hunger-games1158898105
hungryhub-team/hh-pegasus-monorepo1204792079
hungryhub-team/hh-relay1239594970

Projects and blueprints

Four NORMAL-mode blueprints run nightly at 00:00 UTC, one per squad:

Blueprint IDNameProjectSquadRepos
10growth-BlueprintgrowthGrowthhh-pegasus-monorepo, hh-pegasus, book-bite, hh-lion, hh-nuxt
11integrations-BlueprintintegrationsIntegrationshh-server, hh-android, hh-menu, hh-menu-frontend-new, hh-vendors-hub
12automation-BlueprintautomationAutomationhh-relay, hh-felidae, eagle-eye, hunger-games
13platform-BlueprintplatformPlatformhungryhub-terraform

All share scope config hungryhub-dora (id: 1, GitHub connection 1).

To add or move a repo: update the blueprint’s scope list via the DevLake API (PATCH /blueprints/<id>). The squad source of truth is hh-relay/squad_channels.yml.

See DevLake DORA Metrics Setup for detailed configuration, debugging, and known limitations.


Grafana dashboards

Grafana folder: DevLake DORA (uid: ffo08wszkd2wwf) Datasource: devlake (uid: dfo08jdx29z40f, MySQL, id: 15)

11 dashboards imported from official DevLake v1.0.3-beta12 JSONs:

  • DORA, DORAByTeam, DORADebug
  • DORADetails ×4
  • EngineeringOverview, EngineeringThroughputAndCycleTime
  • GitHub, Homepage

Common operations

Check container status

ssh root@62.238.41.27
cd /opt/devlake
docker compose ps
docker compose logs -f devlake

Deploy / update

# Initial deploy
./deploy/deploy.sh

# Update (pull new images, restart)
./deploy/update.sh

Trigger a manual pipeline run

Via the DevLake API (use DEVLAKE_API_TOKEN from .env):

# Trigger nightly sync for a project (replace 10-13 for other projects)
# 10=growth, 11=integrations, 12=automation, 13=platform
curl -X POST https://devlake.hungryhub.com/api/blueprints/10/trigger \
  -H "Authorization: Bearer <DEVLAKE_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{}'

SSH onto the VPS

ssh root@62.238.41.27

Update DevLake version

  1. Edit docker-compose.yml — bump devlake and config-ui image tags.
  2. Run ./deploy/update.sh.
  3. Verify on https://devlake.hungryhub.com.

Database operations

Check disk usage

avn service get devlake-mysql --project hh-development --format json | \
  python3 -c "import json,sys; svc=json.load(sys.stdin); print(svc.get('disk_space_mb'), 'MB disk')"

Scale up if > 60% full:

avn service update devlake-mysql --plan startup-8 --project hh-development

Restore from Aiven backup

  1. Go to Aiven Console → hh-developmentdevlake-mysqlBackups.
  2. Fork to a new service using the desired backup point.
  3. Export: mysqldump -h <new-host> -u avnadmin -p lake > lake_backup.sql
  4. Import: mysql -h <live-host> -u avnadmin -p lake < lake_backup.sql
  5. Restart: docker compose -f /opt/devlake/docker-compose.yml restart

ENCRYPTION_SECRET — do not rotate casually

All connector credentials stored in the DevLake DB are encrypted with this key. Rotating it without re-entering every connector secret will break all data pipelines.

Only rotate if the key is compromised:

  1. openssl rand -base64 32 — generate new secret
  2. aws ssm put-parameter --profile prod --region ap-southeast-1 --name /devlake/encryption-secret --type SecureString --value '<new>' --overwrite
  3. ./deploy/pull-secrets.sh
  4. Restart containers
  5. Re-enter all connector API tokens in the config-ui

Infrastructure provisioning scripts

ScriptPurpose
deploy/provision-vps.shCreates Hetzner VPS
deploy/install-docker.shInstalls Docker on VPS
deploy/pull-secrets.shFetches SSM secrets → /opt/devlake/.env
deploy/deploy.shFirst-time compose up
deploy/update.shPull + restart on version bump
ops/aiven/Aiven MySQL provisioning
ops/cloudflare/Cloudflare Tunnel creation
ops/aws/IAM user for SSM access