17 KiB
StackOps Infrastructure Setup Guide
Полная инструкция для AI-агента по развёртыванию StackOps платформы с нуля на чистом VPS.
Требования
- VPS с Debian 12+/Ubuntu 22+, минимум 4 CPU / 6GB RAM / 50GB disk
- Root SSH доступ
- Домен, направленный на IP сервера (Cloudflare DNS)
- Docker Desktop на локальной машине (для сборки образов)
Переменные окружения
Перед началом определи эти переменные — они используются во всех шагах:
VPS_IP="82.114.226.118"
VPS_USER="root"
DOMAIN="nodeup.ru"
CF_API_TOKEN="<cloudflare api token с Zone:DNS:Edit>"
CF_ZONE_ID="<zone id домена>"
GITEA_ADMIN_USER="stackops"
GITEA_ADMIN_PASS="stackops-admin-2026"
GRAFANA_ADMIN_PASS="stackops-grafana-2026"
STACKOPS_API_TOKEN="dev-secret-token"
ADMIN_EMAIL="admin@stackops.dev"
ADMIN_PASSWORD="admin"
Шаг 1: DNS записи (Cloudflare API)
Создай A-записи для всех поддоменов:
for name in "@" "app" "git" "grafana" "prom" "*.app"; do
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"A\",\"name\":\"${name}\",\"content\":\"${VPS_IP}\",\"ttl\":1,\"proxied\":false}"
done
| Запись | Назначение |
|---|---|
@ (nodeup.ru) |
Корневой домен |
app |
StackOps API + UI |
git |
Gitea + Container Registry |
grafana |
Grafana дашборды |
prom |
Prometheus (резерв) |
*.app |
Wildcard для пользовательских приложений |
Важно: proxied: false — иначе HTTP-01 ACME challenge не пройдёт.
Шаг 2: Установка k3s
ssh ${VPS_USER}@${VPS_IP} 'apt-get update -qq && apt-get install -y -qq curl git'
ssh ${VPS_USER}@${VPS_IP} 'curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable=traefik" sh -'
Traefik отключён — вместо него используется nginx-ingress.
Скопируй kubeconfig локально:
mkdir -p ~/.kube
ssh ${VPS_USER}@${VPS_IP} 'cat /etc/rancher/k3s/k3s.yaml' \
| sed "s/127.0.0.1/${VPS_IP}/" > ~/.kube/config-nodeup
export KUBECONFIG=~/.kube/config-nodeup
Проверь:
kubectl get nodes # STATUS: Ready
Шаг 3: Helm репозитории
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add jetstack https://charts.jetstack.io
helm repo add kubevela https://kubevela.github.io/charts
helm repo add ot-helm https://ot-container-kit.github.io/helm-charts/
helm repo add gitea-charts https://dl.gitea.com/charts/
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
Шаг 4: nginx-ingress
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set controller.service.type=LoadBalancer \
--set controller.kind=DaemonSet \
--set controller.hostPort.enabled=false \
--timeout 10m --wait
k3s ServiceLB (Klipper) привязывает порты 80/443 к хосту автоматически.
Не используй hostPort.enabled=true — конфликтует с Klipper.
Проверь:
kubectl get pods -n ingress-nginx # controller Running
curl -s http://${VPS_IP}/ # должен вернуть 404 от nginx
Шаг 5: cert-manager + ClusterIssuer
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--set crds.enabled=true \
--timeout 10m --wait
Создай ClusterIssuer с HTTP-01 challenge (не DNS-01 — cert-manager из k3s подов может иметь проблемы с доступом к Cloudflare API из-за IPv6 таймаутов):
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ${ADMIN_EMAIL}
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: nginx
EOF
Проверь:
kubectl get clusterissuer # READY: True
Важно: Для получения сертификатов Ingress должен содержать аннотацию:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
toml-converter добавляет её автоматически при tls = true в stackfile.toml.
Шаг 6: KubeVela
helm install kubevela kubevela/vela-core \
--namespace vela-system --create-namespace \
--timeout 8m --wait
Проверь:
kubectl get pods -n vela-system # vela-core и cluster-gateway Running
Шаг 7: Операторы (CloudNativePG + Redis)
# CloudNativePG (PostgreSQL)
kubectl apply --server-side \
-f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.23/releases/cnpg-1.23.0.yaml
# Redis operator
helm install redis-operator ot-helm/redis-operator \
--namespace redis-operator --create-namespace \
--timeout 5m --wait
Шаг 8: KubeVela ComponentDefinitions
Зарегистрируй кастомные типы компонентов для postgres и redis:
kubectl apply -f kubevela/components/
Файлы:
kubevela/components/postgres.yaml— обёртка над CloudNativePG Cluster CRDkubevela/components/redis.yaml— обёртка над OT Systems Redis CRD
Проверь:
kubectl get componentdefinitions # postgres и redis
Шаг 9: Gitea (Git + Container Registry)
helm install gitea gitea-charts/gitea \
--namespace gitea --create-namespace \
-f infra/04-gitea/values.yaml \
--timeout 10m --wait
Ключевые настройки в values.yaml:
- SQLite (без внешнего PostgreSQL)
packages.ENABLED: true— включает Container Registryactions.ENABLED: true— включает Gitea Actions- Ingress с
cert-manager.io/cluster-issuerиproxy-body-size: 500m(для push больших образов)
Проверь:
curl -sk https://git.${DOMAIN}/ # Gitea login page
Создай репозитории:
for repo in infra stackops test-app; do
curl -sk -X POST "https://git.${DOMAIN}/api/v1/user/repos" \
-u "${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASS}" \
-H "Content-Type: application/json" \
-d "{\"name\":\"${repo}\",\"private\":false}"
done
Шаг 10: Настройка k3s для pull из Gitea Registry
ssh ${VPS_USER}@${VPS_IP} "mkdir -p /etc/rancher/k3s && cat > /etc/rancher/k3s/registries.yaml <<EOF
mirrors:
git.${DOMAIN}:
endpoint:
- \"https://git.${DOMAIN}\"
configs:
\"git.${DOMAIN}\":
auth:
username: ${GITEA_ADMIN_USER}
password: ${GITEA_ADMIN_PASS}
EOF
systemctl restart k3s"
После рестарта k3s подожди ~10 секунд и проверь:
kubectl get nodes # Ready
Шаг 11: Мониторинг (Prometheus + Grafana + Loki)
# Prometheus + Grafana
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring --create-namespace \
-f infra/05-monitoring/values-prometheus.yaml \
--timeout 10m --wait
# Loki + Promtail
helm install loki grafana/loki-stack \
--namespace monitoring \
-f infra/05-monitoring/values-loki.yaml \
--timeout 5m --wait
Проверь:
curl -sk https://grafana.${DOMAIN}/ # 302 → login
# Логин: admin / ${GRAFANA_ADMIN_PASS}
После входа в Grafana добавь Loki datasource:
- URL:
http://loki:3100 - Тип: Loki
Шаг 12: Сборка и деплой StackOps API
12.1 Собрать Docker образ
cd server/
docker build --platform linux/amd64 \
-t git.${DOMAIN}/stackops/stackops-api:v1 \
-f api/Dockerfile .
12.2 Запушить в Gitea Registry
echo "${GITEA_ADMIN_PASS}" | docker login git.${DOMAIN} -u ${GITEA_ADMIN_USER} --password-stdin
docker push git.${DOMAIN}/stackops/stackops-api:v1
12.3 Создать namespace, RBAC, PVC
kubectl apply -f infra/06-stackops/rbac.yaml
Это создаёт:
- Namespace
stackops - ServiceAccount
stackops-apiс ClusterRolestackops-deployer(полный доступ — нужен для kubectl apply в произвольные неймспейсы) - PVC
stackops-data(1Gi, local-path) для SQLite
12.4 Создать секрет
kubectl create secret generic stackops-secrets \
--namespace stackops \
--from-literal=api-token=${STACKOPS_API_TOKEN} \
--dry-run=client -o yaml | kubectl apply -f -
12.5 Задеплоить
Перед деплоем обнови image в infra/06-stackops/deployment.yaml:
image: git.<DOMAIN>/stackops/stackops-api:v1
Также добавь env vars для логина:
kubectl apply -f infra/06-stackops/deployment.yaml
kubectl set env deployment/stackops-api -n stackops \
STACKOPS_ADMIN_EMAIL=${ADMIN_EMAIL} \
STACKOPS_ADMIN_PASSWORD=${ADMIN_PASSWORD}
Проверь:
kubectl rollout status deployment/stackops-api -n stackops
curl -sk https://app.${DOMAIN}/ # StackOps UI (login page)
curl -sk -X POST https://app.${DOMAIN}/api/login \
-H "Content-Type: application/json" \
-d "{\"email\":\"${ADMIN_EMAIL}\",\"password\":\"${ADMIN_PASSWORD}\"}"
# → {"token":"..."}
Шаг 13: Gitea Actions Runner (CI/CD)
13.1 Получить registration token
# Сначала создай API token для Gitea
GITEA_API_TOKEN=$(curl -sk -X POST \
"https://git.${DOMAIN}/api/v1/users/${GITEA_ADMIN_USER}/tokens" \
-u "${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASS}" \
-H "Content-Type: application/json" \
-d '{"name":"ci-token","scopes":["all"]}' | python3 -c "import sys,json; print(json.load(sys.stdin)['sha1'])")
# Получи registration token
RUNNER_TOKEN=$(curl -sk \
"https://git.${DOMAIN}/api/v1/admin/runners/registration-token" \
-H "Authorization: token ${GITEA_API_TOKEN}" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
13.2 Обновить токен в манифесте
В infra/08-gitea-runner/runner.yaml замени токен в Secret:
stringData:
token: "${RUNNER_TOKEN}"
13.3 Задеплоить runner
kubectl apply -f infra/08-gitea-runner/runner.yaml
Runner содержит два контейнера:
runner— gitea/act_runner, подключается к Gitea по in-cluster DNS (gitea-http.gitea.svc.cluster.local:3000)dind— Docker-in-Docker сайдкар (privileged) дляdocker build/docker pushвнутри CI джобов
Проверь:
kubectl get pods -n gitea-runner # 2/2 Running
kubectl logs deployment/gitea-runner -n gitea-runner -c runner --tail=5
# → "Runner registered successfully", "Starting runner daemon"
Шаг 14: Деплой приложения через StackOps
14.1 Создать проект
curl -sk -X POST https://app.${DOMAIN}/stackops.v1.StackOpsService/CreateProject \
-H "Authorization: Bearer ${STACKOPS_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "my-app",
"gitUrl": "https://git.'${DOMAIN}'/stackops/my-app.git",
"gitBranch": "main",
"namespace": "my-app"
}'
Сохрани id из ответа.
14.2 stackfile.toml в корне репозитория
[app]
name = "my-app"
image = "git.<DOMAIN>/stackops/my-app:latest"
replicas = 1
port = 8080
[ingress]
host = "my-app.app.<DOMAIN>"
path = "/"
tls = true # ← автоматически добавит cert-manager аннотацию
[dependencies.postgres]
version = "15"
storage = "1Gi"
[dependencies.redis]
version = "7"
storage = "1Gi"
14.3 Задеплоить (StackOps клонирует из git)
curl -sk -X POST https://app.${DOMAIN}/stackops.v1.StackOpsService/DeployProject \
-H "Authorization: Bearer ${STACKOPS_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"projectId": "<project-id>"}'
StackOps сделает:
git cloneрепозитория- Прочитает
stackfile.toml - Сконвертирует в KubeVela Application YAML
kubectl create namespace+kubectl apply- KubeVela развернёт: PostgreSQL → Redis → App → Ingress + TLS
14.4 CI/CD workflow (.gitea/workflows/ci.yaml)
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
container:
image: docker:27
steps:
- uses: actions/checkout@v4
- name: Build and push
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.<DOMAIN> -u stackops --password-stdin
docker build --platform linux/amd64 -t git.<DOMAIN>/stackops/my-app:${{ gitea.sha }} .
docker tag git.<DOMAIN>/stackops/my-app:${{ gitea.sha }} git.<DOMAIN>/stackops/my-app:latest
docker push git.<DOMAIN>/stackops/my-app:${{ gitea.sha }}
docker push git.<DOMAIN>/stackops/my-app:latest
- name: Deploy
run: |
apk add --no-cache curl
curl -sf -X POST http://stackops-api.stackops.svc.cluster.local:8080/stackops.v1.StackOpsService/DeployProject \
-H "Authorization: Bearer ${{ secrets.STACKOPS_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"projectId":"${{ secrets.PROJECT_ID }}"}'
Secrets в настройках репозитория (Settings > Actions > Secrets):
REGISTRY_PASSWORD— пароль от GiteaSTACKOPS_TOKEN— API токен StackOpsPROJECT_ID— ID проекта из шага 14.1
Результат
После выполнения всех шагов будет работать:
| URL | Сервис | Credentials |
|---|---|---|
https://app.<DOMAIN> |
StackOps API + UI | email/password → token |
https://git.<DOMAIN> |
Gitea + Registry | GITEA_ADMIN_USER / GITEA_ADMIN_PASS |
https://grafana.<DOMAIN> |
Grafana | admin / GRAFANA_ADMIN_PASS |
https://<app>.app.<DOMAIN> |
Пользовательские приложения | — |
Namespaces в кластере:
ingress-nginx— ingress controllercert-manager— автоматические TLS сертификатыvela-system— KubeVela OAM enginecnpg-system— CloudNativePG операторredis-operator— Redis операторgitea— Gitea + Valkeygitea-runner— CI/CD runner (DinD)monitoring— Prometheus + Grafana + Loki + Promtailstackops— StackOps API + SQLite
Flow деплоя:
git push → Gitea Actions → docker build → push registry → StackOps API
→ git clone → toml-converter → KubeVela Application → k8s
→ CloudNativePG (postgres) + Redis operator + Deployment + Ingress + TLS
Известные нюансы
-
Первый деплой на чистом VPS медленный — k3s тянет все образы с нуля (5-10 мин на helm install). Используй
--timeout 10mна helm install. -
nginx-ingress + k3s Klipper — не включай
hostPort.enabled=true, будет конфликт портов. Klipper ServiceLB сам привяжет 80/443. -
cert-manager DNS-01 vs HTTP-01 — используй HTTP-01. DNS-01 через Cloudflare API может таймаутить из k3s подов (IPv6 → fallback → timeout в Go HTTP client cert-manager'а).
-
Gitea Container Registry — нужна аннотация
nginx.ingress.kubernetes.io/proxy-body-size: 500mна Gitea ingress, иначе docker push больших образов вернёт 413. -
k3s registry auth — настраивается через
/etc/rancher/k3s/registries.yaml, после изменения нуженsystemctl restart k3s. -
KubeVela gateway trait + cert-manager — toml-converter автоматически добавляет
cert-manager.io/cluster-issuer: letsencrypt-prodна ингресс приtls = trueв stackfile.toml. -
StackOps API нуждается в git — Docker образ должен содержать
gitв runtime (дляgit cloneпри деплое из репозитория). -
Gitea Actions runner — registration token одноразовый. При пересоздании runner нужно получить новый token через API.
-
IPv6 — если на VPS нет рабочего IPv6, отключи:
sysctl -w net.ipv6.conf.all.disable_ipv6=1. Иначе некоторые Go HTTP клиенты могут таймаутить пытаясь подключиться по IPv6.