Recently, I discussed with a friend how they organize their docker-compose stacks into their personal home lab.They told me they used Portainer to manage all their containers. So I decided to give it a go.

At first glance, it seemed similar to what I already had: a tmux session, with each docker-compose stack in its pane. But then I saw that you can setup a Portainer stack by pointing it to a Git repository. Now this is more appealing! Doing this allows the following:

  • Version controlled docker-compose.yaml files
  • Trigger stack update via the UI or webhook
  • Environment variables via the UI

I was sold!

I thought all I needed to do was install Portainer and Gitea, then set up some webhooks.

Planning

My home lab setup is simple: an old laptop with Ubuntu and a Raspberry Pi with DietPi. I run all my Docker workloads on the laptop and lightweight web applications on the Raspberry Pi. Both machines do not have a lot of storage, so files are store on my NAS via CIFS mounts. These mounts are accessed directly or mounted as volumes for Docker containers.

The plan was simple:

  • DietPi
    • Install Portainer and connect to Ubuntu’s dockerd via TCP
    • Install Gitea and create repos for each stacks
    • Create webhooks on Portainer and trigger them from Gitea after each push
  • Ubuntu
    • Keep everything the same, except create git repos for each stack and publish to Gitea

Execution

As expected, things didn’t go according to plan. DietPi makes it super simple to install software, but when you need to deviate…it is HELL!

Problems and Solutions

Without a doubt, following the default instructions on the DietPi website will get Gitea up and running in under 5 minutes.

To be clear, we’re installing Gitea on a Raspberry Pi running the DietPi distribution. We will be using SQLite as our database and storing all of Gitea’s files on a CIFS mount.

If you’re following this as a guide, now is the time to install Gitea using dietpi-software. But hold off on anything else; we’ll need to tweak a few things.

Here’s the list of changes that were required to make this all work.

Setup gitea user

The gitea is setup without shell access, but it needs it ¯\(ツ)/¯.

To solve this, execute the command:

$ useradd -s ${SHELL} gitea

You may encounter LC_LANG warnings. To address these, run:

$ locale-gen
Generating locales (this might take a while)...
  en_US.UTF-8... done
Generation complete.

CIFS Mount Permissions

This change will allow Gitea to create directories in our CIFS mount.

DietPi includes a useful tool for mounting external drives, named dietpi-drive_manager. Use it to connect your network drive.

dietpi-drive-manager

DietPi uses systemd to manage mounts, with /etc/fstab serving as the key reference.

Our initial step is to assign ownership of the CIFS mount to the gitea user. This is done by setting the uid option to gitea.

#----------------------------------------------------------------
# NETWORK
#----------------------------------------------------------------
//192.168.1.33/git-hosting /mnt/git-hosting cifs cred=/var/lib/dietpi/dietpi-drive_manager/mnt-git-hosting.cred,iocharset=utf8,uid=gitea,gid=dietpi,file_mode=0770,dir_mode=0770,vers=2.1,nofail,noauto,x-systemd.automount

To play it safe, let’s keep the gid as dietpi. This precaution ensures that other processes running under dietpi can still modify the directory if needed.

Remember to run systemctl daemon-reload whenever you change fstab.

To check if it worked, look at the directory’s owner with:

$ ls -lah /mnt/git-hosting
total 4.0K
drwxrwx--- 2 gitea dietpi    0 Feb  9 03:53 .
drwxr-xr-x 7 root  root   4.0K Feb  9 04:27 ..
drwxrwx--- 2 gitea dietpi    0 Feb  9 03:53 @Recycle
drwxrwx--- 2 gitea dietpi    0 Feb  9 03:56 gitea

SQLite on a CIFS mount

Using SQLite on a CIFS mount is optional. You might prefer to use the MySQL database that DietPi recommends and comes pre-installed.

If you place the database in a CIFS mount, you’ll get errors about the database being locked. This is a known issue and the workaround is to mount the CIFS mount with the nobrl option.

Don’t use nobrl as the default, this option prevents file locking. Multiple users modifying the same files could lead to corruption.

To enable this, let’s modify /etc/fstab by adding this option. Follow the same steps above, now my entry looks like so:

#----------------------------------------------------------------
# NETWORK
#----------------------------------------------------------------
//192.168.1.33/git-hosting /mnt/git-hosting cifs cred=/var/lib/dietpi/dietpi-drive_manager/mnt-git-hosting.cred,iocharset=utf8,uid=gitea,gid=dietpi,file_mode=0770,dir_mode=0770,vers=2.1,nofail,noauto,nobrl,x-systemd.automount

Lastly, run systemctl daemon-reload to save changes.

For more context, check out Lee Cheng Hui’s blog post.

Dropbear SSH Server

Although DietPi recommends Dropbear for SSH, it doesn’t seem compatible with Gitea’s SSH logins.

To fix this, run dietpi-software > SSH Server > OpenSSH Server.

dietpi-ssh-server

dietpi-openssh-server

Although OpenSSH Server is noted to use more resources, it was necessary for my setup to function correctly.

Access Local Ports from Portainer

After installing Portainer on DietPi with dietpi-software, I encountered an issue: Portainer couldn’t access the Gitea server on the same machine.

Gitea is running directly on the Raspberry Pi, while Portainer is running in a Docker container. Here’s Gitea’s Portainer install script. Portainer performs basic checks, essentially boiling down to a Docker run command flagged with restart=always.

We can choose to forgo DietPi’s version and run this ourselves or modify the dietpi-software script.

I chose to modify.

vim /boot/dietpi/dietpi-software

Whichever your choice is, you need to update the docker run command by adding --add-host=host.docker.internal:host-gateway. This will allow processes running inside Docker containers to access hosts’ processes via host.docker.internal instead of localhost.

My command now looks like this:

G_EXEC_OUTPUT=1 G_EXEC docker run -d -p '9002:9000' --add-host=host.docker.internal:host-gateway --name=portainer --restart=always -v '/run/docker.sock:/var/run/docker.sock' -v 'portainer_data:/data' 'portainer/portainer-ce'

Again, you can simply run this command without modifying the script.

Completing Gitea Setup

We haven’t finished setting up Gitea yet. Please visit the Gitea URL on port 3000 to complete the process. You can now select SQLite as the database and opt to store all of Gitea’s data on the CIFS mount.

You will need to create a user, which will become the administrator of the Gitea instance. Create a repository with a docker-compose.yaml and you should be good to go!

Webhooks on Portainer and Gitea

With a Gitea repository ready, you can add a stack as follows:

portainer-git-repository

Then enable GitOps, to reveal the webhook link.

portainer-webhook

The webhook url may have a host using your .local address. This didn’t work for me, so I replaced that with 127.0.0.1 when adding it on Gitea’s webhook settings.

gitea-webhook

Conclusion

After applying all these changes, we can now commit changes to docker-compose.yaml files push them to Gitea and have it trigger a stack reload if there were any changes.

Discovering all these issues set me back a whole afternoon, if this helps at least one person it was time well spent :D