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.

Before you start

  • Books: T19-L04 · Check Every Change Automatically, Release Versions and Tell the Lab, and T19-L02 · Work on a Shared Gitea Repository From Your Computer With OpenCode for cloning with OpenCode.
  • 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).
  • An authenticator app on your phone, one that shows six-digit codes.
  • 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.

Watch it once (65 seconds)

No sound. A yellow dot points at each part while the caption explains it. Read the tour as text (opens in a new tab).

Worked example: a Gitea the lab can rely on

Structure it

Step 1: Write down who needs what. One line per group of people, before you click anything.

WhoMayTeam
Mia and ITChange everything, including teams and settingsOwners
Jonas and the other scientistsChange protocols through pull requests, publish releasesScientists
Lena and the other studentsRead the protocols, report problems as issuesStudents
NobodyPush directly to main(the rule from T19-L03 stays)

Understand it

Step 2: Match each risk to what protects against it.

What could go wrongWhat protects youSteps
A password is guessed or phishedTwo-factor sign-in, the institute login7 to 12
Someone leaves and keeps accessTeams, the institute login, removing them from the organisation5, 6 and What goes wrong
A repository is deleted by mistakeA mirror on another server13
The server diesA nightly backup, stored somewhere else14
The backup doesn't workA restore test15, 16
You are awayA second owner and a runbook6, 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.

The New Organization form: name rna-lab, visibility Private (Visible only to organization members), and the Create Organization button

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.

The Transfer Ownership dialog: notes on who keeps access, Repository Name qpcr-protocol, New Owner rna-lab, and the red Perform Transfer button

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 Scientists team's settings: All repositories, General Access, and the table with Write for Code, Issues, Pull Requests, Releases, Wiki and Projects, Read for Packages and Actions

(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.

The Scientists team: Repository access All repositories with the Write permissions on the left, and on the right one member, jonas

The repository's Collaborators page: no individual collaborators, and under Teams: Owners, Scientists and Students

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.

Enroll in Two-Factor Authentication: a square code to scan, the secret as text below it, and the Passcode field with Verify

The Security page after enrolling: a green message with the single-use recovery key, and Your account is currently enrolled in two-factor authentication

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.

The Add Authentication Source form: type OAuth2, name CampusID, provider OpenID Connect, client ID and secret, the discovery address, Skip local 2FA ticked, and Additional Scopes email profile

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):

[service]
ALLOW_ONLY_EXTERNAL_REGISTRATION = true

[oauth2_client]
ENABLE_AUTO_REGISTRATION = true
USERNAME = email

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.

Gitea's sign-in page: username and password fields, and below them the button Sign in with CampusID

The institute login page: Log in to Your Account, with Lena's email address and password filled in

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.

Fork Repository to Propose Changes: You cannot edit this repository directly

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.

User Account Management: jonas, lena, mia and site-admin, with the 2FA column showing ticks for jonas, mia and site-admin and a cross for lena

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:

  • Git Remote Repository URL: https://github.com/<your organisation>/qpcr-protocol.git.
  • 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.

Mirror Settings with the form filled in: the target repository's address, Authorization with username and a hidden password, Sync when commits are pushed ticked, and the interval 8h0m0s

The push mirror row: the target address, Push (8h0m0s) and a Last update time, with buttons to edit, synchronise and remove

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.

The copy on the second server: the same files, the last merge of pull request #6 and 28 commits

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:

OpenCode's question Permission required: Access files outside the project directory, D:/oc-book/lab-backups/*, with the buttons Deny, Allow always and Allow once

Click Allow once, or Allow always for the backup folder only.

OpenCode's answer, part 1: the gitea dump command it ran as the git user, the copy of 341,209 bytes on the backup drive with matching checksums, and a table of what is inside

OpenCode's answer, part 2: what the backup does not contain, above all the server's SSH host keys, and two warnings: the backup holds secrets and password hashes, and it was taken while Gitea was running

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.

OpenCode's answer: restore-test.sh restored the newest backup into gitea-restore-test on port 3997; all 22 checks pass, with a table comparing organisation, members, teams, issues, pull requests, releases, tags and branches between the restored server and book-gitea

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.

OpenCode's answer: pushed to main; a table showing push mirrors, webhooks, Actions with runners, tokens, secrets and variables, and mail all OFF, the site name RESTORE TEST, and the settings it checked independently

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.

The restored test server on port 3997: rna-lab/qpcr-protocol with its five merged pull requests, each with its author, branch and approval; the Actions tab is missing because Actions are off here

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.

OPERATIONS.md in rna-lab/gitea-operations, next to backup-gitea.sh and restore-test.sh: Where it runs, People (mia and site-admin as admins and owners; jonas with password and two-factor; lena through CampusID only), Signing in, and Nightly backup

The rest of the runbook: the two restore-test commands and the last successful run on 2026-09-25 with 22 of 22 checks matched, six steps for when someone leaves, eight steps before an upgrade, and Who to ask

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)

  1. Structure it (15 min): your table of who needs what, as in step 1. Count the groups: that's your number of teams.
  2. Understand it (10 min): your risk table, as in step 2. Mark which risks you cover today.
  3. 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.
  4. 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.

WhatDetailsWhat it means for you
VersionsGitea 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 settingsEvery 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 teamsAn 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 mirrorMinimum 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.
Backupgitea 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.
RestoreInto 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.
UpgradesRead 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.
GitHubOrganisations 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.

Further reading and tools