Skip to content

Strapi CI/CD — GitHub Actions, Docker, and Automated Deployment

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn how to set up continuous integration and deployment for Strapi — building Docker images, running automated tests with GitHub Actions, deploying to production servers, and implementing zero-downtime deployment strategies.

What You'll Learn

  • How to create a GitHub Actions workflow for Strapi
  • How to Dockerize a Strapi application
  • How to run automated tests in CI
  • How to deploy Strapi to a VPS or cloud platform
  • How to implement zero-downtime deployment
  • How to manage database migrations in deployment pipelines

Why It Matters

Manual deployment is error-prone and slow. Every time you deploy by SSH-ing into a server and running commands, you risk forgetting a step, deploying with a bug, or causing downtime. CI/CD automates the entire process: tests run automatically, build artifacts are created consistently, and deployment happens with a single push to the main branch. This makes deployments fast, repeatable, and safe.

Real-World Use

A Strapi-powered e-commerce backend receives feature updates twice per week. Before CI/CD, deployments took 30 minutes of manual work and caused 10 minutes of downtime each time. After implementing GitHub Actions with Docker, a push to main triggers automated tests, builds the Docker image, pushes it to a registry, and deploys to the server with zero downtime. The entire process takes 4 minutes, and deployments happen during business hours without affecting users.

Learning Path

flowchart LR
  A["Environment Variables"] --> B["CI/CD
-- You are here"]:::current B --> C["Performance"] C --> D["Security & Monitoring"] classDef current fill:#4945ff,color:#fff,stroke-width:2px

Dockerizing Strapi

Create a Dockerfile that builds and serves Strapi in production:

# Dockerfile
FROM node:20-alpine AS build

WORKDIR /app

# Install dependencies first (layer caching)
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Copy application code
COPY . .

# Build admin panel
RUN NODE_ENV=production npm run build

# Production stage
FROM node:20-alpine AS production

WORKDIR /app
RUN apk add --no-cache tini

COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/build ./build
COPY --from=build /app/config ./config
COPY --from=build /app/database ./database
COPY --from=build /app/public ./public
COPY --from=build /app/src ./src
COPY --from=build /app/package.json ./

EXPOSE 1337

ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "node_modules/@strapi/strapi/bin/strapi.js", "start"]
# docker-compose.yml
version: "3.8"

services:
  strapi:
    build:
      context: .
      target: production
    ports:
      - "1337:1337"
    environment:
      NODE_ENV: production
      DATABASE_CLIENT: postgres
      DATABASE_HOST: db
      DATABASE_PORT: 5432
      DATABASE_NAME: strapi
      DATABASE_USERNAME: strapi
      DATABASE_PASSWORD: "${DATABASE_PASSWORD}"
      DATABASE_SSL: "false"
      APP_KEYS: "${APP_KEYS}"
      JWT_SECRET: "${JWT_SECRET}"
      ADMIN_JWT_SECRET: "${ADMIN_JWT_SECRET}"
      API_TOKEN_SALT: "${API_TOKEN_SALT}"
      PUBLIC_URL: "${PUBLIC_URL}"
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: strapi
      POSTGRES_USER: strapi
      POSTGRES_PASSWORD: "${DATABASE_PASSWORD}"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U strapi"]
      interval: 5s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  postgres_data:

GitHub Actions Workflow

Create a CI/CD pipeline that tests, builds, and deploys:

# .github/workflows/deploy.yml
name: Strapi CI/CD

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: strapi_test
          POSTGRES_USER: strapi
          POSTGRES_PASSWORD: test_password
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run linting
        run: npm run lint || true

      - name: Run tests
        run: npm test
        env:
          DATABASE_CLIENT: postgres
          DATABASE_HOST: localhost
          DATABASE_PORT: 5432
          DATABASE_NAME: strapi_test
          DATABASE_USERNAME: strapi
          DATABASE_PASSWORD: test_password
          APP_KEYS: ${{ secrets.APP_KEYS }}
          API_TOKEN_SALT: ${{ secrets.API_TOKEN_SALT }}
          ADMIN_JWT_SECRET: ${{ secrets.ADMIN_JWT_SECRET }}
          JWT_SECRET: ${{ secrets.JWT_SECRET }}

  build-and-deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ secrets.DOCKER_USERNAME }}/strapi:latest
            ${{ secrets.DOCKER_USERNAME }}/strapi:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Deploy to server
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          script: |
            cd /opt/strapi
            export DATABASE_PASSWORD=${{ secrets.DATABASE_PASSWORD }}
            export APP_KEYS=${{ secrets.APP_KEYS }}
            export JWT_SECRET=${{ secrets.JWT_SECRET }}
            docker compose pull strapi
            docker compose up -d --no-deps strapi
            docker image prune -f

Database Migrations in CI

Handle database schema changes during deployment:

# .github/workflows/deploy.yml (deploy step extended)
- name: Run database migrations
  uses: appleboy/ssh-action@v1.0.0
  with:
    host: ${{ secrets.DEPLOY_HOST }}
    username: ${{ secrets.DEPLOY_USER }}
    key: ${{ secrets.DEPLOY_SSH_KEY }}
    script: |
      cd /opt/strapi
      # Before deploying new version, run migrations against current database
      docker compose run --rm strapi \
        node node_modules/@strapi/strapi/bin/strapi.js migrate
# Manual migration script for sensitive operations
# scripts/migrate.sh
#!/bin/bash
echo "Running pre-deployment migrations..."
docker compose run --rm strapi \
  node node_modules/@strapi/strapi/bin/strapi.js migrate

echo "Migrations complete. Proceeding with deployment..."

Zero-Downtime Deployment

Ensure your deployment does not interrupt API requests:

# docker-compose.prod.yml (with load balancer)
version: "3.8"

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - strapi-blue
      - strapi-green
    restart: unless-stopped

  strapi-blue:
    build:
      context: .
      target: production
    environment:
      - NODE_ENV=production
      # ... other env vars
    restart: unless-stopped

  strapi-green:
    build:
      context: .
      target: production
    environment:
      - NODE_ENV=production
      # ... other env vars
    restart: unless-stopped
# nginx.conf — Blue-green deployment
upstream strapi_backend {
    server strapi-blue:1337 weight=1;
    server strapi-green:1337 weight=1;
}
#!/bin/bash
# scripts/zero-downtime-deploy.sh
# Blue-green deployment script

COLOR="blue"
CURRENT_COLOR=$(docker compose ps --services | grep -E "strapi-(blue|green)" | head -1)

if [[ "$CURRENT_COLOR" == *"blue"* ]]; then
  COLOR="green"
fi

echo "Deploying to $COLOR..."

# Deploy new version to the inactive service
docker compose up -d --no-deps --scale strapi-blue=0 --scale strapi-green=0 "strapi-$COLOR"

echo "Waiting for health check..."
sleep 10

# Switch traffic to the new service
docker compose exec nginx sh -c "
  sed -i 's/server strapi-${COLOR}:1337 weight=0/server strapi-${COLOR}:1337 weight=1/' /etc/nginx/nginx.conf
  sed -i 's/server strapi-${COLOR}:1337 weight=1/server strapi-${COLOR}:1337 weight=0/' /etc/nginx/nginx.conf
  nginx -s reload
"

echo "Deployment to $COLOR complete."

Automated Testing in CI

Run linting, type checking, and tests as part of the pipeline:

# .github/workflows/test.yml — Run on every pull request
name: Test

on:
  pull_request:
    branches: [main, develop]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npm run lint

  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npx tsc --noEmit

  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: strapi_test
          POSTGRES_USER: strapi
          POSTGRES_PASSWORD: test_password
        ports:
          - 5432:5432
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npm test
        env:
          DATABASE_CLIENT: postgres
          DATABASE_HOST: localhost
          DATABASE_PORT: 5432
          DATABASE_NAME: strapi_test
          DATABASE_USERNAME: strapi
          DATABASE_PASSWORD: test_password

Common Mistakes

  1. Not running tests before deployment. Deploying without running tests means broken code reaches production. Always run the full test suite in CI before deploying. Use separate test and deploy jobs so deployment only happens when tests pass.

  2. Hardcoding secrets in Docker images. Building secrets into Docker images makes them available to anyone who pulls the image. Pass secrets at runtime through environment variables or secrets mount points.

  3. Forgetting database migrations in the deploy script. If you add a content type field and deploy without running migrations, the new code crashes because the column does not exist. Always include migration commands in the deployment pipeline.

  4. Deploying during peak hours without zero-downtime strategy. A deployment that causes even 10 seconds of downtime during peak traffic loses users and revenue. Implement blue-green or rolling deployments to avoid downtime.

  5. Not rolling back when a deployment fails. A failed deployment should automatically roll back to the previous version. Keep the previous Docker image tagged and ready. Add rollback logic to the deployment script.

Practice Questions

  1. What are the benefits of using Docker for Strapi deployment? Answer: Docker ensures consistent environments across development, testing, and production. It packages the application with all dependencies, eliminates "it works on my machine" issues, and simplifies scaling.

  2. How does blue-green deployment achieve zero downtime? Answer: Blue-green deployment runs two identical environments. One serves traffic while the other is updated. After the update, traffic switches to the updated environment. If something goes wrong, traffic switches back.

  3. What should a complete Strapi CI/CD pipeline include? Answer: Linting and type checking, automated tests, Docker image build and push, database migrations, deployment to staging, smoke tests, deployment to production, and rollback capability.

  4. Challenge: Set up a complete CI/CD pipeline: (1) Create a Dockerfile with multi-stage builds, (2) Set up docker-compose.yml with Strapi and PostgreSQL services, (3) Create a GitHub Actions workflow with test and deploy jobs, (4) Configure the test job to run linting, type checking, and integration tests, (5) Configure the deploy job to build and push a Docker image, (6) Implement blue-green deployment on a test server, (7) Add a rollback step that deploys the previous image if smoke tests fail, (8) Test the full pipeline by making a change, pushing it, and verifying the automated deployment.

FAQ

Can I deploy Strapi without Docker?

Yes, you can deploy directly to a VPS with Node.js and PM2. However, Docker provides consistency, easier scaling, and simpler rollbacks. For single-server deployments, PM2 is a simpler alternative.

How do I handle file uploads in Docker?

Use a persistent volume for the uploads directory or use an external upload provider like S3 or Cloudinary. Volumes survive container restarts and replacements.

Should I run Strapi in a container orchestration platform?

For production at scale, consider Kubernetes or AWS ECS. These provide auto-scaling, self-healing, and rolling updates. For small to medium projects, Docker Compose on a single VPS is sufficient.

How do I manage environment variables in CI/CD?

Store secrets in GitHub Secrets (or your CI provider's secrets manager). Reference them with ${{ secrets.SECRET_NAME }} in workflow files. Never hardcode secrets in YAML files or Dockerfiles.

How do I test the deployment without affecting production?

Deploy to a staging environment first. Staging should mirror production as closely as possible. Run smoke tests against staging before promoting to production.

Mini Project

Your task: Set up a complete CI/CD pipeline for Strapi.

  1. Create a Dockerfile with multi-stage builds (build stage and production stage).
  2. Create a docker-compose.yml file with Strapi and PostgreSQL services.
  3. Create a GitHub Actions workflow with these jobs:
    • test: runs on every push, includes linting and integration tests
    • build-and-deploy: runs after tests pass on main branch
  4. Configure the build job to build and push a Docker image to Docker Hub.
  5. Configure the deploy job to SSH into a server and run docker compose up.
  6. Create a health check endpoint in Strapi and add a smoke test step after deployment.
  7. Add a rollback step that redeploys the previous image if the smoke test fails.
  8. Test the pipeline: push a change to main and watch the automated deployment.

What's Next

Now that you have CI/CD set up, proceed to Performance to learn how to optimize your Strapi application with Caching, CDN, database tuning, and clustering. After that, complete the series with Security & Monitoring.

Related lessons:

  • Docker — Containerization fundamentals
  • Node.js — Production Node.js deployment
  • PostgreSQL — Database in container environments

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro