← Back to Blog

Gitea 28 Is Out: What Changed, How to Upgrade Safely, and Gitea vs Forgejo

Published · by RS Computers

Gitea Forgejo Self-hosting

Gitea 28 is the release that follows 1.27. The project dropped the "1." from its version numbers, so 28.0.0 (30 September 2026) is what would have been called 1.28.0, and 28.1.0 (6 October 2026) is the version to install today. If you run any 1.x release, plan the Gitea upgrade now: the two 28 releases fix 29 security vulnerabilities between them, and Gitea only patches its newest major release. The upgrade itself is a backup, a binary or image swap and a restart. In our lab, going from 1.27.3 to 28.1.0 on PostgreSQL ran 15 database migrations, and Gitea was answering again 5.8 seconds later. Below are the runbook and the rollback we tested, a Docker tag that quietly keeps you on 1.27, and what changes if you would rather move to Forgejo.

Key facts, checked on 12 October 2026:

What changed in Gitea 28, in plain words

The new number is the smallest change. Self-Host Weekly wrote that Gitea "pulled a Java" (selfh.st, 2 October 2026), the way Java went from 1.8 to 9. The first number now counts major releases: 28.1.0 is a maintenance release of the 28 line, and the next major will be 29.

The new features most admins will notice are an audit log of security events (off until you set [audit] RECORD_OUTPUT = database), bot accounts that only use tokens, deploy tokens that act as an HTTPS password for one repository, a way for admins to view the site as another user, required approval from CODEOWNERS, and an Actions queue page with in-browser previews of build artifacts.

Read the breaking changes before you upgrade. The notes flag five of them; the last two rows below are not flagged but can still trip you up:

ChangeWhat happensWhat to do first
New egress rules (#39426)Migrations and mirrors connect through an internal proxy that checks every address. ALLOWED_DOMAINS, BLOCKED_DOMAINS and ALLOW_LOCALNETWORKS are deprecated, the external preset is gone, and an invalid BLOCKED_HOST_LIST entry stops startup.Rewrite the lists as ALLOWED_HOST_LIST and BLOCKED_HOST_LIST, plus EGRESS_MODE = strict if only listed hosts may be used.
Actions history expires (#38855)Finished runs, logs and artifacts are deleted after 400 days.Set [actions] RUN_RETENTION_DAYS = 0 beforehand to keep everything.
Git 2.25 or newer (#39131)Gitea will not start on an older Git.Run git --version. Debian 13 ships 2.47.3.
Sign-up closed, DOMAIN ignored (#39400)Self-registration is off unless DISABLE_REGISTRATION = false is written out. The domain comes only from ROOT_URL.Set ROOT_URL. Reopen sign-up only if strangers should register.
Stricter workflows (#39358)A job-level if: is checked before the matrix expands, and fail-fast is enforced.Move matrix conditions into include/exclude or step-level if:.
WebSocket notifications (#36965)Live counters use /-/ws instead of server-sent events.Let your reverse proxy pass WebSocket upgrade headers.
DownloadsNo 32-bit x86 builds, and file names lost their OS version suffix.Fix download scripts.

Who has to upgrade now

Everyone on 1.x, because 1.27 no longer gets security fixes. Upgrade today if one of these describes your server:

We tested 1.27.3 straight to 28.1.0, with no stop at 28.0.0. If you are further behind, read every release post in between and rehearse on a copy first.

Our lab: what we upgraded and what we measured

We used a Debian 13 virtual machine with 2 vCPU and 2 GB of RAM in our Amsterdam lab. Gitea 1.27.3 ran from the official binary under systemd, with PostgreSQL 17.11 from Debian. Through the API, plus one git push, we created a user, alice, and a private repository with 7 commits, a label, a milestone, an issue with a comment, a release and a wiki page. A small script recorded the same fingerprint before and after each step: version, login, commit count, head commit, issue, release, wiki text and database schema version.

StepResult in our lab
Backup with gitea dump0.33 seconds, a 58 KB archive
Backup with pg_dump0.6 seconds, 710 KB
First start of 28.1.0Answering after 5.8 seconds; 15 migrations, schema 343 to 358
Data after the upgradeIdentical fingerprint: commits, issue, comment, release, wiki and password login
gitea doctor check --all13 of 13 checks OK
MemoryGitea 178 MB on 1.27.3, about 170 MB on 28.1.0; whole VM with PostgreSQL 495 MB
Full rollback to 1.27.36 seconds
Docker with SQLite, 1.27.3 to 28.1.0Answering after 2.2 seconds, 110.5 MiB of RAM

Our data is tiny, so treat these as the fixed cost of the procedure. Large instances take longer, and Gitea's upgrade guide warns that the site is down during the first start.

The upgrade runbook for a binary install

Paths are the defaults of Gitea's binary install: /usr/local/bin/gitea, data in /var/lib/gitea, configuration in /etc/gitea/app.ini, database giteadb.

Step 1: find what will break before it breaks

# as root
git --version
grep -nE "ALLOWED_DOMAINS|BLOCKED_DOMAINS|ALLOW_LOCALNETWORKS|HOST_LIST|^DOMAIN|DISABLE_REGISTRATION|RETENTION_DAYS|EVENT_SOURCE_UPDATE_TIME" /etc/gitea/app.ini
journalctl -u gitea --no-pager -o cat | grep -i deprecation | sort -u

Hold each line the grep finds against the table above. We added old-style egress settings to our test configuration to see the warnings. Gitea 1.27.3 only warned that [webhook].ALLOWED_HOST_LIST "will be/has been removed in v28.0.0"; the warnings about [migrations] keys appeared only once 28.1.0 ran. The grep is the more reliable check.

Step 2: stop Gitea and take two backups

# as root
mkdir -p /var/backups/gitea
chown git:git /var/backups/gitea && chmod 700 /var/backups/gitea
systemctl stop gitea
su - git -c "cd /var/lib/gitea && gitea dump -c /etc/gitea/app.ini --type tar.gz --file /var/backups/gitea/gitea-dump-1.27.3.tar.gz"
su - postgres -c "pg_dump -Fc giteadb" > /var/backups/gitea/giteadb-1.27.3.pgdump
cp -p /usr/local/bin/gitea /var/backups/gitea/gitea-1.27.3
cp -p /etc/gitea/app.ini /var/backups/gitea/app.ini.1.27.3
ls -lh /var/backups/gitea

gitea dump packs the repositories, data, configuration and an SQL copy of the database. The pg_dump file is what you restore the database from, because Gitea's backup docs suggest native database tools (mysqldump on MySQL) and warn that restoring its own SQL dump still has open issues. The old binary and app.ini are your way back. Copy the folder off the server too, for example with our restic offsite backup guide.

Step 3: download 28.1.0 and check the signature

# as root
cd /root
wget https://dl.gitea.com/gitea/28.1.0/gitea-28.1.0-linux-amd64
wget https://dl.gitea.com/gitea/28.1.0/gitea-28.1.0-linux-amd64.asc
gpg --keyserver hkps://keys.openpgp.org --recv 7C9E68152594688862D62AF62D9AE806EC1592E2
gpg --verify gitea-28.1.0-linux-amd64.asc gitea-28.1.0-linux-amd64

Look for Good signature from "Teabot <teabot@gitea.io>". A warning that the key is not certified is normal; BAD signature means stop.

Step 4: swap the binary and watch the migrations

# as root
install -m 755 gitea-28.1.0-linux-amd64 /usr/local/bin/gitea
gitea --version
systemctl start gitea
journalctl -u gitea --since "10 min ago" --no-pager -o cat | grep -E "Migration|\[W\]|\[E\]"

Expect gitea version 28.1.0 built with go1.27.1, then the database steps from Migration[343]: Add max_parallel column to action_run_job to Migration[357]: Normalize legacy team authorize values. Keep the file name gitea, as the install guide asks, and let the first start finish: killing it with -9 can interrupt internal tasks.

Step 5: prove nothing was lost

# as root
curl -s http://127.0.0.1:3000/api/v1/version
curl -s -u alice:ALICE_PASSWORD http://127.0.0.1:3000/api/v1/user | jq -r .login
curl -s -u alice:ALICE_PASSWORD http://127.0.0.1:3000/api/v1/repos/alice/demo/branches/main | jq -r .commit.id
su - git -c "cd /var/lib/gitea && gitea doctor check --all -c /etc/gitea/app.ini"

Use your own account and repository, and note the values before you upgrade. Ours returned {"version":"28.1.0"}, alice and the old commit ID, and the doctor ended with All done (checks: 13). Push and clone worked too. One visible change: the sign-up page now said "Registration is disabled. Please contact your site administrator.", because our configuration never set DISABLE_REGISTRATION.

Step 6: move old egress settings to the new names

With our legacy settings, 28.1.0 started but logged [E] lines, one saying the "external" builtin "was replaced by EGRESS_MODE = lax" and was ignored. We replaced the old [migrations] and [webhook] lines in /etc/gitea/app.ini with:

# edited as root in /etc/gitea/app.ini
[migrations]
EGRESS_MODE = strict
ALLOWED_HOST_LIST = github.com, codeberg.org

After systemctl restart gitea those errors were gone. A migration from GitHub worked in 2.3 seconds, and a host outside the list was refused:

# as root
curl -s -u alice:ALICE_PASSWORD -X POST http://127.0.0.1:3000/api/v1/repos/migrate -H "Content-Type: application/json" -d '{"clone_addr":"https://gitlab.com/gitlab-org/gitlab-test.git","repo_name":"hello-gitlab","repo_owner":"alice","service":"git"}' | jq -c '{full_name, message}'

Output: {"full_name":null,"message":"You can not import from disallowed hosts."}. Without strict, the default lax mode allows every public host and blocks only private ranges.

Rolling back: the old binary alone won't do it

We put the 1.27.3 binary back on the upgraded database. It refused to run, and systemd kept restarting it in a loop:

# printed by Gitea 1.27.3 in journalctl, not a command
[F] Migration Error: Your database (migration version: 358) is for a newer Gitea, you can not use the newer database for this old Gitea release (343).

A rollback means restoring the backups from step 2. This is the sequence we ran:

# as root
systemctl stop gitea
su - postgres -c "dropdb giteadb"
su - postgres -c "createdb -O gitea -T template0 -E UTF8 --locale=en_US.UTF-8 giteadb"
su - postgres -c "pg_restore -d giteadb" < /var/backups/gitea/giteadb-1.27.3.pgdump
mv /var/lib/gitea/data /var/lib/gitea/data.failed-upgrade
mkdir /root/restore
tar -xzf /var/backups/gitea/gitea-dump-1.27.3.tar.gz -C /root/restore
mv /root/restore/data /var/lib/gitea/data
mv /root/restore/repos /var/lib/gitea/data/gitea-repositories
chown -R git:git /var/lib/gitea/data
cp /var/backups/gitea/gitea-1.27.3 /usr/local/bin/gitea
systemctl start gitea
su - git -c "cd /var/lib/gitea && gitea admin regenerate hooks -c /etc/gitea/app.ini"

It took 6 seconds, and the fingerprint matched the pre-upgrade one, schema 343 included. On its next restart Gitea logged ten [W] lines comparing column defaults, such as db default is false, struct default is FALSE. They were warnings only, and Gitea kept running. The commit we had pushed after the upgrade was gone, as expected, so announce a short freeze and decide quickly.

Docker: the :1 tag never reaches 28

Many compose files pin a major version, which under the old scheme was gitea/gitea:1. On Docker Hub, the 1 and 1.27 tags were last updated on 29 August 2026 and point to 1.27.3, while 28 and latest point to 28.1.0 (tag list). We started docker.gitea.com/gitea:1, ran docker compose pull, and stayed on {"version":"1.27.3"}. Because the tag itself stays on 1.27.3, an auto-update tool that follows :1 has nothing newer to pull either. Change the tag on purpose, after a backup:

# as root, in the folder with compose.yaml (ours: /srv/gitea, volume ./gitea)
docker exec -u git -w /tmp gitea gitea dump -c /data/gitea/conf/app.ini --type tar.gz --file /tmp/gitea-dump.tar.gz
docker cp gitea:/tmp/gitea-dump.tar.gz /root/gitea-dump-1.27.3.tar.gz
docker exec gitea rm /tmp/gitea-dump.tar.gz
docker compose stop
tar -czf /root/gitea-volume-1.27.3.tar.gz -C /srv/gitea gitea
sed -i 's#docker.gitea.com/gitea:1$#docker.gitea.com/gitea:28.1.0#' compose.yaml
docker compose pull
docker compose up -d
curl -s http://127.0.0.1:3001/api/v1/version

Our compose file maps the web port to 3001. On our upgrade run the dump went to /data and failed with "permission denied", because the git user cannot write there, so the volume tar was our only backup that time. The /tmp version above, which we ran afterwards on the upgraded container, produced a complete archive. The new container answered after 2.2 seconds, ran the same 15 migrations, kept every commit and used 110.5 MiB of RAM; it bundles Git 2.54.0. Pin :28 from now on to receive 28.x updates. Our Docker on a VPS guide covers the basics.

Moving to Forgejo instead

Forgejo began as a Gitea fork in October 2022 and became a hard fork in early 2024 (Forgejo's comparison), and the database schemas have drifted apart since. A Forgejo blog post from December 2024 is titled "Gitea 1.22 is the last version to allow a transparent upgrade to Forgejo" (Forgejo blog), and the current upgrade guide still names 1.22 as the limit. We checked it anyway: Forgejo 16.0.5's forgejo migrate, pointed at a copy of our Gitea 28 database, stopped with:

# printed by forgejo migrate, run as the git user, not a command to copy
[F] Migration Error: Your database (migration version: 358) is for a newer Forgejo, you can not use the newer database for this old Forgejo release (305).

The safe path is to move repositories one by one with Forgejo's migration tool, which the same blog post expects to keep working as long as Gitea's API stays stable. Install Forgejo on a new server first, as in our Forgejo Git server guide.

Make a read-only token on Gitea

# as the repository owner (alice), on any machine that reaches your Gitea
curl -s -u alice:ALICE_PASSWORD -X POST https://git.example.com/api/v1/users/alice/tokens -H "Content-Type: application/json" -d '{"name":"migrate","scopes":["read:repository","read:issue","read:user","read:misc"]}' | jq -r .sha1

Pull the repository into Forgejo

# as root, on the Forgejo server
curl -s -X POST http://127.0.0.1:3000/api/v1/repos/migrate \
  -H "Authorization: token FORGEJO_ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d '{"clone_addr":"https://git.example.com/alice/demo.git","service":"gitea","auth_token":"GITEA_READ_TOKEN","repo_owner":"OWNER","repo_name":"demo","private":true,"wiki":true,"issues":true,"labels":true,"milestones":true,"releases":true,"pull_requests":true}' | jq -c '{full_name,private,empty}'

OWNER is an existing user or organization on Forgejo. Our test repository moved from Gitea 28.1.0 into the official codeberg.org/forgejo/forgejo:16.0.5 container in 0.62 seconds, with both branches, all commits, the issue with its label, milestone and comment, the open pull request, the release and the wiki. Authors are kept as text: Forgejo stored "alice" as the original author because no matching account existed. The API has switches only for wiki, issues, labels, milestones, releases, pull requests and LFS, so accounts, SSH keys, webhooks, Actions secrets and packages must be recreated. Our two test servers shared a private network, so we allowed local networks for migrations in Forgejo; with a public Gitea, leave that off.

Keep code in sync until the cut-over

The same call with "mirror":true,"mirror_interval":"10m" creates a pull mirror. It carries Git data only; we asked for issues and got none. A commit pushed to Gitea appeared in Forgejo after a sync, and on cut-over day one call made the mirror a normal repository:

# as root, on the Forgejo server
curl -s -X POST -H "Authorization: token FORGEJO_ADMIN_TOKEN" http://127.0.0.1:3000/api/v1/repos/OWNER/demo/mirror-sync
curl -s -X POST -H "Authorization: token FORGEJO_ADMIN_TOKEN" http://127.0.0.1:3000/api/v1/repos/OWNER/demo/convert | jq -c '{full_name,mirror}'

The last command printed "mirror":false. For repositories with active issues, run the full migration during a freeze instead. Plan new CI runners too: Forgejo Runner 13.0 (3 August 2026) removed its Gitea compatibility (runner PR 1611), so one runner cannot serve both forges during the move. Our CI runner and staging guide shows how to give them their own server.

Gitea vs Forgejo in October 2026

Gitea 28Forgejo 16
Who decidesA Technical Oversight Committee of six: three maintainers elected yearly and three appointed by the Gitea company (governance doc)Its contributors collectively; the domains are held by Codeberg e.V., a German non-profit (FAQ)
LicenceMITGPLv3 or later since v9.0
Release cadenceAims for a major release every three to four months (release docs)A major release each quarter on fixed dates; every fourth is an LTS (schedule)
Security fixes forThe newest major release onlyCurrent release and LTS: 16.0.5 and 15.0.9 shipped together on 17 September 2026
Security processPrivate GitHub advisory or security@gitea.iosecurity@forgejo.org, a team shared with Codeberg; advance notice of security releases open to everyone
CIGitea Actions with Gitea Runner 5.0.0 (8 October 2026)Forgejo Actions with Forgejo Runner 13.x
FederationA [federation] setting, off by default, for NodeInfo statistics and signed requestsIn active development and marked experimental
Paid editionsGitea Enterprise and Gitea CloudNone

If you already run Gitea 1.23 or newer, upgrading to 28 is the low-effort path, and this release carries real security work. If non-profit governance, the GPL or an LTS line with about 15 months of support matter more to you, Forgejo is a good home, but moving is a project: a new server, repository-by-repository migration, new runners and recreated accounts.

Frequently asked questions

Why is it Gitea 28 and not Gitea 1.28?

Gitea dropped the historical "1." prefix from its version numbers with this release, so the version after 1.27.3 is 28.0.0. The first number now counts major releases; 28.1.0 is a maintenance release of the 28 line, and the next major will be 29.

Can I upgrade straight from Gitea 1.27 to 28.1.0?

Yes. In our lab, Gitea 1.27.3 went directly to 28.1.0 without installing 28.0.0 first. The first start ran 15 database migrations, schema 343 to 358, and all repositories, issues, releases and logins survived. Back up first and read the breaking changes.

Can I downgrade Gitea 28 back to 1.27?

Not on the upgraded database. Gitea 1.27.3 refuses to start on a schema 358 database and exits to protect your data. A rollback means restoring the database dump and data from before the upgrade, which took 6 seconds in our small test; anything pushed after the upgrade is lost.

Is Gitea 1.27 still supported?

Gitea's security policy gives security fixes only to the most recent major release and the main branch. On 12 October 2026 the newest 1.27 release was still 1.27.3 from 29 August, while 28.0.0 and 28.1.0 had fixed 29 more CVEs. Treat 1.27 as unsupported.

Can I migrate from Gitea 28 to Forgejo?

Not in place. Forgejo only upgrades Gitea databases up to version 1.22, through Forgejo 10.0, and Forgejo 16.0.5 refused our Gitea 28 database. Forgejo's migration API moved our code, issues, pull requests, releases and wiki one repository at a time; accounts, webhooks and secrets must be recreated.

How much RAM does Gitea 28 need?

Little for a small team. In our lab, Gitea 28.1.0 used about 170 MB on a 2 GB virtual machine, and the whole machine with PostgreSQL used 495 MB. The Docker version with SQLite used 110.5 MiB. CI runners need far more, so give them their own server.

Book the maintenance window

For a small instance the whole job fits in a lunch break: stop, two backups, a verified binary, one start, a check. To rehearse first, restore last night's backup onto a spare server and upgrade the copy. Our lab VM matched the 2 vCPU and 2 GB of a VPS Micro, which is enough for Gitea or Forgejo for a small team. A VPS Mini (4 vCPU, 4 GB) leaves room for a CI runner beside it, and a VDS Small (4 dedicated vCPU, 8 GB) suits a runner of its own. All run on KVM in Amsterdam, Dublin or Prishtina with NVMe, unmetered traffic and their own IPv4 and IPv6, and you can move up a plan later with a short reboot. Availability by city is on the plans page. If you want us to run the upgrade or the Forgejo move for you, it is quoted per job: message us on Telegram.

← All articles

Chat on Telegram