Файлы деплоя
Эта страница описывает все файлы, необходимые для деплоя нового сервиса. Каждый раздел содержит готовые примеры, которые можно скопировать и адаптировать.
Обзор
flowchart LR
A[git push] --> B[GitLab CI]
B --> C[Docker Build]
C --> D[Registry]
D --> E[Nomad Deploy]
E --> F[Traefik Routes]Пайплайн деплоя работает следующим образом:
- Вы пушите код в GitLab.
- GitLab CI собирает Docker-образ и пушит его в container registry.
- GitLab CI запускает деплой через Nomad, который подтягивает новый образ.
- Nomad регистрирует сервис в Consul, а Traefik подхватывает теги маршрутизации.
- Traefik терминирует TLS и направляет трафик на ваш сервис.
1. Dockerfile
Dockerfile использует multi-stage сборку для минимизации размера итогового образа.
# ---- 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;"]# ---- 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"]# ---- 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:
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-эндпоинтом.
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. Вам нужно только определить стадии и расширить шаблоны.
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:
- maininclude:
- 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:
- main4. Nomad job-файл
Nomad job-файл определяет конфигурацию запуска: количество инстансов, лимиты ресурсов, health-проверки и теги маршрутизации Traefik.
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
}
}
}
}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.
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 = 256domain = "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 = 256domain = "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Основные различия между окружениями:
| Параметр | Dev | Stage | Prod |
|---|---|---|---|
| Домен | *.dev.meduza.io | *.stage.meduza.io | *.meduza.io |
| Инстансы фронтенда | 1 | 1 | 2+ |
| Инстансы бэкенда | 1 | 1 | 2+ |
| Память бэкенда | 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.mdTIP
Для сервиса только с фронтендом можно упростить структуру — поместите всё в корень репозитория и уберите директорию backend/.