Gitea ARM64 Setup
I finally nuked my GitHub account after the third “free tier” email threatening to suspend my private repos. The last straw? A CI job failing because GitHub Actions couldn’t spell “ARM64” correctly. So I moved everything to Gitea running on a DGX Spark, here’s exactly how I did it, including the parts that broke first.
Quick Take
- Gitea runs perfectly on ARM64 with Docker when you force the right platform
- SQLite as the database keeps the setup simple and fast
- Tor access works without fighting container networking
- Backups are encrypted and scheduled automatically
The Docker Compose That Actually Starts
gitea:
image: gitea/gitea:1.21.4
platform: linux/arm64
container_name: gitea
environment:
- GITEA__database__DB_TYPE=sqlite3
- GITEA__server__ROOT_URL=http://localhost:3002/
- GITEA__server__HTTP_PORT=3000
- GITEA__server__OFFLINE_MODE=false
volumes:
- /data/gitea:/data
ports:
- "3002:3000"
restart: always
Run docker compose up -d and watch the container start. If it fails with standard_init_linux.go:228: exec user process caused: exec format error, you forgot the platform: linux/arm64 line. That’s the first thing I missed when copying from an x86 tutorial.
Gitea’s ARM64 image exists, but Docker defaults to amd64 unless you tell it otherwise. The error above is Docker’s polite way of saying “this binary won’t run on your CPU.” Add the platform line and try again. For reference, the official ARM64 image is tagged as gitea/gitea:linux-arm64 in some documentation, but the :latest tag now correctly resolves to the ARM64 variant when the platform is specified.
Why SQLite Beats PostgreSQL Here
sqlite3 /data/gitea/gitea.db "SELECT COUNT(*) FROM repo;"
I started with PostgreSQL because that’s what all the tutorials use. Then I noticed Gitea’s ARM64 image ships with SQLite baked in, and the performance difference on a DGX Spark is negligible. SQLite handles 50 repos without breaking a sweat, and the backup is just a single file you can encrypt and ship offsite.
The gotcha? SQLite locks the database during writes. If you run backups while Gitea is active, the backup will fail or corrupt. Schedule backups for 02:00 when no one’s pushing code. You can verify the lock status with:
lsof /data/gitea/gitea.db
If the file is locked, you’ll see gitea in the output.
Tor Access Without Container Hell
docker exec -it gitea bash -c "apt update && apt install -y tor && apt install -y dnsutils"
Tor access works, but only if you install the Tor client inside the container. The official Gitea image doesn’t include it, so you have to add it yourself. After installing, edit /data/gitea/app.ini and add:
[proxy]
PROXY_ENABLED = true
PROXY_URL = http://localhost:9050
Restart Gitea, and your repos become reachable via .onion addresses. The gotcha? The container’s clock must be accurate, or Tor connections fail silently. Add GITEA__server__OFFLINE_MODE=false to keep NTP working. You can verify the time sync with:
docker exec -it gitea date
If the time is off by more than a few seconds, Tor will refuse to connect.
Backups That Don’t Lie to You
tar --exclude='*.pack' -czf /backup/gitea-$(date +%F).tar.gz /data/gitea
I tested two backup strategies: rsync to another machine and encrypted tar archives. The tar method wins because it preserves file permissions and handles SQLite’s lock file gracefully. The exclusion of .pack files is critical, those are Git packfiles that can be regenerated.
The backup script runs daily at 02:00 via systemd timer. The encryption step uses age with a key stored in a hardware security module. If the key is missing, the backup fails loudly instead of silently. Here’s the full script I use:
#!/bin/bash
BACKUP_DIR="/backup"
DATE=$(date +%F)
AGE_KEY_FILE="/etc/age/key.txt"
# Create backup
tar --exclude='*.pack' -czf ${BACKUP_DIR}/gitea-${DATE}.tar.gz /data/gitea
# Encrypt backup
age -e -a -p -i ${AGE_KEY_FILE} ${BACKUP_DIR}/gitea-${DATE}.tar.gz > ${BACKUP_DIR}/gitea-${DATE}.tar.gz.age
# Clean up unencrypted backup
rm ${BACKUP_DIR}/gitea-${DATE}.tar.gz
The systemd timer file looks like this:
[Unit]
Description=Daily Gitea Backup
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
Git Credentials That Don’t Fight You
[credential "http://localhost:3002"]
helper = store
Store your credentials in ~/.git-credentials with the format https://cipherfox:<TOKEN>@localhost:3002. The gotcha? If you use Docker’s internal DNS (like in OpenHands Sandbox), replace localhost:3002 with gitea:3000. The .gitconfig trick with insteadOf saves the day:
[url "http://gitea:3000/"]
insteadOf = http://localhost:3002/
Without this, every push from inside a container tries to hit the host’s loopback instead of the Gitea service. You can verify the configuration with:
git config --global --get-regexp url
What I Actually Use
- Gitea: because GitHub’s ARM64 CI is a joke and I’m done waiting for fixes
- DGX Spark: the only ARM64 server that doesn’t throttle under sustained load
- age encryption: because tar backups need to survive cloud provider outages
Gitea ARM64 Setup
Self-hosted Git on ARM64 with Docker