01CI/CD Doesn't Have to Be Complicated
Most CI/CD tutorials assume you're building a pipeline for a team of 50 engineers deploying microservices to Kubernetes. If you're a solo developer or small team deploying Docker Compose applications to a VPS, those guides are overkill.
I run CI/CD for all my side projects using self-hosted Gitea with built-in Actions (GitHub Actions-compatible) on the same server that runs my applications. The entire pipeline — push code, run tests, build image, deploy — takes under 2 minutes and costs nothing beyond the server I'm already paying for.
Here's how to set up a practical CI/CD pipeline for Docker Compose deployments.
02The Simple Architecture
For side projects, the simplest architecture is:
1. Git push to your self-hosted Gitea (or GitHub)
2. CI runner picks up the change and runs your pipeline
3. Pipeline builds a Docker image, runs tests
4. On success, SSH into your production server and run docker compose pull && docker compose up -d
That's it. No container registry needed (we'll build directly on the target), no complex orchestration, no blue-green deployments. For a side project serving hundreds or even thousands of users, this is perfectly adequate.
If you need zero-downtime deployments, we can add that with a simple health check, but honestly, the 2-3 seconds of downtime during docker compose up -d has never been a problem for any of my projects.
03Setting Up Gitea with Actions
Gitea 1.19+ includes built-in Actions that are compatible with GitHub Actions workflows. This means you can reuse existing GitHub Actions from the marketplace:
[docker-compose.yml]
1# gitea/docker-compose.yml2services: 3 gitea: 4 image: gitea/gitea:1.225 container_name: gitea6 restart: unless-stopped7 ports: 8 - "3000:3000"9 - "2222:22"10 volumes: 11 - gitea_data:/data12 environment: 13 - GITEA__actions__ENABLED=true1415 runner: 16 image: gitea/act_runner:latest17 container_name: gitea-runner18 restart: unless-stopped19 volumes: 20 - /var/run/docker.sock:/var/run/docker.sock21 - runner_data:/data22 environment: 23 - GITEA_INSTANCE_URL=http://gitea:300024 - GITEA_RUNNER_REGISTRATION_TOKEN=${RUNNER_TOKEN}2526volumes: 27 gitea_data: 28 runner_data: Register the runner token from Gitea's admin panel under Site Administration > Actions > Runners. The runner will automatically pick up jobs from any repository with Actions enabled.
04A Practical Deployment Pipeline
Create .gitea/workflows/deploy.yml in your project repository:
[deploy.yml]
1# .gitea/workflows/deploy.yml2name: Deploy3on: 4 push: 5 branches: [main]67jobs: 8 deploy: 9 runs-on: ubuntu-latest10 steps: 11 - uses: actions/checkout@v41213 - name: Run tests14 run: docker compose -f docker-compose.test.yml run --rm test1516 - name: Deploy to production17 uses: appleboy/ssh-action@v118 with: 19 host: ${{ secrets.DEPLOY_HOST }}20 username: ${{ secrets.DEPLOY_USER }}21 key: ${{ secrets.DEPLOY_KEY }}22 script: |23 cd /opt/myapp24 docker compose pull25 docker compose up -d26 docker compose ps05GitHub Actions Alternative
If you prefer using GitHub with a self-hosted runner (free for public repositories, and even private repos get 2,000 minutes/month free), the workflow is nearly identical. GitHub Actions has a larger marketplace of pre-built actions and better documentation.
The key insight for side projects is: don't overthink it. A deployment pipeline that runs tests and does docker compose up -d on your server covers 90% of what you need. You can add image scanning, staging environments, and canary deployments later if your project grows to need them.
Browse our devops category for Docker Compose configurations of CI/CD tools including Gitea, Drone CI, Jenkins, and Woodpecker CI.