Back to Top

Setup for PimCore Development Environment

Updated 10 June 2026

Docker-based environment for a fresh Pimcore installation.

Directory Structure

├── app
├── docker
│   ├── data
│   ├── nginx
│   │   ├── certs
│   │   │   ├── pimcore.local.crt
│   │   │   └── pimcore.local.key
│   │   └── default.conf
│   ├── php
│   │   ├── Dockerfile
│   │   └── pimcore-supervisor.conf
│   └── php.ini
└── docker-compose.yml

Services

  • php (pimcore_php) — Custom Dockerfile — PHP 8.4-FPM + Supervisord workers
  • nginx (pimcore_nginx) — nginx:stable — HTTPS reverse proxy
  • db (pimcore_db) — mysql:8.0 — Application database
  • redis (pimcore_redis) — redis:7-alpine — Cache / session (2 GB LRU)
  • rabbitmq (pimcore_rabbitmq) — rabbitmq:3-management — Message broker
  • opensearch (pimcore_opensearch) — opensearchproject/opensearch:2.11.0 — Full-text search
  • mercure (pimcore_mercure) — dunglas/mercure — Real-time SSE hub

All containers share a bridge network named pimcore. Service hostnames (db, redis, etc.) resolve automatically within it.

Ports

  • 443 — Nginx — Pimcore HTTPS
  • 5672 — RabbitMQ — AMQP
  • 15672 — RabbitMQ — Management UI (guest / guest)
  • 9201 — OpenSearch — REST API

Configuration Files

docker-compose.yml

services:
  php:
    build:
      context: ./docker
      dockerfile: php/Dockerfile
    container_name: pimcore_php
    command: >
      bash -c "
      mkdir -p /var/www/html/var /var/www/html/public/var &&
      chown -R www-data:www-data /var/www/html/var /var/www/html/public/var &&
      chmod -R 775 /var/www/html/var /var/www/html/public/var &&
      /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf
      "
    volumes:
      - ./app:/var/www/html
      - ./docker/php.ini:/usr/local/etc/php/php.ini:ro
      - ./app/.docker/supervisord.conf:/etc/supervisor/conf.d/pimcore.conf:ro
    depends_on:
      - db
      - redis
      - opensearch
    environment:
      - APP_ENV=prod
    networks:
      - pimcore

  nginx:
    image: nginx:stable
    container_name: pimcore_nginx
    ports:
      - "443:443"
    volumes:
      - ./app:/var/www/html
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
      - ./docker/nginx/certs:/etc/nginx/certs:ro
    depends_on:
      - php
    networks:
      - pimcore

  db:
    image: mysql:8.0
    container_name: pimcore_db
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: pimcore
      MYSQL_USER: pimcore
      MYSQL_PASSWORD: pimcore
    volumes:
      - ./docker/data/mysql:/var/lib/mysql
    networks:
      - pimcore

  redis:
    image: redis:7-alpine
    container_name: pimcore_redis
    command: redis-server --maxmemory 2gb --maxmemory-policy allkeys-lru
    volumes:
      - ./docker/data/redis:/data
    networks:
      - pimcore

  rabbitmq:
    image: rabbitmq:3-management
    container_name: pimcore_rabbitmq
    ports:
      - "5672:5672"
      - "15672:15672"
    environment:
      RABBITMQ_DEFAULT_USER: guest
      RABBITMQ_DEFAULT_PASS: guest
    networks:
      - pimcore

  opensearch:
    image: opensearchproject/opensearch:2.11.0
    container_name: pimcore_opensearch
    environment:
      discovery.type: single-node
      plugins.security.disabled: "true"
      OPENSEARCH_JAVA_OPTS: "-Xms2g -Xmx2g"
    volumes:
      - ./docker/data/opensearch:/usr/share/opensearch/data
    ports:
      - "9201:9200"
    networks:
      - pimcore

  mercure:
    image: dunglas/mercure:latest
    container_name: pimcore_mercure
    environment:
      SERVER_NAME: ":80"
      MERCURE_PUBLISHER_JWT_KEY: "YOUR_JWT_SECRET_KEY"
      MERCURE_SUBSCRIBER_JWT_KEY: "YOUR_JWT_SECRET_KEY"
      MERCURE_EXTRA_DIRECTIVES: |
        anonymous
        cors_origins *
    networks:
      - pimcore

networks:
  pimcore:

Change YOUR_JWT_SECRET_KEY to a long random string — must match MERCURE_JWT_KEY in app/.env.local.

docker/php/Dockerfile

FROM php:8.4-fpm

# System dependencies + supervisor (queue worker manager) + graphviz (Pimcore workflow diagrams)
# libwebp-dev: WebP image support for GD (product images may be WebP)
RUN apt-get update && apt-get install -y --no-install-recommends \
    git unzip libicu-dev libzip-dev libpng-dev libjpeg-dev libfreetype6-dev \
    libwebp-dev \
    libonig-dev libxml2-dev libxslt1-dev libcurl4-openssl-dev \
    libpq-dev libssl-dev librabbitmq-dev \
    graphviz \
    supervisor \
    && rm -rf /var/lib/apt/lists/*

# PHP extensions — configure GD with JPEG + WebP + FreeType before installing
RUN docker-php-ext-configure gd --with-jpeg --with-webp --with-freetype \
    && docker-php-ext-install \
    pdo pdo_mysql intl zip gd opcache bcmath soap xsl exif

# Redis extension
RUN pecl install redis && docker-php-ext-enable redis

# AMQP extension
RUN pecl install amqp && docker-php-ext-enable amqp

# Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

# Opcache config
COPY php.ini /usr/local/etc/php/php.ini

# Supervisord config — manages php-fpm + all Pimcore queue workers
COPY php/pimcore-supervisor.conf /etc/supervisor/conf.d/pimcore.conf

WORKDIR /var/www/html

# Wrapper: always run bin/console as www-data so var/ files get correct ownership
RUN printf '#!/bin/sh\nexec su -s /bin/sh www-data -c "php /var/www/html/bin/console $*"\n' \
    > /usr/local/bin/pimcore && chmod +x /usr/local/bin/pimcore

# supervisord runs in foreground and manages php-fpm + workers
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"]

docker/php.ini

memory_limit=2G
max_execution_time=300
upload_max_filesize=512M
post_max_size=600M

opcache.enable=1
opcache.memory_consumption=512
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0

realpath_cache_size=4096k
realpath_cache_ttl=600
Set opcache.validate_timestamps=1 in development so code changes apply without a container restart.

docker/php/pimcore-supervisor.conf

# Supervisord program definitions for Pimcore PHP container
# supervisord is started as the container entrypoint; it manages php-fpm and all queue workers.

[program:php-fpm]
command=/usr/local/sbin/php-fpm --nodaemonize
autostart=true
autorestart=true
priority=1
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true

[program:messenger-consume]
command=php /var/www/html/bin/console messenger:consume pimcore_generic_data_index_queue scheduler_generic_data_index pimcore_core pimcore_maintenance pimcore_scheduled_tasks pimcore_image_optimize --memory-limit=250M --time-limit=3600
numprocs=1
startsecs=0
autostart=true
autorestart=true
user=www-data
priority=10
process_name=%(program_name)s_%(process_num)02d
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true

[program:consume-asset-update]
command=php /var/www/html/bin/console messenger:consume pimcore_asset_update --memory-limit=250M --time-limit=3600
numprocs=1
startsecs=0
autostart=true
autorestart=true
user=www-data
priority=10
process_name=%(program_name)s_%(process_num)02d
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true

[program:consume-execution-engine]
command=php /var/www/html/bin/console messenger:consume pimcore_generic_execution_engine --memory-limit=250M --time-limit=3600
numprocs=1
startsecs=0
autostart=true
autorestart=true
user=www-data
priority=10
process_name=%(program_name)s_%(process_num)02d
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true

[program:maintenance]
command=bash -c 'sleep 3600 && exec php /var/www/html/bin/console pimcore:maintenance'
autostart=true
autorestart=true
user=www-data
priority=10
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true

docker/nginx/default.conf

server {
    listen 80;
    server_name pimcore.local;

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name pimcore.local;

    ssl_certificate /etc/nginx/certs/pimcore.local.crt;
    ssl_certificate_key /etc/nginx/certs/pimcore.local.key;

    root /var/www/html/public;
    index index.php;

    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
    gzip_min_length 1000;
    gzip_comp_level 4;
    gzip_vary on;
    gzip_proxied any;

    # Mercure SSE hub proxy — handles both /hub (browser SSE) and /hub/.well-known/mercure (PHP publisher)
    location /hub {
        rewrite ^/hub(.*)$ /.well-known/mercure break;
        proxy_pass http://mercure;
        proxy_read_timeout 24h;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header Connection "";
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_buffering off;
        proxy_cache off;
        chunked_transfer_encoding on;
    }

    location = /admin {
        return 302 /pimcore-studio/login;
    }

    location = /admin/ {
        return 302 /pimcore-studio/login;
    }

    # Pimcore thumbnail routing: /6/image-thumb__6__name/file.jpg
    # Studio generates URLs without the /var/tmp/thumbnails/ prefix.
    # Remap the path to the real location; fall back to PHP to generate missing thumbnails.
    location ~ "^/[0-9]+/image-thumb__[0-9]+__" {
        try_files /var/tmp/thumbnails$uri /index.php$is_args$args;
        expires max;
        log_not_found off;
    }

    location / {
        try_files $uri /index.php$is_args$args;
    }

    location ~* ^/catalog-import/ {
        client_max_body_size 600m;
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\.php(/|$) {
        client_max_body_size 600m;
        fastcgi_pass php:9000;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param HTTPS on;
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
        fastcgi_read_timeout 300;
    }

    # Block direct access to PHP files (security best practice)
    location ~ \.php$ {
        return 404;
    }

    location /pimcore-studio/public/review/ {
        alias /var/www/html/public/review/;
        autoindex on;
        try_files $uri $uri/ =404;
    }

    location ~* \.(jpg|jpeg|png|gif|webp|svg|css|js|ico|xml)$ {
        try_files $uri /index.php$is_args$args;
        expires max;
        log_not_found off;
    }
}

app/.env

Committed with safe placeholder values:

APP_ENV=dev
APP_DEBUG=true

APPLICATION_SECRET=CHANGE_ME_APPLICATION_SECRET_KEY_LONG_ENOUGH_FOR_VALIDATION

DATABASE_URL=mysql://pimcore:pimcore@db:3306/pimcore

PIMCORE_ADMIN_USER=admin
PIMCORE_ADMIN_PASSWORD=admin

PIMCORE_OPENSEARCH_DSN=opensearch://opensearch:9200

PIMCORE_MESSENGER_TRANSPORT_DSN_PREFIX=amqp://guest:guest@rabbitmq:5672/%2f/
RABBITMQ_DSN=amqp://guest:guest@rabbitmq:5672/%2f

MERCURE_JWT_KEY=CHANGE_ME_THIS_IS_MY_SECRET_KEY_THAT_IS_LONG_ENOUGH_FOR_VALIDATION
MERCURE_URL=http://localhost/hub
MERCURE_SERVER_URL=http://mercure/.well-known/mercure

app/.env.local

Created on the server only — never committed:

APP_ENV=prod
APP_DEBUG=false

# Generate with: openssl rand -hex 32
APPLICATION_SECRET=<your-random-secret>

PIMCORE_BASE_URL=https://YOUR_DOMAIN_OR_IP

# Must match MERCURE_PUBLISHER_JWT_KEY in docker-compose.yml
MERCURE_JWT_KEY=<same-key-as-docker-compose>

Fresh Instance Setup

1. Clone the repository

git clone <repo-url> pimcore-docker 
cd pimcore-docker

2. Create app/.env.local

Copy the template above and fill in real values.

3. Generate SSL certificates

mkdir -p docker/nginx/certs

openssl req \
  -x509 \
  -nodes \
  -days 3650 \
  -newkey rsa:2048 \
  -keyout docker/nginx/certs/pimcore.local.key \
  -out docker/nginx/certs/pimcore.local.crt \
  -subj "/CN=pimcore.local"

For a real domain, place your CA-signed .crt and .key in docker/nginx/certs/ and update default.conf.

4. Build and start containers

docker compose up -d --build

Wait ~30 seconds, then verify all services show Up:

docker compose ps

5. Install Composer dependencies

docker exec pimcore_php composer install --no-dev --optimize-autoloader --no-interaction -d /var/www/html

6. Install Pimcore (first run only)

docker exec pimcore_php pimcore pimcore:install --admin-username=admin --admin-password=admin --mysql-host-socket=db --mysql-database=pimcore --mysql-username=pimcore --mysql-password=pimcore --no-interaction

On subsequent deploys, run migrations instead:

docker exec pimcore_php pimcore doctrine:migrations:migrate --no-interaction

7. Install assets, warm cache, build index

docker exec -u www-data pimcore_php php /var/www/html/bin/console assets:install --symlink
docker exec -u www-data pimcore_php php /var/www/html/bin/console cache:warmup --env=prod
docker exec -u www-data pimcore_php php /var/www/html/bin/console generic-data-index:update-index-settings --all


8. Verify

  • https://YOUR_SERVER_IP/pimcore-studio/login — Pimcore admin login
  • http://YOUR_SERVER_IP:15672 — RabbitMQ UI (guest / guest)
  • http://YOUR_SERVER_IP:9201 — OpenSearch REST API

Common Commands

# View logs
docker compose logs -f

# View PHP container logs
docker compose logs -f php

# Run Pimcore console commands
docker exec pimcore_php pimcore <command>

# Shell access to PHP container
docker exec -it pimcore_php bash

# Clear cache
docker exec -u www-data pimcore_php php /var/www/html/bin/console cache:clear

# Restart PHP container/workers after a code change
docker compose restart php

# Reload Nginx without downtime
docker exec pimcore_nginx nginx -s reload

# Rebuild PHP image after Dockerfile changes
docker compose up -d --build php

# Stop all containers
docker compose down

# Wipe all persistent data (DESTRUCTIVE)
docker compose down
rm -rf docker/data/mysql
rm -rf docker/data/opensearch
rm -rf docker/data/redis

Troubleshooting

Containers won’t start — check logs:

docker compose up --build 
docker compose logs php

502 Bad Gateway — PHP may still be initialising. Check supervisor:

docker exec pimcore_php supervisorctl status

All programs should show RUNNING. If php-fpm shows FATAL:

docker exec pimcore_php supervisorctl tail php-fpm

Database refused — MySQL takes 20–30 s on first boot. Watch until “ready for connections”:

docker compose logs -f db

Permission errors on var/:

docker exec pimcore_php bash -c "chown -R www-data:www-data /var/www/html/var /var/www/html/public/var && chmod -R 775 /var/www/html/var /var/www/html/public/var"

OpenSearch index empty after import — check workers and queue:

docker exec pimcore_php supervisorctl status messenger-consume:* 
docker exec pimcore_php pimcore messenger:stats

The ./app directory is bind-mounted — PHP file edits take effect immediately. Dockerfile or supervisor config changes require docker compose up -d --build php.

. . .

Leave a Comment

Your email address will not be published. Required fields are marked*


Be the first to comment.

Back to Top

Message Sent!

If you have more details or questions, you can reply to the received confirmation email.

Back to Home