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 loginhttp://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.
Be the first to comment.