Run Your Organisation's Gitea: Teams, Safe Sign-In and a Restore You Have Tested | Heidelberg AI Curriculum
T19-L05
Run Your Organisation's Gitea: Teams, Safe Sign-In and a Restore You Have Tested
Move the lab's repositories into an organisation with teams, add two-factor and institute sign-in, mirror to GitHub, back up the whole Gitea, prove the restore on a separate test server, and hand over with a one-page runbook.
T19-L05 · Share & review code · Level 5 Operator · 22-minute read, plus 3 hours of doing
Almost what you need
Since T19-L04 · Check Every Change Automatically, Release Versions and Tell the Lab, the qPCR protocol checks itself and announces every release. But it still lives in Mia's personal account. Jonas has access because Mia added him by hand. Lena, a new student, starts in October: she should read the protocols, not change them. A password is the only lock. And when the head of the lab asks "If this server dies tonight, what do we lose?", nobody can answer: IT makes backups, but nobody has ever restored one.
In this book you become the person who runs the lab's Gitea. You move the work into an organisation with teams, make sign-in safe, keep copies somewhere else, prove that a restore works, and write it all down so someone else can take over. You are at Level 5 Operator: others depend on this Gitea, so it has to keep working without you.
After this you can
Move repositories into an organisation and give access through teams instead of person by person.
Turn on two-factor sign-in, and connect the institute login so people sign in with the account they already have.
Check who can sign in how, and who still has no second factor.
Keep an automatic copy of a repository on another server, such as GitHub.
Back up the whole Gitea, restore it on a separate test server, and hand the work over with a one-page runbook.
Administrator rights on your Gitea, or IT next to you. Steps marked (administrator) need them; the rest needs only your own account.
Access to the server Gitea runs on (for steps 9, 14 and 15), for you or for IT, and for the restore test a second machine with Docker or Podman, the programs that run Gitea in a container (a sealed-off box for one program).
For the institute login: from IT, a client ID, a client secret and a discovery address. They come from the institute's login system (Microsoft Entra ID, Keycloak and others).
on your phone, one that shows six-digit codes.
An authenticator app
Somewhere for copies: a GitHub account (or a second Git server), and a backup drive or network share.
About 3 hours 20 minutes: 22 to read, 3 hours to do.
Last verified 25 September 2026. Everything in this book is free and built into Gitea. Gitea can require two-factor sign-in for everyone, but that includes people who sign in through the institute login, so this book checks instead of requiring it. Details for experts at the end of this book.
The idea in one page
An organisation owns repositories instead of a person. People come and go; the organisation stays, and so do its repositories, rules and history.
Teams hold the permissions. A team says, for each part of a repository (code, issues, pull requests, releases and more): no access, read or write. You put people into teams; you don't give access repository by repository.
Owners can change everything, including the teams. Keep at least two, so the lab isn't locked out when one is away.
A password alone is one lock.Two-factor sign-in adds a six-digit code from your phone. Single sign-on lets people use the institute login, which usually has its own second factor; when someone leaves, IT switches off one account.
A mirror copies one repository to another server after every push. It protects the files and their history, not the issues, pull requests or settings.
A backup holds everything: repositories, the database with issues, pull requests, users and settings, and uploaded files. A backup you have never restored is a hope, not a backup.
A runbook is one page that lets someone else do all of this without you.
Step 1: Write down who needs what. One line per group of people, before you click anything.
Who
May
Team
Mia and IT
Change everything, including teams and settings
Owners
Jonas and the other scientists
Change protocols through pull requests, publish releases
Scientists
Lena and the other students
Read the protocols, report problems as issues
Students
Nobody
Push directly to main
(the rule from T19-L03 stays)
Understand it
Step 2: Match each risk to what protects against it.
What could go wrong
What protects you
Steps
A password is guessed or phished
Two-factor sign-in, the institute login
7 to 12
Someone leaves and keeps access
Teams, the institute login, removing them from the organisation
5, 6 and What goes wrong
A repository is deleted by mistake
A mirror on another server
13
The server dies
A nightly backup, stored somewhere else
14
The backup doesn't work
A restore test
15, 16
You are away
A second owner and a runbook
6, 17, 18
Choose tools
Gitea's organisations and teams: built in, no extra software.
An authenticator app, and the institute login connected through OpenID Connect, the standard most login systems speak.
A push mirror to GitHub or any other Git server.
Gitea's own backup command, run by OpenCode, and a separate test Gitea for the restore.
Build it: one home for the lab's work
Step 3: Create the organisation. Click + at the top right, then New Organization. Organization Name:rna-lab. Visibility:Private, so only members see it. Leave the Permissions box as it is. Click Create Organization.
You should see: the organisation's page. Gitea has created a team Owners, with you in it.
Step 4: Move the repository in. Open qpcr-protocol, then Settings. At the bottom, under Danger Zone, click Transfer Ownership. Type the repository name to confirm, enter rna-lab as New Owner, and click Perform Transfer.
You should see: the repository at its new address, …/rna-lab/qpcr-protocol. Its history, issues, pull requests, protection rule, checks, webhook, secret and release all moved with it. The old address still works: Gitea forwards it, for the browser and for clones. Update your clones when convenient: ask OpenCode to change the remote to the new address.
Step 5: Create the teams. In the organisation, click Teams, then New Team. Team Name:Scientists, with a one-line description. Visibility:Limited. Repository access:All repositories, so future repositories are included too. Permission:General Access, then in the table: Write for Code, Issues, Pull Requests, Releases, Wiki and Projects; Read for Packages and Actions. Click Create Team.
(The picture shows the finished team's Settings; when you create it, the button at the bottom says Create Team.)
Then create Students the same way, with Read everywhere. With All repositories, Students can also read gitea-operations from step 14; it holds scripts and the runbook, no passwords. If that's too much, choose Specific repositories for Students. Write access to code still doesn't let anyone push to main: the protection rule from T19-L03 applies to every team.
Step 6: Put people into teams, and remove direct access. Open Scientists, type jonas next to Add team member and click it. Then, in the repository, Settings → Collaborators: remove Jonas under Collaborators, where you added him in T19-L02.
You should see: no individual collaborators, three teams. From now on, access means team membership, and one look at Teams shows who can do what.
Finally, open Owners and add a second person, so the organisation never depends on you alone. In our pictures that's IT's account, site-admin.
Build it: sign-in you can trust
Step 7: Turn on two-factor sign-in for yourself. Click your picture → Settings → Security → Enroll in Two-Factor Authentication. Scan the square code with your authenticator app, type the six digits it shows, and click Verify.
You should see: a green message with a recovery key. Store it in your password manager now; it's shown only once and lets you in if you lose your phone. From now on Gitea asks for the six digits after your password.
Step 8 (administrator): Connect the institute login. First, IT registers Gitea in the institute's login system with this return address: https://<your Gitea>/user/oauth2/CampusID/callback. They give you the client ID, the client secret and the discovery address. Then, in Gitea, click your picture → Site Administration → Identity & Access → Authentication Sources → Add Authentication Source:
Authentication Type: OAuth2. Authentication Name:CampusID, or your login's name, without spaces. It appears on the sign-in button and in the return address.
OAuth2 Provider: OpenID Connect. Then the Client ID, the Client Secret and the OpenID Connect Auto Discovery URL from IT.
Tick Skip local 2FA only if IT confirms that the institute login always asks for a second factor. Otherwise leave it unticked. (When you edit the source later, this box sits right under the name.)
Additional Scopes:email profile, so Gitea learns the person's email address and name.
Click Add Authentication Source.
Step 9 (administrator): Let new people in only through the institute login. On the server, add these lines to Gitea's settings file app.ini and restart Gitea (IT knows where it is; Details for experts has the Docker form):
The first setting closes Gitea's own sign-up form. The others create an account automatically the first time someone signs in with the institute login, named after the part of their email address before the @.
Step 10: Lena signs in.(Lena's screen.) On Gitea's sign-in page she clicks Sign in with CampusID, and signs in on the institute's page.
You should see: Lena lands on her own Gitea start page, with the account lena created. (Mia's screen.) Mia adds lena to Students, as in step 6.
Step 11: Check it as Lena.(Lena's screen.) She opens protocol.md in rna-lab/qpcr-protocol and clicks the pencil.
You should see:You cannot edit this repository directly. Lena can read everything, open issues and comment, but she can't change the protocol. That is the Students team working.
Step 12 (administrator): Check who has a second factor.Site Administration → Identity & Access → User Accounts.
You should see: a tick in 2FA for every account that signs in with a password. To see how someone signs in, click the pencil at the end of the row: Authentication Source says Local (a password) or CampusID. Ask every Local account without a tick to do step 7. Lena's cross is fine: she signs in only through CampusID, which asks for her second factor.
Build it: copies you can rely on
Step 13: Mirror the repository to GitHub. First check with your data protection officer that the repository may be copied to GitHub. On GitHub, create an empty private repository (no README). Then create a token: your picture → Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token. Resource owner: the organisation that owns the repository; Repository access: only that repository; Permissions:Contents: Read and write. Note its expiry date. Some organisations must approve a token before it works. In Gitea, open qpcr-protocol → Settings; under Mirror Settings:
Open Authorization: your GitHub username, and the token as Password.
Tick Sync when commits are pushed; keep the interval at 8h0m0s.
Click Add Push Mirror, then the round arrows (Synchronize Now) in the new row.
You should see: a time under Last update, and on the other server the same files and the same 28 commits. In our pictures a second Gitea stands in for GitHub; the form is the same. The mirror makes the copy identical to Gitea, deleting whatever differs, so nobody should ever change the GitHub copy directly.
Step 14: Back up the whole Gitea with OpenCode. A mirror copies one repository's files; the backup copies everything. Whoever may work on the server does this, you or IT. In Gitea, create the repository gitea-operations in rna-lab (+ → New Repository, owner rna-lab, private, with a README), then clone and open it in OpenCode as in T19-L02. In a new session, send the prompt below, with two changes. Replace the first sentence with where your Gitea runs, for example Our Gitea runs in the Docker container gitea on the server lab-git; I can reach it with ssh lab-git. And replace D:\oc-book\lab-backups with a network share or drive that is not on the Gitea server and that few people can read. Backup files never go into a repository.
Our lab's Gitea runs in the Podman container book-gitea on this computer. Only touch that container; leave every other container alone.
1. Make a full backup with Gitea's own command (gitea dump, run as the git user inside the container).
2. Copy the backup file out of the container into D:\oc-book\lab-backups, with the date in its name. That folder stands for our backup drive.
3. Tell me what the backup contains, how big it is, and what it does not contain.
4. Write the steps as a script backup-gitea.sh in this repository, so IT can run it every night. Don't commit yet.
OpenCode asks before it touches files outside the project:
Click Allow once, or Allow always for the backup folder only.
You should see: the backup file on the backup drive, and a list of what's in it and what isn't. Read the second part carefully. In our run OpenCode found that the server's SSH host keys are missing (without them, every colleague's git push warns after a restore), and that the backup contains password hashes and secrets, so the backup drive must be readable by few people. It also notes that the backup was taken while Gitea was running. Gitea's documentation recommends stopping it during the backup; for a small lab, a nightly run at a quiet hour is the usual compromise, and the restore test shows whether it worked.
Last, ask IT to run backup-gitea.sh every night. The next morning, check that a new file with that day's date arrived on the backup drive.
Step 15: Test the restore. Change the names to yours, and the version to the one your live Gitea runs (Site Administration shows it at the bottom). Ideally the test server runs on another machine than the live one; in our pictures both run on one computer. Send:
First add the server's SSH host keys to backup-gitea.sh, as you suggested, and run it once more.
Then prove the backup works: start a second, separate Gitea from the newest backup file. Use a new Podman container named gitea-restore-test, Gitea 1.27.2, on port 3997 of this computer. Don't stop or change book-gitea.
When it runs, check through its API, as mia, that the organisation rna-lab is there with its repositories, issues, pull requests and releases, and compare the numbers with book-gitea. Tell me the address so I can sign in.
Write the steps as restore-test.sh in this repository. Don't commit yet.
You should see: a table in which every number from the restored server matches the live one, and an address to sign in. OpenCode checks with Gitea's own admin commands inside the test server; it never needs your password or codes, so never type them into OpenCode. In our run it gave Mia a new password on the test server only, and asked how to handle two-factor there; we kept it on.
Step 16: Make the test server harmless. A restored copy is a full Gitea: it came back with the push mirror, the webhook to the lab chat, the Actions secret and the mail settings of the live server. Before anyone clicks around on it, send:
The restored copy also brought back the live server's push mirror, the webhook to the lab chat, Actions with their secrets, and the mail settings. Left running, it could push old data over our copy on the other server or post in the chat.
On the test server only, switch off mirroring, webhooks, Actions and mail before it starts, and make it look different, so nobody mistakes it for the live Gitea (for example the site name RESTORE TEST).
Put this into restore-test.sh, run the restore test again, and show me that each of them is off. Then commit and push restore-test.sh with the message "Keep the restore test from pushing, posting or mailing".
If Git reports a conflict, stop: don't resolve it yourself. Show me both versions of the lines and who wrote each.
You should see: every line OFF, and the site name RESTORE TEST. In our run the first comparison failed: the live server had one more member than the backup, because we had added the second owner after the backup was taken. OpenCode made a fresh backup and all 22 checks matched. A mismatch can mean the backup is older than today's changes; that's the check working.
Now sign in at the test address in your browser (with your usual code; never type passwords or codes into OpenCode) and look around, as a colleague would.
You should see: the same pull requests and releases as on the live server, and no Actions tab. Only now do you have a backup. When you're done, remove the test server (./restore-test.sh --teardown).
Step 17: Write the runbook, and hand over. Send:
Write OPERATIONS.md for whoever runs this Gitea after me, one page, in plain words:
where it runs; who the owners and administrators are (mia, and IT's account site-admin); how people sign in (CampusID for new people, two-factor for password accounts);
the nightly backup and where the copies go (D:\oc-book\lab-backups for the backup, the push mirror for the repository); how to run the restore test, and that we last ran it successfully today;
what to do when someone leaves; and what to check before an upgrade.
Then commit backup-gitea.sh, restore-test.sh and OPERATIONS.md with the message "Runbook, backup and restore test for our Gitea" and push to main.
If Git reports a conflict, stop: don't resolve it yourself. Show me both versions of the lines and who wrote each.
You should see: the push to main, and the three files in rna-lab/gitea-operations.
Step 18: Read it as your successor would. Our first version ran to 165 lines; we asked for one printed page, about 400 words: keep every instruction and fact, drop explanations. The result is short but uses IT words (exit 3, image); if your successor isn't from IT, ask for plain words instead. Then read it line by line against what you know. Ours said Lena had a password without two-factor; she has none and signs in only through CampusID, so we asked OpenCode to correct the line. Fill in Who to ask yourself, with names and phone numbers, and keep a printed copy: when Gitea is down, nobody can read the runbook inside it.
You should see: a page someone else could follow tonight. One last thing: put the next restore test in the calendar, three months from today.
Company: the onboarding checklist
Step 1: Owners are two people from IT; HR writes (the onboarding checklist and templates); All staff reads.
Steps 3 to 6: an organisation people-ops; HR's repositories move in; direct access is removed.
Steps 8 to 12: sign-in through the company's Microsoft Entra ID; only the two IT owners keep password accounts, both with two-factor sign-in.
Steps 13 to 18: a mirror to the company's GitHub organisation; IT's nightly backup, a restore test every quarter, and the runbook in the IT wiki's repository.
What goes wrong
The CampusID button ends in an error.Symptom: after the institute login, Gitea shows redirect_uri did not match, or returned missing fields: email, unable to create an account automatically. Fix: the first means the source's name contains a space or IT registered a different return address: use a name without spaces and give IT the exact address from step 8. The second means Additional Scopes lacks email profile.
A colleague with a password account clicks the CampusID button.Symptom: Jonas, who already has an account, sees Register New Account and Link to Existing Account. Fix: choose Link to Existing Account and sign in once with the old password and code. From then on the CampusID button signs him in. Registering a new account would give him a second, empty one.
Someone lost their phone and their recovery key.Symptom: they can't get past the six-digit code. Fix: an administrator opens User Accounts, clicks the pencil next to the person and clicks Reset 2FA. Check it's really them first (in person or by phone), then they do step 7 again.
Scripts stop working after you turn on two-factor.Symptom: a script or tool that signed in with your password is refused. Fix: with two-factor on, only an access token works there: Settings → Applications → Generate Token. Git over SSH isn't affected.
Someone left but can still push.Symptom: IT switched off their institute account, yet pushes from their laptop still arrive. Fix: SSH keys and tokens don't pass through the institute login. Remove the person from the organisation (Members → Remove), and as administrator tick Disable Sign-In in their account. Their next push is refused with Your account is disabled.
The mirror can't be added, or stops.Symptom:You cannot import from disallowed hosts, or Last update gets older and older. Fix: a server inside your network must be allowed by the administrator (Details for experts). An expired GitHub token stops the mirror silently: put its expiry date in the runbook.
After a restore, SSH warns that the host has changed.Symptom:git push stops with REMOTE HOST IDENTIFICATION HAS CHANGED. Fix: the server's SSH host keys weren't restored. Put back the copy that step 14 adds to the backup; without it, everyone has to accept the new key once.
Do it yourself (3 hours)
Structure it (15 min): your table of who needs what, as in step 1. Count the groups: that's your number of teams.
Understand it (10 min): your risk table, as in step 2. Mark which risks you cover today.
Choose tools (10 min): ask IT for the three values for the institute login (step 8), and decide where copies go: GitHub for the mirror, a backup drive for the backup.
Build it (145 min): steps 3 to 6 for one repository first, then the rest; steps 7 to 12; steps 13 to 18. Plan the restore test (steps 15 and 16) for a quiet afternoon.
Exit check
A colleague who has never done it restores last night's backup on a test server using only your runbook, signs in there and finds the organisation with its repositories, issues and pull requests. It passes when, in addition, every password account shows a tick under 2FA, a student account can read but not edit, and the mirror's Last update is less than a day old.
Rule to remember
A backup counts only after someone else has restored it from your notes.
Details for experts
Checked on 25 September 2026. You don't need this to follow the book; it's for setting things up or answering a colleague.
What
Details
What it means for you
Versions
Gitea 1.27.2, OpenCode Desktop 2.0.16. In the pictures, Dex (a small OpenID Connect server) stands in for CampusID, and a second Gitea stands in for GitHub.
Your login page looks different; Gitea's side is the same.
Docker settings
Every app.ini line can also be an environment variable, e.g. GITEA__service__ALLOW_ONLY_EXTERNAL_REGISTRATION=true.
IT may run Gitea this way; the effect is the same.
Requiring two-factor
[security] TWO_FACTOR_AUTH = enforced makes every account set up Gitea's two-factor, institute-login accounts included: they see You must set up two-factor authentication.
Use it only without an institute login.
Groups to teams
An authentication source can read a group claim and map groups to teams (Map claimed groups to Organization teams). LDAP connects Active Directory directly.
IT's group membership can decide team membership.
Push mirror
Minimum interval 10 minutes. Servers inside your network need [migrations] ALLOW_LOCALNETWORKS or ALLOWED_DOMAINS. Pull mirrors come from New Migration.
The GitHub token needs Contents: read and write for that one repository.
Backup
gitea dump, run as the git user, writes one zip: the database as SQL, repositories, app.ini, uploaded files, Actions logs. Not included: the SSH host keys (/data/ssh in Docker) and console logs. Gitea's documentation recommends stopping Gitea during the dump.
Backups contain password hashes and secrets: store them where few people can read them.
Restore
Into a fresh Gitea of the same version: the database from gitea-db.sql, repos/ and data/ into place, then gitea admin regenerate hooks. No single restore command exists.
That's why the restore is a tested script.
Upgrades
Read the release notes, back up, then upgrade. Afterwards check Site Administration → System Notices and the log for deprecation warnings.
The runbook's before an upgrade list.
GitHub
Organisations and teams; Require two-factor authentication per organisation; single sign-on (SAML) on Enterprise plans; no one-click full backup.
GitHub can require two-factor per organisation; Gitea only for the whole server.