Documents
docker
docker
Type
External
Status
Published
Created
Mar 5, 2026
Updated
May 20, 2026
Updated by
Dosu Bot
Source
View

import DockerEnvTable from '/docs/snippets/docker-env-table.md'

Running Strapi in a Docker container#

This page guides you through running Strapi in Docker containers for development and production environments, including Dockerfile examples, Docker Compose configurations, and troubleshooting common issues.

Strapi does not build any official container images. The following instructions are provided as a courtesy to the community. If you have any questions please reach out on .

Containerizing Strapi makes the runtime environment reproducible across machines and simplifies dependency management for deployment. This page covers building custom Docker images for an existing Strapi 5 project, with separate instructions for development and production environments, a troubleshooting section, and a list of community tools. If you would rather not write your own Dockerfile, see Community tools and images for packaged alternatives.

  • installed on your machine
  • v2 or later
  • A supported version of Node.js
  • An existing Strapi 5 project, or a new one created with the Quick Start guide
  • npm as your package manager (the examples on this page use npm, but you can adapt the commands for yarn or pnpm)

Development environment#

Development images use npm run develop and mount your local source code for hot-reload. Before creating the Dockerfile, set up 2 required files: .dockerignore and .env.

Create a .dockerignore file#

A .dockerignore file prevents local files from being copied into the Docker image. Without it, your local node_modules directory gets included in the build context, which causes architecture mismatches (e.g., x64 binaries on ARM) and increases the image size.

Create a .dockerignore file at the root of your Strapi project:

node_modules/
.tmp/
.cache/
.git/
build/
.env

Create the Dockerfile#

The following Dockerfile can be used to build a non-production Docker image for a Strapi project.

FROM node:22-alpine
# Installing libvips-dev for sharp compatibility
RUN apk update && apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev bash vips-dev git
ARG NODE_ENV=development
ENV NODE_ENV=${NODE_ENV}

WORKDIR /opt/
COPY package.json package-lock.json ./
RUN npm install -g node-gyp
RUN npm config set fetch-retry-maxtimeout 600000 -g && npm ci
ENV PATH=/opt/node_modules/.bin:$PATH

WORKDIR /opt/app
COPY . .
RUN chown -R node:node /opt/app
USER node
EXPOSE 1337
CMD ["npm", "run", "develop"]

:::tip Optional: pre-build the admin panel
You can add RUN ["npm", "run", "build"] before the EXPOSE line to pre-build the admin panel and speed up the first start. This is not required since npm run develop rebuilds it in watch mode.
:::

:::tip Optional: reduce image size with virtual packages
For single-stage Dockerfiles like this dev image, you can use the --virtual flag to clean up build dependencies after npm ci, producing a leaner image:

RUN apk add --no-cache --virtual .build-deps \
    build-base gcc autoconf automake zlib-dev libpng-dev bash vips-dev git \
    && npm ci \
    && apk del .build-deps

This is not needed in the production Dockerfile, which already discards build dependencies through its multi-stage build.
:::

:::note Alternative base image for restricted networks
If your CI environment has limited network access (e.g., DNS restrictions that prevent downloading Sharp prebuilt binaries from GitHub), consider using node:22-slim instead of node:22-alpine. The Debian-based slim image avoids the need to compile native dependencies like libvips from source, and Sharp's prebuilt binaries work out of the box:

FROM node:22-slim
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*

This trades a slightly larger image for fewer build dependencies and fewer network requirements. :::

Set up environment variables#

Create a .env file at the root of your Strapi project. Docker Compose reads this file automatically when starting containers, so there is no need to export them in your shell.

The following example contains placeholder values. Replace them with your own values before starting the containers:

# Server
HOST=0.0.0.0
PORT=1337

# Database
# Use 'mysql' for MySQL or MariaDB, and change DATABASE_PORT to 3306
DATABASE_CLIENT=postgres
DATABASE_HOST=strapiDB
DATABASE_PORT=5432
DATABASE_NAME=strapi
DATABASE_USERNAME=strapi
DATABASE_PASSWORD=strapi

# Secrets
APP_KEYS=toBeModified1,toBeModified2
API_TOKEN_SALT=tobemodified
ADMIN_JWT_SECRET=tobemodified
TRANSFER_TOKEN_SALT=tobemodified
JWT_SECRET=tobemodified
ENCRYPTION_KEY=tobemodified

# Environment
NODE_ENV=development

Add Docker Compose for the database#

The following docker-compose.yml starts a database container and a Strapi container on a shared network.

services:
  strapi:
    container_name: strapi
    build: .
    image: strapi:latest
    restart: unless-stopped
    env_file: .env # All variables from .env are injected into the container
    volumes:
      - ./config:/opt/app/config
      - ./src:/opt/app/src
      - ./package.json:/opt/package.json
      - ./package-lock.json:/opt/package-lock.json
      - ./.env:/opt/app/.env # Needed because Strapi uses dotenv to read .env in development
      - ./public/uploads:/opt/app/public/uploads
    ports:
      - "1337:1337"
    networks:
      - strapi
    depends_on:
      strapiDB:
        condition: service_healthy

  strapiDB:
    container_name: strapiDB
    # platform: linux/amd64 # Uncomment if you encounter platform errors on Apple Silicon
    restart: unless-stopped
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${DATABASE_USERNAME}
      POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
      POSTGRES_DB: ${DATABASE_NAME}
    volumes:
      - strapi-data:/var/lib/postgresql/data/
      #- ./data:/var/lib/postgresql/data/ # if you want to use a bind folder
    ports:
      - "5432:5432" # Exposed for local debugging tools; remove if not needed
    networks:
      - strapi
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USERNAME} -d ${DATABASE_NAME}"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  strapi-data:

networks:
  strapi:
    name: strapi
    driver: bridge
services:
  strapi:
    container_name: strapi
    build: .
    image: strapi:latest
    restart: unless-stopped
    env_file: .env # All variables from .env are injected into the container
    volumes:
      - ./config:/opt/app/config
      - ./src:/opt/app/src
      - ./package.json:/opt/package.json
      - ./package-lock.json:/opt/package-lock.json
      - ./.env:/opt/app/.env # Needed because Strapi uses dotenv to read .env in development
      - ./public/uploads:/opt/app/public/uploads
    ports:
      - "1337:1337"
    networks:
      - strapi
    depends_on:
      strapiDB:
        condition: service_healthy

  strapiDB:
    container_name: strapiDB
    # platform: linux/amd64 # Uncomment if you encounter platform errors on Apple Silicon
    restart: unless-stopped
    image: mysql:8.4
    environment:
      MYSQL_USER: ${DATABASE_USERNAME}
      MYSQL_ROOT_PASSWORD: ${DATABASE_PASSWORD}
      MYSQL_PASSWORD: ${DATABASE_PASSWORD}
      MYSQL_DATABASE: ${DATABASE_NAME}
    volumes:
      - strapi-data:/var/lib/mysql
      #- ./data:/var/lib/mysql # if you want to use a bind folder
    ports:
      - "3306:3306" # Exposed for local debugging tools; remove if not needed
    networks:
      - strapi
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  strapi-data:

networks:
  strapi:
    name: strapi
    driver: bridge
services:
  strapi:
    container_name: strapi
    build: .
    image: strapi:latest
    restart: unless-stopped
    env_file: .env # All variables from .env are injected into the container
    volumes:
      - ./config:/opt/app/config
      - ./src:/opt/app/src
      - ./package.json:/opt/package.json
      - ./package-lock.json:/opt/package-lock.json
      - ./.env:/opt/app/.env # Needed because Strapi uses dotenv to read .env in development
      - ./public/uploads:/opt/app/public/uploads
    ports:
      - "1337:1337"
    networks:
      - strapi
    depends_on:
      strapiDB:
        condition: service_healthy

  strapiDB:
    container_name: strapiDB
    # platform: linux/amd64 # Uncomment if you encounter platform errors on Apple Silicon
    restart: unless-stopped
    image: mariadb:11.4
    environment:
      MYSQL_USER: ${DATABASE_USERNAME}
      MYSQL_ROOT_PASSWORD: ${DATABASE_PASSWORD}
      MYSQL_PASSWORD: ${DATABASE_PASSWORD}
      MYSQL_DATABASE: ${DATABASE_NAME}
    volumes:
      - strapi-data:/var/lib/mysql
      #- ./data:/var/lib/mysql # if you want to use a bind folder
    ports:
      - "3306:3306" # Exposed for local debugging tools; remove if not needed
    networks:
      - strapi
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  strapi-data:

networks:
  strapi:
    name: strapi
    driver: bridge

Build and run#

  1. Build and start all containers:

    docker compose up --build
    
  2. Open http://localhost:1337/admin in your browser to access the Strapi admin panel.

To stop the containers, run docker compose down. Add the -v flag to also remove the database volume.

Production environment#

Production images differ from development images in 3 key ways: they use multi-stage builds to reduce image size, they install only production dependencies in the final stage, and they run npm run start instead of the develop command. This ensures the image contains only what is needed to serve the application, without development tooling or source maps. A reverse proxy should sit in front of the Strapi container in production (see deployment documentation).

Create the production Dockerfile#

The following Dockerfile.prod uses a multi-stage build. The first stage installs all dependencies, including devDependencies needed for the build step, and builds the admin panel. The second stage copies only production assets into the final image.

Do not set NODE_ENV=production before npm ci. Doing so causes npm to skip devDependencies, which Strapi needs to compile the admin panel. The build may appear to succeed but produce a broken or incomplete admin bundle.

# Build stage
FROM node:22-alpine AS build
RUN apk update && apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev bash vips-dev git > /dev/null 2>&1

WORKDIR /opt/
COPY package.json package-lock.json ./
RUN npm install -g node-gyp
RUN npm config set fetch-retry-maxtimeout 600000 -g && npm ci
ENV PATH=/opt/node_modules/.bin:$PATH

WORKDIR /opt/app
COPY . .
# Uncomment the following lines to set the admin panel URL at build time.
# Without this, the admin panel defaults to localhost:1337.
# ARG STRAPI_ADMIN_BACKEND_URL
# ENV STRAPI_ADMIN_BACKEND_URL=${STRAPI_ADMIN_BACKEND_URL}
ENV NODE_ENV=production
RUN npm run build

# Production stage
FROM node:22-alpine
RUN apk add --no-cache vips-dev
ENV NODE_ENV=production

WORKDIR /opt/
COPY --from=build /opt/package.json /opt/package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force # --omit=dev replaces the deprecated --only=production
ENV PATH=/opt/node_modules/.bin:$PATH

WORKDIR /opt/app
COPY --from=build /opt/app ./

RUN chown -R node:node /opt/app
USER node
EXPOSE 1337
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
  CMD wget --quiet --tries=1 --spider http://localhost:1337/_health || exit 1
CMD ["npm", "run", "start"]

:::tip Optimize image size
The COPY --from=build /opt/app ./ line copies the entire application directory, including source files. For a leaner image, you can replace it with selective copies (e.g., dist/build/, config/, public/, src/). The Strapi 5 admin bundle lands in dist/build/, so make sure that path is included. This is optional since Strapi needs most of these files at runtime.
:::

:::info Key difference from the development Dockerfile
The build stage installs all dependencies (including devDependencies) because the npm run build step needs them to compile the admin panel. The production stage then installs only production dependencies, keeping the final image lean.
:::

Add Docker Compose for production#

The following docker-compose.prod.yml is suitable for production deployments. It uses PostgreSQL and includes healthchecks. The Strapi port binds to 127.0.0.1 so that only a local reverse proxy can reach it.

services:
  strapi:
    container_name: strapi
    build:
      context: .
      dockerfile: Dockerfile.prod
    image: strapi:latest
    restart: always
    env_file: .env
    environment:
      NODE_ENV: production # Overrides the development value from .env
    ports:
      - "127.0.0.1:1337:1337"
    networks:
      - strapi
    depends_on:
      strapiDB:
        condition: service_healthy

  strapiDB:
    container_name: strapiDB
    restart: always
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${DATABASE_USERNAME}
      POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
      POSTGRES_DB: ${DATABASE_NAME}
    volumes:
      - strapi-data:/var/lib/postgresql/data/
    networks:
      - strapi
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USERNAME} -d ${DATABASE_NAME}"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  strapi-data:

networks:
  strapi:
    name: strapi
    driver: bridge
Do not expose database ports to the host. The strapiDB service above has no ports mapping, making it accessible only to other containers on the strapi network. Persist uploads. The production docker-compose does not mount a volume for /opt/app/public/uploads. If you use the default local upload provider, add a named volume (e.g., strapi-uploads:/opt/app/public/uploads) or configure an external provider such as AWS S3 or Cloudinary. Secure your secrets. The production docker-compose uses env_file: .env, which is acceptable for single-host deployments. For orchestrated environments, prefer Docker secrets, environment variables injected by your orchestrator (Kubernetes, ECS), or a dedicated secret manager (HashiCorp Vault, AWS Secrets Manager).

Build and publish#

To build a production Docker image, run the following command:

docker build \
  -t mystrapiapp:latest \
  -f Dockerfile.prod .

After building, you can publish the image to a Docker registry. For production usage, use a private registry since your Docker image may contain sensitive configuration.

Popular container registries include:

Community tools and images#

Strapi does not provide official Docker images (see FAQ). The following community-maintained tools and images can help you get started.

If you would like to add your tool to this list, please open a pull request on the .

The @strapi-community/dockerize CLI#

The @strapi-community/dockerize package is a CLI tool that generates a Dockerfile and docker-compose.yml file for a Strapi project.

To get started, run npx @strapi-community/dockerize@latest within an existing Strapi project folder and follow the CLI prompts.

For more information, see the official or the .

Community-maintained Docker images#

Pre-built Docker images maintained by community members are available for Strapi 5. These images let you run Strapi without writing your own Dockerfile.

These images are community-maintained and not officially supported by Strapi. Review their documentation and source code before using them in production.

  • : Actively maintained image that tracks Strapi 5 releases. Available on .
  • : Supports both AMD64 and ARM64 architectures. Tracks Strapi 5 releases.

Troubleshooting#

The following section details some common issues with Sharp and ARM builds on Apple Silicon-powered machines.

Sharp and libvips errors#

Sharp is the image processing library used by Strapi. It depends on libvips, which requires native compilation on Alpine-based images. Common error messages include Cannot find module 'sharp' or Error: sharp: Installation error.

To resolve Sharp issues:

  1. Run docker exec <container> node -e "require('sharp')". If it errors with a missing library, the runtime stage is missing the Alpine packages above. If it errors with a glibc/musl mismatch, switch to node:22-slim (see step 2).

  2. Verify that your Dockerfile installs the required Alpine packages:

    RUN apk update && apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev bash vips-dev git
    
  3. If the issue persists, switch to node:22-slim (Debian-based) to avoid native library compatibility problems Alpine Linux uses a lightweight C library called musl instead of the standard glibc used by Debian and Ubuntu. Some npm packages ship pre-compiled binaries built for glibc that do not work on Alpine. Switching to a Debian-based image like node:22-slim avoids this issue entirely.:

    FROM node:22-slim
    RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
    
  4. For ARM builds (see below), add the following environment variable before installing dependencies:

    ENV SHARP_IGNORE_GLOBAL_LIBVIPS=1
    

Apple Silicon and ARM builds#

The platform: linux/amd64 flag in docker-compose files forces containers to run under x86 emulation on ARM-based machines (Apple M1/M2/M3). This works but is slower than native ARM builds.

For native ARM performance:

  • Remove the platform: linux/amd64 line from your docker-compose file.
  • Use node:22-alpine or node:22-slim as your base image. Both support ARM64 natively.
  • Ensure Sharp dependencies are installed correctly (see Sharp and libvips errors).

Database connection issues#

If Strapi cannot connect to the database in Docker, check the following:

  1. Verify that DATABASE_HOST matches the service name in your docker-compose file (e.g., strapiDB), not localhost or 127.0.0.1. Containers communicate over the Docker network using service names.

  2. Check for port conflicts with local database instances. If a database is already running locally on the same port:

    • Stop the local database,
    • or change the host-side port mapping in your docker-compose file (e.g., "5433:5432").
  3. Set the connection pool min value to 0 in your database configuration, as Docker may kill idle connections:

    module.exports = ({ env }) => ({
      connection: {
        client: env('DATABASE_CLIENT'),
        // ...
        pool: {
          min: 0,
          max: 10,
        },
      },
    });
    
    export default ({ env }) => ({
      connection: {
        client: env('DATABASE_CLIENT'),
        // ...
        pool: {
          min: 0,
          max: 10,
        },
      },
    });
    
  4. If the database container becomes unreachable after periods of inactivity, add timeout settings to the pool configuration in your database configuration:

    pool: {
      min: 0,
      max: 10,
      acquireTimeoutMillis: 60000,
      idleTimeoutMillis: 30000,
    },
    

What to do next?#

Now that Strapi is running in a Docker container, you can:

Set up the admin panel and create your first content types with the Content-Type Builder.

Configure a reverse proxy and deploy to production (see deployment documentation).

Explore environment configuration for fine-tuning your Strapi instance.