Strapi CI/CD — GitHub Actions, Docker, and Automated Deployment
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
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.
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.
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.
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.
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
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.
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.
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.
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
Mini Project
Your task: Set up a complete CI/CD pipeline for Strapi.
- Create a Dockerfile with multi-stage builds (build stage and production stage).
- Create a docker-compose.yml file with Strapi and PostgreSQL services.
- 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
- Configure the build job to build and push a Docker image to Docker Hub.
- Configure the deploy job to SSH into a server and run docker compose up.
- Create a health check endpoint in Strapi and add a smoke test step after deployment.
- Add a rollback step that redeploys the previous image if the smoke test fails.
- 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