Introduction#
CI/CD β short for Continuous Integration and Continuous Deployment β is the backbone of modern DevOps.
It ensures that code changes are automatically built, tested, and delivered to production with minimal manual effort.
CI/CD is part of the Infrastructure as Code (IaC) philosophy: your pipelines, configuration, and deployments are written as code, version-controlled, and reproducible.
In this article, I will explain each step of a CI/CD pipeline, illustrating it with my self-hosted homelab setup using Gitea, Hugo, Docker, Semaphore, and Ansible.
π§ Planning#
The first phase is planning. Here you define what needs to be done and how.
Good planning reduces ambiguities for the next phases.
- Decide on tasks and specifications.
- Choose your tools: a Kanban board (GitLab, Trello) works well.
GitLab boards are great to organize and track features, bugs, and improvements.
π» Coding#
The coding phase is where you implement your planned tasks.
- Use an IDE to write your code efficiently.
- Use Git for version control and a remote Git server (GitHub, GitLab, or Gitea) to store code and collaborate.
Each commit on your remote server can trigger pipelines that build, test, release, and deploy automatically.
For Gitea, this requires:
- A
.gitea/workflows
folder in your repository. - A Gitea Runner configured to execute jobs.
Example pipeline structure:
name: My Pipeline
run-name: Build & Deploy
on:
push:
paths:
- <folder-to-watch>
branches:
- main
jobs:
build:
runs-on: <runner>
container:
image: <build-image>
steps:
(...)
test:
runs-on: <runner>
needs: build
steps:
(...)
release:
runs-on: <runner>
needs: test
steps:
(...)
deploy:
runs-on: <runner>
needs: release
steps:
(...)
In this article, I will use my blog as an example, though simpler setups exist.
ποΈ Build#
During the build phase, your code is compiled, packaged, or generated.
For my Hugo blog:
hugo --minify
- The generated files are stored as artifacts for later stages:
- name: Upload files
uses: actions/upload-artifact@v3
with:
name: hugo-artifacts
path: public/
Artifacts allow later jobs (release, deploy) to access the build output reliably.
π§ͺ Test#
Testing ensures your code is secure, functional, and maintainable.
- Run unit tests, integration tests, linters, SAST/DAST, or custom checks.
- For my blog, tests could include grammar checks or broken link detection.
π¦ Release#
The release phase packages your artifacts for deployment.
- First, download the artifacts:
- name: Download files
uses: actions/download-artifact@v3
with:
name: hugo-artifacts
path: public/
- Then, create a Docker image with your files and push it to your registry:
- name: Push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
<GITEA_URL>/${{ secrets.REGISTRY_USER }}/blog:<version>
Notes:
- Secrets store credentials securely (
REGISTRY_USER
in Gitea).- The version is automatically incremented from the last tag.
π Deploy#
Deployment can be automated using tools like ArgoCD or Semaphore.
- In my setup, I trigger deployments via webhooks from the CI pipeline.
- Manual deployment is also possible if automation is not required.
π Monitor#
Monitoring ensures your application and infrastructure remain healthy and performant.
- Define what to monitor (uptime, metrics, logs).
- Choose your tools: dashboards, notifications, alerting.
In my homelab:
- Kuma Uptime for availability.
- Prometheus + Grafana for metrics, with Discord notifications via webhooks.
βοΈ Operate#
Post-deployment operations ensure the system remains updated and consistent.
- In my homelab, I use Ansible and Semaphore to manage configuration and updates automatically.
Conclusion#
CI/CD is like an assembly line for software: each job handles a specific step, moving your code from development to production smoothly and automatically.