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
name: CI/CD Deploy
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

env:
  IMAGE: ghcr.io/${{ github.repository_owner }}/my-app

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hadolint/hadolint-action@v3.1.0
        with:
          dockerfile: Dockerfile

  test:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

  build-push:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{{ github.actor }}
          password: ${{{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: ${{{ env.IMAGE }}:latest

  deploy:
    needs: build-push
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: appleboy/ssh-action@v1.0.3
        with:
          host:     ${{{ secrets.SSH_HOST }}
          username: ${{{ secrets.SSH_USER }}
          key:      ${{{ secrets.SSH_KEY }}
          script: |
            docker pull $IMAGE:latest
            docker stop app || true && docker rm app || true
            docker run -d --name app --restart unless-stopped -p 3000:3000 $IMAGE:latest
            docker image prune -f
GitLab CIDocker-in-Docker, кеш, environment, SSH деплойDocs ↗
.gitlab-ci.yml
image: docker:24
services: [ docker:24-dind ]

variables:
  IMAGE:  $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
  LATEST: $CI_REGISTRY_IMAGE:latest

stages: [ lint, test, build, deploy ]

lint:
  stage: lint
  image: node:20-alpine
  cache:
    key:   $CI_COMMIT_REF_SLUG
    paths: [ node_modules/ ]
  script: [ npm ci, npm run lint ]

test:
  stage: test
  image: node:20-alpine
  script: [ npm ci, npm test -- --coverage ]
  coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'

build:
  stage: build
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build --cache-from $LATEST -t $IMAGE -t $LATEST .
    - docker push $IMAGE && docker push $LATEST
  only: [ main ]

deploy:
  stage: deploy
  image: alpine
  environment:
    name: production
    url:  https://projectforfamilystablelife.top
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s) && echo "$SSH_KEY" | ssh-add -
    - ssh-keyscan $SSH_HOST >> ~/.ssh/known_hosts
  script:
    - 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
name: Deploy
on:
  push:
    branches: [ main ]

jobs:
  build-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: npm
      - run: npm ci && npm test
      - name: Build & Push
        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
      - 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
      - name: Notify Telegram
        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
pipeline {
  agent { label 'docker' }
  environment {
    IMAGE = "registry.example.com/my-app"
    TAG   = "${env.BUILD_NUMBER}-${env.GIT_COMMIT[0..6]}"
  }
  options {
    timeout(time: 30, unit: 'MINUTES')
    buildDiscarder(logRotator(numToKeepStr: '10'))
  }
  stages {
    stage('Lint & Test') {
      parallel {
        stage('Lint') {
          steps { 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' } }
        }
      }
    }
    stage('Build & Push') {
      when { branch 'main' }
      steps {
        script {
          docker.withRegistry('https://registry.example.com', 'reg-creds') {
            def img = docker.build("${IMAGE}:${TAG}")
            img.push(); img.push('latest')
          }
        }
      }
    }
    stage('Deploy') {
      when { branch 'main' }
      input { message "Deploy to production?"; ok 'Deploy' }
      steps {
        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