Self-Host Forgejo with CI/CD Runner using Docker Compose
This guide documents a setup for a self-hosted Forgejo Git service with an integrated CI/CD runner. The entire stack is managed with Docker Compose and uses the host’s Docker socket for running jobs, which is simpler and more efficient than a Docker-in-Docker approach for a single-server environment.
Prerequisites
- A Linux server with Docker and Docker Compose installed.
- A domain name pointed at your server’s IP address (e.g.,
git.yourdomain.com). - The Group ID (GID) of the
dockergroup on your host machine. Find it by running this command on your server and noting the number:getent group docker | cut -d: -f3
Directory Structure
Your final directory structure will look like this:
forgejo/
├── .env
├── docker-compose.yml
├── forgejo-data/
└── runner/
└── data/
Configuration Files
You will need to create two files: .env and docker-compose.yml.
1. The .env File
This file stores all your secrets and version tags. Create a file named .env in the forgejo/ directory.
# .env
# Version Tags
SERVER_VER=12
DB_VER=17-alpine
RUNNER_VER=9
# Database Credentials
POSTGRES_USER=forgejo
POSTGRES_PASSWORD=a_very_strong_and_secret_password
POSTGRES_DB=forgejo
# Forgejo URLs (Replace with your actual domain)
FORGEJO_ROOT_URL=https://git.yourdomain.com
FORGEJO_SSH_DOMAIN=git.yourdomain.com
2. The docker-compose.yml File
This file defines all the services. Create a file named docker-compose.yml in the forgejo/ directory.
# docker-compose.yml
networks:
forgejo:
external: false
services:
# --- forgejo server ---
forgejo-server:
image: codeberg.org/forgejo/forgejo:${SERVER_VER:-12}
container_name: forgejo-server
environment:
- USER_UID=1001
- USER_GID=1001
- FORGEJO__actions__ENABLED=true
- FORGEJO__database__DB_TYPE=postgres
- FORGEJO__database__HOST=forgejo-db:5432
- FORGEJO__database__NAME=${POSTGRES_DB}
- FORGEJO__database__USER=${POSTGRES_USER}
- FORGEJO__database__PASSWD=${POSTGRES_PASSWORD}
- FORGEJO__server__ROOT_URL=${FORGEJO_ROOT_URL}
- FORGEJO__server__SSH_DOMAIN=${FORGEJO_SSH_DOMAIN}
- FORGEJO__server__SSH_PORT=555
- FORGEJO__server__SSH_LISTEN_PORT=22
restart: unless-stopped
networks:
- forgejo
volumes:
- ./forgejo-data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
# Map 3000 to localhost and any other specific IPs you need
- "127.0.0.1:3009:3000"
- "555:22"
depends_on:
forgejo-db:
condition: service_healthy
logging:
driver: "json-file"
options: { max-size: "5m", max-file: "3" }
# --- forgejo DB ---
forgejo-db:
image: postgres:${DB_VER:-17-alpine}
container_name: forgejo-db
restart: unless-stopped
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
networks:
- forgejo
volumes:
- ./forgejo-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTG-RES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
logging:
driver: "json-file"
options: { max-size: "5m", max-file: "3" }
# --- forgejo runner ---
forgejo-runner:
image: code.forgejo.org/forgejo/runner:${RUNNER_VER:-9}
depends_on:
forgejo-server:
condition: service_started
container_name: forgejo-runner
networks:
- forgejo
user: 1001:1001
group_add:
# GID of the 'docker' group on the host. Find with:
# getent group docker | cut -d: -f3
- <YOUR_DOCKER_GROUP_ID>
volumes:
- ./runner/data:/data
- ./runner/data/config.yml:/data/config.yml:ro
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped
command: '/bin/sh -c "sleep 5; forgejo-runner daemon --config /data/config.yml"'
logging:
driver: "json-file"
options: { max-size: "5m", max-file: "3" }
Remember to replace <YOUR_DOCKER_GROUP_ID> with the number from the prerequisites.
Setup Instructions
This setup involves a multi-stage process to correctly register the runner.
Step 1: Initial Runner Registration
The runner container needs a .runner file to start, but that file can only be created by the register command. To break this catch-22, we temporarily change its startup command.
- Modify
docker-compose.ymlfor registration: Temporarily change thecommandin theforgejo-runnerservice to keep it alive without starting the daemon.# In docker-compose.yml command: 'sleep infinity' - Start the stack:
docker compose up -d - Get a registration token: Go to your Forgejo UI, click your profile picture -> Settings -> Actions -> Runners, and click Create new runner. Copy the token.
- Register the runner:
docker compose exec forgejo-runner forgejo-runner registerFollow the prompts, pasting the token when asked.
Step 2: Final Configuration
Now that the runner is registered, we can apply the final, correct configuration.
- Stop the stack:
docker compose down - Create the runner’s
config.yml: This file tells the runner to place job containers on the shared network.# Find your full network name docker network ls | grep forgejo # It will be something like forgejo_forgejo # Create the config file nano ~/forgejo/runner/data/config.ymlAdd this content to the file, using your full network name:
# ~/forgejo/runner/data/config.yml container: network: forgejo_forgejo - Edit the
.runnerfile for internal networking: The registration process used your public URL. We must change it to the internal Docker service name.nano ~/forgejo/runner/data/.runnerFind the
addressline and change it:- From:
"address": "https://git.yourdomain.com", - To:
"address": "http://forgejo-server:3000",
- From:
- Restore the final
commandindocker-compose.yml: Change thecommandin theforgejo-runnerservice back to the one that starts the daemon.# In docker-compose.yml command: '/bin/sh -c "sleep 5; forgejo-runner daemon --config /data/config.yml"'
Step 3: Final Startup
Apply the final changes and start the full stack.
docker compose up -d
Verification and Usage
Your Forgejo instance and runner are now fully configured!
-
Verify: Go to your Forgejo “Runners” settings page. You should see your new runner with a green “Idle” status.
-
Usage: To use your runner, create a
.forgejo/workflows/ci.ymlfile in one of your repositories:# .forgejo/workflows/ci.yml name: CI on: [push] jobs: build: # This label comes from the default registration runs-on: docker steps: - name: Check out repository code uses: actions/checkout@v4 - name: Test command run: echo "🎉 Workflow is running successfully on a self-hosted runner!"
Push this file, and the workflow will run on your new setup.