Skip to content

Файлы деплоя

Эта страница описывает все файлы, необходимые для деплоя нового сервиса. Каждый раздел содержит готовые примеры, которые можно скопировать и адаптировать.

Обзор

mermaid
flowchart LR
    A[git push] --> B[GitLab CI]
    B --> C[Docker Build]
    C --> D[Registry]
    D --> E[Nomad Deploy]
    E --> F[Traefik Routes]

Пайплайн деплоя работает следующим образом:

  1. Вы пушите код в GitLab.
  2. GitLab CI собирает Docker-образ и пушит его в container registry.
  3. GitLab CI запускает деплой через Nomad, который подтягивает новый образ.
  4. Nomad регистрирует сервис в Consul, а Traefik подхватывает теги маршрутизации.
  5. Traefik терминирует TLS и направляет трафик на ваш сервис.

1. Dockerfile

Dockerfile использует multi-stage сборку для минимизации размера итогового образа.

dockerfile
# ---- Build Stage ----
FROM node:20-alpine AS build

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# ---- Serve Stage ----
FROM nginx:1.27-alpine

COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=build /app/dist /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
dockerfile
# ---- Dependencies Stage ----
FROM node:22-alpine AS deps

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# ---- Production Stage ----
FROM node:22-alpine

RUN apk add --no-cache tini

WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules
COPY . .

ENV NODE_ENV=production
EXPOSE 3000

ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "src/app.js"]
dockerfile
# ---- Frontend Build Stage ----
FROM node:20-alpine AS frontend-build

WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build

# ---- Backend Dependencies Stage ----
FROM node:22-alpine AS backend-deps

WORKDIR /app/backend
COPY backend/package.json backend/package-lock.json ./
RUN npm ci --omit=dev

# ---- Frontend Serve Image ----
FROM nginx:1.27-alpine AS frontend

COPY frontend/nginx.conf /etc/nginx/nginx.conf
COPY --from=frontend-build /app/frontend/dist /usr/share/nginx/html

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

# ---- Backend Production Image ----
FROM node:22-alpine AS backend

RUN apk add --no-cache tini

WORKDIR /app

COPY --from=backend-deps /app/backend/node_modules ./node_modules
COPY backend/ .

ENV NODE_ENV=production
EXPOSE 3000

ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "src/app.js"]

TIP

Для full-stack репозитория с отдельными образами фронтенда и бэкенда каждый target собирается явно в CI:

bash
docker build --target frontend -t registry/my-tool-frontend .
docker build --target backend -t registry/my-tool-backend .

2. nginx.conf

Конфигурация nginx для раздачи SPA с правильным кэшированием, gzip-сжатием и health-эндпоинтом.

nginx
worker_processes auto;

events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    sendfile    on;
    tcp_nopush  on;
    tcp_nodelay on;

    keepalive_timeout 65;

    # ---- Gzip ----
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_min_length 256;
    gzip_types
        text/plain
        text/css
        text/javascript
        application/javascript
        application/json
        application/xml
        image/svg+xml;

    server {
        listen 80;
        server_name _;
        root /usr/share/nginx/html;
        index index.html;

        # ---- Health check ----
        location /health {
            access_log off;
            return 200 'ok';
            add_header Content-Type text/plain;
        }

        # ---- Hashed assets — cache aggressively ----
        location /assets/ {
            expires 1y;
            add_header Cache-Control "public, immutable";
            try_files $uri =404;
        }

        # ---- index.html — never cache ----
        location = /index.html {
            add_header Cache-Control "no-cache, no-store, must-revalidate";
            add_header Pragma "no-cache";
            add_header Expires "0";
        }

        # ---- SPA fallback ----
        location / {
            try_files $uri $uri/ /index.html;
        }
    }
}

WARNING

SPA fallback (try_files $uri $uri/ /index.html) обязателен. Без него обновление страницы на клиентском маршруте вроде /dashboard/settings вернёт 404.


3. .gitlab-ci.yml

CI-пайплайн подключает общие шаблоны из репозитория meduza/infra. Вам нужно только определить стадии и расширить шаблоны.

yaml
include:
  - project: meduza/infra
    ref: main
    file:
      - /ci-templates/docker-build.yml
      - /ci-templates/nomad-deploy.yml

stages:
  - build
  - deploy

build:
  extends: .docker_build
  variables:
    DOCKER_IMAGE_NAME: my-tool-frontend

deploy_dev:
  extends: .nomad_deploy
  variables:
    NOMAD_JOB: my-tool
    NOMAD_ENV: dev
  environment:
    name: dev
  only:
    - dev

deploy_stage:
  extends: .nomad_deploy
  variables:
    NOMAD_JOB: my-tool
    NOMAD_ENV: stage
  environment:
    name: stage
  only:
    - stage

deploy_prod:
  extends: .nomad_deploy
  variables:
    NOMAD_JOB: my-tool
    NOMAD_ENV: prod
  environment:
    name: prod
  when: manual
  only:
    - main
yaml
include:
  - project: meduza/infra
    ref: main
    file:
      - /ci-templates/docker-build.yml
      - /ci-templates/nomad-deploy.yml

stages:
  - build
  - deploy

build_frontend:
  extends: .docker_build
  variables:
    DOCKER_IMAGE_NAME: my-tool-frontend
    DOCKER_BUILD_TARGET: frontend

build_backend:
  extends: .docker_build
  variables:
    DOCKER_IMAGE_NAME: my-tool-backend
    DOCKER_BUILD_TARGET: backend

deploy_dev:
  extends: .nomad_deploy
  variables:
    NOMAD_JOB: my-tool
    NOMAD_ENV: dev
  environment:
    name: dev
  needs:
    - build_frontend
    - build_backend
  only:
    - dev

deploy_stage:
  extends: .nomad_deploy
  variables:
    NOMAD_JOB: my-tool
    NOMAD_ENV: stage
  environment:
    name: stage
  needs:
    - build_frontend
    - build_backend
  only:
    - stage

deploy_prod:
  extends: .nomad_deploy
  variables:
    NOMAD_JOB: my-tool
    NOMAD_ENV: prod
  environment:
    name: prod
  needs:
    - build_frontend
    - build_backend
  when: manual
  only:
    - main

4. Nomad job-файл

Nomad job-файл определяет конфигурацию запуска: количество инстансов, лимиты ресурсов, health-проверки и теги маршрутизации Traefik.

hcl
variable "domain" {
  type = string
}

variable "image_tag" {
  type    = string
  default = "latest"
}

variable "frontend_count" {
  type    = number
  default = 1
}

variable "frontend_cpu" {
  type    = number
  default = 100
}

variable "frontend_memory" {
  type    = number
  default = 64
}

job "my-tool" {
  datacenters = ["dc1"]
  type        = "service"

  group "frontend" {
    count = var.frontend_count

    network {
      port "http" {
        to = 80
      }
    }

    service {
      name = "my-tool-frontend"
      port = "http"

      tags = [
        "traefik.enable=true",
        "traefik.http.routers.my-tool-frontend.rule=Host(`${var.domain}`)",
        "traefik.http.routers.my-tool-frontend.entrypoints=websecure",
        "traefik.http.routers.my-tool-frontend.tls=true",
        "traefik.http.routers.my-tool-frontend.tls.certresolver=letsencrypt",
      ]

      check {
        type     = "http"
        path     = "/health"
        interval = "10s"
        timeout  = "2s"
      }
    }

    task "frontend" {
      driver = "docker"

      config {
        image = "registry.meduza.io/my-tool-frontend:${var.image_tag}"
        ports = ["http"]
      }

      resources {
        cpu    = var.frontend_cpu
        memory = var.frontend_memory
      }
    }
  }
}
hcl
variable "domain" {
  type = string
}

variable "image_tag" {
  type    = string
  default = "latest"
}

variable "frontend_count" {
  type    = number
  default = 1
}

variable "frontend_cpu" {
  type    = number
  default = 100
}

variable "frontend_memory" {
  type    = number
  default = 64
}

variable "backend_count" {
  type    = number
  default = 1
}

variable "backend_cpu" {
  type    = number
  default = 200
}

variable "backend_memory" {
  type    = number
  default = 256
}

job "my-tool" {
  datacenters = ["dc1"]
  type        = "service"

  # ---- Frontend Group ----
  group "frontend" {
    count = var.frontend_count

    network {
      port "http" {
        to = 80
      }
    }

    service {
      name = "my-tool-frontend"
      port = "http"

      tags = [
        "traefik.enable=true",
        "traefik.http.routers.my-tool-frontend.rule=Host(`${var.domain}`)",
        "traefik.http.routers.my-tool-frontend.entrypoints=websecure",
        "traefik.http.routers.my-tool-frontend.tls=true",
        "traefik.http.routers.my-tool-frontend.tls.certresolver=letsencrypt",
      ]

      check {
        type     = "http"
        path     = "/health"
        interval = "10s"
        timeout  = "2s"
      }
    }

    task "frontend" {
      driver = "docker"

      config {
        image = "registry.meduza.io/my-tool-frontend:${var.image_tag}"
        ports = ["http"]
      }

      resources {
        cpu    = var.frontend_cpu
        memory = var.frontend_memory
      }
    }
  }

  # ---- Backend Group ----
  group "backend" {
    count = var.backend_count

    network {
      port "http" {
        to = 3000
      }
    }

    service {
      name = "my-tool-backend"
      port = "http"

      tags = [
        "traefik.enable=true",
        "traefik.http.routers.my-tool-backend.rule=Host(`${var.domain}`) && PathPrefix(`/api`)",
        "traefik.http.routers.my-tool-backend.entrypoints=websecure",
        "traefik.http.routers.my-tool-backend.tls=true",
        "traefik.http.routers.my-tool-backend.tls.certresolver=letsencrypt",
      ]

      check {
        type     = "http"
        path     = "/health"
        interval = "10s"
        timeout  = "2s"
      }
    }

    task "backend" {
      driver = "docker"

      config {
        image = "registry.meduza.io/my-tool-backend:${var.image_tag}"
        ports = ["http"]
      }

      resources {
        cpu    = var.backend_cpu
        memory = var.backend_memory
      }

      # ---- Environment from Consul KV ----
      template {
        data = <<-EOT
          {{ range ls "my-tool/config" }}
          {{ .Key }}={{ .Value }}
          {{ end }}
        EOT

        destination = "secrets/env.env"
        env         = true
      }
    }
  }
}

TIP

Блок Consul template подтягивает конфигурацию из Consul KV. Например, если вы сохраните DATABASE_URL по ключу my-tool/config/DATABASE_URL, он станет переменной окружения в бэкенд-контейнере. Это позволяет менять конфигурацию без передеплоя.


5. Nomad vars-файлы

Каждое окружение получает свой vars-файл. Они передаются в nomad job run через -var-file.

hcl
domain = "my-tool.dev.meduza.io"

image_tag = "dev"

frontend_count  = 1
frontend_cpu    = 100
frontend_memory = 64

backend_count  = 1
backend_cpu    = 200
backend_memory = 256
hcl
domain = "my-tool.stage.meduza.io"

image_tag = "stage"

frontend_count  = 1
frontend_cpu    = 100
frontend_memory = 64

backend_count  = 1
backend_cpu    = 200
backend_memory = 256
hcl
domain = "my-tool.meduza.io"

image_tag = "latest"

frontend_count  = 2
frontend_cpu    = 200
frontend_memory = 128

backend_count  = 2
backend_cpu    = 500
backend_memory = 512

Основные различия между окружениями:

ПараметрDevStageProd
Домен*.dev.meduza.io*.stage.meduza.io*.meduza.io
Инстансы фронтенда112+
Инстансы бэкенда112+
Память бэкенда256 МБ256 МБ512 МБ

Размещение файлов

Вот где располагается каждый файл в репозитории:

my-tool/
├── frontend/
│   ├── src/
│   ├── nginx.conf          # Конфигурация nginx для SPA
│   ├── package.json
│   └── ...
├── backend/
│   ├── src/
│   ├── package.json
│   └── ...
├── deploy/
│   ├── my-tool.nomad.hcl   # Nomad job-спецификация
│   ├── dev.vars.hcl         # Переменные для dev-окружения
│   ├── stage.vars.hcl       # Переменные для stage-окружения
│   └── prod.vars.hcl        # Переменные для prod-окружения
├── Dockerfile               # Multi-stage Dockerfile
├── .gitlab-ci.yml           # CI/CD-пайплайн
└── README.md

TIP

Для сервиса только с фронтендом можно упростить структуру — поместите всё в корень репозитория и уберите директорию backend/.

Документация по инфраструктуре