$ stages: - test - build - deploy variables: DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA # Stage 1: Run tests
test: stage: test image: node:20-alpine script: - -weight: 500;">npm ci - -weight: 500;">npm run test - -weight: 500;">npm run lint rules: - if: $CI_MERGE_REQUEST_IID - if: $CI_COMMIT_BRANCH == "main" # Stage 2: Build Docker image
build: stage: build image: -weight: 500;">docker:24.0 services: - -weight: 500;">docker:24.0-dind script: - -weight: 500;">docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - -weight: 500;">docker build -t $DOCKER_IMAGE . - -weight: 500;">docker push $DOCKER_IMAGE rules: - if: $CI_COMMIT_BRANCH == "main" # Stage 3: Deploy to production
deploy: stage: deploy image: alpine:latest script: - -weight: 500;">apk add --no-cache openssh-client - mkdir -p ~/.ssh - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - ssh -o StrictHostKeyChecking=no user@your-server.com " -weight: 500;">docker pull $DOCKER_IMAGE && -weight: 500;">docker -weight: 500;">stop app || true && -weight: 500;">docker rm app || true && -weight: 500;">docker run -d --name app -p 3000:3000 $DOCKER_IMAGE " rules: - if: $CI_COMMIT_BRANCH == "main" when: manual
stages: - test - build - deploy variables: DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA # Stage 1: Run tests
test: stage: test image: node:20-alpine script: - -weight: 500;">npm ci - -weight: 500;">npm run test - -weight: 500;">npm run lint rules: - if: $CI_MERGE_REQUEST_IID - if: $CI_COMMIT_BRANCH == "main" # Stage 2: Build Docker image
build: stage: build image: -weight: 500;">docker:24.0 services: - -weight: 500;">docker:24.0-dind script: - -weight: 500;">docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - -weight: 500;">docker build -t $DOCKER_IMAGE . - -weight: 500;">docker push $DOCKER_IMAGE rules: - if: $CI_COMMIT_BRANCH == "main" # Stage 3: Deploy to production
deploy: stage: deploy image: alpine:latest script: - -weight: 500;">apk add --no-cache openssh-client - mkdir -p ~/.ssh - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - ssh -o StrictHostKeyChecking=no user@your-server.com " -weight: 500;">docker pull $DOCKER_IMAGE && -weight: 500;">docker -weight: 500;">stop app || true && -weight: 500;">docker rm app || true && -weight: 500;">docker run -d --name app -p 3000:3000 $DOCKER_IMAGE " rules: - if: $CI_COMMIT_BRANCH == "main" when: manual
stages: - test - build - deploy variables: DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA # Stage 1: Run tests
test: stage: test image: node:20-alpine script: - -weight: 500;">npm ci - -weight: 500;">npm run test - -weight: 500;">npm run lint rules: - if: $CI_MERGE_REQUEST_IID - if: $CI_COMMIT_BRANCH == "main" # Stage 2: Build Docker image
build: stage: build image: -weight: 500;">docker:24.0 services: - -weight: 500;">docker:24.0-dind script: - -weight: 500;">docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - -weight: 500;">docker build -t $DOCKER_IMAGE . - -weight: 500;">docker push $DOCKER_IMAGE rules: - if: $CI_COMMIT_BRANCH == "main" # Stage 3: Deploy to production
deploy: stage: deploy image: alpine:latest script: - -weight: 500;">apk add --no-cache openssh-client - mkdir -p ~/.ssh - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - ssh -o StrictHostKeyChecking=no user@your-server.com " -weight: 500;">docker pull $DOCKER_IMAGE && -weight: 500;">docker -weight: 500;">stop app || true && -weight: 500;">docker rm app || true && -weight: 500;">docker run -d --name app -p 3000:3000 $DOCKER_IMAGE " rules: - if: $CI_COMMIT_BRANCH == "main" when: manual
graph LR A[Push to main] --> B[test] B --> C[build] C --> D{deploy} D -->|manual trigger| E[Production]
graph LR A[Push to main] --> B[test] B --> C[build] C --> D{deploy} D -->|manual trigger| E[Production]
graph LR A[Push to main] --> B[test] B --> C[build] C --> D{deploy} D -->|manual trigger| E[Production]
security-scan: stage: test image: aquasec/trivy:latest script: - trivy image --severity HIGH,CRITICAL $DOCKER_IMAGE allow_failure: true rules: - if: $CI_COMMIT_BRANCH == "main"
security-scan: stage: test image: aquasec/trivy:latest script: - trivy image --severity HIGH,CRITICAL $DOCKER_IMAGE allow_failure: true rules: - if: $CI_COMMIT_BRANCH == "main"
security-scan: stage: test image: aquasec/trivy:latest script: - trivy image --severity HIGH,CRITICAL $DOCKER_IMAGE allow_failure: true rules: - if: $CI_COMMIT_BRANCH == "main"
test: cache: key: ${CI_COMMIT_REF_SLUG} paths: - node_modules/
test: cache: key: ${CI_COMMIT_REF_SLUG} paths: - node_modules/
test: cache: key: ${CI_COMMIT_REF_SLUG} paths: - node_modules/ - Run your tests
- Build your application
- Deploy to staging or production
- Run security scans - A GitLab account (free tier works fine)
- A project with at least a basic application
- Understanding of basic shell commands
- A server or cloud instance for deployment (AWS, Azure, or a VPS) - Test stage — Installs dependencies, runs tests and linting
- Build stage — Creates a Docker image and pushes to GitLab's registry
- Deploy stage — SSH's into your server and deploys the new image (manual trigger for safety) - Add monitoring with Prometheus and Grafana
- Set up automatic rollbacks on health check failures
- Implement canary deployments for zero-risk releases
- Use GitLab Environments to track deployment history