# 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](#4b-registreren-op-de-app-server) 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: ```bash 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: ```bash # 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 `/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//_data/gitea/conf/app.ini` ### 1c. `app.ini` aanpassen en Forgejo herstarten ```bash 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: ```ini [actions] ENABLED = true DEFAULT_ACTIONS_URL = https://code.forgejo.org ``` Herstart Forgejo: ```bash 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 ```bash apt update && apt upgrade -y apt install -y curl ca-certificates gnupg git rsync ufw ``` ### 2b. Docker + Compose plugin installeren ```bash 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 ```bash 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) ```bash 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. ```bash # 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 ```bash 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: ```yaml 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`: ```bash 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`: ```bash 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: > ```bash > 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: ```bash 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: ```bash 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: ```bash 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`. ```yaml 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`. ```yaml 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-` 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: ```bash 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: ```bash 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: ```bash 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: ```bash 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: ```yaml 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. |