|
|
||
|---|---|---|
| .forgejo/workflows | ||
| .vscode | ||
| .gitignore | ||
| config.go | ||
| db.go | ||
| engine.go | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| main.go | ||
| README.md | ||
| secrets.go | ||
| table_migrations.go | ||
| tables.go | ||
gitea to forgejo migration, the hard way
We do this not because it is easy, but because we thought it would be easy
-- Some guy on internet
I wrote this tool to automate migration from gitea to forgejo. Somebody told me this was "a drop-in replacement"... if you're here, you know it's not. In fact, it was until gitea v1.22, but now the two projects diverged too much.
See these link: [https://forgejo.org/2024-12-gitea-compatibility/]
Important notes
I wrote this to handle my specific use-case, with my specific gitea and forgejo versions. If everything is the same as my environment, with some luck, it works. If you use different versions, a different environment, you're practically on our own... But please contribute to this repo, in any case: if you're on this journey, let me know how you handled it.
My environment
I am moving some gitea instances (v1.25.2 on mariadb) to forgejo (13.0.3 on postgres), so I'm changing both application AND database.
I'm managing everything via docker compose, and I use Traefik as reverse proxy. If you're not using Traefik, simply ignore alle the "labels" and the "backend" network.
Here's a sample docker-compose.yaml before the migration:
services:
gitea:
image: docker.gitea.com/gitea:1.25.2
container_name: gitea
volumes:
- ./data:/data
- ./backup:/backup
networks:
- default # used for db comunication
- backend # used for reverse proxy
ports:
# - "3000:3000" # port not exposed: we use a reverse proxy
- "22:22"
depends_on:
- db
restart: always
labels:
- "traefik.docker.network=proxy-backend"
- "traefik.enable=true"
- "traefik.http.routers.git-example.rule=Host(`git.example.com`)"
- "traefik.http.routers.git-example.entrypoints=https"
- "traefik.http.routers.git-example.tls=true"
- "traefik.http.routers.git-example.tls.certresolver=letsencrypt"
- "traefik.http.routers.git-example.service=git-example"
- "traefik.http.services.git-example.loadbalancer.server.port=3000"
- "traefik.http.middlewares.git-example_headers.headers.customRequestHeaders.X-Forwarded-Proto=https"
- "traefik.http.routers.git-example.middlewares=git-example_headers"
db:
image: mariadb:11
restart: always
container_name: gitea_db
environment: # from .env file
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MARIADB_AUTO_UPGRADE=1
ports:
- 127.0.0.1:23348:3306
volumes:
- ./db/:/var/lib/mysql
healthcheck:
test: [ "CMD", "healthcheck.sh", "--connect", "--innodb_initialized" ]
start_period: 1m
start_interval: 10s
interval: 1m
timeout: 5s
retries: 3
networks:
backend:
external: true
name: proxy-backend
how to do this migration
Pre-flight check
Check that source db is healthy.
docker compose exec -u git gitea gitea doctor check --run check-db-consistency --fix
# if you want to be extra secure, run all checks
docker compose exec -u git gitea gitea doctor check --all --fix
(adapt this command to your environment)
backup now
Stop all container (docker compose down) and make a backup. Really. This is important, don't skip this step.
Please assume things will go bad and prepare accordingly.
test your backup
Restore your backup elseware and check everyhing is ok. You can skip this step, if you want, but only if your data means nothing to you.
modify compose
In docker-compose.yaml we need to:
- add the new postgres service
- add the new forgejo service
- expose (temporarly) mariadb and postgres ports
- remove (or comment out) the gitea service
services:
# gitea:
# image: docker.gitea.com/gitea:1.25.2
# container_name: gitea
# volumes:
# - ./data:/data
# - ./backup:/backup
# networks:
# - default # used for db comunication
# - backend # used for reverse proxy
# ports:
# # - "3000:3000" # port not exposed: we use a reverse proxy
# - "22:22"
# depends_on:
# - db
# restart: always
# labels:
# - "traefik.docker.network=proxy-backend"
# - "traefik.enable=true"
# - "traefik.http.routers.git-example.rule=Host(`git.example.com`)"
# - "traefik.http.routers.git-example.entrypoints=https"
# - "traefik.http.routers.git-example.tls=true"
# - "traefik.http.routers.git-example.tls.certresolver=letsencrypt"
# - "traefik.http.routers.git-example.service=git-example"
# - "traefik.http.services.git-example.loadbalancer.server.port=3000"
# - "traefik.http.middlewares.git-example_headers.headers.customRequestHeaders.X-Forwarded-Proto=https"
# - "traefik.http.routers.git-example.middlewares=git-example_headers"
db:
image: mariadb:11
restart: always
container_name: gitea_db
environment: # from .env file
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MARIADB_AUTO_UPGRADE=1
ports:
- 127.0.0.1:3306:3306 # remove me after migration!!!
volumes:
- ./db/:/var/lib/mysql
healthcheck:
test: [ "CMD", "healthcheck.sh", "--connect", "--innodb_initialized" ]
start_period: 1m
start_interval: 10s
interval: 1m
timeout: 5s
retries: 3
postgres:
image: postgres:18-alpine
container_name: forgejo_db
restart: always
shm_size: 128mb
environment: # from .env file
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes:
- ./postgres:/var/lib/postgresql
ports:
- 127.0.0.1:5432:5432 # remove me after migration!!!
forgejo:
image: codeberg.org/forgejo/forgejo:13
container_name: forgejo
environment:
- USER_UID=1000
- USER_GID=1000
- FORGEJO__database__DB_TYPE=postgres
- FORGEJO__database__HOST=postgres:5432
- FORGEJO__database__NAME=${POSTGRES_DB}
- FORGEJO__database__USER=${POSTGRES_USER}
- FORGEJO__database__PASSWD=${POSTGRES_PASSWORD}
restart: always
networks:
- default # used for db comunication
- backend # used for reverse proxy
volumes:
# - ./data:/data # don't mount for now
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
depends_on:
- postgres
ports:
- 13000:3000 # this is temporary
- 22:22
labels:
- "traefik.docker.network=proxy-backend"
- "traefik.enable=true"
- "traefik.http.routers.git-example.rule=Host(`git.example.com`)"
- "traefik.http.routers.git-example.entrypoints=https"
- "traefik.http.routers.git-example.tls=true"
- "traefik.http.routers.git-example.tls.certresolver=letsencrypt"
- "traefik.http.routers.git-example.service=git-example"
- "traefik.http.services.git-example.loadbalancer.server.port=3000"
- "traefik.http.middlewares.git-example_headers.headers.customRequestHeaders.X-Forwarded-Proto=https"
- "traefik.http.routers.git-example.middlewares=git-example_headers"
networks:
backend:
external: true
name: proxy-backend
build the thing
Build and prepare to run the program (look at ENV for configuration parameters).
go build .
If needed, copy the binary in the target system.
Migration time
# start both db:
docker compose up -d db postgres
# start forgejo, to create db stucture in postgres
docker compose up -d forgejo
# connect to port 13000 with a browser and start installation, it's ok to
# create a new admin user with random credentials (it'll be deleted anyway in 2 minutes)
# stop forgejo
docker compose down forgejo
# run the program and pray
export MARIADB_HOST=localhost
export MARIADB_PORT=3306
export MARIADB_USER=gitea
export MARIADB_PASSWORD=gitea
export MARIADB_DATABASE=gitea
export POSTGRES_HOST=localhost
export POSTGRES_PORT=5432
export POSTGRES_USER=forgejo
export POSTGRES_PASSWORD=forgejo
export POSTGRES_DATABASE=forgejo
export SECRET_KEY="KEEP IT SECRET!"
./gitea2forgejo
# stop mariadb
docker compose down db
# if you use indexer, remove the relevant local data: it should be recreated automatically
rm -rf data/gitea/indexers
edit data/gitea/conf/app.ini and update db connection details
[database]
DB_TYPE = postgres
HOST = postgres:5432
NAME = forgejo
USER = forgejo
PASSWD = forgejo # you surely didn't use this password
LOG_SQL = false
SCHEMA =
SSL_MODE = disable
now enable the ./data:/data volume mapping in docker-compose.yaml under the forgejo service and start the service:
# start the service
docker compose up -d forgejo
# run the doctor
docker compose exec -u git forgejo forgejo doctor check --all --fix
Manually test everything.
With this tool I managed to migrate passwords, totp, webauthn credentials, secrets... It costed tears but finally it worked.
Conclusion
Like I said before: this software is not "full-featured": it was ment as a way to automate my migration (which went succesfully, BTW). It is a good starting point if you're planning to do one yourself.
If you do, please contribute back!
Scoreboard
If you managed to finish your migration, you can send a PR to update this scoreboard:
| Date | Player Name | Instance | Gitea | Forgejo | Notes |
|---|---|---|---|---|---|
| Dec 2025 | paspo | my personal dumpster | 1.25.2 - mariadb 11 | 13.0.3 - postgres 18 | First! |
| Dec 2025 | paspo | BgLUG | 1.25.2 - mariadb 11 | 13.0.3 - postgres 18 |