What is CI/CD and why do you need it
CI (Continuous Integration) — we collect and test each edit automatically.
CD (Continuous Delivery/Deployment) — after success, roll out to the stand/product without hands.
Pros: fewer manual errors, fast releases, transparency for the team, and predictable rollouts. 🧘♂️
Server: basic training (VPS)
# 1) Docker and compose plugin (Ubuntu)
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# 2) Application Catalog
sudo mkdir -p /opt/myapp && sudo chown -R $USER:$USER /opt/myapp
cd /opt/myapp
# 3) .env (on the server, not committed)
cat > .env << 'ENV'
PORT=3000
ENV
# 4) docker-compose.yml
cat > docker-compose.yml << 'YAML'
services:
web:
image: registry.example.com/myapp:latest
env_file: .env
ports:
- "80:3000"
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s
timeout: 3s
retries: 5
YAML
# 5) First launch (without CI yet)
docker compose pull && docker compose up -d
Dockerfile (minimum example for Node/PNPM)
# Dockerfile
FROM node:20-alpine AS deps
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm i --frozen-lockfile
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules node_modules
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist dist
COPY package.json ./
RUN corepack enable && corepack prepare pnpm@latest --activate && pnpm i --prod --frozen-lockfile
EXPOSE 3000
CMD ["node", "dist/server.js"]
GitHub Actions: image build and SSH deployment
Add .github/workflows/deploy.yml to the repository:
name: CI/CD Deploy
on:
push:
branches: [ "main" ]
permissions:
contents: read
packages: write
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.REGISTRY_URL }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build & push image
uses: docker/build-push-action@v6
with:
push: true
context: .
tags: ${{ secrets.REGISTRY_URL }}/${{ secrets.IMAGE_NAME }}:latest
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
set -e
cd /opt/myapp
docker compose pull
docker compose up -d
docker image prune -f
Secrets: REGISTRY_URL, REGISTRY_USER, REGISTRY_PASSWORD, IMAGE_NAME, SERVER_HOST, SERVER_USER, SERVER_SSH_KEY.
GitLab CI: an alternative
# .gitlab-ci.yml
stages: [build, deploy]
variables:
IMAGE: $CI_REGISTRY_IMAGE:latest
build:
stage: build
image: docker:27.0
services: [ "docker:27.0-dind" ]
script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
- docker build -t $IMAGE .
- docker push $IMAGE
artifacts:
expire_in: 1 week
when: on_success
paths: []
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
script:
- ssh -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_HOST "
set -e
cd /opt/myapp &&
docker compose pull &&
docker compose up -d &&
docker image prune -f
"
only:
- main
Zero downtime and rollbacks
Zero‑downtime:
restart: unless-stopped+healthcheck+up -d— the new container starts, the old one goes out.Release tags: in addition to
:latestpush:v1.4.2— it's easier to roll back.Rollback: switch the tag to
docker-compose.ymland repeatpull/up -d.Blue/green scheme (simplified): keep two services
web_blueandweb_green, balance traffic through Nginx.
Minimum checks in the pipeline
# example of a step with tests in GitHub Actions
- name: Install deps & test
run: |
npm ci
npm run test -- --ci
The threshold for the "evening" CI: run unit tests, collect a build, pass the linter (ESLint/flake8), check the types (tsc/mypy).
Monitoring and logging
Add
/healthand/metrics(Prometheus format) to the service.Log collection:
docker logs→ Loki/ELK; status code/latency alerts.In CI — artifacts with test and linter reports to see regressions.
Secrets and security
Secrets only in Secrets/Variables CI;
.env— on the server.Disable SSH login with password, leave login with key, restrict ports.
Do not store private keys in the repository, even in encrypted form.
🧯 Typical problems and quick solutions
Symptom | Reason | Fixed |
|---|---|---|
CI crashes on assembly | Low RAM/timeout | Layer cache, lighter image base, increase timeout |
The application does not start | Port/ENV/migration | Check |
Slow deployment | Heavy image | Multi-stage, alpine, .dockerignore, dependency caching |
"Works for me" | Different versions of Node/Python | Fix versions in Docker, use lock files |
The disk is full | Old images/containers |
|
In the attachment "Kodik - programming training" — short lessons and mini-projects. We are interesting!
And we also have an active Telegram channel, where we discuss cool ideas, share experiences and analyze tasks together — learning becomes not only useful, but also fun.
Total
CI/CD is not "big magic" but a set of simple steps. Build an image, push it to the registry, restart the service on VPS — and you have fast, repeatable releases without manual routine. Start today, and tomorrow the team will forget what a "manual deployment" looked like.
What are you going to use to run the autodeploy — GitHub Actions or GitLab CI?
