I’ve been using TrueNAS for a number of years, from when it was FreeBSD-based to now that it’s Linux-based. Right now I use them both: TrueNAS Core (FreeBD) and TrueNAS Scale (Linux). The TrueNAS Scale box has been mostly for fiddling with containers, while the TrueNAS Core box is the heavy lifter: all of my Carbon Copy Cloner backups go there, and there are FreeBSD jails running my Plex server as weill as my Gitea server.

Now that the FreeBSD-based version is getting long in the tooth and will soon be unsupported (FreeBSD 13, which it’s based on will be end of life in January 2026), I’ve been thinking about upgrading that box to Scale for the last year. TrueNAS (supposedly) makes it easy to convert the running system from FreeBSD to Linux, but I’ve not tried it yet. I need to move my Plex server and Gitea server over first, so this past weekend I took the plunge. Now, TrueNAS does a lot with containers so at first I was thinking of using containers for this but… and I’m going to be honest here, TrueNAS has burnt me a few times with community containers. I don’t even remember how many times I had to redo my Pi-hole container because things just… broke. It got so annoying that I eventually bought a Raspberry Pi to run Pi-hole and left the containerized thing behind me. Gitea, and Plex, are pretty critical to my daily workflows (well, Plex is critical to my wife’s daily workflow anyways 😉), so trusting them to the world of containers is not something I’m prepared to do at this time. So I opted for a virtual machine, but wanted something as light as possible to get the same performance efficiencies I was getting out of my FreeBSD jails.

Enter Alpine Linux — a distro I’ve never installed before but ended up being pleasantly surprised with!

So this was the challenge: setup a new Alpine Linux VM on TrueNAS Scale, backup the FreeBSD-based Gitea and restore it, on Linux, in the new virtual machine and then transparently swap the two. And the bonus challenge: Alpine’s community repo provided Gitea 1.23.8 and I was running 1.24.2 on FreeBSD.

The tl;dr is it worked. But it took some work. Read on if interested… this is mostly for myself as I expect few people to be as hackish (dare I say: reckless?) as me when it comes to this but hey, you never know!

Installing Alpine Linux

The first step was to setup the virtual machine and install Alpine. Word to the wise: see how much space you are using before you create the virtual machine! For some reason, I assumed 30GB for the virtual machine would be enough space. With FreeBSD jails I never had to worry about the amount of available space, it had access to the entire storage pool, which was about 18TB. Suffice it to say, I didn’t check, and the Gitea backup zip (which was 34GB) quickly exceeded the space I had allocated. Recreation of the virtual machine was required.

This virtual machine has one very specific purpose: to host my git repos. But to do that, more than what Alpine itself provided out of the box was required; in this case the virtual ISO was chosen, which is Alpine optimized for virtual machines. Setting up virtual machines in TrueNAS is left as an exercise to the reader — nothing really special there.

Upon completion of the install and subsequent reboot, the community repos needed to be enabled by uncommenting the “community” line, as the root user:

vi /etc/apk/repositories
apk update

Additional software needs to be installed, and some of these are preferences, like sudo and vim, while others were needed by Gitea. MySQL was used on FreeBSD, so MariaDB needed to be installed. After installing sudo, given that my user was in the wheel group, it needed to be added to /etc/sudoers to ensure appropriate access was granted, as root:

apk add sudo vim
visudo

Installing MariaDB

At this point, everything could be done with sudo rather than directly as root, so to install MariaDB as my regular user:

sudo apk add mariadb mariadb-client
sudo mariadb-install-db --user=mysql --datadir=/var/lib/mysql
sudo rc-service mariadb start

That said, typing sudo quickly got boring so a running sudo su - to become root and finish things off. Most of the subsequent commands, unless noted otherwise, are run as root. Yes, I do security for a living and no, I don’t care about running amuck as root on a fresh system not exposed to the internet. YOLO (as the kids say)!

The next step was to secure and start MariaDB, securely create ~root/.mysql_secret and paste the chosen root password for MariaDB in it, then start it and ensure it starts on every boot, as root:

mariadb-secure-installation
touch ~root/.mysql_secret
chmod 600 ~root/.mysql_secret
vim ~root/.mysql_secret
rc-service mariadb restart
rc-update add mariadb default

On the FreeBSD server, dumping the MySQL database directly, rather than relying on the Gitea backup, as root:

mysqldump -u root -p gitea > ~/gitea-db.sql

The gitea-db.sql file was copied from the FreeBSD jail to the Linux virtual machine for importing into MariaDB. At this point, MariaDB was insisting that “BINLOG ADMIN” privileges needed to be granted to the gitea user that was created to own the Gitea database. The error it spit out was: ERROR 1193 (HY000): Unknown system variable ‘GTID_PURGED’”. It ended up being easily fixed by commenting out the “GLOBAL.GTID_PURGED” comment in the database dump (thanks StackOverflow). Now it could be imported, as root:

mariadb --default-character-set=utf8mb4 -u gitea -p gitea <gitea-db.sql

Installing Gitea

Now began the trial and error of getting Gitea installed, setup, and seeing if it could be made to work. Obviously we need to install Gitea itself, which the Alpine community repo provides. This was a deliberate choice, as it would have provided the appropriate init scripts and being unfamiliar with Alpine, I didn’t want to have to figure it out — it’s been quite some time since I’ve fiddled with SysV-init! Additionally, at some point being aligned with what Alpine provided was a goal, so this was a hack to get things moved over now, rather than at some point in the future. As root:

apk add gitea

At the time of this adventure, what was provided by Alpine was Gitea 1.23.8 and what was in my FreeBSD jail was 1.24.2. This is important to remember as it explains some of the madness later. There is a backup and restore document to follow indicating how to make the backup, and how to restore. Simple and straightforward.

Until it wasn’t, because an annoyance quickly arose: unzip kept spitting out a “unzip: short read” error on my backup when trying to unzip it. At first I thought maybe due to the size there was some network-based corruption, so used rsync to copy it again to be sure — no dice, the same problem occurred. With a little hunting, it seems the unzip that came with BusyBox (which Alpine uses) might not be able to handle such large (34GB) zip files, but the real unzip could, as root:

apk add unzip
unzip gitea-backup-dump.zip

This started to work, but then failed with an “error: invalid zip file with overlapped components (possible zip bomb)” error.. sigh. So doing the rationale and safe thing, as root:

UNZIP_DISABLE_ZIPBOMB_DETECTION=TRUE unzip gitea-backup-dump.zip

Remember kids, this is not the sort of stuff you want to run in production! Fortunately, my existing Gitea server was still running and if this thing went sideways.. well, that’s what all the notes are for. 😉

This time it unzipped properly and could start moving files around, as root:

mv /etc/gitea/app.ini /etc/gitea/app.ini.org
cp app.ini /etc/gitea/
mv data/* /var/lib/gitea/data/
mv log/* /var/log/gitea/
mkdir /var/lib/gitea/git
mv repos/* /var/lib/gitea/git/

On the FreeBSD setup, the user and group for Gitea was the git user. On Alpine’s install the user is gitea and the group is www-data so there were some permissions that needed to be adjusted, as root:

chown -R gitea:www-data /etc/gitea /var/lib/gitea /var/log/gitea/

One thing to note: the path layouts are quite different, so the Gitea app.ini configuration file needed to be adjusted to the new paths. In most cases this was just changing /var/db/gitea to /var/lib/gitea and the user for the RUN_AS option had to be changed from git to gitea as well.

Next, the ssh keys stored in /usr/local/git/.ssh in the FreeBSD jail had to be copied over to /var/lib/gitea/.ssh in the Linux virtual machine. These need to be corrected by running, as the gitea user:

gitea admin regenerate keys

There were a lot of frequent uses of the gitea doctor check command to clear up any errors, but it was all quite self-explanatory; note these all need to be run by the gitea user as it really doesn’t like to be executed as root (for probably very good reasons). The one error that kept coming up was the mismatched database version, which wasn’t a surprise but at that time, the workaround remained a mystery. Gitea refused to start because the database was versioned with a newer version of Gitea than was installed:

2025/11/16 08:25:28 cmd/web.go:205:serveInstalled() [I] PING DATABASE mysql
2025/11/16 08:25:28 ...dels/db/collation.go:185:preprocessDatabaseCollation() [W] Current database is using a case-insensitive collation "utf8mb4_general_ci", although Gitea could work with it, there might be some rare cases which don't work as expected.
2025/11/16 08:25:28 ...ations/migrations.go:481:Migrate() [F] Migration Error: Your database (migration version: 321) is for a newer Gitea, you can not use the newer database for this old Gitea release (312).
Gitea will exit to keep your database safe and unchanged. Please use the correct Gitea release, do not change the migration version manually (incorrect manual operation may lose data).

Disappointing, but not surprising. Also not something that was about to deter me and my enjoyable Sunday afternoon sitting on the couch fiddling with this stuff. This was a proper adventure the likes of which I don’t often get to enjoy!

And here’s where I decided to take the plunge: I could hold off my upgrade desires until Alpine’s community repo provided the right version of Gitea, or could I go out on a limb and grab the binary from upstream and replace what was on-disk. My thinking was: it’s a blob of Go, pretty self-contained, and the download is literally a versioned Gitea binary. What’s the worst that could happen?

So I downloaded the 1.24.2 version from upstream which matched the version that was running on FreeBSD. Slightly unorthodox, as this still an experiment and my intention, if this worked, was to ride it out until the Alpine community repo provided the right version, so I downloaded it to /etc/gitea/ and just… changed the binary, as root:

mv /usr/bin/gitea /usr/bin/gitea-1.23.8
ln -s /etc/gitea/gitea-1.24.2-linux-amd64 /usr/bin/gitea
GITEA_WORK_DIR=/var/lib/gitea gitea

Surprisingly, it worked. Well, thus far anyways. No errors! And the web interface runs as well, although granted it can’t be connected to yet as it’s listening to a UNIX socket, but the fact that it starts without complaint is pretty impressive, as the gitea user:

GITEA_WORK_DIR=/var/lib/gitea gitea web -c /etc/gitea/app.ini

Installing nginx

Gitea needs a proxy web server running in front of it, and my prior install used nginx so it made sense to use the same thing here. The contents of /usr/local/etc/nginx/nginx.conf on the FreeBSD setup were copied to /etc/nginx/httpd.d/default.conf. The important piece is to ensure that the proxy_pass configuration directive in nginx’s default.conf is the same as that in Gitea’s app.ini.

That said, now Let’s Encrypt is required because SSL is important. Because my Gitea server is not accessible via the internet, DNS must used for verification, so a few things needed to be installed, as root:

apk add certbot certbot-nginx certbot-dns

The Cloudflare credentials stored on the FreeBSD setup, which used acme.sh, are easy enough to use with certbot. Create ~root/.cloudflare.ini and ensure it is mode 0600 so only root can read it, then add the appropriate credentials. To create and install the certificate, as root:

certbot certonly --dns-cloudflare -d git.mydomain.com --installer nginx

Obviously change git.mydomain.com to whatever the hostname for your Gitea server is. Finally, update the nginx configuration to point to the Let’s Encrypt certificates produced and then setup certbot for automatic renewals following the instructions. Now we can start nginx and Gitea and make them start at boot, as root:

rc-service nginx start
rc-update add nginx default
rc-service gitea start
rc-update add gitea default

Final steps

At this point Gitea is running and the web interface works with HTTPS. Going into the Gitea “Site Administration” settings and making sure there are no errors in the “Maintenance > Self Check” section is a good idea. That said, for this migration, there were a lot of git repos which meant there were a lot of git hooks to regenerate because the old hooks from the FreeBSD system were using different paths to the gitea binary and directories. This is easily fixed with gitea to fix things, as the gitea user:

gitea admin regenerate hooks --config /etc/gitea/app.ini
gitea admin regenerate keys --config /etc/gitea/app.ini

This will regenerate both the git hooks and the ssh keys needed.

In the end, it worked great. In fact, I was surprised that it worked as well as it did! At the time of this writing, Gitea 1.25.1 is available and part of me wants to give it a go, however I’ve solved the primary problem: moving my Gitea server from FreeBSD to Linux, from TrueNAS Core jails to a TrueNAS Scale virtual machine. I think I’ll wait for the Alpine community repo to get me to the right version so I don’t have to hack my way to newer versions. Gitea is awesome, and is so much lighter to operate than GitLab (which I used before, but I don’t need all those features, many of which Gitea now has anyways). These are for my own projects and code that I don’t want to host on GitHub, and I don’t need all the latest and greatest features. Arguably, I could have chosen a different distro that maybe provided more recent versions but even then, my goal was lightweight and Alpine delivers that in spades. It’s shocking how little memory and CPU it uses, so it felt like a good fit for my needs.

Once Alpine catches up to or surpasses the 1.24.2 version I’m running, I can dispense with the hack which made me a little nervous at first but I honestly think it’ll be fine. I have to change some .git/config files on a few systems for a bunch of repos to connect as the gitea user and not git but that’s fine also and only matters for those that are ssh-based. DNS changes were required, but now everything is connecting to the new Gitea server as easily as they were to the old.

The only thing remaining, which I’ve not had time for yet (as it’s a little more disruptive), is setting up network bridging on the TrueNAS host. This will let me mount volumes from the TrueNAS server in the virtual machines so I can store the Gitea backups on the more spacious NAS. But that’s something for another day, and really most necessary for when I migrate the Plex server over.

If this was interesting, amusing, or even remotely helpful then it was worth the time keeping notes and writing it!

Share on: TwitterLinkedIn


Related Posts


Published

Category

Linux

Tags

Stay in touch