Back to Top

Setup for PimCore Development Environment

Updated 10 June 2026

Docker-based environment for a fresh Pimcore installation.

Directory Structure

  • docker-compose.yml — all service definitions
  • docker/ — Docker build context and config files
    • php/
    • Dockerfile — custom PHP 8.4-FPM image
    • pimcore-supervisor.conf — Supervisord worker definitions
    • nginx/
    • default.conf — Pimcore HTTPS vhost (port 443)
    • certs/ — SSL certificates (pimcore.local.crt / .key)
    • php.ini — PHP runtime configuration
    • data/ — persistent volume data (git-ignored)
    • mysql/ — MySQL database files
    • redis/ — Redis AOF/RDB snapshots
    • opensearch/ — OpenSearch index data
  • app/ — Pimcore application source (Symfony)
    • .env — default environment variables (committed)
    • .env.local — local overrides (not committed)
    • .docker/
    • supervisord.conf — optional Supervisord override

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

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/*

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

RUN pecl install redis && docker-php-ext-enable redis
RUN pecl install amqp && docker-php-ext-enable amqp

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY php.ini /usr/local/etc/php/php.ini
COPY php/pimcore-supervisor.conf /etc/supervisor/conf.d/pimcore.conf

WORKDIR /var/www/html

RUN printf '#!/bin/shnexec su -s /bin/sh www-data -c "php /var/www/html/bin/console $*"n' 
    > /usr/local/bin/pimcore && chmod +x /usr/local/bin/pimcore

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

[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=512M --time-limit=3600
numprocs=3
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=512M --time-limit=3600
numprocs=1
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=512M --time-limit=3600
numprocs=1
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 _;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl default_server;
    server_name _;

    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;

    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_buffering off;
        proxy_cache off;
        chunked_transfer_encoding on;
    }

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

    location ~ "image-thumb__[0-9]+__" {
        try_files /var/tmp/thumbnails$uri /index.php$is_args$args;
        expires max;
        add_header Cache-Control "public, immutable";
        log_not_found off;
    }

    location / {
        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;
    }

    location ~ .php$ { return 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

# Logs
docker compose logs -f
docker compose logs -f php

# Console commands (as www-data)
docker exec pimcore_php pimcore <command>

# Shell access
docker exec -it pimcore_php bash

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

# Restart 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 everything
docker compose down

# Wipe all data (destructive)
docker compose down && rm -rf docker/data/mysql docker/data/opensearch 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