Guides Guide

Docker for PrestaShop: Development Environment Setup

Set up Docker for PrestaShop development — multi-version testing, module workflow, database management, Xdebug debugging, and production considerations.

Why we run every PrestaShop on Docker

Our entire dev and staging infrastructure at mypresta.rocks is Docker — one TrueNAS host, 20+ containers (ps178-dev, ps8-dev, ps9-dev, mypresta-rocks, plus client staging shops), a shared ps-redis, Nginx Proxy Manager out front. We've made every mistake on this page at least once.

The problem: PS 1.7 wants PHP 7.1, PS 8.x wants 8.1, PS 9.x wants 8.3+. MySQL 5.7 vs 8.0 changes the auth plugin. Installing that natively is how you spend a Saturday troubleshooting instead of writing modules. Each container ships exactly the runtime its PrestaShop expects.

  • Multi-version testing. PS 1.7, 8.x and 9.x at once. A module audit is an afternoon, not a week of rebuilding LAMP stacks.
  • Isolation. Each shop has its own PHP, MySQL, filesystem. A botched install can't bleed into another.
  • Reproducibility. Everything in docker-compose.yml. Laptop, staging, colleague's machine — same setup.
  • Easy cleanup. docker compose down -v and it's gone.
We run 20+ PrestaShop containers from 1.6 through 9.1 on a single TrueNAS box. The patterns below are what we've kept after a decade of refinement.

Prerequisites

Installing Docker

Ubuntu/Debian: curl -fsSL https://get.docker.com | sh, then sudo usermod -aG docker $USER. macOS: Docker Desktop — switch the file-sharing implementation to VirtioFS in settings. Windows: WSL2 first, then Docker Desktop with the WSL2 backend. Hyper-V mode is slower; we'd avoid it.

The four concepts that matter

  • Image: read-only template. The official one is prestashop/prestashop on Docker Hub.
  • Container: a running instance. Several from one image — that's how multi-version setups work.
  • Volume: persistent storage. No volume, no data after the container is removed. We've lost test data forgetting this.
  • Network: virtual network so containers can reach each other (PrestaShop ↔ MySQL).

Hardware

Budget 1.5–2GB RAM per PrestaShop instance. 16GB is comfortable on a developer laptop for 2–3 versions. Our dev box has 64GB and NVMe because we run 20+ containers. Spinning disks will make you hate Docker — don't try.

Single-container setup

Drop this in a docker-compose.yml:

version: '3.8'
services:
  prestashop:
    image: prestashop/prestashop:8.2
    container_name: my-ps-shop
    ports:
      - "8080:80"
    environment:
      - DB_SERVER=db
      - DB_USER=prestashop
      - DB_PASSWD=prestashop_password
      - DB_NAME=prestashop
      - PS_DOMAIN=localhost:8080
      - PS_FOLDER_ADMIN=admin-dev
      - PS_FOLDER_INSTALL=disabled
      - ADMIN_MAIL=admin@yourshop.com
      - ADMIN_PASSWD=admin_password_123
    volumes:
      - ps-data:/var/www/html
    depends_on:
      - db

  db:
    image: mysql:8.0
    container_name: my-ps-shop-db
    environment:
      - MYSQL_ROOT_PASSWORD=root_password
      - MYSQL_DATABASE=prestashop
      - MYSQL_USER=prestashop
      - MYSQL_PASSWORD=prestashop_password
    volumes:
      - db-data:/var/lib/mysql

volumes:
  ps-data:
  db-data:

DB_SERVER has to match the service name (db) exactly — single most common reason a fresh compose fails. PS_FOLDER_INSTALL=disabled stops the installer rerunning on every restart. docker compose up -d, then watch with docker compose logs -f prestashop; allow 1–2 minutes the first time.

Store at http://localhost:8080, Back Office at http://localhost:8080/admin-dev.

Always use named volumes for the database. Without one, removing the container destroys every product, order and customer. We've lost a half-day of test data forgetting this on a "quick" disposable container.

Multi-version testing

This is the case Docker was built for. Three PrestaShop versions side by side, each on its own port and database:

version: '3.8'
services:
  ps178:
    image: prestashop/prestashop:1.7.8
    ports: ["8081:80"]
    environment:
      - DB_SERVER=ps178-db
      - PS_DOMAIN=localhost:8081
      # ... same pattern as above
    volumes:
      - ps178-data:/var/www/html
    networks: [ps-network]

  ps178-db:
    image: mysql:5.7
    volumes: [ps178-db-data:/var/lib/mysql]
    networks: [ps-network]

  ps82:
    image: prestashop/prestashop:8.2
    ports: ["8082:80"]
    environment:
      - DB_SERVER=ps82-db
      - PS_DOMAIN=localhost:8082
    volumes:
      - ps82-data:/var/www/html
    networks: [ps-network]

  ps82-db:
    image: mysql:8.0
    command: --default-authentication-plugin=mysql_native_password
    volumes: [ps82-db-data:/var/lib/mysql]
    networks: [ps-network]

  ps9:
    image: prestashop/prestashop:9.0
    ports: ["8083:80"]
    environment:
      - DB_SERVER=ps9-db
      - PS_DOMAIN=localhost:8083
    volumes:
      - ps9-data:/var/www/html
    networks: [ps-network]

  ps9-db:
    image: mysql:8.0
    command: --default-authentication-plugin=mysql_native_password
    volumes: [ps9-db-data:/var/lib/mysql]
    networks: [ps-network]

networks:
  ps-network:
    driver: bridge

Pick a port scheme — ours is 8178 for PS 1.7.8, 8082 for 8.2, 8090 for 9.0. Past ten containers, port numbers stop being memorable; that's when we put Nginx Proxy Manager in front and gave each shop a hostname like ps8-dev.mypresta.rocks. Do this on day one if you're going past a handful.

Always separate databases. Schema diverges between majors; migrations will corrupt each other.

Warning: docker network connect / disconnect on a running container rewrites the port mapping. We've watched it silently kill port 443 on our NPM container, taking every site down at once. Define the network in compose and recreate the container if you need to change it.

Module development workflow

Bind-mount your module directory — every edit immediately live. Biggest single win Docker gives you for module work:

volumes:
  - ps82-data:/var/www/html
  - /home/user/modules/my_module:/var/www/html/modules/my_module

For multi-version testing, mount the same host directory into every container. Edit one file, refresh three tabs. We mount every ~/modules/mpr* into all of ps178-dev, ps8-dev and ps9-dev for exactly this.

Permissions: the recurring trap

Host files are owned by you (UID 1000); Apache runs as www-data (UID 33). PrestaShop tries to write to var/cache, img, modules and gets denied — that's why module uploads fail on fresh setups.

# Interactive MySQL session
docker exec -it ps82-db mysql -u root -p'root' prestashop

# Run a single query
docker exec ps82-db mysql -u root -p'root' -e "SELECT COUNT(*) FROM ps_product;" prestashop

Keep a fix-container-perms.sh. We run ours more than we'd like, especially after rsync'ing modules in.

Cache

docker exec ps82 rm -rf /var/www/html/var/cache/*

Or disable the template cache in Advanced Parameters → Performance during active work. Re-enable before you call the work done — cache-related bugs only show up with cache on.

Database management

  phpmyadmin:
    image: phpmyadmin:latest
    ports: ["9090:80"]
    environment:
      - PMA_HOSTS=ps178-db,ps82-db,ps9-db
      - PMA_USER=root
      - PMA_PASSWORD=root
    networks: [ps-network]

phpMyAdmin

One container serves every database on the network:

# Export
docker exec ps82-db mysqldump -u root -p'root' prestashop > backup.sql

# Import
docker exec -i ps82-db mysql -u root -p'root' prestashop < backup.sql

Import and export

  mailpit:
    image: axllent/mailpit
    ports:
      - "8025:8025"  # Web UI
      - "1025:1025"  # SMTP
    networks: [ps-network]

Two production gotchas: pipe through gunzip for compressed dumps, and always pass SET NAMES utf8mb4 at the start of any restore via docker exec — otherwise UTF-8 product names land double-encoded and the fix is uglier than the prevention.

Persistent or ephemeral

Persistent (named volumes): anything you'd be sad to lose. Ephemeral: installer tests. We keep an ephemeral PS 9.0 container for "does this module install cleanly on a blank store?" — fresh every run.

Email testing with Mailpit

Mailpit catches every outgoing email and shows it in a web UI:

FROM prestashop/prestashop:8.2
RUN pecl install xdebug && docker-php-ext-enable xdebug
COPY xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini

Back Office → Advanced Parameters → E-mail: SMTP mailpit, port 1025, no encryption, no auth. Caught mail at http://localhost:8025. One shared Mailpit serves every dev shop on our network.

Debugging with Xdebug

The official image doesn't ship Xdebug. Build a thin wrapper:

[xdebug]
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.idekey=VSCODE
{
  "version": "0.2.0",
  "configurations": [{
    "name": "Listen for Xdebug (Docker)",
    "type": "php",
    "request": "launch",
    "port": 9003,
    "pathMappings": {
      "/var/www/html/modules/my_module": "${workspaceFolder}"
    }
  }]
}

On Linux, host.docker.internal doesn't exist by default — add extra_hosts: ["host.docker.internal:host-gateway"]. We've lost half an hour to that one.

VS Code

Install PHP Debug and add to .vscode/launch.json:

docker exec my-ps-shop chown -R www-data:www-data /var/www/html/var
docker exec my-ps-shop chown -R www-data:www-data /var/www/html/img
docker exec my-ps-shop chown -R www-data:www-data /var/www/html/modules

pathMappings is what makes the breakpoint fire. Without it VS Code silently does nothing.

PHPStorm

Settings → PHP → Servers: localhost + container port, map /var/www/html to the project. Xdebug port 9003 under PHP → Debug, "Start Listening."

Useful commands

TaskCommand
Start all containersdocker compose up -d
Stop all containersdocker compose down
Stop and delete all datadocker compose down -v
View logsdocker compose logs -f
Clear PS cachedocker exec CONTAINER rm -rf /var/www/html/var/cache/*
MySQL CLIdocker exec -it CONTAINER-db mysql -u root -p'PASS' prestashop
Export databasedocker exec CONTAINER-db mysqldump -u root -p'PASS' prestashop > backup.sql
Import databasedocker exec -i CONTAINER-db mysql -u root -p'PASS' prestashop < backup.sql
Shell into containerdocker exec -it CONTAINER bash
Check resource usagedocker stats --no-stream
Rebuild after Dockerfile changesdocker compose up -d --build
Run PrestaShop consoledocker exec CONTAINER php bin/console cache:clear --env=prod
Run Composer in moduledocker exec -w /var/www/html/modules/my_module CONTAINER composer install

Production: our honest opinion

For most merchants, Docker is a dev tool. Production usually runs better on traditional hosting — a single LAMP stack is easier to monitor and hand off, native file I/O beats Docker volumes (PrestaShop loads thousands of PHP files per request), and most PrestaShop-friendly hosts give you cPanel, not a Docker daemon.

Docker in production earns its keep with CI/CD, sales-event capacity bursts, ten or more stores, or zero-downtime deployments.

Compose vs Kubernetes vs Swarm. Compose handles 20–30 containers on one host — what we run, plenty. Kubernetes adds auto-scaling and multi-node orchestration at the cost of a real infra team; for PrestaShop we've yet to see a case where it was the right call. Docker Swarm is an awkward middle ground we'd skip — community moved on. And kompose (the "convert compose to k8s" tool) sounds great until you try it and find the output needs as much hand-editing as writing it fresh.

Two host settings worth setting

  • live-restore: true in /etc/docker/daemon.json. Daemon restarts stop killing running containers.
  • Custom data-root on a fast SSD pool, not the OS disk. Images and MySQL volumes grow; system disks fill quietly.

Things that go wrong

"Can't connect to database"

  • DB_SERVER doesn't match the MySQL service name (identical, dashes and all).
  • MySQL hasn't finished starting. Use a healthcheck and depends_on: condition: service_healthy.
  • Containers on different networks — put both on a named bridge.
  • MySQL 8.0 defaults to caching_sha2_password; PrestaShop's mysqli needs mysql_native_password. Add command: --default-authentication-plugin=mysql_native_password.

Slow I/O on macOS

Bind mounts on macOS used to be glacial (10–30s per page load). Three fixes: VirtioFS in Docker Desktop settings (biggest single win), mount less (only the module dir, rest on a named volume), Mutagen (two-way file sync that sidesteps the mount layer — sledgehammer, works).

SSL in development

Payment modules and social login refuse plain HTTP. Nginx Proxy Manager terminates SSL for every container — what we use, cleanest option. mkcert for locally-trusted certs. Traefik for auto-discovery and provisioning.

MySQL eating all your RAM

command: >
  --innodb-buffer-pool-size=128M
  --max-connections=50

On Docker Desktop, raise the Settings → Resources memory limit to at least 6GB once you have 3+ containers. Stop ones you aren't using: docker compose stop ps178.

Reinstall loops

If PrestaShop reinstalls every restart, the /var/www/html volume isn't persisting or PS_FOLDER_INSTALL=disabled isn't being read. Check docker volume ls shows a real (not anonymous) volume.

Mixed content after a port change

docker exec ps82-db mysql -u root -p'root' -e "
  UPDATE ps_configuration SET value='localhost:8082'
    WHERE name IN ('PS_SHOP_DOMAIN','PS_SHOP_DOMAIN_SSL');
  UPDATE ps_shop_url SET domain='localhost:8082', domain_ssl='localhost:8082';
" prestashop

One warning: never run that query on a live client shop without a backup. We've watched it brick a production frontend mid-afternoon.

Related reading

Loading...
Back to top