No description
Find a file
paspo 6a38ef1a80
All checks were successful
Binary build / go-build (push) Successful in 33s
fix build action
2025-12-23 17:47:02 +01:00
.forgejo/workflows fix build action 2025-12-23 17:47:02 +01:00
.vscode secret key in sample launch.json 2025-12-22 11:42:11 +01:00
.gitignore docs WIP 2025-12-22 12:35:52 +01:00
config.go move config 2025-12-22 16:10:56 +01:00
db.go move db-reated function into its own file 2025-12-22 16:10:05 +01:00
engine.go enable structured logging 2025-12-22 16:11:29 +01:00
go.mod go mod tidy 2025-12-23 14:22:48 +01:00
go.sum enable structured logging 2025-12-22 16:11:29 +01:00
LICENSE license 2025-12-22 15:46:18 +01:00
main.go enable structured logging 2025-12-22 16:11:29 +01:00
README.md added new instance to scoreboard 2025-12-23 16:50:18 +01:00
secrets.go totp secret support 2025-12-22 11:09:59 +01:00
table_migrations.go fix missing migrations 2025-12-23 16:48:58 +01:00
tables.go turn Table.SkipFields into variadic function 2025-12-22 16:10:36 +01:00

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