Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/RUN-A-NODE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Step-by-step guide to staking $GITLAWB, registering your node on-chain, and earn

- A wallet with at least **10,000 $GITLAWB** (minimum stake) plus a small amount of ETH on Base for gas
- Docker or Rust 1.91+ (for running the node process)
- A public HTTP URL (your-host.com) — can be a VPS, Fly.io app, or anything reachable
- A public HTTP URL (your-host.com) — can be a VPS, Fly.io app, or anything reachable. A Fly.io config is provided at `infra/fly/fly.toml` (deploy from the repo root with `fly deploy -c infra/fly/fly.toml`)

---

Expand Down
36 changes: 36 additions & 0 deletions infra/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# infra/

Deployment configuration, organized by target — one subdirectory per platform.

```text
infra/
├── fly/
│ └── fly.toml # Fly.io app config (gitlawb-node-test)
└── aws/ # Terraform: single EC2 + Docker (see aws/README.md)
```

## Deploying to Fly.io

Run from the **repo root** so the Docker build context includes `crates/`,
`Cargo.toml`, and `bootstrap-peers.json`:

```sh
fly deploy -c infra/fly/fly.toml
```

The `dockerfile` path inside `fly.toml` is resolved relative to the config
file, so it points to `../../Dockerfile`.

## Deploying to AWS

See [`aws/README.md`](aws/README.md) — Terraform for a single EC2 instance
running the published `ghcr.io/gitlawb/node` image with Docker compose.

## What intentionally stays at the repo root

- `Dockerfile` / `Dockerfile.bins` — shared by the release CI workflow
(`.github/workflows/release.yml`), `scripts/build-bins.sh`, and Fly builds.
- `docker-compose.yml` — local dev stack; bundled into the macOS app by
`scripts/build-macos-app.sh` and used for repo detection by the app.

Future targets should follow the same per-platform layout.
13 changes: 13 additions & 0 deletions infra/aws/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Terraform state and workdir — never commit (state contains secrets)
*.tfstate
*.tfstate.*
.terraform/
crash.log
crash.*.log

# Local variable files may contain secrets; keep the example only
*.tfvars
*.tfvars.json
!terraform.tfvars.example

# Note: .terraform.lock.hcl IS committed (reproducible provider versions)
46 changes: 46 additions & 0 deletions infra/aws/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

138 changes: 138 additions & 0 deletions infra/aws/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# gitlawb node on AWS (Terraform)

Single EC2 instance running the published node image + Postgres via Docker
compose, with a persistent encrypted EBS volume, Elastic IP, SSM access, and
daily snapshots.

```text
Elastic IP ──► EC2 t4g.small (Amazon Linux 2023, arm64)
7545/tcp docker compose:
7546/udp ├─ node (ghcr.io/gitlawb/node, pulled — not built)
└─ postgres:16-alpine
EBS gp3 volume mounted at /mnt/data
├─ node/ → container /data (repos + identity key)
└─ postgres/ → postgres data dir
```

## Prerequisites

- Terraform ≥ 1.6
- AWS credentials configured (`aws sts get-caller-identity` works)
- AWS CLI + [Session Manager plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) (for shell access)
- A default VPC in the target region (or pass `subnet_id`)

## Quick start

```sh
cd infra/aws
cp terraform.tfvars.example terraform.tfvars # edit: public_url at minimum
terraform init
terraform plan
terraform apply # ⚠ creates billable resources (~$25/mo: EC2 + EBS + snapshots)
```

After apply (~3-5 min for first boot to pull images and start):

```sh
curl "$(terraform output -raw api_url)/health"
```

## ⚠ First boot: back up the identity key

The node generates `/data/keys/identity.pem` on first start — it defines the
node's DID. **Losing it permanently changes the node's identity.** Back it up
immediately:

```sh
$(terraform output -raw ssm_session_command)
# in the session:
sudo cat /mnt/data/node/keys/identity.pem
```

Store the key somewhere safe (password manager / offline). The volume's
`prevent_destroy` guard and daily DLM snapshots protect against accidents, but
are not a substitute for an offline backup.

## Shell access

SSM Session Manager — no SSH port, no keys to manage:

```sh
$(terraform output -raw ssm_session_command)
```

Bootstrap log: `/var/log/gitlawb-bootstrap.log`. Stack lives in `/opt/gitlawb`
(`docker compose ps`, `docker compose logs node`).

SSH is off by default; set `ssh_ingress_cidr` + `ssh_key_name` if you need it.

## Upgrading the node

User-data only runs at first boot, so upgrades go through SSM:

```sh
$(terraform output -raw upgrade_command)
```

This runs `docker compose pull && docker compose up -d` on the instance.

- With `image_tag = "latest"` (default) that picks up the newest release.
- With a **pinned tag**, first edit the tag in `/opt/gitlawb/compose.yaml` on
the instance (via SSM session), then run the upgrade command — and keep
`image_tag` in terraform.tfvars in sync so a future instance replacement
boots the same version.

Replace the instance itself (OS/AMI/instance-type changes) with
`terraform apply -replace=aws_instance.node` — the data volume reattaches and
`/data` (including the identity key) survives.

## Changing configuration

User-data only runs at first boot, and the instance ignores `user_data` drift
(`ignore_changes`), so editing terraform.tfvars values that feed the bootstrap
(`bootstrap_peers`, `public_url`, integrations, `image_tag`) does **not**
affect a running instance on `terraform apply`. To roll out such changes,
either edit `/opt/gitlawb/.env` on the instance (SSM session, then
`docker compose up -d`), or replace the instance:

```sh
terraform apply -replace=aws_instance.node
```

The data volume reattaches; repos, postgres data, and the identity key survive.

## Remote state (optional)

Local state is the default. To move state to S3: create a versioned bucket,
uncomment the `backend "s3"` block in `versions.tf`, then:

```sh
terraform init -migrate-state
```

## Teardown

`terraform destroy` will **fail on the data volume by design**
(`prevent_destroy`). To tear everything down:

1. Back up the identity key (above) and take a final snapshot if you may return.
2. Remove the `prevent_destroy` line from `aws_ebs_volume.data` in `main.tf`.
3. `terraform destroy`.

Note: DLM snapshots created by the policy are not deleted by destroy — clean
them up in the EC2 console if unwanted. The Elastic IP is released on destroy.

## Security notes

- Postgres password: generated by Terraform, stored as an SSM SecureString,
fetched at boot via the instance profile — never in user-data or state-free
files on disk (only in `/opt/gitlawb/.env`, mode 600). It IS in Terraform
state — treat state as sensitive (another reason for the S3 backend).
- Sensitive optional vars (`operator_private_key`, `pinata_jwt`,
`s3_access_key_id`, `s3_secret_access_key`) follow the same SSM path.
- SSM secrets use the AWS-managed `aws/ssm` key by default; set
`ssm_kms_key_id` to encrypt with a customer-managed KMS key instead (the
instance role is granted `kms:Decrypt` on that key automatically).
- IMDSv2 is required; metrics port is closed unless `metrics_ingress_cidr` is set.
- The node serves plain HTTP on 7545. For TLS, put a DNS name + proxy
(ALB/CloudFront/Caddy) in front and set `public_url` accordingly.
63 changes: 63 additions & 0 deletions infra/aws/compose.yaml.tftpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Rendered by Terraform (compose.yaml.tftpl) and written to /opt/gitlawb/compose.yaml
# by user-data. Adapted from the repo-root docker-compose.yml: pulls the published
# image instead of building, and binds data dirs to the dedicated EBS volume.
# `$${VAR}` entries are resolved by docker compose from /opt/gitlawb/.env at runtime.

services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${pg_db}
POSTGRES_USER: ${pg_user}
POSTGRES_PASSWORD: $${POSTGRES_PASSWORD}
volumes:
- /mnt/data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${pg_user}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped

node:
image: ${image_repo}:${image_tag}
depends_on:
postgres:
condition: service_healthy
ports:
- "${gitlawb_port}:${gitlawb_port}" # HTTP API + git smart-HTTP
- "${p2p_port}:${p2p_port}/udp" # libp2p QUIC
%{ if expose_metrics ~}
- "${metrics_port}:${metrics_port}" # Prometheus /metrics
%{ endif ~}
volumes:
- /mnt/data/node:/data
environment:
DATABASE_URL: postgresql://${pg_user}:$${POSTGRES_PASSWORD}@postgres:5432/${pg_db}
GITLAWB_HOST: 0.0.0.0
GITLAWB_PORT: "${gitlawb_port}"
GITLAWB_P2P_PORT: "${p2p_port}"
GITLAWB_REPOS_DIR: /data/repos
GITLAWB_KEY: /data/keys/identity.pem
GITLAWB_PUBLIC_URL: $${GITLAWB_PUBLIC_URL}
GITLAWB_BOOTSTRAP_PEERS: $${GITLAWB_BOOTSTRAP_PEERS}
GITLAWB_AUTO_SYNC: $${GITLAWB_AUTO_SYNC}
GITLAWB_MAX_PACK_BYTES: $${GITLAWB_MAX_PACK_BYTES}
# On-chain PoS (optional — empty unless set in terraform.tfvars)
GITLAWB_CHAIN_RPC_URL: $${GITLAWB_CHAIN_RPC_URL}
GITLAWB_CONTRACT_NODE_STAKING: $${GITLAWB_CONTRACT_NODE_STAKING}
GITLAWB_OPERATOR_PRIVATE_KEY: $${GITLAWB_OPERATOR_PRIVATE_KEY}
# IPFS pinning (optional)
GITLAWB_PINATA_JWT: $${GITLAWB_PINATA_JWT}
# Shared S3-compatible pack storage (optional)
GITLAWB_TIGRIS_BUCKET: $${GITLAWB_TIGRIS_BUCKET}
AWS_ACCESS_KEY_ID: $${S3_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: $${S3_SECRET_ACCESS_KEY}
AWS_ENDPOINT_URL_S3: $${S3_ENDPOINT_URL}
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:${gitlawb_port}/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
restart: unless-stopped
Loading
Loading