Skip to main content
Eigene Docker Images selbst hosten

Eigene Docker Images selbst hosten

·904 words·5 mins
Table of Contents

Schon mal darüber nachgedacht einen Docker Container selbst zu erstellen? Also ein Grundsystem mit allen Programmen oder Scripten die man gerne in so einer Kombination sehen möchte? Mit Forgejo lässt sich das komplett in der eigenen Infrastruktur abbilden: Git-Repository, automatischer Build-Prozess per CI/CD und eine eigene Container Registry – alles unter einem Dach.

Dieser Beitrag zeigt Schritt für Schritt wie es geht, am Beispiel eines Alpine-basierten Images mit vorinstallierten Bildbearbeitungs- und OCR-Tools.


Was wir bauen
#

  • Ein Dockerfile mit mehreren vorinstallierten Tools (Tesseract, Ghostscript, etc.)
  • Ein Forgejo-Repository als Heimat für den Code
  • Einen Forgejo Actions Workflow, der bei jedem Push automatisch baut
  • Das fertige Image landet in der Forgejo Container Registry – pullbar von überall

Voraussetzungen
#

  • Eine laufende Forgejo-Instanz (z.B. hinter Caddy als Reverse Proxy)
  • Ein laufender Forgejo Actions Runner mit Zugriff auf den Docker Socket
  • Docker auf dem Server

Schritt 1: Der Forgejo Actions Runner
#

Der Runner muss Zugriff auf den Docker Socket des Hosts haben, damit er Images bauen kann. In der compose.yaml sieht das so aus:

services:
  forgejo-runner:
    image: code.forgejo.org/forgejo/runner:9
    container_name: forgejo-runner
    command: ["/bin/forgejo-runner", "daemon", "--config", "/data/config.yml"]
    user: "0:0"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./runner-data:/data
    restart: unless-stopped

Wichtig ist außerdem die Runner-Konfiguration (runner-data/config.yml). Dort muss der Docker-Socket für Job-Container freigegeben werden:

container:
  docker_host: "automount"
  valid_volumes:
    - '**'

Mit docker_host: "automount" wird der Socket automatisch in jeden Job-Container durchgereicht – ohne das scheitern alle Docker-Befehle im Workflow.


Schritt 2: Das Dockerfile
#

Ein gutes Dockerfile für ein Tool-Image nutzt Multi-Stage-Builds: in Stage 1 werden Abhängigkeiten kompiliert, Stage 2 enthält nur das Laufzeit-Minimum.

# Stage 1: build
FROM python:3.12-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential gcc \
    zlib1g-dev libjpeg-dev libpng-dev libzbar-dev \
    tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng \
    ghostscript qpdf ca-certificates \
    && apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Stage 2: runtime
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
    tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng \
    ghostscript qpdf libzbar0 libjpeg62-turbo libpng16-16 \
    && apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
# Nur die eigene App ins Image, keine nutzerspezifischen Scripts
COPY api.py /app/api.py
ENV PYTHONUNBUFFERED=1
EXPOSE 51822
CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "51822"]

Der Trick: nutzerspezifische Scripts kommen nicht ins Image, sondern werden später per Volume gemountet. Das Image bleibt dadurch generisch und wiederverwendbar.


Schritt 3: Repository-Struktur
#

mein-tool/
├── .forgejo/
│   └── workflows/
│       └── build.yml       # CI/CD Workflow
├── dockerbuild/
│   ├── Dockerfile
│   ├── requirements.txt
│   └── api.py              # Fester Bestandteil des Images
├── scripts/                # Beispiel-Scripts, per Volume gemountet
│   └── example_script.py
└── docker-compose.yml

Schritt 4: Secrets anlegen
#

Damit der Workflow sich an der Container Registry anmelden kann, braucht er Zugangsdaten. Diese werden als Secrets im Repository hinterlegt:

In Forgejo → Repository → Settings → Secrets → Actions:

  • REGISTRY_USER → Forgejo-Benutzername
  • REGISTRY_TOKEN → Personal Access Token mit package:write-Berechtigung (Forgejo Profil → Settings → Applications)

Schritt 5: Der Workflow
#

# .forgejo/workflows/build.yml
name: Build & Push Docker Image

on:
  push:
    branches:
      - master

jobs:
  build:
    runs-on: docker
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install Docker CLI
        run: |
          apt-get update && apt-get install -y --no-install-recommends \
            ca-certificates curl gnupg
          install -m 0755 -d /etc/apt/keyrings
          curl -fsSL https://download.docker.com/linux/debian/gpg \
            | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
          . /etc/os-release
          echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
            https://download.docker.com/linux/debian ${VERSION_CODENAME} stable" \
            > /etc/apt/sources.list.d/docker.list
          apt-get update && apt-get install -y docker-ce-cli

      - name: Login to Forgejo Container Registry
        run: |
          echo "${{ secrets.REGISTRY_TOKEN }}" | docker login forgejo.example.com \
            --username "${{ secrets.REGISTRY_USER }}" \
            --password-stdin

      - name: Build and Push
        run: |
          docker build \
            -t forgejo.example.com/${{ github.repository_owner }}/mein-tool:latest \
            -f dockerbuild/Dockerfile \
            ./dockerbuild
          docker push forgejo.example.com/${{ github.repository_owner }}/mein-tool:latest

Ab jetzt passiert folgendes bei jedem git push auf master:

  1. Forgejo Actions startet den Workflow
  2. Der Runner installiert den Docker CLI
  3. Er meldet sich an der eigenen Container Registry an
  4. Das Image wird gebaut und gepusht

Schritt 6: Den Container nutzen
#

Einmalig auf dem Zielsystem einloggen:

docker login forgejo.example.com

Die docker-compose.yml auf dem Zielsystem:

services:
  mein-tool:
    image: forgejo.example.com/BENUTZERNAME/mein-tool:latest
    container_name: mein-tool
    volumes:
      - ./import:/data/import:z
      - ./output:/data/output:z
      - ./scripts:/app/scripts:z    # Scripts von außen mounten
    ports:
      - "51822:51822"

Image aktualisieren:

docker compose pull && docker compose up -d

Was jetzt noch möglich ist
#

Das ist erst der Anfang. Mit demselben Runner lassen sich noch viele weitere Dinge automatisieren:

Verschiedene Image-Tags: Mit Git-Tags lassen sich stabile Versionen einfrieren. Ein v1.0-Tag baut mein-tool:v1.0 zusätzlich zu latest.

Automatische Tests: Vor dem Push kann der Workflow Tests ausführen – schlägt ein Test fehl, wird kein kaputtes Image gepusht.

Multi-Arch-Images: Mit docker buildx lassen sich Images für amd64 und arm64 gleichzeitig bauen – praktisch wenn der Server x86 ist, aber z.B. Raspberry Pis das Image pullen sollen.

Andere Sprachen und Tools: Der Runner ist nicht auf Docker beschränkt. Hugo-Blogs bauen und deployen, Go-Binaries kompilieren, Python-Tests mit pytest, Markdown-Linting – alles was in einem Debian-Container läuft, läuft auch im Workflow.

Benachrichtigungen: Bei fehlgeschlagenen Builds lässt sich eine Benachrichtigung per Webhook, Matrix oder Gotify verschicken.


Fazit
#

Mit Forgejo, einem Actions Runner und ein paar Zeilen YAML ist ein vollständiger selbstgehosteter Build- und Distributionsprozess für Docker Images aufgebaut. Kein Docker Hub, keine externe Registry, keine Abhängigkeit von Drittdiensten.

Der Aufwand für die Einrichtung zahlt sich schnell aus: jede Änderung am Code landet nach einem git push automatisch als fertiges Image in der eigenen Registry – pullbar von jedem System das Zugriff auf die Forgejo-Instanz hat.