A small, working CI/CD example that builds a React/Vite client and a .NET 6 API, pushes Docker images to GitLab Container Registry, and deploys them with a local GitLab Runner + Docker Compose.
- Learn and demonstrate the simplest possible CI/CD.
- Keep a clear split between build (CI) and deploy (CD).
- Use Docker multi-arch builds, GitLab Container Registry, and a local Runner to deploy with Docker Compose using a single
TAG(dev/main).
-
Client: Vite/React (Node 20). Built as static assets and served by Nginx.
Build-time argument:VITE_SERVER_API(in CI set to/api/todo). -
Server: .NET 6 API listening on
:8080(ASPNETCORE_URLS=http://0.0.0.0:8080). -
Docker: Client Dockerfile (build → nginx), Server Dockerfile (.NET 6).
-
Deploy:
/opt/app/docker-compose.registry.ymlon the server selects images by${TAG}and pulls fresh images each deploy. -
GitLab CI (
.gitlab-ci.yml): Stagesbuild → docker → deploy. Builds and pushes images to the registry, then deploys via a local Runner (shellexecutor, tagdeploy-local).
.
├─ client/ # Vite/React app (Dockerfile inside)
├─ Server/
│ └─ Server/ # .NET 6 API (Dockerfile inside)
├─ docker-compose.yml # optional local dev compose
├─ docker-compose.registry.yml # used on the server for CD
└─ .gitlab-ci.yml # build, push, deploy
Note: The production compose for CD lives on the server at
/opt/app/docker-compose.registry.yml. Its content is shown below for reference.
- Docker & Docker Compose v2
- (Maintainers) GitLab Runner installed on the server, registered for this project with tag
deploy-local, and member of thedockergroup.
git clone <REPO_URL>
cd MosheAzraf-project
docker compose up -d --build
# Client: http://localhost:3000 | API: http://localhost:8080# Pick a tag (dev or main) without creating a file:
TAG=dev docker compose -f docker-compose.registry.yml up -d
# Client: http://localhost:3000If the project is private, run
docker login registry.gitlab.comfirst.
API
cd Server/Server
dotnet restore
dotnet run --urls http://0.0.0.0:8080Client
cd client
npm ci
VITE_SERVER_API=http://localhost:8080/api/todo npm run dev
# Opens on http://localhost:5173- Pushing to
devproduces:registry.gitlab.com/mosheazraf-group/mosheazraf-project:devregistry.gitlab.com/mosheazraf-group/mosheazraf-project/client:dev
- Pushing to
mainproduces the same with:main. - Images are multi-arch (
linux/amd64,linux/arm64) viadocker buildx.
- Local GitLab Runner (
shell, tagdeploy-local) runs the deploy jobs on the server. - Each deploy job writes the tag to
/opt/app/.env(TAG=devorTAG=main) and then runs:docker compose --env-file /opt/app/.env -f /opt/app/docker-compose.registry.yml pull docker compose --env-file /opt/app/.env -f /opt/app/docker-compose.registry.yml up -d
File: /opt/app/docker-compose.registry.yml
services:
api:
image: registry.gitlab.com/mosheazraf-group/mosheazraf-project:${TAG}
container_name: todo-server
ports:
- "8080:8080"
environment:
ASPNETCORE_URLS: http://0.0.0.0:8080
volumes:
- todo_data:/data
restart: unless-stopped
pull_policy: always
client:
image: registry.gitlab.com/mosheazraf-group/mosheazraf-project/client:${TAG}
container_name: todo-client
ports:
- "3000:80"
depends_on:
- api
restart: unless-stopped
pull_policy: always
volumes:
todo_data:- Client:
http://<host>:3000 - API base:
http://<host>:8080 - Todos endpoint:
http://<host>:8080/api/todo
# What is running?
docker ps
# Which image/tag is live?
docker inspect todo-client --format '{{.Config.Image}}'
docker inspect todo-server --format '{{.Config.Image}}'
# Logs
docker logs -n 100 todo-client
docker logs -n 100 todo-server- Job stuck on “pending” — Ensure the Runner is registered for this project and has
tag_list = ["deploy-local"]in/etc/gitlab-runner/config.toml. manifest unknownwhen pulling — The requested tag doesn’t exist yet. Push to the matching branch so CI builds and pushes that tag.- Cannot write
/opt/app/.env— The Runner user (gitlab-runner) must be able to write to/opt/app(e.g.,chown -R gitlab-runner:gitlab-runner /opt/app).
To demonstrate a clean, minimal CI/CD:
- CI builds & pushes images.
- CD deploys with a single
TAGswitch. - Real, production-like mechanics with minimal moving parts (images, registry, runner, compose).