Introduction#
Tired of manually deploying my Hugo blog every time I publish an article. I decided to set up a local CI/CD pipeline with GitLab Runner. The result: automatic, reliable, and without depending on external services.
Prerequisites#
You need:
- A server with Docker installed
- A repository on GitLab (can be self-hosted or gitlab.com)
- Hugo installed locally for testing
- SSH access configured on your server
Installing GitLab Runner#
First, install GitLab Runner on your server. I did it on Docker because I already had the daemon running.
docker pull gitlab/gitlab-runner:latest
docker run -d --name gitlab-runner \
--restart always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /srv/gitlab-runner/config:/etc/gitlab-runner \
gitlab/gitlab-runner:latestThis mounts the Docker socket so the runner can execute nested containers. Important for building images.
Registering the Runner#
You need a token from your GitLab project. You can find it at:
Configuración del proyecto → CI/CD → Runners
Then you run:
docker exec -it gitlab-runner gitlab-runner register \
--url https://gitlab.com/ \
--registration-token TU_TOKEN_AQUI \
--executor docker \
--docker-image alpine:latest \
--docker-volumes /var/run/docker.sock:/var/run/docker.sock \
--description "Runner Local Hugo"Choose Docker as the executor. It’s the cleanest option for this case.
Configuring the pipeline#
At the root of your repository, create .gitlab-ci.yml:
stages:
- build
- deploy
variables:
DEPLOY_PATH: /home/deploy/blog-hugo/public
build:
stage: build
image: alpine:latest
before_script:
- apk add --no-cache hugo git
script:
- hugo --minify
- echo "Build completado"
artifacts:
paths:
- public/
expire_in: 1 week
only:
- main
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client rsync
- mkdir -p ~/.ssh
- echo "$DEPLOY_KEY" | base64 -d > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H localhost >> ~/.ssh/known_hosts
script:
- rsync -avz --delete public/ deploy@localhost:${DEPLOY_PATH}
- ssh deploy@localhost 'sudo systemctl restart nginx'
only:
- main
when: on_successEnvironment variables#
In Configuración del proyecto → CI/CD → Variables, add:
DEPLOY_KEY: Your private SSH key in base64 (cat ~/.ssh/id_rsa | base64 -w0)DEPLOY_PATH: Path where you want the files (I use/home/deploy/blog-hugo/public)
Deploy user#
On your server, create a specific user:
sudo useradd -m -s /bin/bash deploy
sudo usermod -aG docker deploy
sudo mkdir -p /home/deploy/blog-hugo/public
sudo chown deploy:deploy /home/deploy/blog-hugoConfigure the runner’s public SSH key:
sudo -u deploy ssh-keygen -t ed25519 -N "" -f /home/deploy/.ssh/id_rsa
cat /home/deploy/.ssh/id_rsa.pub >> /home/deploy/.ssh/authorized_keysVerify it works#
Push to the main branch:
git add .
git commit -m "Test CI/CD"
git push origin mainIn GitLab you see the pipeline in real time. If everything is fine, in seconds your blog will be deployed.
sudo -u deploy cat /home/deploy/blog-hugo/public/index.htmlFinal notes#
- The local runner never leaves your network. Full control.
- Build times are fast because everything is on the local machine.
- If you need to cache dependencies, configure persistent volumes in Docker.
- I’ve put restrictions on the
mainbranch to avoid accidental deployments.
After three months running without issues. It’s simple but effective.
Recommended Equipment#
- Raspberry Pi 3 B+ — Lightweight, low-power server to start your homelab
- Raspberry Pi 4 (4GB) — The perfect base for homelab, Docker, and monitoring
Affiliate links. No extra cost for you.