- New: smart-home/home-assistant-dashboard-conventions (Mushroom cards, view tabs, no Bubble Cards) - Updated: rke2, ceph, galera, proxmox, brainstorming, compound-learning, 1password-cli, smart-home-automation skills - New references: ceph-cluster-administration, docker-volume-forensics, ceph-crush-weight, ceph-ec-mixed-size
275 lines
11 KiB
Markdown
275 lines
11 KiB
Markdown
# Gitea Backup CronJob — Session Detail (2026-07-14)
|
|
|
|
## Context
|
|
|
|
After deploying InfluxDB backup CronJob (§4.3i), user requested the
|
|
same backup pattern for Gitea. Unlike InfluxDB (single PVC, single
|
|
backup command), Gitea has two data stores:
|
|
1. **PVC** (`/data`) — repos, LFS, config, attachments, avatars
|
|
2. **External Galera DB** (10.0.30.70:3306) — users, issues, PRs, labels
|
|
|
|
## Design Decisions
|
|
|
|
### tar over `gitea dump`
|
|
- `gitea dump` produces a zip but can miss LFS objects and repos outside
|
|
the default path
|
|
- `tar czf /data` captures everything reliably
|
|
- Trade-off: larger archive (143 MB), but complete
|
|
|
|
### kubectl exec + kubectl cp for PVC access
|
|
- PVC is RWO, mounted by the running `gitea-0` pod
|
|
- Can't mount it in the backup pod (Multi-Attach error)
|
|
- Solution: exec into the running pod to tar, then cp the archive out
|
|
- Requires RBAC: ServiceAccount with pods/exec permission
|
|
|
|
### Separate mysqldump initContainer
|
|
- `mysql:8.0` image has `mysqldump`
|
|
- Credentials from existing `gitea-db` K8s secret (already used by Gitea)
|
|
- `--single-transaction` for consistent dump without locking
|
|
|
|
### S3 credentials reuse
|
|
- Same Norris S3 account as Postgres/InfluxDB backups
|
|
- New ExternalSecret per namespace, same 1Password item keys
|
|
- New bucket: `homelab-gitea-backup`
|
|
|
|
## Manifest Files Created
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `clusters/main/gitea/backup-rbac.yaml` | ServiceAccount + Role (pods/exec, pods/get) + RoleBinding |
|
|
| `clusters/main/gitea/backup-s3-secret.yaml` | ExternalSecret → 1Password `postgres-s3-backup` creds |
|
|
| `clusters/main/gitea/backup-cronjob.yaml` | CronJob: 2 initContainers (files + db) + 1 upload container |
|
|
|
|
Commit: `1679786`
|
|
|
|
## CronJob Spec
|
|
|
|
- **Schedule:** `30 3 * * *` (03:30 UTC — 30 min after InfluxDB backup)
|
|
- **initContainer 1 (gitea-files):** `bitnami/kubectl:1.31` — kubectl exec tar + kubectl cp
|
|
- **initContainer 2 (gitea-db):** `mysql:8.0` — mysqldump against Galera
|
|
- **Container (upload):** `amazon/aws-cli:2.35.22` — aws s3 cp to Norris S3
|
|
- **Shared volume:** emptyDir (passed between all containers)
|
|
- **ServiceAccount:** `gitea-backup` (with RBAC for kubectl exec/cp)
|
|
|
|
## S3 Upload Paths
|
|
|
|
```
|
|
s3://homelab-gitea-backup/<YYYY-MM-DD>/gitea-data.tar.gz
|
|
s3://homelab-gitea-backup/<YYYY-MM-DD>/gitea-db.sql.gz
|
|
```
|
|
|
|
## Execution Results
|
|
|
|
### S3 Bucket Creation — CRITICAL: `--region us-east-1` not `nsc-nbg`
|
|
|
|
Both buckets (`homelab-influxdb-backup` + `homelab-gitea-backup`) were
|
|
created successfully via one-off pods. **CRITICAL PITFALL**: `aws s3 mb`
|
|
with `--region nsc-nbg` fails with `InvalidLocationConstraint: The
|
|
nsc-nbg location constraint is not valid`. Must use `--region us-east-1`
|
|
for bucket creation. The `nsc-nbg` region works for subsequent `aws s3 cp`
|
|
operations but NOT for `s3 mb`.
|
|
|
|
```bash
|
|
# CORRECT — us-east-1 for bucket creation
|
|
kubectl run aws-mb-$bucket --image=amazon/aws-cli:2.35.22 --restart=Never \
|
|
--env=AWS_ACCESS_KEY_ID=$AKI --env=AWS_SECRET_ACCESS_KEY=$SAK \
|
|
--command -- sh -c "aws s3 mb s3://$bucket \
|
|
--endpoint-url https://rgw.nbg.nsc.noris.cloud --region us-east-1"
|
|
```
|
|
|
|
### InfluxDB Backup — VERIFIED SUCCESSFUL ✅
|
|
|
|
InfluxDB backup CronJob ran successfully:
|
|
- 77 shards backed up
|
|
- `backup.tar.gz` = 25.4 MB
|
|
- Uploaded to `s3://homelab-influxdb-backup/2026-07-14/backup.tar.gz`
|
|
- Total time: ~45 seconds
|
|
|
|
### Gitea Backup — BLOCKED by Image Pull Issues
|
|
|
|
The Gitea CronJob requires a kubectl image for the initContainer that
|
|
runs `kubectl exec` + `kubectl cp`. Multiple images failed:
|
|
|
|
1. **`bitnami/kubectl:1.31`** — tag does NOT exist on Docker Hub
|
|
(`failed to resolve reference: docker.io/bitnami/kubectl:1.31: not found`)
|
|
2. **`bitnami/kubectl:1.31.4`** — same NotFound error
|
|
3. **`bitnami/kubectl:latest`** — pulling but extremely slow (Docker Hub rate-limit)
|
|
4. **`alpine/k8s:1.31.4`** — pulling but took 3+ minutes, still Pending
|
|
|
|
The `amazon/aws-cli:2.35.22` and `mysql:8.0` images pull fine (already
|
|
cached from the InfluxDB backup CronJob). Only the kubectl image is
|
|
problematic.
|
|
|
|
### kubectl cp Fails on Large Files
|
|
|
|
Attempting `kubectl cp` of the 143 MB gitea-data.tar.gz from the
|
|
mgmt-runner into an upload pod failed with:
|
|
```
|
|
write tcp 10.0.30.124:35976->10.0.30.51:6443: write: connection reset by peer
|
|
websocket: close 1006 (abnormal closure): unexpected EOF
|
|
```
|
|
|
|
`kubectl cp` uses the K8s API websocket to stream file data. Large files
|
|
(>~50 MB) are unreliable — the websocket connection resets before
|
|
transfer completes. Small files (<1 MB) work fine.
|
|
|
|
### ArgoCD Reports Success But Doesn't Create New Resources
|
|
|
|
When new manifest files (backup-rbac.yaml, backup-s3-secret.yaml,
|
|
backup-cronjob.yaml) were added to the existing `gitea-config` Application's
|
|
directory (`clusters/main/gitea/`), ArgoCD showed:
|
|
- Sync status: `Succeeded`
|
|
- Operation phase: `Succeeded`
|
|
- Message: `successfully synced (all tasks run)`
|
|
- BUT: `kubectl get cronjob -n gitea` returned `No resources found`
|
|
|
|
This is a known ArgoCD behavior with `ServerSideApply=true` — new files
|
|
in an existing directory aren't always detected on the first sync cycle.
|
|
The hard-refresh annotation was applied but still showed `OutOfSync`.
|
|
|
|
**Fix that worked:** Transfer the manifest files to VM200 via base64
|
|
encoding through SSH, then `kubectl apply -f` manually. The CronJob,
|
|
RBAC, and ExternalSecret were all created successfully this way.
|
|
|
|
```bash
|
|
# Transfer files from Hermes host to VM200 via base64
|
|
B64=$(base64 -w0 /path/to/manifest.yaml)
|
|
ssh root@10.0.30.124 "echo '$B64' | base64 -d > /root/iac-homelab/path/to/manifest.yaml"
|
|
ssh root@10.0.30.124 "KUBECONFIG=/root/.kube/config kubectl apply -f /root/iac-homelab/path/to/manifest.yaml"
|
|
```
|
|
|
|
### VM200 Repo Remote Not Updated After Gitea Migration
|
|
|
|
The mgmt-runner's `/root/iac-homelab` repo still pointed at the old
|
|
Gitea on CT108 (`http://...@10.0.30.105:3000/...`). After ArgoCD was
|
|
switched to K8s Gitea, the VM200 repo became stale. New commits pushed
|
|
to K8s Gitea were invisible to VM200.
|
|
|
|
**Fix**: Update the remote URL on VM200 to point at K8s Gitea, or
|
|
transfer files via base64 encoding through SSH.
|
|
|
|
### Manual Backup from mgmt-runner — Successful Workaround
|
|
|
|
When the CronJob pod approach is blocked by image issues, run the backup
|
|
directly from the mgmt-runner (VM200, 10.0.30.124) which has kubectl +
|
|
mysqldump already installed:
|
|
|
|
```bash
|
|
ssh -i ~/.ssh/id_ed25519_proxmox root@10.0.30.124 '
|
|
KUBECONFIG=/root/.kube/config
|
|
set -e
|
|
|
|
GITEA_POD=$(kubectl -n gitea get pod -l app.kubernetes.io/name=gitea \
|
|
-o jsonpath="{.items[0].metadata.name}")
|
|
AKI=$(kubectl -n gitea get secret gitea-s3-backup \
|
|
-o jsonpath="{.data.aws_access_key_id}" | base64 -d)
|
|
SAK=$(kubectl -n gitea get secret gitea-s3-backup \
|
|
-o jsonpath="{.data.aws_secret_access_key}" | base64 -d)
|
|
DB_PASS=$(kubectl -n gitea get secret gitea-db \
|
|
-o jsonpath="{.data.password}" | base64 -d)
|
|
DATE=$(date +%Y-%m-%d)
|
|
|
|
# 1. tar /data from gitea pod
|
|
kubectl -n gitea exec $GITEA_POD -- tar czf /tmp/gitea-data.tar.gz -C / data
|
|
kubectl -n gitea cp $GITEA_POD:/tmp/gitea-data.tar.gz /tmp/gitea-data.tar.gz
|
|
kubectl -n gitea exec $GITEA_POD -- rm -f /tmp/gitea-data.tar.gz
|
|
|
|
# 2. mysqldump against Galera
|
|
mysqldump --host=10.0.30.70 --port=3306 --user=gitea \
|
|
--password="$DB_PASS" --single-transaction \
|
|
--routines --triggers gitea | gzip > /tmp/gitea-db.sql.gz
|
|
|
|
# 3. Upload via one-off pod with emptyDir (avoid hostPath — pods run on
|
|
# different nodes!)
|
|
# See pitfall below about hostPath
|
|
'
|
|
```
|
|
|
|
**Pitfall: hostPath volumes are node-local.** A pod scheduled on
|
|
worker-01 cannot read files from the mgmt-runner's `/tmp` via hostPath.
|
|
The pod lands on whichever node the scheduler chooses. Use `kubectl cp`
|
|
to transfer files into a pod with emptyDir, or use `nodeName:` to pin
|
|
the pod to a specific node (fragile — not recommended for production).
|
|
|
|
**Pitfall: `kubectl cp` to upload pod fails for large files.** The
|
|
143 MB gitea-data.tar.gz caused websocket connection resets. Workaround:
|
|
split the upload into smaller chunks, or use an initContainer that
|
|
generates the data directly (avoiding the cp step entirely).
|
|
|
|
## Recommended Fix for CronJob
|
|
|
|
The CronJob approach needs a kubectl image that can be reliably pulled.
|
|
Options:
|
|
|
|
1. **Pre-pull the image on all worker nodes** — `crictl pull alpine/k8s:1.31.4`
|
|
on each worker, then the CronJob pulls instantly from local cache.
|
|
2. **Use a different kubectl image from a non-Docker-Hub registry** — e.g.
|
|
`registry.k8s.io/kubectl-sidecar` or an internally hosted image.
|
|
3. **Restructure the CronJob to avoid kubectl entirely** — mount the PVC
|
|
read-only (requires PVC to be RWX, or stop the pod first).
|
|
4. **Run backup as a script on the mgmt-runner** via systemd timer or
|
|
crontab — pragmatic but not GitOps-managed.
|
|
|
|
Until the image issue is resolved, the manual mgmt-runner approach
|
|
(above) produces correct backups.
|
|
|
|
## Remaining Steps
|
|
|
|
1. ~~Create S3 bucket `homelab-gitea-backup`~~ ✅ Done
|
|
2. ~~Create S3 bucket `homelab-influxdb-backup`~~ ✅ Done
|
|
3. ~~InfluxDB backup test~~ ✅ Verified (25.4 MB uploaded)
|
|
4. **Fix Gitea CronJob kubectl image** — pre-pull or use alternative
|
|
5. **Gitea backup test run** — verify both files in S3
|
|
6. Monitor first scheduled runs (03:00 + 03:30 UTC)
|
|
|
|
## Pitfalls Summary
|
|
|
|
### `bitnami/kubectl:1.31` tag does NOT exist
|
|
The bitnami/kubectl image uses full semver tags (`1.31.4`, not `1.31`).
|
|
However, even with correct tags, Docker Hub rate-limiting causes
|
|
extremely slow pulls (3+ minutes to Pending). Pre-pull on all workers
|
|
or use an alternative registry.
|
|
|
|
### S3 bucket creation: `--region us-east-1` not `nsc-nbg`
|
|
`aws s3 mb` with `--region nsc-nbg` fails with `InvalidLocationConstraint`.
|
|
Use `--region us-east-1` for bucket creation. The `nsc-nbg` region works
|
|
for subsequent `aws s3 cp` operations.
|
|
|
|
### `kubectl cp` unreliable for large files (>50 MB)
|
|
WebSocket connection resets before transfer completes. Small files
|
|
(<1 MB) work fine. For large file transfers, consider splitting or
|
|
avoiding `kubectl cp` entirely.
|
|
|
|
### VM200 repo remote may be stale after Gitea migration
|
|
After switching ArgoCD from CT108 to K8s Gitea, the mgmt-runner's local
|
|
repo remote still points at the old CT108 URL. New commits are invisible.
|
|
Fix: `git remote set-url origin <new-url>` on VM200, or transfer files
|
|
via base64 through SSH.
|
|
|
|
### hostPath volumes are node-local
|
|
A pod using `hostPath: /tmp` reads from the NODE it's scheduled on, not
|
|
from the machine that created the files. If files are on the mgmt-runner
|
|
but the pod runs on a worker, the files are invisible. Use emptyDir +
|
|
`kubectl cp`, or pin the pod with `nodeName:` (fragile).
|
|
|
|
### RBAC required for kubectl exec/cp
|
|
Without explicit Role granting `pods/exec` + `pods/get`, the backup
|
|
pod's kubectl commands fail with `forbidden`. Must create ServiceAccount,
|
|
Role, and RoleBinding before the CronJob can function.
|
|
|
|
### kubectl cp namespace syntax
|
|
`kubectl cp` requires `namespace/pod:path` format. Missing the
|
|
namespace prefix causes failures when the backup pod and target pod
|
|
are in different namespaces (though typically same namespace here).
|
|
|
|
## Pattern Reuse
|
|
|
|
This pattern (PVC tar via kubectl exec + external DB dump + S3 upload)
|
|
applies to any K8s service with both PVC data and an external database:
|
|
- Gitea (PVC + Galera)
|
|
- Future services with similar dual-data-store architecture
|
|
|
|
For services with only PVC data (no external DB), skip the mysqldump
|
|
initContainer. For services with only an external DB (no PVC), skip
|
|
the kubectl exec initContainer.
|