2026-05-11 17:52:37 -04:00
# forgejo
2026-05-11 18:13:54 -04:00
Self-hosted Forgejo git server for archiving GitHub repos and mirroring active ones. Runs on a single EC2 instance within the Sea Haven VPC, fronted by the `seahaven-com` ALB for HTTPS.
2026-05-11 17:52:37 -04:00
## Architecture
2026-05-15 17:59:31 -04:00
- **EC2**: t4g.small (arm64), Amazon Linux 2023, 50GB gp3 EBS — look up instance ID with:
```
aws cloudformation describe-stacks --stack-name forgejo --query 'Stacks[0].Outputs[?OutputKey==`InstanceId` ].OutputValue' --output text
```
2026-05-11 18:13:54 -04:00
- **Network**: Private subnet (us-east-1a), behind `seahaven-com` ALB for SSL termination
2026-05-11 19:03:42 -04:00
- **DNS**: `forgejo.seahaven.com` — Route53 alias record pointing to the `seahaven-com` ALB (not a direct A record)
2026-05-11 18:13:54 -04:00
- **TLS**: Wildcard cert on ALB, HTTP internally on port 3000
2026-05-14 18:08:06 -04:00
- **Backup**: Nightly `forgejo dump` to S3 + EBS snapshots via DLM (see [3-2-1 Backup Strategy ](#3-2-1-backup-strategy ))
2026-05-11 17:52:37 -04:00
- **Admin access**: SSM Session Manager (no SSH port exposed)
2026-05-11 19:03:42 -04:00
- **CI/CD**: GitHub Actions with OIDC role `githubdeploy-forgejo`
2026-05-11 17:52:37 -04:00
### Ports
2026-05-11 18:13:54 -04:00
| Port | Protocol | Source | Purpose |
|------|----------|--------|---------|
| 443 | HTTPS | ALB (public) | Web UI + HTTP git clone |
| 3000 | HTTP | ALB → instance | Internal traffic from ALB |
| 2222 | SSH | VPC + VPN | Git SSH operations |
2026-05-11 17:52:37 -04:00
2026-05-14 18:08:06 -04:00
## 3-2-1 Backup Strategy
2026-05-11 19:03:42 -04:00
2026-05-14 18:08:06 -04:00
All backups follow a 3-2-1 strategy: 3 copies, 2 storage types, 1 offsite provider.
2026-05-11 19:03:42 -04:00
2026-05-14 18:08:06 -04:00
| Copy | Location | Type | Retention |
|------|----------|------|-----------|
| Live | EBS volume (us-east-1) | Block | N/A |
| Near-site | S3 replica (us-west-2) | Object | Archive: indefinite, noncurrent versions: 90d |
| Offsite | GCS `forgejo-backups-offsite-seahaven` (GCP us-central1) | Object | 2-year locked retention |
2026-05-11 19:03:42 -04:00
2026-05-14 18:08:06 -04:00
**Daily data flow:**
2026-05-11 19:03:42 -04:00
2026-05-14 18:08:06 -04:00
| Time (UTC) | Event |
|------------|-------|
| 05:00 | `forgejo dump` → `s3://forgejo-backups-328440206208/archive/{date}/` |
| ~05:01 | S3 CRR replicates to `forgejo-backups-replica-328440206208` (us-west-2) |
| 06:00 | DLM EBS snapshot (30-day retention) |
| 08:00 | Verification Lambda checks all 3 locations, posts to Slack |
| 10:00 | GCS Storage Transfer pulls from S3 to GCS offsite |
2026-05-11 19:03:42 -04:00
2026-05-14 18:08:06 -04:00
**S3 source lifecycle:** Standard 30d → Glacier (no expiration).
**Immutability layers:**
- S3 Versioning on both source and replica buckets
- S3 Object Lock (Governance, 90d) on the replica bucket
- GCS Bucket Lock (2yr, irreversible) on the offsite bucket
### Verification
The `forgejo-backup-verification` Lambda runs daily at 08:00 UTC and checks:
1. S3 source has a recent dump under `archive/`
2. S3 replica has replicated the latest dump
3. GCS offsite has received the latest transfer
4. EBS snapshots exist within the last 48 hours
On the 1st of each month at 09:00 UTC, it runs a restore test: downloads the latest dump, extracts the archive, and runs SQLite integrity checks.
### Manual backup
2026-05-11 19:03:42 -04:00
```bash
sudo /usr/local/bin/forgejo-backup.sh
```
2026-05-14 18:08:06 -04:00
### Restore from S3
For backups older than 30 days (Glacier), restore the object first:
```bash
aws s3api restore-object --bucket forgejo-backups-328440206208 \
--key "archive/< date > /forgejo-< date > .tar.gz" \
--restore-request '{"Days":7,"GlacierJobParameters":{"Tier":"Standard"}}'
# Wait ~3-5 hours for restore to complete, then:
```
Download and restore:
```bash
aws s3 cp s3://forgejo-backups-328440206208/archive/< date > /forgejo-< date > .tar.gz /tmp/
systemctl stop forgejo
mkdir -p /tmp/forgejo-restore & & tar -xzf /tmp/forgejo-< date > .tar.gz -C /tmp/forgejo-restore
cd /tmp/forgejo-restore
cp app.ini /etc/forgejo/app.ini
cp gitea-db.sqlite3 /var/lib/forgejo/data/forgejo.db
rm -rf /var/lib/forgejo/data/repositories
cp -a repos /var/lib/forgejo/data/repositories
cp -a data/. /var/lib/forgejo/data/
[ -d lfs ] & & cp -a lfs/. /var/lib/forgejo/data/lfs/
[ -d custom ] & & cp -a custom/. /var/lib/forgejo/custom/
chown -R forgejo:forgejo /var/lib/forgejo /etc/forgejo/app.ini
systemctl start forgejo
rm -rf /tmp/forgejo-restore /tmp/forgejo-< date > .tar.gz
```
### Restore from GCS (disaster recovery)
```bash
gcloud config set project sea-haven-backups
gsutil cp gs://forgejo-backups-offsite-seahaven/archive/< date > /forgejo-< date > .tar.gz /tmp/
# Then follow the same restore steps as S3 above
```
2026-05-11 19:03:42 -04:00
## Autodiscovery
An hourly cron job checks the `Sea-Haven-Industries` GitHub org for new repositories and mirrors them into Forgejo automatically.
- **Active repos** are created as mirrors (ongoing sync).
- **Archived repos** are created as static one-time imports.
- **Script**: `/usr/local/bin/forgejo-autodiscover.sh`
- **Log**: `/var/log/forgejo-autodiscover.log`
## Token Refresh
A daily cron at 4:30 UTC reads the GitHub PAT from Secrets Manager (`forgejo/github-pat` ) and updates the git remote URL on every mirror repository so credentials stay current.
- **Script**: `/usr/local/bin/forgejo-refresh-tokens.sh`
## PAT Rotation
The GitHub personal access token used for mirroring is a fine-grained PAT scoped to `Sea-Haven-Industries` with **Contents: Read-only** permissions and a 1-year expiration. It is stored in Secrets Manager at `forgejo/github-pat` .
To rotate:
1. Create a new fine-grained PAT on GitHub with the same scope.
2. Update the secret value in Secrets Manager (`forgejo/github-pat` ).
3. The daily token-refresh cron will pick it up automatically.
To force immediate propagation:
```bash
sudo /usr/local/bin/forgejo-refresh-tokens.sh
```
## Secrets Manager
| Secret | Purpose |
|--------|---------|
| `forgejo/admin-password` | Forgejo admin user password |
| `forgejo/api-token` | Forgejo API token (used by autodiscovery and token refresh scripts) |
| `forgejo/github-pat` | GitHub fine-grained PAT for mirroring |
2026-05-14 18:08:06 -04:00
| `forgejo/gcs-sa-key` | GCP service account key for offsite backup verification |
| `forgejo/gcs-transfer-credentials` | AWS IAM credentials for GCS Storage Transfer Service |
| `forgejo/slack-webhook` | Slack webhook URL for backup verification alerts |
2026-05-11 19:03:42 -04:00
2026-05-11 17:52:37 -04:00
## First-time setup
After the stack deploys, connect via SSM and create the admin user:
```bash
2026-05-15 17:59:31 -04:00
INSTANCE_ID=$(aws cloudformation describe-stacks --stack-name forgejo --query 'Stacks[0].Outputs[?OutputKey==`InstanceId` ].OutputValue' --output text)
aws ssm start-session --target "$INSTANCE_ID"
2026-05-11 17:52:37 -04:00
sudo -u forgejo /usr/local/bin/forgejo admin user create \
--admin \
--username adam \
--password '< password > ' \
--email adam@seahavenind .com \
--config /etc/forgejo/app.ini
```
2026-05-11 18:13:54 -04:00
Admin password is stored in Secrets Manager at `forgejo/admin-password` .
Access the web UI at `https://forgejo.seahaven.com` .
2026-05-11 17:52:37 -04:00
## Migrating repos from GitHub
### Archived repos (one-time import)
In the Forgejo web UI: **New Migration → GitHub** → paste the GitHub repo URL. Use a GitHub personal access token for private repos. These are full imports (code, issues, PRs, releases).
### Active repos (mirror sync)
Same migration flow, but check **This Repository Will Be A Mirror** . Forgejo polls GitHub hourly (`DEFAULT_INTERVAL = 1h` in app.ini) and keeps the mirror in sync.
2026-05-14 18:08:06 -04:00
## GCP Offsite Setup (one-time)
Run the setup script to create the GCS offsite bucket, service account, and store credentials:
```bash
./scripts/gcp-setup.sh
```
This creates the `sea-haven-backups` GCP project with a locked-retention GCS bucket. After running, configure the Storage Transfer job in the GCP Console using the AWS credentials from `forgejo/gcs-transfer-credentials` .
2026-05-11 17:52:37 -04:00
## Deployment
```bash
npm install
2026-05-14 18:08:06 -04:00
npx cdk deploy --all
2026-05-11 17:52:37 -04:00
```
2026-05-14 18:08:06 -04:00
This deploys two stacks:
- `forgejo-replica` (us-west-2) — S3 replica bucket with Object Lock
- `forgejo` (us-east-1) — main stack with Forgejo instance, CRR, and verification Lambda
2026-05-11 17:52:37 -04:00
CI/CD is handled by GitHub Actions — PRs run CI, merges to `main` deploy via the reusable CDK workflow.
2026-05-15 17:59:31 -04:00
## Post-deploy: store Slack webhook
2026-05-14 18:08:06 -04:00
2026-05-15 17:59:31 -04:00
Store the Slack webhook URL for backup verification alerts:
2026-05-14 18:08:06 -04:00
```bash
aws secretsmanager create-secret --name forgejo/slack-webhook \
--secret-string "https://hooks.slack.com/services/YOUR/WEBHOOK/URL" \
--region us-east-1
```
2026-05-11 17:52:37 -04:00
## Updating Forgejo
Update the `FORGEJO_VERSION` constant in `lib/forgejo-stack.ts` and deploy. This replaces the instance, so ensure the latest EBS snapshot is available for data recovery if needed. Alternatively, update in-place via SSM:
```bash
2026-05-15 17:59:31 -04:00
INSTANCE_ID=$(aws cloudformation describe-stacks --stack-name forgejo --query 'Stacks[0].Outputs[?OutputKey==`InstanceId` ].OutputValue' --output text)
aws ssm start-session --target "$INSTANCE_ID"
2026-05-11 17:52:37 -04:00
sudo systemctl stop forgejo
sudo curl -Lo /usr/local/bin/forgejo "https://codeberg.org/forgejo/forgejo/releases/download/v< NEW_VERSION > /forgejo-< NEW_VERSION > -linux-arm64"
sudo chmod +x /usr/local/bin/forgejo
sudo systemctl start forgejo
```