{}const=>[]async()letfn</>var
DevelopmentBasics

CI/CD in a nutshell: auto-deploy in one evening

A simple explanation of CI/CD and a ready-made example of auto-deployment through GitHub Actions.

К

Kodik

Author

4 min read

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. 🧘‍♂️

🔥 100,000+ students already with us

Tired of reading theory?
Time to code!

Kodik — an app where you learn to code through practice. AI mentor, interactive lessons, real projects.

🤖 AI 24/7
🎓 Certificates
💰 Free
🚀 Start learning
Joined today

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 :latest push :v1.4.2 — it's easier to roll back.

  • Rollback: switch the tag to docker-compose.yml and repeat pull/up -d.

  • Blue/green scheme (simplified): keep two services web_blue and web_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 /health and /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 .env, docker logs, healthcheck, run migrations

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

docker system prune -f, log rotation

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?

🎯Stop procrastinating

Liked the article?
Time to practice!

In Kodik, you don't just read — you write code immediately. Theory + practice = real skills.

Instant practice
🧠AI explains code
🏆Certificate

No registration • No card