server-up/docs/forgejo-actions-setup.md
bes-r 34edd95c9b
Some checks failed
Deploy server-up (prod) / deploy (push) Successful in 6s
Build and push image / build (push) Has been cancelled
v0.3.1 - UI restyle, inklapbaar menu, taalmodule
2026-06-02 23:25:30 +02:00

20 KiB

Forgejo Actions — deploy-pipeline opzetten voor Server Up

Volledig stappenplan om vanuit je laptop via Forgejo automatisch te deployen naar een Ubuntu/Debian server met Docker Compose. Inclusief een dev/prod-splitsing zodat je dev-server alleen de ontwikkelversie krijgt en prod-servers alleen stabiele releases.

Al eerder opgezet? Lees dit eerst

Als je de pipeline al hebt ingericht en alleen de dev/prod-splitsing wilt toevoegen, hoef je niet alles opnieuw te doen. Voer alleen deze stappen uit:

  1. Runner opnieuw registreren met label dev — zie Stap 4b
  2. dev-branch aanmaken in je repo: git checkout -b dev && git push -u origin dev
  3. Workflow-bestanden bijwerken — commit en push de gewijzigde .forgejo/workflows/deploy.yml en nieuwe deploy-prod.yml naar dev
  4. /opt/server-up/.env aanmaken voor handmatige builds buiten de pipeline:
    echo "SU_VERSION=dev-handmatig" > /opt/server-up/.env
    

Architectuur

[ Laptop ]
    |
    | git push naar dev-branch
    v
[ Forgejo VPS ] ──── dispatcht workflow ────> [ Dev-server ]
                                               - act_runner (label: dev)
                                               - Docker + Compose
                                               - /opt/server-up/

[ Forgejo VPS ] ──── dispatcht workflow ────> [ Prod-server(s) ]
                                               - act_runner (label: prod)
                                               - Docker + Compose
                                               - /opt/server-up/

Twee soorten servers:

  • Dev-server — draait de dev branch. Wordt automatisch bijgewerkt bij elke push naar dev. Bedoeld voor testen.
  • Prod-server(s) — draaien de main branch of een v*-tag. Worden alleen bijgewerkt als je bewust naar main merget of een release tagt.

De routing werkt via runner-labels: elke server registreert zijn runner met een eigen label (dev of prod). De workflow kiest op dat label. Zo pikt een dev-server nooit een prod-deploy op en andersom.

We draaien de runner in host mode: jobs draaien direct op de host, niet in een container. Dat is voor deploy-jobs verreweg het simpelst — Docker socket en bind-mount paden kloppen vanzelf.


Wat je nodig hebt

  • Forgejo VPS met root- of admin-toegang
  • App-server (Ubuntu 22.04/24.04 of Debian 12) met root- of sudo-toegang
  • DNS-naam voor je Forgejo-instance (bv. forgejo.example.com) met geldig TLS-certificaat
  • Server Up uitgecheckt lokaal, met Dockerfile en docker-compose.yml

Stap 1 — Actions globaal aanzetten in Forgejo

Forgejo heeft geen UI-toggle voor Actions. Je zet het aan in app.ini. Zolang het uit staat zie je geen Actions-tab in repo's.

1a. Snelle check: staat het al aan?

Open een willekeurige repo in Forgejo. Zie je bovenaan een Actions-tab? Dan is het al aan — ga door naar stap 1d.

1b. app.ini vinden

SSH naar je Forgejo VPS:

# Zoek de containernaam
docker ps --format 'table {{.Names}}\t{{.Image}}' | grep -i forgejo

# Bekijk huidige Actions-config
docker exec forgejo cat /data/gitea/conf/app.ini | grep -A2 "\[actions\]" \
  || echo "Geen [actions] blok gevonden"

# Vind het hostpad van de data-map
docker inspect forgejo --format \
  '{{ range .Mounts }}{{ .Source }} -> {{ .Destination }}{{ "\n" }}{{ end }}'

Zoek de regel waar Destination /data is. Het bestand staat dan op <Source>/gitea/conf/app.ini. Typische plekken:

  • /var/lib/forgejo/gitea/conf/app.ini
  • ./forgejo/gitea/conf/app.ini (relatief aan je docker-compose.yml)
  • Bij een named volume: /var/lib/docker/volumes/<naam>/_data/gitea/conf/app.ini

1c. app.ini aanpassen en Forgejo herstarten

APP_INI=/var/lib/forgejo/gitea/conf/app.ini   # pas aan naar jouw pad
sudo cp "$APP_INI" "$APP_INI.bak"
sudo nano "$APP_INI"

Voeg onderaan toe — of update het bestaande [actions]-blok:

[actions]
ENABLED = true
DEFAULT_ACTIONS_URL = https://code.forgejo.org

Herstart Forgejo:

docker compose restart forgejo   # in de map waar je docker-compose.yml staat
# of zonder compose:
docker restart forgejo

Refresh een repo-pagina — de Actions-tab moet nu zichtbaar zijn.

1d. Repo-Actions inschakelen

Ga in je repo naar Settings → Actions → General → Enable Repository Actions (dit is standaard aan zodra Actions globaal aan staan).


Stap 2 — App-server voorbereiden

SSH naar de app-server als root of met sudo.

2a. Systeem up-to-date

apt update && apt upgrade -y
apt install -y curl ca-certificates gnupg git rsync ufw

2b. Docker + Compose plugin installeren

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | \
  gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg

# Voor Ubuntu vervang 'debian' door 'ubuntu'
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
  > /etc/apt/sources.list.d/docker.list

apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker
docker --version && docker compose version

2c. Deploy-user aanmaken

useradd -m -s /bin/bash deploy
usermod -aG docker deploy
mkdir -p /opt/server-up
chown deploy:deploy /opt/server-up

De runner draait als deploy. Door deze user in de docker-groep te zetten kan hij docker compose zonder sudo aanroepen.

2d. Firewall (optioneel maar aanbevolen)

ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

De runner heeft alleen uitgaande HTTPS naar je Forgejo nodig — geen inkomende poort.


Stap 3 — Act_runner installeren

Forgejo gebruikt act_runner als runner-daemon.

# Controleer de nieuwste versie op:
# https://code.forgejo.org/forgejo/act_runner/releases
VERSION=6.2.2
ARCH=amd64   # of arm64 voor ARM-servers
curl -fsSL -o /usr/local/bin/act_runner \
  "https://code.forgejo.org/forgejo/runner/releases/download/v${VERSION}/forgejo-runner-${VERSION}-linux-${ARCH}"
chmod +x /usr/local/bin/act_runner
act_runner --version

Werkdirectory en config

mkdir -p /etc/act_runner /var/lib/act_runner
chown deploy:deploy /var/lib/act_runner
act_runner generate-config > /etc/act_runner/config.yaml
chown root:deploy /etc/act_runner/config.yaml
chmod 640 /etc/act_runner/config.yaml

Open /etc/act_runner/config.yaml en pas deze velden aan:

runner:
  file: /var/lib/act_runner/.runner
  capacity: 1
  timeout: 30m
  insecure: false
  fetch_timeout: 5s
  fetch_interval: 2s
cache:
  enabled: true
  dir: /var/lib/act_runner/cache
host:
  workdir_parent: /var/lib/act_runner/host-work

Stap 4 — Runner registreren

4a. Token ophalen uit Forgejo

Ga in Forgejo naar je repo → Settings → Actions → Runners → Create new Runner. Kopieer het token.

Kies het niveau dat bij je gebruik past:

  • Repo-niveau (alleen deze repo) — veiligst
  • Organization-niveau — handig als je meerdere repo's hebt
  • Site-niveau — alleen als je veel repo's wilt deployen

4b. Registreren op de app-server

Dev-server — registreer met label dev:

sudo -u deploy bash -c '
cd /var/lib/act_runner && \
act_runner register \
  --no-interactive \
  --instance https://jouw-forgejo-url \
  --token JOUW_TOKEN \
  --name dev-server \
  --labels self-hosted,dev \
  --config /etc/act_runner/config.yaml
'

Prod-server — registreer met label prod:

sudo -u deploy bash -c '
cd /var/lib/act_runner && \
act_runner register \
  --no-interactive \
  --instance https://jouw-forgejo-url \
  --token JOUW_TOKEN \
  --name prod-server-1 \
  --labels self-hosted,prod \
  --config /etc/act_runner/config.yaml
'

Al geregistreerd met andere labels? Stop de service, verwijder het .runner-bestand en registreer opnieuw:

systemctl stop act_runner
rm /var/lib/act_runner/.runner
# voer daarna bovenstaand register-commando uit

Controleer in Forgejo onder Settings → Actions → Runners — je runner staat als Idle.


Stap 5 — Runner als systemd-service instellen

Doe dit op elke server na de registratie:

cat > /etc/systemd/system/act_runner.service << 'EOF'
[Unit]
Description=Forgejo Actions runner
After=network-online.target docker.service
Wants=network-online.target

[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/var/lib/act_runner
ExecStart=/usr/local/bin/act_runner daemon --config /etc/act_runner/config.yaml
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only
PrivateTmp=true

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now act_runner
systemctl status act_runner

Logs volgen:

journalctl -u act_runner -f

Stap 6 — Branches inrichten

De workflows zijn opgezet rond twee branches:

Branch Doel Deploy naar
dev Ontwikkeling, testen Dev-server
main Stabiele releases Prod-server(s)

Maak de dev-branch aan als die er nog niet is:

git checkout -b dev
git push -u origin dev

Typische werkwijze:

feature-branch → merge naar dev → automatisch op dev-server getest
dev → stabiel → merge naar main → automatisch op prod-server(s)
main → git tag v1.2.3 → deploy met versienummer ingebakken

Stap 7 — Workflow-bestanden

De repo bevat twee deploy-workflows in .forgejo/workflows/:

deploy.yml — dev-server

Triggert bij elke push naar dev. Draait alleen op runners met label dev.

name: Deploy server-up (dev)

on:
  push:
    branches: [dev]
  workflow_dispatch:

concurrency:
  group: server-up-deploy-dev
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: [self-hosted, dev]
    timeout-minutes: 20

    env:
      DEPLOY_DIR: /opt/server-up

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - name: Bepaal versie
        id: version
        run: |
          set -euo pipefail
          VERSION="dev-${GITHUB_SHA:0:8}"
          echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
          echo "SU_VERSION=${VERSION}" >> "$GITHUB_ENV"
          echo "CHANNEL=dev" >> "$GITHUB_ENV"
          echo "Deploying version: ${VERSION}"          

      - name: Sync code naar deploy-directory
        run: |
          set -euo pipefail
          rsync -a --delete \
            --exclude='.git/' \
            --exclude='.forgejo/' \
            ./ "$DEPLOY_DIR/"          

      - name: Schrijf VERSION-bestand
        working-directory: ${{ env.DEPLOY_DIR }}
        run: echo "${VERSION}" > VERSION

      - name: Build image
        working-directory: ${{ env.DEPLOY_DIR }}
        run: |
          docker compose build --pull
          docker tag "server-up:${VERSION}" server-up:latest
          docker tag "server-up:${VERSION}" server-up:dev          

      - name: Deploy
        working-directory: ${{ env.DEPLOY_DIR }}
        run: |
          set -euo pipefail
          docker compose up -d --remove-orphans
          docker compose ps          

      - name: Wachten op healthcheck
        run: |
          set -euo pipefail
          for i in {1..40}; do
            status=$(docker inspect --format '{{.State.Health.Status}}' server-up 2>/dev/null || echo "starting")
            echo "poll $i: $status"
            if [ "$status" = "healthy" ]; then
              echo "server-up is healthy als versie ${VERSION}"
              exit 0
            fi
            sleep 3
          done
          echo "server-up werd niet healthy in tijd — logs:"
          cd "$DEPLOY_DIR" && docker compose logs --tail=200
          exit 1          

      - name: Oude images opruimen (behoud laatste 5)
        if: success()
        run: |
          docker images server-up --format '{{.Tag}} {{.ID}}' \
            | grep -vE '^(latest|release|dev) ' \
            | tail -n +6 \
            | awk '{print $2}' \
            | xargs -r docker rmi -f || true
          docker image prune -f          

deploy-prod.yml — prod-servers

Triggert bij push naar main of een v*-tag. Draait alleen op runners met label prod.

name: Deploy server-up (prod)

on:
  push:
    branches: [main]
    tags: ['v*']
  workflow_dispatch:

concurrency:
  group: server-up-deploy-prod
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: [self-hosted, prod]
    timeout-minutes: 20

    env:
      DEPLOY_DIR: /opt/server-up

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - name: Bepaal versie
        id: version
        run: |
          set -euo pipefail
          if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
            VERSION="${GITHUB_REF_NAME}"
            CHANNEL="release"
          else
            VERSION="main-${GITHUB_SHA:0:8}"
            CHANNEL="main"
          fi
          echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
          echo "CHANNEL=${CHANNEL}" >> "$GITHUB_ENV"
          echo "SU_VERSION=${VERSION}" >> "$GITHUB_ENV"
          echo "Deploying version: ${VERSION} (${CHANNEL})"          

      - name: Sync code naar deploy-directory
        run: |
          set -euo pipefail
          rsync -a --delete \
            --exclude='.git/' \
            --exclude='.forgejo/' \
            ./ "$DEPLOY_DIR/"          

      - name: Schrijf VERSION-bestand
        working-directory: ${{ env.DEPLOY_DIR }}
        run: echo "${VERSION}" > VERSION

      - name: Build image
        working-directory: ${{ env.DEPLOY_DIR }}
        run: |
          docker compose build --pull
          docker tag "server-up:${VERSION}" server-up:latest
          docker tag "server-up:${VERSION}" "server-up:${CHANNEL}"          

      - name: Deploy
        working-directory: ${{ env.DEPLOY_DIR }}
        run: |
          set -euo pipefail
          docker compose up -d --remove-orphans
          docker compose ps          

      - name: Wachten op healthcheck
        run: |
          set -euo pipefail
          for i in {1..40}; do
            status=$(docker inspect --format '{{.State.Health.Status}}' server-up 2>/dev/null || echo "starting")
            echo "poll $i: $status"
            if [ "$status" = "healthy" ]; then
              echo "server-up is healthy als versie ${VERSION}"
              exit 0
            fi
            sleep 3
          done
          echo "server-up werd niet healthy in tijd — logs:"
          cd "$DEPLOY_DIR" && docker compose logs --tail=200
          exit 1          

      - name: Oude images opruimen (behoud laatste 5)
        if: success()
        run: |
          docker images server-up --format '{{.Tag}} {{.ID}}' \
            | grep -vE '^(latest|release|main) ' \
            | tail -n +6 \
            | awk '{print $2}' \
            | xargs -r docker rmi -f || true
          docker image prune -f          

Wat doet een deploy-workflow?

  1. Code uitchecken in de runner-workspace
  2. Versienummer bepalen (dev-<sha> voor dev, v1.2.3 voor tags)
  3. Code naar /opt/server-up synchen via rsync --delete
  4. Docker-image bouwen met docker compose build --pull en SU_VERSION ingebakken
  5. Container herstarten met docker compose up -d
  6. Wachten tot de healthcheck groen is (max. ~2 minuten)
  7. Oude images opruimen (behoudt de laatste 5)

Stap 8 — Handmatig triggeren

Elke workflow heeft workflow_dispatch, waarmee je hem handmatig start zonder een commit:

Forgejo UI: Actions → selecteer de workflow → Run workflow

Handig als je de pipeline opnieuw wilt draaien na een serverherstel of configuratiewijziging.


Stap 9 — Eerste deploy testen

Vanuit je repo op je laptop:

git checkout dev
echo "# Deploy test $(date)" >> README.md
git add README.md
git commit -m "ci: trigger first deploy"
git push origin dev

In Forgejo: ga naar de Actions-tab van je repo. Je ziet de workflow-run starten. Klik erop om de logs live te volgen.

Op de server parallel meekijken:

journalctl -u act_runner -f
# in een tweede shell:
docker logs -f server-up

Als alles goed gaat: groen vinkje, container draait nieuwe image.


Stap 10 — Rollback

Omdat images getagd worden met de git SHA is rollback eenvoudig:

cd /opt/server-up

# Beschikbare images bekijken
docker images server-up --format '{{.Tag}}\t{{.CreatedAt}}'

# Rollback naar een eerdere SHA — pas aan
SU_VERSION=dev-abcdef12 docker compose up -d

Voor extra zekerheid kun je in de workflow de huidige tag wegschrijven vóór de deploy:

echo "$(docker inspect server-up --format '{{.Config.Image}}')" \
  > /opt/server-up/.last-good-image

Stap 11 — Uitbreidingen

CI vóór deploy (tests, lint)

Splits in twee jobs zodat tests in isolatie draaien en de deploy alleen bij groen start:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build --target build -t server-up:test .
      # voeg je eigen testcommando's toe

  deploy:
    needs: test
    runs-on: [self-hosted, dev]
    steps:
      # ... zoals hierboven

Nieuwe prod-server toevoegen

  1. Installeer act_runner op de nieuwe server (stap 3)
  2. Maak een nieuw runner-token aan in Forgejo
  3. Registreer met --labels self-hosted,prod (stap 4b)
  4. Stel de systemd-service in (stap 5)

De deploy-prod.yml workflow pikt de nieuwe runner automatisch op bij de volgende push naar main of een tag.

Image registry

Voor grotere setups wil je niet op de productieserver bouwen. Push de image naar de Forgejo Container Registry (ingebouwd onder Packages) vanuit een aparte build-job, en trek alleen op de app-server. Scheelt CPU en disk op productie.


Beveiligingstips

  • Runner draait niet als root — de deploy-user heeft alleen rechten op /opt/server-up en is lid van de docker-groep. Het Server Up-container zelf draait als root (user: "0:0" in compose), dat is een bewuste keuze voor Docker socket-toegang.
  • Repo-niveau token is veiliger dan site-niveau — bij een lek heeft een aanvaller niet meteen toegang tot alle repo's.
  • Branch protection op main — Repo → Settings → Branches → Add Rule. Vereis dat pushes via een PR gaan en de dev-pipeline groen is.
  • Secrets nooit in compose of git — zet gevoelige waarden in Forgejo Secrets en schrijf ze in de workflow-step naar .env.
  • Runner-binary updaten — periodiek bijwerken: nieuwe versie naar /usr/local/bin/act_runner schrijven en systemctl restart act_runner. Geen herregistratie nodig.
  • Forgejo zelf — zet 2FA aan, draai achter een reverse proxy met TLS, configureer fail2ban op de SSH-poort.

Problemen oplossen

Symptoom Oorzaak / oplossing
Workflow blijft "queued" hangen Geen runner met het juiste label online. Check systemctl status act_runner en of het label (dev/prod) overeenkomt met runs-on in de workflow.
permission denied bij Docker deploy-user niet in docker-groep, of service gestart vóór die toevoeging — systemctl restart act_runner.
rsync: not found apt install rsync op de server.
SU_VERSION ontbreekt bij handmatige build Maak /opt/server-up/.env aan: echo "SU_VERSION=dev-handmatig" > /opt/server-up/.env
Container draait oude code na rebuild Deploy-directory niet bijgewerkt. Controleer: docker exec --workdir /app server-up grep -n "trim_blocks" core/boilerplates.py. Als dit True toont, trigger de pipeline handmatig of sync de code zelf: rsync -a --delete --exclude='.git/' --exclude='.forgejo/' ./ /opt/server-up/
Boilerplate geeft yaml: line 5 fout Zelfde oorzaak als boven — trim_blocks=True in de draaiende container. Fix: rebuild na correcte sync. Eerder geïnstalleerde stacks met kapotte compose-bestanden opnieuw installeren via de app-store.
Healthcheck-step faalt altijd Check docker compose logs. Vaak ontbreekt een env-var of klopt een pad niet.
TLS-fout bij registreren Zet runner.insecure: true in config alleen tijdelijk voor self-signed certs. Een geldig certificaat (Let's Encrypt) is de structurele oplossing.