Gitea Docker CI/CD Workflow Setup
This guide outlines how to set up a local CI/CD workflow using Gitea running in Docker. The goals are:
- Use Gitea as the primary Git repository.
- Optionally mirror repositories to GitHub.
- Build Docker images for selected repositories (e.g., NextJS applications) using Gitea Runners.
- Host the built Docker images in a local Docker Registry.
- Automatically update running Docker containers when new images are available.
Prerequisites
- Docker installed and running on your host machine.
- Docker Compose installed.
- A running Gitea instance, preferably managed via Docker Compose.
Goal Architecture
- Developer: Pushes code to a Gitea repository.
- Gitea:
- (Optional/Manual) Mirrors the repository to GitHub.
- Triggers a Gitea Action workflow on push/tag.
- Gitea Runner (Docker):
- Picks up the workflow job.
- Checks out the code.
- Builds a Docker image using Docker-in-Docker or by mounting the host's Docker socket.
- Pushes the built image to a Local Docker Registry.
- Local Docker Registry (Docker): Stores the built application images.
- Watchtower (or similar tool - Docker): Monitors the Local Docker Registry for new image versions and automatically updates the running application containers.
- Application Container (Docker): Runs the application (e.g., NextJS app) using the image from the Local Docker Registry.
Step 1: Set up Gitea <-> GitHub Synchronization (Mirroring)
Configure Gitea to push changes to a corresponding GitHub repository.
- Generate a GitHub Personal Access Token (PAT):
- Go to GitHub Settings -> Developer settings -> Personal access tokens -> Tokens (classic) or Fine-grained tokens.
- Generate a token with
reposcope (or specific repository write access). Copy the token securely.
- Configure Mirroring in Gitea:
- Navigate to your Gitea repository.
- Go to
Settings->Repository->Mirror Settings. - Click
Add Push Mirror. - Git Remote Repository URL: Enter the HTTPS URL of your GitHub repo (e.g.,
https://github.com/your-username/your-repo.git). - Authorization:
- Username: Your GitHub username.
- Password/Token: Paste the GitHub PAT.
- Sync Interval: Set the automatic sync frequency (e.g.,
1h,8h) or leave blank/0for manual sync only. - Click
Add Mirror Repository.
- Manual Push (Optional): Click the "Synchronize Now" button next to the mirror entry to push immediately.
Note: Gitea becomes the source of truth. Changes made directly on GitHub won't sync back automatically without a pull mirror setup.
Step 2: Set up a Local Docker Registry
This container stores the Docker images built by the runner.
-
Add Registry Service to
docker-compose.yml:# In your docker-compose.yml version: '3.7' services: # ... your other services (like Gitea) ... registry: image: registry:2 container_name: local-docker-registry ports: # Map to a host port, e.g., 5000. Change if needed. - "5000:5000" environment: REGISTRY_AUTH: htpasswd REGISTRY_AUTH_HTPASSWD_REALM: Local Registry REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd volumes: # Persist registry data - ./registry-data:/var/lib/registry - ./auth:/auth restart: always networks: # Use a common network if Gitea/Runner need to access it by name - your_shared_network # Optional: Replace with your network name volumes: registry-data: # ... other volumes ... networks: your_shared_network: # Optional: Define if not already defined external: false # Or true if defined elsewhere -
Run the Registry:
docker-compose -f <your-compose-file.yml> up -d registry -
Configure Docker Daemon for Insecure Registry:
-
Your Docker daemon needs to trust this HTTP registry. Edit
/etc/docker/daemon.json(create if it doesn't exist):{ "insecure-registries" : ["YOUR_HOST_IP:5000"] }- Important: Replace
YOUR_HOST_IPwith the actual network IP address of the machine running the registry (e.g.,192.168.1.100). Do not uselocalhostor127.0.0.1if the runner is in a separate container.
- Important: Replace
-
Restart the Docker daemon:
sudo systemctl restart docker # Or equivalent command for your OS (e.g., restart Docker Desktop) -
Security Note: This uses HTTP and is insecure. For sensitive environments, configure TLS for the registry.
-
-
Add auth to the registry
-
Create Auth Directory: Near your
docker-compose.ymlfile.mkdir auth -
Generate htpasswd File: Replace
<your_desired_registry_username>with your chosen username. You will be prompted for a password - remember it!
htpasswd -B -c ./auth/htpasswd <your_desired_registry_username> -
Step 3: Set up Gitea Runners
Runners execute Gitea Actions workflows.
-
Enable Actions in Gitea:
-
Ensure Actions are enabled in your Gitea instance's
app.iniconfiguration file:[actions] ENABLED = true -
Restart your Gitea container after changing
app.ini.
-
-
Get Runner Registration Token:
- In Gitea, go to
Site Administration->Runners(for global runners) or RepositorySettings->Actions->Runners(for repository-specific runners). - Note the
Registration Token.
- In Gitea, go to
-
Add Runner Service to
docker-compose.yml:- This example mounts the host's Docker socket.
# In your docker-compose.yml version: '3.7' services: # ... your gitea, registry services ... gitea-runner: image: gitea/act_runner:latest # Check for the latest official image tag container_name: gitea-runner environment: # Get these from Gitea Admin/Repo Settings -> Actions -> Runners GITEA_INSTANCE_URL: http://your_gitea_url:3000 # URL to your Gitea instance GITEA_RUNNER_REGISTRATION_TOKEN: YOUR_REGISTRATION_TOKEN_HERE GITEA_RUNNER_NAME: my-docker-runner # Optional name # Define runner capabilities. 'docker' is used in the example workflow. # Adjust labels as needed for your workflows. GITEA_RUNNER_LABELS: docker:docker://host.docker.internal/var/run/docker.sock,ubuntu-latest:docker://node:18-bullseye volumes: # Mount Docker socket from host - allows runner to use host Docker - /var/run/docker.sock:/var/run/docker.sock # Persist runner config - ./gitea-runner-data:/data restart: always networks: - your_shared_network # Ensure it can reach Gitea and the Registry volumes: gitea-runner-data: # ... other volumes ... networks: your_shared_network: # Define if not already defined # ...- Replace:
http://your_gitea_url:3000with your Gitea instance URL andYOUR_REGISTRATION_TOKEN_HEREwith the token from Gitea. - Labels: Ensure the
GITEA_RUNNER_LABELSmatch theruns-on:directive in your workflow files.
-
Run the Runner:
docker-compose -f <your-compose-file.yml> up -d gitea-runner -
Verify Registration: Check the
Runnerssection in Gitea (Admin or Repository Settings) to confirm the runner is registered and idle.
Step 4: Create Gitea Actions Workflow for Building and Pushing
Define the build process for your application (e.g., NextJS).
-
Create a
Dockerfile:- Add a
Dockerfileto the root of your application's repository. Example for NextJS (multi-stage):
# Stage 1: Build the application FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm install COPY . . # Example: Set build-time args if needed # ARG NEXT_PUBLIC_API_URL # ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL RUN npm run build # Stage 2: Production image FROM node:18-alpine WORKDIR /app ENV NODE_ENV=production # Uncomment the following line in case you want to disable telemetry during runtime. # ENV NEXT_TELEMETRY_DISABLED 1 COPY --from=builder /app/public ./public # Automatically leverage output traces to reduce image size # [https://nextjs.org/docs/advanced-features/output-file-tracing](https://nextjs.org/docs/advanced-features/output-file-tracing) COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static # Expose the port Next.js runs on EXPOSE 3000 ENV PORT 3000 # Optional: Run as non-root user # RUN addgroup --system --gid 1001 nodejs # RUN adduser --system --uid 1001 nextjs # USER nextjs # Command to run the application CMD ["node", "server.js"] - Add a
-
Create the Workflow File:
- In your repository, create
.gitea/workflows/build-push.yaml:
name: Build and Push Docker Image on: push: branches: - main # Trigger on pushes to the main branch tags: - 'v*.*.*' # Trigger on version tags like v1.0.0 jobs: build-and-push: # Use a runner label defined in your runner's environment variables runs-on: docker steps: - name: Checkout Code uses: actions/checkout@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 # Optional: Login step if your registry requires authentication # - name: Log in to local registry # uses: docker/login-action@v2 # with: # registry: YOUR_HOST_IP:5000 # Use the same IP as in daemon.json # username: ${{ secrets.REGISTRY_USERNAME }} # Store credentials in Gitea secrets # password: ${{ secrets.REGISTRY_PASSWORD }} # Store credentials in Gitea secrets - name: Determine Image Tag id: meta run: | # Use Git tag if present, otherwise use 'latest' for default branch if [[ ${GITHUB_REF} == refs/tags/* ]]; then TAG=${GITHUB_REF#refs/tags/} else TAG=latest fi echo "tag=${TAG}" >> $GITHUB_OUTPUT echo "Using tag: ${TAG}" - name: Build and Push Docker Image uses: docker/build-push-action@v4 with: context: . file: ./Dockerfile push: true # IMPORTANT: Tag format is REGISTRY_HOST:PORT/IMAGE_NAME:TAG tags: YOUR_HOST_IP:5000/your-app-image-name:${{ steps.meta.outputs.tag }} # Optional: Add build arguments # build-args: | # ARG_NAME=value cache-from: type=gha # Optional: Enable build cache cache-to: type=gha,mode=max # Optional: Enable build cache- Replace:
YOUR_HOST_IP:5000with your registry's IP and port.your-app-image-namewith your desired image name.
runs-on: docker: Ensure this matches a label on your Gitea Runner.- Secrets: If using registry authentication, store credentials (
REGISTRY_USERNAME,REGISTRY_PASSWORD) in the Gitea repository'sSettings->Secrets.
- In your repository, create
-
Commit and Push: Add
.gitea/workflows/build-push.yamland yourDockerfileto the repository, commit, and push to Gitea. -
Monitor Workflow: Check the
Actionstab in your Gitea repository to view the workflow execution status.
Step 5: Automatically Update Containers with Watchtower
Use Watchtower to monitor the local registry and redeploy containers when a new image is available.
-
Add Application and Watchtower Services to
docker-compose.yml:# In your docker-compose.yml version: '3.7' services: # ... your gitea, registry, runner services ... # Your Application container (Example: NextJS) my-app: # IMPORTANT: Use the full image path from your local registry image: YOUR_HOST_IP:5000/your-app-image-name:latest # Watchtower tracks this tag container_name: my-app-container ports: - "8080:3000" # Map host port 8080 to container port 3000 (adjust as needed) restart: always networks: - your_shared_network labels: # Required label for Watchtower to manage this container when using --label-enable - "com.centurylinklabs.watchtower.enable=true" # Watchtower service watchtower: image: containrrr/watchtower:latest container_name: watchtower volumes: # Mount Docker socket to allow Watchtower to manage other containers - /var/run/docker.sock:/var/run/docker.sock # Optional: Mount Docker config if registry requires authentication # Make sure docker login has been run on the host first # - $HOME/.docker/config.json:/config.json # Check every 5 minutes (300s), remove old images, only update containers with the specific label command: --cleanup --label-enable --interval 300 restart: always networks: - your_shared_network # Needs network access if registry needs authentication networks: your_shared_network: # Define if not already defined # ...- Replace:
YOUR_HOST_IP:5000/your-app-image-name:latestwith your registry IP, port, image name, and the tag Watchtower should monitor (usuallylatest). image:directive: Ensure your application service (my-app) uses the full image path from your local registry.labels:: Add thecom.centurylinklabs.watchtower.enable=truelabel to any container you want Watchtower to auto-update.command:: Use--label-enablefor safety, so Watchtower only touches labeled containers. Adjust--intervalas needed.
- Replace:
-
Run Application and Watchtower:
# Bring up the application and Watchtower (and others if not running) docker-compose -f <your-compose-file.yml> up -d
Summary Workflow
- Push code (matching
on:criteria in workflow) to Gitea. - Gitea triggers the Actions workflow (
build-push.yaml). - Gitea Runner executes the job: checks out code, builds image, pushes tagged image (e.g.,
:latestor:v1.x.x) to the local registry (YOUR_HOST_IP:5000). - Watchtower (running via Docker) polls the registry.
- If Watchtower detects a new image digest for the tag specified in your application container's
image:directive (e.g.,:latest), it pulls the new image and redeploys the application container using the updated image. - (Optional) Gitea pushes code changes to GitHub via the configured mirror.
You now have an automated build and deployment pipeline hosted locally with Gitea! Remember to replace all placeholders (like YOUR_HOST_IP, image names, network names, tokens) with your actual values.