메뉴 닫기

리눅스 CI/CD 자동 배포 기초, GitHub Actions·GitLab CI·Jenkins로 파이프라인 구축하기

리눅스 CI/CD 자동 배포 기초, GitHub Actions·GitLab CI·Jenkins로 파이프라인 구축하기

🚀 코드 커밋만으로 자동 빌드·배포까지 완성하는 DevOps 핵심 가이드

코드를 수정하고 저장소에 커밋한 뒤, 매번 수동으로 서버에 접속해 빌드와 배포를 반복하는 일은 생각보다 많은 시간과 노력을 소모합니다.
특히 팀 단위 개발 환경에서는 이런 과정이 반복될수록 작업 속도가 떨어지고 오류 발생 가능성이 높아집니다.
이럴 때 도움이 되는 것이 바로 CI/CD(Continuous Integration / Continuous Deployment)입니다.
자동화된 파이프라인을 구성하면 코드 변경이 있을 때마다 빌드와 테스트, 배포까지 한 번에 처리할 수 있어 개발 효율성과 안정성이 크게 향상됩니다.

이번 글에서는 리눅스 환경을 기반으로 GitHub Actions, GitLab CI, Jenkins와 같은 주요 CI/CD 도구를 활용해 자동 빌드·배포 파이프라인을 만드는 방법을 살펴봅니다.
각 도구별 특징과 설정 방법을 비교하면서, 실제 운영 환경에서 적용할 수 있는 실전 팁도 함께 안내하겠습니다.



🔗 CI/CD와 자동 배포 개념 이해하기

CI/CD는 Continuous Integration(지속적 통합)과 Continuous Deployment(지속적 배포)의 약자로, 개발에서 운영까지의 과정을 자동화하는 핵심 DevOps 기법입니다.
이 방식에서는 개발자가 새로운 코드를 저장소에 커밋하면, 자동으로 빌드와 테스트를 수행한 뒤 안정성이 검증된 코드를 운영 서버에 배포합니다.
덕분에 배포 주기가 단축되고, 코드 품질과 안정성이 높아집니다.

기존 수동 배포 방식은 빌드 환경 설정, 파일 전송, 서버 재시작 등 많은 단계를 거쳐야 했지만, CI/CD 파이프라인을 구축하면 이 모든 과정이 스크립트와 자동화 도구를 통해 일괄 처리됩니다.
이 과정에서 사람이 개입하는 시간과 오류 가능성이 크게 줄어들어, 서비스 운영 효율이 향상됩니다.

💡 CI와 CD의 차이

많은 사람들이 CI와 CD를 혼동하지만, 두 개념은 분명한 차이가 있습니다.
CI는 주로 개발 단계에서 코드 변경 사항을 자주 통합하고, 이를 자동으로 테스트 및 빌드하는 과정에 집중합니다.
반면, CD는 빌드된 애플리케이션을 자동으로 운영 환경에 배포하는 단계까지 포함합니다.
CI는 코드 품질 보장을, CD는 빠르고 안정적인 배포를 목표로 합니다.

💎 핵심 포인트:
CI/CD를 도입하면 코드가 저장소에 커밋되는 즉시 빌드·테스트·배포가 자동으로 진행되어, 릴리즈 속도와 품질이 모두 향상됩니다.

  • ⚙️코드 변경 시 자동 빌드 및 테스트 실행
  • 🚀안정성이 검증된 코드 자동 배포
  • 📈배포 주기 단축 및 품질 향상

🛠️ GitHub Actions로 간단히 자동화 시작하기

GitHub Actions는 GitHub 저장소와 완벽하게 통합된 CI/CD 서비스로, YAML 파일 한 장만 작성하면 코드 빌드부터 테스트, 배포까지 전 과정을 자동화할 수 있습니다.
리눅스 환경뿐만 아니라 Windows, macOS 환경에서도 동작하며, 클라우드 인프라와의 연동도 손쉽게 가능합니다.
또한 GitHub Marketplace를 통해 다양한 커뮤니티 액션을 불러와서 재사용할 수 있어, 설정 부담이 적습니다.

기본적으로 GitHub Actions 워크플로우는 .github/workflows 디렉토리에 저장되며, 특정 이벤트(예: push, pull request)가 발생하면 자동 실행됩니다.
예를 들어 main 브랜치에 코드가 푸시되면 빌드와 테스트가 진행되고, 조건에 따라 서버 배포까지 이뤄질 수 있습니다.

⚡ GitHub Actions 기본 예시

CODE BLOCK
name: CI

on:
  push:
    branches: [ "main" ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm install
      - run: npm test

💎 핵심 포인트:
워크플로우를 작성할 때는 실행 환경(runs-on), 이벤트(on), 작업 순서(steps)를 명확히 정의해야 하며, 민감한 정보는 GitHub Secrets로 관리하는 것이 안전합니다.

  • 📂.github/workflows 디렉토리에 YAML 파일 생성
  • ⚙️on 속성으로 트리거 이벤트 지정
  • 🔑배포에 필요한 API 키나 비밀번호는 GitHub Secrets로 관리



⚙️ GitLab CI로 체계적인 파이프라인 구성

GitLab CI는 저장소와 이슈, MR, 패키지, 컨테이너 레지스트리까지 한곳에서 관리하는 올인원 CI/CD 플랫폼입니다.
프로젝트 루트에 .gitlab-ci.yml 파일을 두면 푸시나 머지 리퀘스트를 트리거로 파이프라인이 자동 실행됩니다.
리눅스 기반의 셀프호스티드 Runner 혹은 GitLab의 공유 Runner를 선택해 작업을 분산 처리할 수 있으며, 스테이지 단위로 빌드, 테스트, 배포를 명확히 분리할 수 있습니다.
권한과 환경변수를 그룹/프로젝트 단위로 중앙관리할 수 있어 보안성과 재사용성이 높습니다.

🧩 기본 .gitlab-ci.yml 구조와 스테이지

일반적으로 stages로 파이프라인의 상위 단계를 정의하고, 각 단계에 속한 job들이 병렬 혹은 순차로 실행됩니다.
캐시와 아티팩트 기능을 활용하면 의존성 다운로드 시간을 줄이고, 테스트 결과나 빌드 산출물을 다음 단계로 안전하게 전달할 수 있습니다.
환경별 배포에는 environmentsrules를 사용해 브랜치나 태그, MR 여부에 따라 조건부 실행을 설정합니다.

CODE BLOCK
stages:
  - build
  - test
  - deploy

variables:
  NODE_ENV: production
  # GitLab UI < Settings < CI/CD < Variables 에서 비밀키를 등록하세요.

cache:
  key: "deps"
  paths:
    - node_modules/

build_app:
  stage: build
  image: node:18-alpine
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week

unit_test:
  stage: test
  image: node:18-alpine
  needs: ["build_app"]
  script:
    - npm ci
    - npm test -- --ci --reporter=junit
  artifacts:
    when: always
    reports:
      junit: junit.xml

deploy_prod:
  stage: deploy
  image: alpine:3.19
  needs: ["unit_test"]
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
  before_script:
    - apk add --no-cache openssh-client rsync
  script:
    - mkdir -p ~/.ssh && echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null 2>&1 || eval $(ssh-agent -s)
    - ssh-keyscan -H $DEPLOY_HOST >> ~/.ssh/known_hosts
    - rsync -az --delete dist/ $DEPLOY_USER@$DEPLOY_HOST:/var/www/app
    - ssh $DEPLOY_USER@$DEPLOY_HOST 'sudo systemctl restart app.service'
  environment:
    name: production
    url: https://example.com
  tags:
    - linux

🔐 러너, 시크릿, 환경전략 설정 팁

배포 자격증명은 프로젝트 혹은 그룹 변수로 저장하고, 보호 브랜치에서만 노출되도록 Protected 옵션을 활성화합니다.
하나의 레포에서 멀티앱을 관리한다면 rules 조건과 only/except(레거시) 대신 규칙 기반 실행을 사용해 브랜치·경로·태그별로 파이프라인을 분기하세요.
셀프호스티드 러너에는 tags를 지정해 특정 작업만 전용 서버에서 실행되도록 구성하면 속도와 보안을 동시에 확보할 수 있습니다.

항목1 항목2
환경 staging, production 등으로 구분하여 URL, 승인, 롤백 전략을 분리합니다.
시크릿 SSH 키, 토큰은 변수로 저장하고 마스킹 처리하여 로그 유출을 방지합니다.

💎 핵심 포인트:
스테이지를 간결하게, 캐시/아티팩트를 적극 활용하고, 규칙 기반 배포와 보호 브랜치 정책으로 안전한 자동화를 구현하세요.

  • 🧱stages를 build, test, deploy로 최소화하여 흐름 단순화
  • 🗃️cache와 artifacts로 의존성 재사용 및 산출물 전달 최적화
  • 🔒배포 키는 Protected 변수로 저장하고 로그 마스킹 활성화
  • 🏷️Runner tags로 작업을 전용 리눅스 서버에 라우팅

🔌 Jenkins로 유연한 배포 환경 만들기

Jenkins는 오랜 기간 현업에서 검증된 오픈소스 CI/CD 서버로, 플러그인 생태계가 풍부하고 온프레미스, 클라우드, 컨테이너 등 어떤 환경에서도 유연하게 동작합니다.
리눅스 서버에 설치한 Jenkins 마스터(컨트롤러)에 여러 에이전트를 연결하면, 프로젝트별로 다른 빌드 도구와 런타임을 격리해 실행할 수 있습니다.
코드를 커밋하면 파이프라인이 트리거되고, 빌드·테스트·배포가 순차 혹은 병렬로 진행되며, 실패 시 즉시 알림과 롤백 전략을 수행하도록 구성할 수 있습니다.

Jenkins의 핵심은 Jenkinsfile입니다.
저장소에 Jenkinsfile을 함께 버전관리하면 파이프라인 정의가 코드로 남아 재현성과 협업 효율이 높아집니다.
Declarative Pipeline 문법을 사용하면 단계별(stage) 선언이 명확해지고, 환경변수, 자격증명, 에이전트 선택 같은 설정을 일관되게 적용할 수 있습니다.

🧩 Declarative Pipeline 기본 예시

CODE BLOCK
pipeline {
  agent { label 'linux' }

  options {
    timeout(time: 20, unit: 'MINUTES')
    buildDiscarder(logRotator(numToKeepStr: '20'))
  }

  environment {
    NODE_ENV = 'production'
    REGISTRY  = 'registry.example.com'
    IMAGE     = 'sample-app'
  }

  triggers {
    pollSCM('* * * * *') // GitHub/GitLab webhooks가 있다면 제거 가능
  }

  stages {
    stage('Checkout') {
      steps {
        checkout scm
      }
    }
    stage('Install') {
      agent { docker { image 'node:18-alpine' } }
      steps {
        sh 'npm ci'
      }
    }
    stage('Test') {
      steps {
        sh 'npm test -- --ci'
        junit 'reports/junit/*.xml'
      }
    }
    stage('Build Image') {
      steps {
        sh "docker build -t ${REGISTRY}/${IMAGE}:${env.BUILD_NUMBER} ."
      }
    }
    stage('Push Image') {
      environment { DOCKER_CONFIG = "${env.WORKSPACE}/.docker" }
      steps {
        withCredentials([usernamePassword(credentialsId: 'registry-cred', usernameVariable: 'USER', passwordVariable: 'PASS')]) {
          sh 'mkdir -p $DOCKER_CONFIG'
          sh "echo \"{ \\\"auths\\\": { \\\"${REGISTRY}\\\": { \\\"auth\\\": \\\"$(echo -n $USER:$PASS | base64)\\\" } } }\" > $DOCKER_CONFIG/config.json"
          sh "docker push ${REGISTRY}/${IMAGE}:${env.BUILD_NUMBER}"
        }
      }
    }
    stage('Deploy') {
      when { branch 'main' }
      steps {
        withCredentials([sshUserPrivateKey(credentialsId: 'prod-ssh', keyFileVariable: 'KEY')]) {
          sh "ssh -i $KEY -o StrictHostKeyChecking=no deploy@prod 'docker service update --image ${REGISTRY}/${IMAGE}:${env.BUILD_NUMBER} app_svc'"
        }
      }
    }
  }

  post {
    success {
      echo '✅ 배포 성공'
    }
    failure {
      echo '❌ 실패: 원인 분석 후 자동 롤백을 고려하세요'
    }
    always {
      cleanWs()
    }
  }
}

🔐 크레덴셜, 에이전트, 플러그인 운용 팁

민감정보는 Jenkins의 Credentials 스토어에 저장하고, 파이프라인에서 withCredentials 블록으로 참조합니다.
여러 프로젝트를 운영한다면 라벨을 활용해 에이전트를 역할별(예: docker, android, ios, maven)로 구분하고, 각 에이전트 이미지를 코드로 관리하면 일관성을 유지할 수 있습니다.
필요한 플러그인만 선별 설치하고, 업데이트 시 호환성 노트를 확인해 장애를 예방하세요.

항목1 항목2
에이전트 전략 컨테이너형(에페메럴) 에이전트로 실행 환경을 이미지로 고정하면 재현성이 높아집니다.
자격증명 토큰, SSH 키는 Credentials로 저장하고 콘솔 로그 마스킹을 활성화합니다.

💎 핵심 포인트:
Jenkinsfile로 파이프라인을 코드화하고, 에이전트 라벨과 Credentials를 표준화하면 프로젝트가 커져도 운영 복잡도를 낮출 수 있습니다.

  • 📄Jenkinsfile을 저장소에 포함해 파이프라인을 버전관리
  • 🔒API 키·SSH 키는 Credentials에 저장하고 withCredentials로만 사용
  • 🏷️에이전트 라벨로 워크로드를 적절한 리눅스 노드에 라우팅
  • 🧪junit, cobertura 등 테스트/커버리지 리포트 플러그인으로 품질 가시화

⚠️ 주의: 관리되지 않은 플러그인 대량 설치는 보안·안정성 리스크가 큽니다. 최소설치 원칙과 정기 업데이트 정책을 유지하세요.



💡 효율적인 CI/CD를 위한 운영 팁

자동화 파이프라인은 만들고 끝이 아닙니다.
코드와 인프라, 팀의 협업 방식이 바뀌면 파이프라인도 함께 진화해야 합니다.
여기서는 리눅스 기반 서비스 운영 관점에서 효율과 안정성을 동시에 끌어올리는 실전 운영 팁을 정리했습니다.
작은 규칙부터 배포 전략, 관측성, 보안까지 단계별로 적용해 보세요.
지속적으로 지표를 확인하고 병목을 제거하면 배포 속도와 품질이 자연스럽게 개선됩니다.

🧭 프로세스와 브랜치 전략

작은 단위로 자주 배포하는 것이 핵심입니다.
트렁크 기반 개발과 짧은 라이프사이클의 기능 브랜치를 기본으로 삼고, 머지 전 자동 테스트를 필수화하세요.
릴리즈 관리가 필요하면 태그 기반 배포와 체인지로그 자동 생성을 도입합니다.
모놀리포를 사용한다면 경로별 변경 감지로 영향받는 모듈만 선택적으로 빌드해 시간을 절약할 수 있습니다.

🧪 테스트 전략과 품질 게이트

테스트 피라미드를 구성해 단위 테스트를 폭넓게, 통합·E2E 테스트는 핵심 경로 위주로 유지합니다.
플레이키 테스트는 격리하고 재시도 로직을 분리해 빌드 전반을 붙잡지 않도록 합니다.
커버리지 기준, 린트, 보안 스캔을 품질 게이트로 묶고, 실패 시 빠르게 피드백하도록 단계 초반에 배치하세요.
성능 회귀를 막기 위해 핵심 API의 지연시간을 간단한 스모크 벤치마크로 점검하는 것도 효과적입니다.

🚀 배포 전략과 롤백

배포는 위험을 관리하는 과정입니다.
블루그린 혹은 카나리 전략으로 점진적 트래픽 전환을 설계하세요.
피처 플래그를 사용하면 코드 머지와 기능 노출을 분리해 위험을 낮출 수 있습니다.
컨테이너 이미지를 불변 아티팩트로 관리하고, 이전 버전으로 즉시 롤백 가능한 커맨드를 플레이북으로 문서화해 둡니다.
DB 마이그레이션은 역방향 스크립트와 호환기간을 고려해 단계적으로 진행합니다.

전략 운영 이점
블루그린 즉시 롤백 용이. 트래픽 스위치로 다운타임 최소화.
카나리 점진적 배포로 리스크 분산. 지표 기반 중단 가능.

🧱 캐시, 병렬화, 실행환경 최적화

의존성 캐시를 체계화하면 빌드 시간을 크게 줄일 수 있습니다.
언어별 패키지 캐시 키를 잠금파일(hash) 기반으로 설계하고, 아티팩트를 다음 단계로 전달해 재빌드를 방지합니다.
테스트는 워크로드를 균형 있게 샤딩해 병렬 처리합니다.
도커 기반 에이전트를 사용한다면 베이스 이미지를 주기적으로 슬림화하고, 미리 컴파일된 의존성을 레이어로 포함해 콜드스타트를 줄이세요.

🔐 시크릿과 공급망 보안

민감정보는 플랫폼의 시크릿 스토어에 저장하고, 로그 마스킹을 활성화합니다.
클라우드 배포에는 단기 자격증명(OIDC 연동 등)을 사용해 키 유출 위험을 줄입니다.
빌드 산출물에 서명을 적용하고, SBOM 생성과 취약점 스캔을 정기화하세요.
외부 액션·플러그인은 신뢰 가능한 버전으로 고정하고 해시 검증을 통해 공급망 위험을 낮춥니다.

📈 관측성과 지표 운영

배포 이후를 관찰해야 개선이 가능합니다.
로그, 메트릭, 트레이스를 연결해 장애를 빠르게 감지합니다.
파이프라인 리드타임, 변경 실패율, 배포 빈도 등 핵심 지표를 대시보드로 시각화하고, 목표치를 정해 지속적으로 개선하세요.
알림은 과잉을 피하고, 중대한 실패 시에만 채널을 태그해 대응 집중도를 높입니다.

💎 핵심 포인트:
작게, 자주, 관측 가능한 배포를 목표로 삼으세요.
피처 플래그와 카나리로 위험을 줄이고, 캐시와 병렬화로 속도를 높이며, 지표와 보안으로 신뢰도를 확보하면 CI/CD의 가치를 끝까지 끌어낼 수 있습니다.

  • 🌱작은 PR과 짧은 브랜치로 변경 규모 축소
  • 🧪플레이키 테스트 격리 및 재시도 별도 처리
  • 🚦카나리·블루그린으로 점진적 배포 설계
  • 🧱캐시 키를 잠금파일 기반으로 설계해 빌드 가속
  • 🔐시크릿은 플랫폼 스토어에 저장하고 로그 마스킹
  • 📈리드타임·배포빈도·실패율 지표를 대시보드화

⚠️ 주의: 파이프라인 단계가 많아질수록 속도와 유지보수성이 급격히 떨어질 수 있습니다. 꼭 필요한 검증만 남기고, 실패가 잦은 단계는 우선 개선하세요.

자주 묻는 질문 (FAQ)

리눅스 서버에서 가장 간단하게 CI/CD를 시작하려면 무엇부터 할까요?
Git 저장소를 준비하고 기본 테스트 스크립트를 만든 뒤, GitHub Actions 혹은 GitLab CI에서 템플릿 워크플로우를 활성화하세요.
배포 대상 서버에는 배포 전용 계정과 SSH 키를 생성하고, 서비스는 systemd로 관리해 재시작 절차를 표준화합니다.
시크릿은 플랫폼의 비밀 변수 저장소에 등록하고 파이프라인에서는 참조만 하도록 구성합니다.
GitHub Actions, GitLab CI, Jenkins는 어떤 기준으로 선택하면 좋을까요?
저장소가 GitHub라면 Actions가 설정과 연동이 가장 단순합니다.
GitLab을 사용한다면 이슈·MR·레지스트리까지 아우르는 GitLab CI가 자연스럽습니다.
온프레미스 제약이나 고도의 커스터마이징이 필요하면 플러그인 생태계가 넓은 Jenkins가 유리합니다.
하이브리드로도 운영 가능하므로 팀의 기술 스택과 규제, 비용을 함께 고려하세요.
SSH로 배포할 때 안전하게 키와 자격증명을 관리하는 방법이 있나요?
개인 키는 Git에 절대 커밋하지 말고 플랫폼 시크릿 스토어에 보관하세요.
known_hosts를 미리 등록해 MITM 위험을 줄이고, 배포 전용 사용자와 제한된 권한의 sudo 정책을 적용합니다.
키는 주기적으로 교체하고 로그에는 마스킹을 적용해 노출을 방지합니다.
도커 이미지로 빌드해서 레지스트리에 푸시하고 배포하려면 어떻게 하나요?
파이프라인에서 Dockerfile로 이미지를 빌드하고 레지스트리 자격증명으로 로그인한 뒤 태그를 기준으로 푸시합니다.
운영 서버는 pull 후 컨테이너를 재기동하거나 오케스트레이션 도구에서 롤링 업데이트를 수행합니다.
이미지 태그는 커밋 SHA나 빌드 번호를 활용해 추적 가능성을 높이세요.
무중단 배포를 하고 싶은데 블루그린과 카나리는 어떻게 다르나요?
블루그린은 두 개의 동일한 환경을 준비하고 트래픽 스위치로 즉시 전환합니다.
카나리는 소수 인스턴스부터 점진적으로 새 버전을 늘리며 지표를 확인합니다.
빠른 롤백은 블루그린이, 위험 분산과 실험적 배포는 카나리가 유리합니다.
환경 변수와 시크릿은 어디에 두고 어떻게 참조해야 하나요?
플랫폼의 시크릿 저장소와 프로젝트 변수 기능을 사용해 중앙에서 관리하세요.
파이프라인 스크립트에는 값을 직접 쓰지 말고 참조만 하며, 로그 마스킹과 보호 브랜치 정책을 함께 적용합니다.
환경별로 키를 분리해 실수로 프로덕션 값을 사용하는 일을 방지하세요.
배포 실패 시 신속하게 롤백하려면 무엇을 준비해야 하나요?
이전 빌드의 아티팩트와 컨테이너 이미지를 유지하고, 롤백 커맨드를 플레이북으로 문서화합니다.
데이터베이스는 역마이그레이션과 호환 기간을 고려해 단계적으로 변경하세요.
배포 후 헬스체크와 알림을 자동화해 실패를 조기에 감지하고 즉시 되돌릴 수 있게 하세요.
운영 비용은 어떻게 달라지나요?
GitHub Actions와 GitLab CI는 사용량 기반 러너 과금이나 분 단위 크레딧을 고려해야 합니다.
자체 러너나 Jenkins는 인프라와 유지관리 비용이 발생하지만 장기적으로 대규모 워크로드에 유리할 수 있습니다.
캐시와 병렬화 최적화로 빌드 시간을 줄이면 비용 절감 효과가 커집니다.

🧾 CI/CD 자동 배포 핵심 정리와 실전 적용 팁

이 글에서는 리눅스 환경을 중심으로 코드를 커밋하면 자동으로 빌드·테스트·배포까지 이어지도록 구성하는 CI/CD의 기본을 정리했습니다.
핵심은 파이프라인을 코드(YAML, Jenkinsfile)로 관리하고, 러너·에이전트 전략을 명확히 하며, 시크릿을 안전하게 다루는 것입니다.
GitHub Actions는 저장소와 밀접히 연동되어 빠른 시작이 강점이고, GitLab CI는 이슈·MR·레지스트리까지 올인원으로 관리하기 좋습니다.
Jenkins는 플러그인과 라벨링으로 복잡한 온프레미스 시나리오에 유연합니다.

운영 측면에서 캐시와 아티팩트로 속도를 끌어올리고, 테스트 피라미드와 품질 게이트로 안정성을 확보하세요.
배포는 블루그린·카나리·피처 플래그로 위험을 통제하고, 컨테이너 이미지를 불변 아티팩트로 관리해 추적성과 롤백 용이성을 높입니다.
관측성(로그·메트릭·트레이스)과 핵심 지표(리드타임, 배포 빈도, 변경 실패율)를 상시 모니터링하면 병목이 보이고 개선 사이클이 빨라집니다.
결국 목표는 작게, 자주, 관측 가능한 배포입니다.


🏷️ 관련 태그 : 리눅스, CI/CD, GitHub Actions, GitLab CI, Jenkins, 자동 배포, DevOps, 파이프라인, Docker, Kubernetes