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:
- Runner opnieuw registreren met label
dev— zie Stap 4b dev-branch aanmaken in je repo:git checkout -b dev && git push -u origin dev- Workflow-bestanden bijwerken — commit en push de gewijzigde
.forgejo/workflows/deploy.ymlen nieuwedeploy-prod.ymlnaardev /opt/server-up/.envaanmaken 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
devbranch. Wordt automatisch bijgewerkt bij elke push naardev. Bedoeld voor testen. - Prod-server(s) — draaien de
mainbranch of eenv*-tag. Worden alleen bijgewerkt als je bewust naarmainmerget 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
Dockerfileendocker-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 jedocker-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?
- Code uitchecken in de runner-workspace
- Versienummer bepalen (
dev-<sha>voor dev,v1.2.3voor tags) - Code naar
/opt/server-upsynchen viarsync --delete - Docker-image bouwen met
docker compose build --pullenSU_VERSIONingebakken - Container herstarten met
docker compose up -d - Wachten tot de healthcheck groen is (max. ~2 minuten)
- 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
- Installeer
act_runnerop de nieuwe server (stap 3) - Maak een nieuw runner-token aan in Forgejo
- Registreer met
--labels self-hosted,prod(stap 4b) - 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-upen is lid van dedocker-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_runnerschrijven ensystemctl 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. |