DevOps · Семейный Домашний проект
Junior DevOps Семейный сайт Ubuntu + Nginx

Привет, я junior DevOps-инженер

Я создал этот сайт для семьи и как практический проект: чистый UI, деплой на Ubuntu, Nginx, SSL и немного автоматизации. Здесь мы храним воспоминания, заметки и планы в одном уютном месте.

Чистый кодHTML / CSS / JS без фреймворков
Ubuntu + Nginxпродакшн на VPS
Семья — главноеличное и тёплое
bash — devops@ubuntu: ~/project $ git push origin main Branch main → remote/main $ docker build -t app:latest . [+] Building 8.2s (7/8) DONE $ nginx -t && systemctl reload nginx nginx: configuration file test ok pm2 restart all [PM2] All processes restarted ● main UTF-8 ● online
Сайт работаетNginx + SSL + автообновление
CI/CD настраиваетсяGitHub Actions в процессе

О проекте

Семейная страница + практика DevOps в одном флаконе.
Учусь на практике

Зачем я это создал

  • Семейный уголок в сетиОдно место для наших моментов, заметок и воспоминаний.
  • Реальный проектЧистая разметка, адаптивный UI, деплой на боевой сервер.
  • DevOps-практикаРазвёртывание, автоматизация, структура — на настоящей странице.

Что дальше

  • Приватная панельJWT-авторизация + отдельный поддомен для семьи.
  • CI/CDGitHub Actions: lint → build → тесты → автодеплой по push.
  • МониторингPrometheus + Grafana для метрик сервера и логов Nginx.

Мой DevOps-путь

Что я изучаю и что уже применил на практике на этом сайте.
Строю навыки
99%
Uptime сервера за последние 30 дней
▲ Отлично
A+
Оценка SSL на ssllabs.com
Let's Encrypt
<1s
Среднее время ответа Nginx
▲ Быстро
v0.3
Текущая версия проекта
В разработке

Текущий стек

  • Linux (Ubuntu 22.04)процессы, сеть, права, systemd, bash-скрипты
  • Nginxreverse proxy, виртуальные хосты, SSL-терминация, gzip
  • Dockerконтейнеризация приложений, volumes, compose, образы
  • Git + GitHubветки, PR, таги, базовые Actions-пайплайны

Изучаю сейчас

Инструменты DevOps

Нажмите на любой инструмент — получите описание, сценарии использования и ссылку на документацию.
Кликабельно

CI/CD Pipeline

Как код попадает из редактора на боевой сервер — шаг за шагом.
Автоматизация

Примеры пайплайнов

Реальные конфиги для GitHub Actions, GitLab CI, Gitea, Jenkins — можно взять за основу.
Для резюме
GitHub ActionsПолный пайплайн: lint → тесты → Docker build → деплой на VPS через SSHDocs ↗
.github/workflows/deploy.yml
# Название workflow — отображается в GitHub Actions UI
name: CI/CD Deploy

# Когда запускать: при push в main ИЛИ при открытии Pull Request в main
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

# Переменные окружения — доступны во всех jobs
# github.repository_owner — имя вашего аккаунта GitHub (автоматически)
env:
  IMAGE: ghcr.io/${{ github.repository_owner }}/my-app

jobs:
    # ── JOB 1: Проверка кода ─────────────────────────────────
  lint:
    runs-on: ubuntu-latest  # виртуальная машина GitHub с Ubuntu
    steps:
      - uses: actions/checkout@v4  # скачиваем код репозитория
      - uses: hadolint/hadolint-action@v3.1.0  # проверяем Dockerfile на ошибки
        with:
          dockerfile: Dockerfile

    # ── JOB 2: Тесты (запускается ПОСЛЕ lint) ────────────────
  test:
    runs-on: ubuntu-latest
    needs: lint  # ждём успешного lint перед запуском
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test  # npm ci = чистая установка зависимостей

    # ── JOB 3: Сборка и загрузка Docker-образа ───────────────
  build-push:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'  # только для push в main, не для PR
    permissions:
      contents: read
      packages: write  # разрешаем записывать в GitHub Container Registry
    steps:
      - uses: actions/checkout@v4
            # Логинимся в ghcr.io — GitHub Container Registry (бесплатный Docker Hub)
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{{ github.actor }}   # ваш GitHub-логин
          password: ${{{ secrets.GITHUB_TOKEN }}  # автоматический токен (не нужно создавать вручную)
            # Собираем Docker-образ и пушим в registry
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: ${{{ env.IMAGE }}:latest  # тег образа: ghcr.io/user/my-app:latest

    # ── JOB 4: Деплой на сервер по SSH ───────────────────────
  deploy:
    needs: build-push
    runs-on: ubuntu-latest
    environment: production  # GitHub Environments — защищённое окружение с approval
    steps:
            # SSH-экшен: подключается к серверу и выполняет команды
      - uses: appleboy/ssh-action@v1.0.3
        with:
          host:     ${{{ secrets.SSH_HOST }}  # IP сервера (добавить в Settings → Secrets)
          username: ${{{ secrets.SSH_USER }}  # пользователь на сервере (например, ubuntu)
          key:      ${{{ secrets.SSH_KEY }}   # приватный SSH-ключ
          script: |
            docker pull $IMAGE:latest          # скачиваем новый образ
            docker stop app || true            # останавливаем старый (|| true = не падать если нет)
            docker rm   app || true            # удаляем старый контейнер
            docker run -d --name app \         # запускаем новый контейнер
              --restart unless-stopped \       # автозапуск при перезагрузке сервера
              -p 3000:3000 $IMAGE:latest       # пробрасываем порт 3000
            docker image prune -f              # удаляем старые образы (экономим место)
GitLab CIDocker-in-Docker, кеш, environment, SSH деплойDocs ↗
.gitlab-ci.yml
# Базовый образ для всех jobs — Docker с поддержкой DinD (Docker-in-Docker)
image: docker:24
services: [ docker:24-dind ]  # Docker-in-Docker — нужен для сборки образов

# CI_ переменные — автоматически заполняются GitLab'ом
variables:
  IMAGE:  $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA  # тег = короткий хеш коммита
  LATEST: $CI_REGISTRY_IMAGE:latest               # тег latest для последней версии

# Этапы пайплайна — выполняются строго по порядку
stages: [ lint, test, build, deploy ]

# ── ЭТАП 1: Проверка кода ──────────────────────────────────
lint:
  stage: lint
  image: node:20-alpine  # свой образ для этого job'а
  cache:
    key:   $CI_COMMIT_REF_SLUG  # ключ кеша = имя ветки
    paths: [ node_modules/ ]    # кешируем зависимости — ускоряет повторные запуски
  script: [ npm ci, npm run lint ]

# ── ЭТАП 2: Тесты с покрытием ─────────────────────────────
test:
  stage: test
  image: node:20-alpine
  script: [ npm ci, npm test -- --coverage ]
  coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'  # regex для парсинга % покрытия

# ── ЭТАП 3: Сборка Docker-образа ──────────────────────────
build:
  stage: build
  before_script:
      # Логинимся в GitLab Container Registry (встроенный registry)
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
      # --cache-from $LATEST — переиспользуем слои предыдущего образа (быстрее)
    - docker build --cache-from $LATEST -t $IMAGE -t $LATEST .
    - docker push $IMAGE && docker push $LATEST  # пушим оба тега
  only: [ main ]  # только для ветки main

# ── ЭТАП 4: Деплой на сервер ──────────────────────────────
deploy:
  stage: deploy
  image: alpine  # лёгкий образ — только SSH клиент
  environment:
    name: production  # GitLab Environments — отображается в Deployments UI
    url:  https://projectforfamilystablelife.top
  before_script:
    - apk add --no-cache openssh-client  # устанавливаем SSH в alpine
    - eval $(ssh-agent -s) && echo "$SSH_KEY" | ssh-add -  # добавляем ключ в агент
    - ssh-keyscan $SSH_HOST >> ~/.ssh/known_hosts  # доверяем серверу (без интерактивного yes)
  script:
      # Один SSH-вызов: скачать образ и рестартовать через docker compose
    - ssh $SSH_USER@$SSH_HOST "docker pull $LATEST && docker compose up -d"
  only: [ main ]
Gitea ActionsSelf-hosted CI — синтаксис как у GitHub Actions, Telegram уведомленияDocs ↗
.gitea/workflows/deploy.yml
# Синтаксис идентичен GitHub Actions — можно копировать конфиги без изменений
name: Deploy
on:
  push:
    branches: [ main ]

jobs:
  build-deploy:
    runs-on: ubuntu-latest  # нужен act_runner на вашем сервере
    steps:
            # Скачиваем код репозитория
      - uses: actions/checkout@v4
            # Настраиваем Node.js с кешем зависимостей
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: npm   # кешируем node_modules между запусками
            # Устанавливаем зависимости и запускаем тесты
      - run: npm ci && npm test
            # Авторизуемся в registry и собираем образ
      - name: Build & Push Docker
        run: |
          echo ${{ secrets.REG_TOKEN }} | docker login ${{ secrets.REGISTRY }} -u ${{ secrets.REG_USER }} --password-stdin
          docker build -t ${{ secrets.REGISTRY }}/app:latest .  # собираем образ
          docker push ${{ secrets.REGISTRY }}/app:latest          # пушим в Gitea registry
            # Деплоим на сервер через SSH (тот же action что в GitHub Actions)
      - uses: appleboy/ssh-action@v1.0.3
        with:
          host:     ${{{ secrets.SSH_HOST }}
          username: ${{{ secrets.SSH_USER }}
          key:      ${{{ secrets.SSH_KEY }}
          script:   docker pull ${{ secrets.REGISTRY }}/app:latest && docker compose up -d
            # Уведомление в Telegram если что-то пошло не так
      - name: Notify Telegram on fail
        if: failure()  # выполнить только при ошибке
        run: curl -s -X POST ${{ secrets.TG_URL }} -d "text=❌ Deploy failed: ${{ gitea.sha }}"
JenkinsDeclarative Pipeline: параллельные стадии, input-approve, Telegram alertDocs ↗
Jenkinsfile
// Declarative Pipeline — современный синтаксис Jenkins
// Groovy DSL: похож на Kotlin/Java но для конфигурации пайплайна
pipeline {
    // Агент — где запускать. 'docker' = на Jenkins-ноде с лейблом 'docker'
  agent { label 'docker' }

  environment {
    IMAGE = "registry.example.com/my-app"
        // BUILD_NUMBER — автоинкремент, GIT_COMMIT[0..6] — первые 7 символов хеша
    TAG   = "${env.BUILD_NUMBER}-${env.GIT_COMMIT[0..6]}"
  }

  options {
    timeout(time: 30, unit: 'MINUTES')  // убить сборку если > 30 минут
    buildDiscarder(logRotator(numToKeepStr: '10'))  // хранить только 10 последних сборок
  }

  stages {
        // ── ЭТАП 1: Параллельные Lint и Test (одновременно!) ────
    stage('Lint & Test') {
      parallel {  // запускаем lint и test одновременно — экономим время
        stage('Lint') {
          steps {
                        // -v $PWD:/app монтируем код, -w /app = рабочая директория
            sh 'docker run --rm -v $PWD:/app -w /app node:20-alpine npm ci && npm run lint'
          }
        }
        stage('Test') {
          steps { sh 'docker run --rm -v $PWD:/app -w /app node:20-alpine sh -c "npm ci && npm test"' }
          post {
            always { junit 'junit.xml' }  // публикуем результаты тестов в UI Jenkins
          }
        }
      }
    }

        // ── ЭТАП 2: Сборка и публикация образа ──────────────────
    stage('Build & Push') {
      when { branch 'main' }  // выполнять только для ветки main
      steps {
        script {
                    // withRegistry — логинимся в registry используя credentials из Jenkins
          docker.withRegistry('https://registry.example.com', 'reg-creds') {
            def img = docker.build("${IMAGE}:${TAG}")
            img.push();       // пушим с тегом BUILD_NUMBER
            img.push('latest')  // и тег latest
          }
        }
      }
    }

        // ── ЭТАП 3: Деплой с ручным подтверждением ──────────────
    stage('Deploy') {
      when { branch 'main' }
            // !! ПАЙПЛАЙН ОСТАНАВЛИВАЕТСЯ и ждёт нажатия кнопки ─ ручной approve
      input { message "Deploy to production?"; ok 'Deploy' }
      steps {
                // sshagent — добавляет SSH-ключ из credentials Jenkins в агент
        sshagent(['deploy-key']) {
          sh "ssh $DEPLOY_HOST 'docker pull ${IMAGE}:latest && docker compose up -d'"
        }
      }
    }
  }

    // ── Всегда выполняется после пайплайна ──────────────────
  post {
    success { sh """curl -s "$TG_URL" -d "text=✅ ${JOB_NAME} #${BUILD_NUMBER} ok" """ }
    failure { sh """curl -s "$TG_URL" -d "text=❌ ${JOB_NAME} #${BUILD_NUMBER} failed" """ }
    always  { cleanWs() }  // очищаем рабочую директорию — экономим место на диске
  }
}

Примеры конфигурации

Реальные конфиги для Nginx, Docker Compose, Kubernetes Deployment, Prometheus, Ansible.
Конфиги
/etc/nginx/sites-available/app.conf
# Nginx — reverse proxy + SSL + security headers
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    # SSL
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    # Security headers
    add_header Strict-Transport-Security "max-age=63072000" always;
    add_header X-Frame-Options           "SAMEORIGIN"      always;
    add_header X-Content-Type-Options    "nosniff"         always;
    add_header Referrer-Policy           "strict-origin-when-cross-origin" always;

    # Gzip
    gzip on;
    gzip_types text/plain text/css application/javascript application/json image/svg+xml;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    location /api/ {
        limit_req zone=api burst=20 nodelay;
        proxy_pass         http://127.0.0.1:3000;
        proxy_set_header   Host              $host;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_read_timeout 30s;
    }

    location / {
        root  /var/www/app;
        index index.html;
        try_files $uri $uri/ /index.html;
    }
}
docker-compose.yml
# Docker Compose — app + postgres + redis + nginx
version: '3.9'

services:
  app:
    image: ghcr.io/user/my-app:latest
    restart: unless-stopped
    environment:
      NODE_ENV:     production
      DATABASE_URL: postgresql://app:${DB_PASS}@db:5432/appdb
      REDIS_URL:    redis://redis:6379
    depends_on:
      db:    { condition: service_healthy }
      redis: { condition: service_started }
    ports: [ "127.0.0.1:3000:3000" ]
    networks: [ app-net ]

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB:       appdb
      POSTGRES_USER:     app
      POSTGRES_PASSWORD: ${DB_PASS}
    volumes: [ pgdata:/var/lib/postgresql/data ]
    healthcheck:
      test:     [ "CMD-SHELL", "pg_isready -U app -d appdb" ]
      interval: 10s
      retries:  5
    networks: [ app-net ]

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
    networks: [ app-net ]

volumes:
  pgdata:

networks:
  app-net:
    driver: bridge
deployment.yaml
# Kubernetes Deployment + Service + HPA
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge:       1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: app
          image: ghcr.io/user/my-app:latest
          ports:
            - containerPort: 3000
          resources:
            requests:
              cpu:    100m
              memory: 128Mi
            limits:
              cpu:    500m
              memory: 512Mi
          env:
            - name:  NODE_ENV
              value: production
            - name: DB_PASS
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key:  db-password
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 15
            periodSeconds:       20
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: my-app-svc
spec:
  selector:
    app: my-app
  ports:
    - port:       80
      targetPort: 3000
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind:       Deployment
    name:       my-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type:               Utilization
          averageUtilization: 70
prometheus.yml + alert.rules.yml
# prometheus.yml
global:
  scrape_interval:     15s
  evaluation_interval: 15s

rule_files:
  - "alert.rules.yml"

alerting:
  alertmanagers:
    - static_configs:
        - targets: [ "alertmanager:9093" ]

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: [ "localhost:9090" ]

  - job_name: node
    static_configs:
      - targets: [ "node-exporter:9100" ]

  - job_name: nginx
    static_configs:
      - targets: [ "nginx-exporter:9113" ]

  - job_name: app
    metrics_path: /metrics
    static_configs:
      - targets: [ "app:3000" ]
---
# alert.rules.yml
groups:
  - name: server
    rules:
      - alert: HighCPU
        expr:  100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
        for:   5m
        labels:
          severity: warning
        annotations:
          summary:     "High CPU on {{ $labels.instance }}"
          description: "CPU > 85% for 5 minutes"

      - alert: InstanceDown
        expr:  up == 0
        for:   1m
        labels:
          severity: critical
        annotations:
          summary: "Instance {{ $labels.instance }} is down"
playbook-deploy.yml
# Ansible Playbook — деплой Docker-приложения на Ubuntu
---
- name: Deploy Application
  hosts: production
  become: true
  vars:
    app_image:   ghcr.io/user/my-app:latest
    app_port:    3000
    app_dir:     /opt/my-app

  tasks:
    - name: Ensure Docker is installed
      apt:
        name:  docker.io
        state: present
        update_cache: yes

    - name: Create app directory
      file:
        path:  "{{ app_dir }}"
        state: directory
        mode:  '0755'

    - name: Copy docker-compose.yml
      template:
        src:  docker-compose.yml.j2
        dest: "{{ app_dir }}/docker-compose.yml"

    - name: Pull latest image
      community.docker.docker_image:
        name:   "{{ app_image }}"
        source: pull
        force_source: true

    - name: Start / Update services
      community.docker.docker_compose_v2:
        project_src: "{{ app_dir }}"
        state:       present
        pull:        always

    - name: Reload Nginx
      systemd:
        name:  nginx
        state: reloaded

    - name: Prune unused Docker images
      command: docker image prune -f
      changed_when: false

Ресурсы

Полезные ссылки для DevOps-инженера — документация, инструменты, обучение.
Полезное

Семейное пространство & облако

Этот сайт — не только DevOps-практика, но и настоящее семейное облако. Через защищённую панель мы обмениваемся фотографиями, файлами и воспоминаниями. Галерея ниже — наши моменты.

Хранение семейных фотографий
Обмен файлами через приватную панель
Доступ только для семьи по паролю
☁️
Семейное облако
Файлы хранятся на нашем собственном сервере Ubuntu. Никаких сторонних облаков — только ваш VPS.

Контакты

Напишите — всегда рад общению.
Связаться
Давайте общаться

Email: homestablelifehello@gmail.com

Gmail
Telegram
GitHub