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 -vand 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/prestashopon 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
| Task | Command |
|---|---|
| Start all containers | docker compose up -d |
| Stop all containers | docker compose down |
| Stop and delete all data | docker compose down -v |
| View logs | docker compose logs -f |
| Clear PS cache | docker exec CONTAINER rm -rf /var/www/html/var/cache/* |
| MySQL CLI | docker exec -it CONTAINER-db mysql -u root -p'PASS' prestashop |
| Export database | docker exec CONTAINER-db mysqldump -u root -p'PASS' prestashop > backup.sql |
| Import database | docker exec -i CONTAINER-db mysql -u root -p'PASS' prestashop < backup.sql |
| Shell into container | docker exec -it CONTAINER bash |
| Check resource usage | docker stats --no-stream |
| Rebuild after Dockerfile changes | docker compose up -d --build |
| Run PrestaShop console | docker exec CONTAINER php bin/console cache:clear --env=prod |
| Run Composer in module | docker 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: truein/etc/docker/daemon.json. Daemon restarts stop killing running containers.- Custom
data-rooton 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_SERVERdoesn'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 needsmysql_native_password. Addcommand: --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
- PrestaShop Local Development: XAMPP, WAMP, Docker & Linux Setup — Docker against the alternatives
- Essential Tools for PrestaShop Development — what we keep alongside Docker
- How to Create a PrestaShop Staging Site — extending these patterns into staging
- Troubleshooting Guide — when the container runs but the shop still misbehaves
- Performance Optimization and Performance Revolution — next stop after "it works"
- PrestaShop's official Docker repository