Personal Blog (

Blog // Self-Hosted CI/CD Pipeline for Node.js App w/ Gitea Actions & Docker

Self-Hosted CI/CD Pipeline for Node.js App w/ Gitea Actions & Docker

In this quick post I'll explain how I used Gitea Actions to create a CI/CD pipeline for my node.js app

Created: December 25, 2025 | Updated: December 27, 2025

cover image
High-level diagram of the CI/CD Pipeline I built w/ Gitea Actions for my Node.js app

I've been using GitHub Actions for my project deployment workflow for the past 5 years now. It's convient, easy to setup, makes it easy to collaborate and let's you deploy apps without worry. However, ever since Microsoft acquired GitHub, they have been aggressively pushing for their in-house AI Copilot and decided to use public repositories for AI training by default. These changes were the final straw that led me to host my own Git instance with Gitea. And I'm glad I did! It has been a rewarding journey so far, taking full ownership of my code repositories without worry.

Up until now however, I've been only using my Gitea instance to host my repositiories and I’ve been manually deploying my projects by cloning them to my linux VPS via SSH. So, to make it easier for me to deploy my projects I decided to automate the process using Gitea Actions, an open-source self hosted alternative to Github Actions. In this post, I’ll walk you through how I configured Gitea Actions to create a robust CI/CD pipeline that automates the building and deployment of my Node.js application CyberCafe!

Prerequisites

📝NOTE
This tutorial assumes that you have a VPS server running Ubuntu with Gitea installed via Docker.

What is Gitea Actions?

Gitea Actions is an automation tool that comes buit-in with Gitea. It can be used to listen for specific events like pushing new code, creating a pull request, or can be set on a schedule, and then automatically runs tasks that are defined in a YAMl file in your project repository. These tasks form a workflow that create a Continuous Integration (CI) and Continuous Deployment (CD) pipeline.

This basically means that you can automate the entire process of building and deploying software without any manual intervention!

Setup

Creating an Act Runner

Gitea comes with Gitea Actions built-in but Gitea itself does not run the jobs, it delegates the jobs to runners. A Runner is a standalone program that you have to set-up, in a Gitea instance they are called 'act runners'. It's recommended to start the act runner on a seperate machine from the one that you have the Gitea instance, so it doesn't consume too many resources but in my use case a single VPS is more than enough.

📝NOTE
The runner is independent from your Gitea Instance and could create potential security issues. Try to avoid creating runners for repositories you don't trust.

I'm using docker compose to start the runner in a container. the official docs state that there is no requirement to create a config.yaml file when using the docker image however without creating a config file you'll get this error:

:Error: open config file "/config.yaml": read /config.yaml: is a directory

Docker will create a directory called /config.yaml instead of a file. To avoid this we have to download the docker image, create a config.yaml file in the same directyory as our docker-compose.yaml and then RUN the container.

First we create a folder for our runner and navigate into it:

mkdir GiteaActRunner
cd GiteaActRunner

pull the latest stable docker image by:

docker pull docker.io/gitea/act_runner:latest 

create configration file without running the container:

docker run --entrypoint="" --rm -it docker.io/gitea/act_runner:latest act_runner generate-config > config.yaml

now we're ready for docker-compose.yaml. Create docker-compose.yaml with your favorite text editor:

 services:
  runner:
    image: gitea/act_runner:latest
    restart: unless-stopped
    environment:
      - CONFIG_FILE=/config.yaml
      - GITEA_INSTANCE_URL=<ENTER_INSTANCE_URL>
      - GITEA_RUNNER_REGISTRATION_TOKEN=<ENTER_REGISTRATION_TOKEN>
      - GITEA_RUNNER_NAME=<RUNNER_NAME>
    volumes:
      - ./config.yaml:/config.yaml
      - ./data:/data
      - /var/run/docker.sock:/var/run/docker.sock

Replace <ENTER_INSTANCE_URL> with your Gitea instance url, in my case it's https://code.gorkyver.com, and replace the <ENTER_REGISTRATION_TOKEN> with the token you can find at your Gitea instance -> Settings -> Actions -> Create new Runner. The <RUNNER_NAME> can be set as anything in my case I named it "HAL9000".

Now we can run Docker as a detached instance by:

docker compose up -d

and we should be able to see our act runner in IDLE mode at our Gitea instance -> Settings -> Actions like this:

ss of act runner in idle mode

If you don't see the runner on your instance you can check to see the logs by:

docker compose logs -f

Node.js App Setup

This process is going to vary based on the app you're hosting so I'll only give an overview of my specific process and compare it to how I manually deploy it, this way you'll have an idea how the process looks like.

I have a Dockerfile in the project repository which basically uses node 24 to install dependincies and then runs the backend.js file via node.:

FROM node:24

#working directory in the container
WORKDIR /app

# Copy package.json and package-lock.json
COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD [ "node", "backend.js" ]

Here's the Manual process for the deployment of my node app that we're going to automate:

Step 1 - SSH into the vps and navigate to project folder
Step 2 - pull latest version of the project repo by git pull
Step 3 - build docker image runningDockerfile: docker build -t node-app .
Step 4 - run docker container on available port: docker run -p 3001:3000 -d node-app

Creating The Workflow

To automate this process with the Act Runner all we have to do is create a deploy.yaml file inside the .gitea/workflows/ directory in our project repository, this will the automate the same steps whenever there's a change on the project repo.

name: Node.js CI/CD Pipeline
on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Build Docker Image
        run: docker build -t node-app .

      - name: Deploy Container
        run: |
          docker stop node-app || true
          docker rm node-app || true
          docker run -d --name node-app --restart always -p 3001:3000 node-app

Explanation

This YAML file instructs the Act Runner to pull the repository, build a Docker image and run it in a container on a specific port. Here's a line by line explanation on what's happening:

on: [push]

The workflow is triggered only where there's a push on the project repository.

jobs:
  build:
    runs-on: ubuntu-latest

This creates a job titled "build", which will run on Ubuntu.

steps:
  - name: Checkout repository
    uses: actions/checkout@v4

actions/checkout@v4 is a Gitea Actions feature that allows your repository to be cloned in to the Act Runner workflow.

  - name: Build Docker Image
    run: docker build -t node-app .

Builds an docker image titled "node-app", based on the Dockerfile in the project repository.

  - name: Deploy Container
    run: |
      docker stop node-app || true
      docker rm node-app || true
      docker run -d --name node-app --restart always -p 3001:3000 node-app

docker stop node-app || true: This line stops the container if it's already running, if it fails to stop it (eg. first deployment) then it skips to the next line thanks to || true.
docker rm node-app || true: Removes the container, skips to next line without failiure if there's no container found.
docker run -d --name node-app --restart always -p 3001:3000 node-app: Runs the node-app container in detached mode with a port mapping of 3001:3000.

Conclusion

Screenshot of job completion process
Now whenever there's an update to your project repo, the build should trigger and deploy your project on your server!


Next:

Comments