Deploying Forgejo with Coolify

I'm going to run my own git forge, with containers and actions!

2026-02-09


Deploying What in the What Now?

Admittedly, the chances of you finding this page without explicitly searching for it are slim. But just for the record:

  • Coolify is a PaaS (platform as a service) that manages app deployments in containers across multiple servers. There is a cloud hosting plan, but you can self-host the entire thing too. If you're hosting multiple web applications, maybe together with other services they use, then Coolify is an option for managing these deployments using a self-hosted setup.
  • Forgejo is a git forge. You can use it to host or collaborate on projects that live in a git repository. It supports many of the features other git forges also support, but it's fully open source and can be self-hosted.

In this blog post I'll talk about my experience with setting up Coolify on a virtual server, and then deploying Forgejo and a runner for workflows through it.

I'm using virtual servers hosted by Hetzner in Germany, via their cloud offering. But none of this is specific to any particular hosting provider.

Coolify Basics

While the focus of this article is the Forgejo setup, let's cover some basics of deploying applications with Coolify first.

Luckily, setting up Coolify is not that complicated. You just bring a server, and run an installation script on it that will ask a few questions and then you'll have your Coolify up and running.

I do recommend starting with a fresh server or cloud instance. Refer to the installation guide linked above for supported operating systems. Spoiler: Recent versions of many Linux distributions work well.

Future-Proofing Coolify

If you just install Coolify as is, it'll work, but it will also just store all its data in the server's file system. That's fine for playing around, and for small deployments too. But if you're building something bigger, here are some points for consideration:

  • Coolify stores all its data in /data. If your hosting provider offers block volumes, it might make sense to keep Coolify data on a separate block volume and mount it here.
  • Coolify does not configure Docker's storage locations; it just uses the defaults if you don't set this up. Again, for playing around and for small deployments, this is fine. But if you're putting /data on a separate volume, you should also think about what part of Docker's files you want on that volume too instead of on the server. For example, /var/lib/docker/volumes will end up holding any data your applications store on a Docker volume.

But don't overthink it in the beginning - it's just files, so all of this can be migrated later, too.

Servers

By default, Coolify will start with a localhost server set up that represents the server Coolify itself is running on. You can deploy applications to it, too.

For production deployments, you'll probably want to isolate services a bit more from each other. For me, internal services on the Coolify server still make some sense. But for large production applications, it's good practice to isolate them on their own servers.

Fortunately, creating new servers in Coolify is not a lot of work. You basically just have to tell it where it is, and how to log in. I'm using the Hetzner cloud, so I even get a button to deploy new instances right from the Coolify UI.

That said, for the initial exploratory period, using only one server for Coolify and your deployments is a good way to get started with minimal resource usage.

Host Names

Coolify wants to assign host names to services deployed with it. You can use sslip.io (the default) for testing, but if you have a domain that you control, you can also set up a wildcard A or CNAME record to point at your Coolify server. That way you'll get fancy host names under your own domain.

Trying out Coolify

Once Coolify is up and running, you can log in with your administrator account, have a look at your dashboard, and see nothing, because you didn't deploy anything yet. From here, the basic process is creating a project, and then deploying services as part of the project.

Try creating a project for playing around, and then click the New button at the top to add a resource. You'll hopefully see a long list of things that can be deployed. At the top of the page there are some generic options, such as deploying something based on a Dockerfile or on a compose file. Further down, you'll find a lot of free or open source applications that Coolify can deploy by default in just a few clicks.

Want a MySQL database for your project? Find MySQL in the list, click a few buttons, and you'll have a MySQL running in a container. Have a web app that you want to use the database with? Add it to the project. If it's on GitHub, you can even deploy straight from there.

Forgejo

This is where Forgejo comes in. If you're going so far that you're self-hosting a PaaS, then maybe you don't want to host your source code on GitHub either. Or maybe you just like self-hosting everything. Either way, self-hosting your own git forge addresses this, and if you like open source, then Forgejo is a great option.

If you've never used Forgejo before, I recommend having a look at codeberg. It's the largest public Forgejo instance, operated by a German non-profit. But you'll probably find that you have used it before, or at least it'll feel that way. If you've only ever used GitHub, then it will probably look a bit more limited. If you've used other git forges, such as GitLab, or Gitea, which is what Forgejo was forked from, then you'll probably feel at home because these are honestly all very similar experiences.

Installation

Let's talk about installing Forgejo. I have good news and bad news. The good news is: Forgejo itself is one of the open source applications Coolify has templates for.

Create a new project for your Forgejo setup, and click the New button to create a new resource. Type "forgejo" into the search field, and you should see four options called "Forgejo". The one that's just named "Forgejo" with nothing else is the basic option, where Forgejo stores all your data in files and SQLite databases. For private personal forges, or small teams, this can easily be good enough.

The other three options bundle Forgejo with a database service. If you're planning more than just a small private forge, choose the database you're the most familiar with.

Whichever you choose, on the next page you'll see the service configuration already filled in with some defaults. In fact, the entire setup has already been created, it just hasn't been deployed yet.

If you want, you can take some time to look through the settings in case there's something you want to change. The prime candidate is probably the host name for the main forgejo service, which you'll be able to configure if you click on the "Forgejo" service. But in general, these settings will be filled out in a way that the service can be deployed straight away.

So let's do just that! Click the "Deploy" button on top, watch some logs scroll past, and eventually, everything should be up and running!

Forgejo Installation

To complete the installation, you need to navigate to the URL shown for the Forgejo service and fill out the form that configures your Forgejo. Make sure that new user registration is disabled if you don't want some unexpected guests.

Once this is done, you'll have a basic git forge running. You can add users, you can create repositories, you can push to them, you can make pull requests, and so on.

But I promised you workflows and actions. Workflows allow your Forgejo you perform tasks defined in your repositories on certain events, such as new pushes.

The catch is that if we're self-hosting the forge, we also need to self-host the runners that execute these tasks. And while you could host a runner on any computer you control, since this article is about deploying things in Coolify, that's what I'll cover next.

Workflows and Runners

Remember when I said I had good news and bad news earlier? The bad news is that, unfortunately, as of this writing, Coolify doesn't seem to have a template for Forgejo runners. But it's not that bad. In Coolify, we can also create resources based on a Dockerfile or on a compose file.

When running on the main Coolify server, or on anything that uses Docker for anything else, there is a conflict to keep in mind. The forgejo runner executes workflows in containers, which means that it can potentially interfere with other container based deployments on the same system. To work around this, we can use Docker-in-Docker: We can run a separate Docker daemon in a privileged container, and use that for the Forgejo runner.

Another thing to keep in mind is that Coolify supports Docker's healthcheck feature. Services that define health checks will show up as "healthy" in the Coolify UI when the health check is positive, and can be restarted when they're not healthy.

What I ended up with is the following compose file. Create a new resource, choose the option with the empty compose file, and just copy and paste into the browser. You might be wondering why the command line for the runner service just does nothing. I'll address that in the next section.

# Forgejo Runner with dind

configs:
  runner-config:
    content: |
      runner:
        envs:
          DOCKER_HOST: tcp://dind.docker.internal:2375
      container:
        docker_host: 'tcp://dind:2375'
        options: '--add-host=dind.docker.internal:host-gateway'

volumes:
  runner-data:
    name: runner-data
    external: true

services:

  dind:
    image: docker:dind@sha256:8bcbad4b45f0bff9d3e809d85a7ac589390f0be8acbc526850c998c35c1243fd
    container_name: forgejo-dind
    privileged: true
    restart: unless-stopped
    healthcheck:
      test:
        - CMD-SHELL
        - 'docker -H tcp://0.0.0.0:2375 info --format "{{.ServerVersion}}" || exit 1'
    command: ["dockerd", "-H", "tcp://0.0.0.0:2375", "--tls=false"]

  runner:
    image: code.forgejo.org/forgejo/runner:12@sha256:2b65f3ba4345026d66de34dc8eddabb7b0f64c0e14905193ae81746f2728caa0
    links:
      - dind
    depends_on:
      dind:
        condition: service_started
    container_name: forgejo-runner
    user: 1000:1000
    environment:
      DOCKER_HOST: tcp://dind:2375
    volumes:
      - runner-data:/data
    configs:
      - source: runner-config
        target: /config.yaml
    restart: unless-stopped
    command: '/bin/sh -c "while : ; do sleep 1 ; done ;"'

Once you save, you'll get an overview showing the two services, again in stopped state. You can change the name to something more descriptive if you want (don't forget to click "Save").

Note about the sha256 hashes in the image lines: I pinned the images to the ones that work for me. In particular, the dind service relies on being able to disable the Docker daemon's tls verification, which is a feature that will at some point go away.

Initial Runner Setup

As I wrote above, you might be wondering why the runner service doesn't actually run the runner. This is because the initial setup of a runner is semi-interactive. We'll need to connect to the container and run the forgejo-runner executable in registration mode one time.

But to do that, the new resource has to be deployed first. Click the "Deploy" button, and after a short while there should be two services. One called "Dind", hopefully showing as healthy, and one called "Runner", showing as "unknown" because the health check isn't in the compose file yet.

Now I know that I wrote above that we need to connect to the container and run a command manually, but you can actually do this right from the Coolify UI. See the "Terminal" tab? Click that, and choose the runner container from the dropdown menu. And there's your command line in the container!

Type the following command to start the runner registration:

$ forgejo-runner register
INFO Registering runner, arch=amd64, os=linux, version=v12.6.4. 
WARN Runner in user-mode.                         
INFO No configuration file specified; using default settings. 
INFO Enter the Forgejo instance URL (for example, https://next.forgejo.org/): 

The URL you enter here is the same URL you accessed your Forgejo instance in a browser earlier. Just copy and paste it in.

Next, you'll be asked for a token:

INFO Enter the runner token:

To obtain a token for the runner, you need to open a browser and log in to your Forgejo instance with an administrator account. Runner configuration can be found under Site Administration > Actions > Runners (or add /admin/actions/runners to the URL of your Forgejo to go there directly).

Click "Create new runner", and copy the token, then paste it into the terminal connection in Coolify.

Finally, you'll be asked for a runner name and some runner labels. Choose something descriptive - the default will be a container ID, and those don't make for good names.

INFO Enter the runner name (if set empty, use hostname: 04863c05d40f): 

The labels are pretty much up to you. These labels define how you refer to the runner in your workflows. For example, if you want to write your workflows so they execute tasks on a "docker" runner then you can put "docker".

INFO Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-20.04:docker://node:20-bookworm,ubuntu-18.04:docker://node:20-bookworm): 

Once the labels are entered, there'll be some more output and at the end, it should confirm that the runner was registered successfully.

INFO Registering runner, name=runner, instance=https://.../, labels=[docker]. 
DEBU Successfully pinged the Forgejo instance server 
INFO Runner registered successfully.    

You can also verify this by reloading the runner administration page in Forgejo where you created the token earlier. It should now show a new runner in "Offline" state.

Finalizing the Runner Setup

Now that the initial setup is complete, go back to the "Configuration" tab in Coolify and click "Edit Compose File". It's time to get rid of the dummy command at the bottom. Replace this line:

    command: '/bin/sh -c "while : ; do sleep 1 ; done ;"'

With this block:

    healthcheck:
      test:
        - CMD
        - pgrep
        - forgejo-runner
      interval: 30s
    command: '/bin/sh -c "sleep 5; forgejo-runner daemon --config /config.yaml"'

What this does is it replaces the dummy command with one that actually runs the forgejo runner, and also adds a simple health check that just verifies that the runner process is running.

Click "Save", and then close the editor pane, then click the "Restart" button in the top right corner of the Coolify UI (not the one next to one of the services). This will restart your runner with the now finished compose file. Both services should come back up as "Healthy" now because we added a healthcheck section for the runner in the last step too.

To verify, once again reload the runner administration page in Forgejo. If all went well, you should now see the new runner in "Idle" state. It's a green badge, so that must mean it worked.

Using the Runner

To use the runner, you need to create a workflow in a repository on your Forgejo instance. Create a new repository through the Forgejo UI (there's a plus shaped button in the top right), and name it "runner-test" or something similar.

To create a workflow, you need to create a yaml file in the .forgejo/workflows/ directory in your repository. You can either do this locally, and push to your Forgejo instance, or you can actually use the Forgejo UI to create a new file and commit it right to your main branch.

Here's a simple Hello World workflow. Replace the label in the "runs-on" line with a label you applied to your runner in the initial setup, and name it something like .forgejo/workflows/test.yaml:

on: [push]
jobs:
  hello:
    runs-on: docker
    steps:
      - run: echo "Hello, world!"

Whether you push from your local computer, or create the commit in the Forgejo UI, Forgejo will treat this as a push to your main branch, and if all went well, it should try to execute the new workflow on your runner.

You'll see the status of your workflows next to the commit name in the UI, as an orange dot for in progress, a green check mark for success, and a red cross for failure. You can also click on the "Actions" tab of your repository and see workflows that have been executed or are currently executing, sorted under their respective commits.

Accordingly, on the Actions tab, you should be able to choose your new commit, and then you should see one "hello" job with three stages. The first "Set up job" stage is the runner's setup for executing the job. The second stage should simply say "Hello, world!", and the third stage should hopefully say that the job completed successfully.

Using Actions

If you're familiar with workflows on other forge platforms, you might be familiar with actions. If you're not, the short story is that they're self-contained steps that can be used to accomplish small tasks in workflows.

One common use case is simply checking out the repository the workflow is running for. Here's an example job you can add to the jobs section of your workflow that uses the checkout action to check out the repository and list all files in the checkout:

on: [push]
jobs:
  checkout:
    runs-on: docker
    steps:
      - uses: actions/checkout@v6
      - run: ls -laR

If you think about this for a minute, it might seem a bit like magic that this just works. How does the action know what to check out from where, and why does it have access? The answer is that Forgejo automatically sets a bunch of environment variables for you, including one with a temporary access token that the workflow can use to access its repository, and that only lives until the workflow completes.

Not Using Actions

Of course, you don't have to use actions. You could implement the git checkout yourself, using the same variables that actions would use. For example, you can get the URL of the Forgejo server, the repository name, and the temporary token and build your own URL for cloning the repository.

SERVER_HOST="${{ forgejo.server_url }}"
REPO_URL="https://${{ forgejo.token }}@${SERVER_HOST#https://}/${{ forgejo.repository }}.git"

It doesn't exactly look nicer. But if you want to, you can have full control over the code that executes as part of your workflows.

The End

And that's it! If everything worked, you now have a Coolify managed Forgejo deployment! In the cloud (or at home), with workflows and actions!