All checks were successful
Build and Publish Web / build (push) Successful in 15s
287 lines
8.5 KiB
Markdown
287 lines
8.5 KiB
Markdown
---
|
|
date: '2026-03-24T20:36:26+01:00'
|
|
draft: false
|
|
title: 'Automatic web update release'
|
|
author: "Jirka"
|
|
tags: ["self-hosted", "gitea", "automation", "docker"]
|
|
categories: ["Blog setup"]
|
|
description: "Last step, how to relase changes to web automatically"
|
|
---
|
|
|
|
Okay, this should be the last step of this article series. Automatic releases to web.
|
|
|
|
The idea is simple, push changes to `master` branch of repo holding this web and automatically release it.
|
|
|
|
We will split this problem into two: Building web with Hugo and second, updating files with webhook. Let's start.
|
|
|
|
> [!WARNING]
|
|
> If you decide to follow my steps, please, be careful and use your head. I do not guarantee they will work for you and do so at your own risk.
|
|
|
|
## Set up Gitea runner
|
|
|
|
Gitea does support actions like GitHub, but you have to provide it runner to execute you tasks. We are going to use the most secured way, runner in docker, which will start another docker container in another docker.
|
|
|
|
I know, it sounds silly, it has a reason. You do not want to have container with privileges to run docker containers. It is even more true, when you consider you can control this container via Gitea actions. So, we will start docker in container and then give Gitea runner rights to that. It is actually much simpler to do, then it may seem like.
|
|
|
|
First, let's get registration runner token. Where you can obtain one depends on scope, from which the runner should be accessible. I want the runner to be usable only by me, so I will get registration token from Gitea → Profile settings → Actions → Runners → Create new Runner.
|
|
|
|
I will store this token into `.env` file inside my `gitea` directory created in [last](./03-gitea.md) article with `RUNNER_TOKEN` name.
|
|
|
|
Next we modify Gitea `docker-compose.yml` file to look as follows:
|
|
```yml
|
|
networks:
|
|
gitea_internal:
|
|
proxy_network:
|
|
gitea_runner_net: # For runner dind comunication
|
|
gitea_net: # For runner gitea comunication, isolated from db
|
|
|
|
|
|
services:
|
|
gitea:
|
|
image: docker.gitea.com/gitea:latest
|
|
container_name: gitea
|
|
environment:
|
|
- USER_UID=${APP_UID}
|
|
- USER_GID=${APP_GID}
|
|
- GITEA__database__DB_TYPE=postgres
|
|
- GITEA__database__HOST=gitea_db:5432
|
|
- GITEA__database__NAME=${DB_NAME}
|
|
- GITEA__database__USER=${DB_USER}
|
|
- GITEA__database__PASSWD=${DB_PASSWORD}
|
|
restart: always
|
|
networks:
|
|
- gitea_internal
|
|
- proxy_network
|
|
- gitea_net
|
|
volumes:
|
|
- ./gitea:/data
|
|
- /etc/timezone:/etc/timezone:ro
|
|
- /etc/localtime:/etc/localtime:ro
|
|
depends_on:
|
|
- gitea_db
|
|
|
|
gitea_db:
|
|
image: docker.io/library/postgres:14
|
|
restart: always
|
|
environment:
|
|
- POSTGRES_USER=${DB_USER}
|
|
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
|
- POSTGRES_DB=${DB_NAME}
|
|
networks:
|
|
- gitea_internal
|
|
volumes:
|
|
- ./postgres:/var/lib/postgresql/data
|
|
|
|
# New
|
|
dind:
|
|
image: docker:dind
|
|
container_name: gitea-dind
|
|
restart: always
|
|
privileged: true
|
|
environment:
|
|
- DOCKER_TLS_CERTDIR=/certs
|
|
volumes:
|
|
- dind_certs:/certs
|
|
- dind_data:/var/lib/docker
|
|
networks:
|
|
gitea_runner_net:
|
|
aliases:
|
|
- docker
|
|
|
|
runner:
|
|
image: gitea/act_runner:latest
|
|
container_name: gitea-runner
|
|
restart: always
|
|
depends_on:
|
|
- dind
|
|
- gitea
|
|
environment:
|
|
- GITEA_INSTANCE_URL=https://git.jirkabuilds.dev
|
|
- GITEA_RUNNER_REGISTRATION_TOKEN=${RUNNER_TOKEN}
|
|
- GITEA_RUNNER_NAME=docker-runner-dind
|
|
- DOCKER_HOST=tcp://docker:2376
|
|
- DOCKER_CERT_PATH=/certs/client
|
|
- DOCKER_TLS_VERIFY=1
|
|
volumes:
|
|
- runner_data:/data
|
|
- dind_certs:/certs:ro
|
|
networks:
|
|
- gitea_net
|
|
- gitea_runner_net
|
|
|
|
volumes:
|
|
dind_certs:
|
|
dind_data:
|
|
runner_data:
|
|
```
|
|
|
|
And that is all, after `docker compose up -d`, runner should show up in the registry.
|
|
|
|
## Set up workflow
|
|
|
|
Next step is to set up workflow inside repository. Before we can do that, we need token for workload, to add access for package creation.
|
|
|
|
To do so, in Gitea got to Settings → Applications → Generate New Token. Give it permission to create package and copy token.
|
|
|
|
Now go to desired repo Settings → Actions → Secrets → Add secret and save token to Value. Name it `PACKAGE_TOKEN`.
|
|
|
|
Now we can use the token in our workflow. Workflow will be defined in `.gitea/workflows/deploy.yml` inside repository. And should contain something like this:
|
|
```yaml
|
|
name: Build and Publish Web
|
|
on:
|
|
push:
|
|
branches:
|
|
- master
|
|
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Code checkout
|
|
uses: actions/checkout@v3
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v3
|
|
with:
|
|
node-version: '18'
|
|
|
|
- name: Hugo Setup
|
|
uses: peaceiris/actions-hugo@v2
|
|
with:
|
|
hugo-version: 'latest'
|
|
extended: true
|
|
|
|
- name: Web Build
|
|
run: |
|
|
cd themes/minimal-black
|
|
npm install
|
|
cd ../..
|
|
hugo --minify
|
|
|
|
- name: Packaging ZIP
|
|
run: |
|
|
cd public
|
|
zip -r ../build.zip ./*
|
|
|
|
# Need to delete old package first, otherwise new will not replace it
|
|
- name: Delete old package (if exists)
|
|
run: |
|
|
curl -X DELETE --user "${{ github.actor }}:${{ secrets.PACKAGE_TOKEN }}" \
|
|
"https://git.jirkabuilds.dev/api/packages/${{ github.repository_owner }}/generic/hugo-build/latest/web.zip" || true
|
|
|
|
- name: Uploading package back to gitea
|
|
run: |
|
|
curl -f --user "${{ github.actor }}:${{ secrets.PACKAGE_TOKEN }}" \
|
|
--upload-file build.zip \
|
|
"https://git.jirkabuilds.dev/api/packages/${{ github.repository_owner }}/generic/hugo-build/latest/web.zip"
|
|
|
|
# TODO - Webhook
|
|
```
|
|
|
|
This will automatically create package on push to master.
|
|
|
|
Now, once the package was successfully built, we can move onto the webhook.
|
|
|
|
## Webhook set up
|
|
|
|
For webhook, we will use `almir/webhook:latest` image, But because we need curl and zip, we will build it on our own.
|
|
Created `webhook` directory next to `web` directory and create `Dockerfile`:
|
|
```
|
|
FROM almir/webhook:latest
|
|
|
|
USER root
|
|
|
|
RUN apk add --no-cache curl unzip
|
|
|
|
WORKDIR /app
|
|
```
|
|
|
|
Next we will specify script, which will be executed on webhook trigger. Create file `deploy.sh` with `+x` privileges to be executable:
|
|
```bash
|
|
#!/bin/sh
|
|
set -e
|
|
|
|
TEMP_DIR=$(mktemp -d)
|
|
ZIP_PATH="$TEMP_DIR/web.zip"
|
|
|
|
curl -fL -o "$ZIP_PATH" "https://git.jirkabuilds.dev/api/packages/jirka/generic/hugo-build/latest/web.zip"
|
|
|
|
# Delete old web data
|
|
rm -rf /site_data/*
|
|
rm -rf /site_data/.[!.]* 2>/dev/null || true
|
|
|
|
unzip -q "$ZIP_PATH" -d /site_data/
|
|
|
|
rm -rf "$TEMP_DIR"
|
|
```
|
|
|
|
As you can see, we are downloading created package and replacing all site content.
|
|
|
|
Next step is to define hook itself. Create `hooks.json` file with following content:
|
|
```
|
|
[
|
|
{
|
|
"id": "deploy-web",
|
|
"execute-command": "/app/deploy.sh",
|
|
"command-working-directory": "/app",
|
|
"trigger-rule": {
|
|
"match": {
|
|
"type": "value",
|
|
"value": "secret token",
|
|
"parameter": {
|
|
"source": "header",
|
|
"name": "X-Deploy-Token"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
```
|
|
You should create `secret`, which you will store to Gitea, same as with token earlier` with name `WEBHOOK_SECRET`. Thanks to it, only the workflow can trigger this webhook.
|
|
|
|
And last step is to create `docker-compose.yml`:
|
|
```
|
|
services:
|
|
webhook:
|
|
build: .
|
|
container_name: release_web_webhook
|
|
volumes:
|
|
- ./hooks.json:/etc/webhook/hooks.json:ro
|
|
- ./deploy.sh:/app/deploy.sh:ro
|
|
- ./../web/site_data:/site_data
|
|
command: ["-verbose", "-hooks=/etc/webhook/hooks.json", "-hotreload"]
|
|
restart: unless-stopped
|
|
networks:
|
|
- proxy_network
|
|
|
|
networks:
|
|
proxy_network:
|
|
```
|
|
|
|
You can see, we are using the same proxy network as for everything. Next step is to add following block to `Caddyfile`:
|
|
```
|
|
hooks.jirkabuilds.dev {
|
|
reverse_proxy release_web_webhook:9000
|
|
}
|
|
```
|
|
|
|
And following line to our root `docker-compose.yml`
|
|
```
|
|
- webhook/docker-compose.yml
|
|
```
|
|
|
|
Now you can restart the stack and webhook should work.
|
|
|
|
Final last step is to replace #TODO in our workflow with following:
|
|
```
|
|
- name: Trigger Webhook
|
|
run: |
|
|
curl -X POST "https://hooks.jirkabuilds.dev/hooks/deploy-web" \
|
|
-H "X-Deploy-Token: ${{ secrets.WEBHOOK_SECRET }}"
|
|
```
|
|
|
|
And that is it. On next push, web should update itself automatically.
|
|
|
|
---
|
|
This article is one from series about this blog and self-hosting. All connected articles can found [here](categories/blog-setup).
|