Self-Hosting Saleor with Docker Compose
What Is Saleor?
Saleor is an open-source headless e-commerce platform built with Python and GraphQL. “Headless” means it provides the backend (products, orders, payments, inventory) through an API, while you build or choose your own frontend storefront. Think of it as a self-hosted alternative to Shopify’s backend, minus the built-in themes — you get an admin dashboard and a GraphQL API, then connect whatever frontend you want.
Important: Saleor Is Headless
Before you dive in, understand what “headless” means for self-hosters:
| Component | Included? | Notes |
|---|---|---|
| Admin dashboard | Yes | Manage products, orders, customers |
| GraphQL API | Yes | Full commerce API |
| Customer storefront | No | You need to build or deploy one separately |
| Payment processing | Plugin-based | Stripe, Braintree, Adyen via apps |
| Email templates | Basic | Customizable via dashboard |
If you want a traditional all-in-one e-commerce platform with a built-in storefront, look at WooCommerce or PrestaShop instead. Saleor is for teams that want API-first commerce with full frontend control.
Prerequisites
- A Linux server (Ubuntu 22.04+ recommended)
- Docker and Docker Compose installed (guide)
- 4 GB of free RAM minimum (Saleor recommends 5 GB for Docker)
- 15 GB of free disk space
- A domain name (recommended for the API and dashboard)
Docker Compose Configuration
Saleor’s production stack needs five services: the API server, a Celery worker for background tasks, PostgreSQL, Valkey (Redis-compatible cache), and the admin dashboard.
Create a docker-compose.yml:
services:
api:
image: ghcr.io/saleor/saleor:3.22.41
container_name: saleor-api
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
environment:
DATABASE_URL: "postgres://saleor:${DB_PASSWORD:-changeme_strong_password}@db:5432/saleor"
CACHE_URL: "redis://cache:6379/0"
CELERY_BROKER_URL: "redis://cache:6379/1"
SECRET_KEY: "${SECRET_KEY:-changeme_generate_with_openssl}" # CHANGE — openssl rand -hex 32
ALLOWED_HOSTS: "localhost,127.0.0.1,api,${API_DOMAIN:-localhost}"
DASHBOARD_URL: "${DASHBOARD_URL:-http://localhost:9000/}"
DEFAULT_FROM_EMAIL: "[email protected]"
EMAIL_URL: "smtp://mail.example.com:587/[email protected]&password=smtp-password&ssl=true"
DEFAULT_CHANNEL_SLUG: "default-channel"
volumes:
- saleor-media:/app/media # Product images and uploads
ports:
- "8000:8000" # GraphQL API
networks:
- saleor-backend
restart: unless-stopped
worker:
image: ghcr.io/saleor/saleor:3.22.41
container_name: saleor-worker
command: celery -A saleor --app=saleor.celeryconf:app worker --loglevel=info -B
depends_on:
- db
- cache
environment:
DATABASE_URL: "postgres://saleor:${DB_PASSWORD:-changeme_strong_password}@db:5432/saleor"
CACHE_URL: "redis://cache:6379/0"
CELERY_BROKER_URL: "redis://cache:6379/1"
SECRET_KEY: "${SECRET_KEY:-changeme_generate_with_openssl}"
DEFAULT_FROM_EMAIL: "[email protected]"
EMAIL_URL: "smtp://mail.example.com:587/[email protected]&password=smtp-password&ssl=true"
volumes:
- saleor-media:/app/media
networks:
- saleor-backend
restart: unless-stopped
dashboard:
image: ghcr.io/saleor/saleor-dashboard:3.22.35
container_name: saleor-dashboard
ports:
- "9000:80" # Admin dashboard
restart: unless-stopped
db:
image: postgres:15-alpine
container_name: saleor-db
environment:
POSTGRES_USER: saleor
POSTGRES_PASSWORD: "${DB_PASSWORD:-changeme_strong_password}" # CHANGE
POSTGRES_DB: saleor
volumes:
- saleor-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U saleor"]
interval: 10s
timeout: 5s
retries: 10
networks:
- saleor-backend
restart: unless-stopped
cache:
image: valkey/valkey:8.1-alpine
container_name: saleor-cache
volumes:
- saleor-cache:/data
networks:
- saleor-backend
restart: unless-stopped
volumes:
saleor-db:
saleor-cache:
saleor-media:
networks:
saleor-backend:
driver: bridge
Create a .env file:
# Generate before first start:
# openssl rand -hex 32
SECRET_KEY=your_generated_secret_here
# Database password
# openssl rand -hex 16
DB_PASSWORD=your_database_password_here
# Your domains (for production)
API_DOMAIN=api.store.example.com
DASHBOARD_URL=https://admin.store.example.com/
Start the stack:
docker compose up -d
First startup takes 1-2 minutes for database migrations. Monitor:
docker logs -f saleor-api
Initial Setup
- Open the dashboard at
http://your-server-ip:9000 - The dashboard will ask for the API URL — enter
http://your-server-ip:8000/graphql/ - Create your admin account through the API first:
docker exec -it saleor-api python manage.py createsuperuser
- Log in to the dashboard with your admin credentials
- Set up your first sales channel, warehouse, and shipping zone
First Steps in the Dashboard
| Step | Where | What to Configure |
|---|---|---|
| 1 | Configuration → Channels | Create a sales channel (currency, country) |
| 2 | Configuration → Warehouses | Add a warehouse with shipping zones |
| 3 | Configuration → Shipping | Define shipping methods and rates |
| 4 | Catalog → Products | Add your first product with variants |
| 5 | Configuration → Taxes | Set up tax configuration |
Building a Storefront
Saleor provides starter storefronts you can deploy alongside the API:
React Storefront (official):
npx degit saleor/storefront my-store
cd my-store
npm install
# Set SALEOR_API_URL=http://your-server-ip:8000/graphql/
npm run dev
The storefront connects to your Saleor API via GraphQL. Any frontend framework works — Next.js, Nuxt, Remix, or even a mobile app.
Reverse Proxy
For production, you need HTTPS on both the API and dashboard. Caddy example:
api.store.example.com {
reverse_proxy localhost:8000
}
admin.store.example.com {
reverse_proxy localhost:9000
}
store.example.com {
reverse_proxy localhost:3000 # Your storefront
}
Update ALLOWED_HOSTS and DASHBOARD_URL in your environment to match:
ALLOWED_HOSTS=localhost,api,api.store.example.com
DASHBOARD_URL=https://admin.store.example.com/
See our Reverse Proxy Guide for Nginx Proxy Manager and Traefik configurations.
Backup
Back up the database and media files:
# Database
docker exec saleor-db pg_dump -U saleor saleor > saleor-backup-$(date +%Y%m%d).sql
# Media (product images, uploads)
docker run --rm -v saleor-media:/data -v $(pwd):/backup \
alpine tar czf /backup/saleor-media-$(date +%Y%m%d).tar.gz /data
See our Backup Strategy Guide for automated approaches.
Troubleshooting
Dashboard Can’t Connect to API
Symptom: Dashboard shows “Could not connect to the API” after setup.
Fix: The dashboard is a static React app served by Nginx. It connects to the API from the user’s browser, not from the server. The API URL must be reachable from the user’s machine, not just from the Docker network:
- If accessing locally:
http://localhost:8000/graphql/ - If accessing remotely:
https://api.store.example.com/graphql/ - Add the correct host to
ALLOWED_HOSTS
Worker Not Processing Orders
Symptom: Orders stuck in processing, emails not sent, webhooks not firing.
Fix: Check the Celery worker:
docker logs saleor-worker
Common issues: Redis/Valkey not running, or CELERY_BROKER_URL doesn’t match the cache service. The worker uses Redis database index 1 (/1), while the API cache uses index 0 (/0).
Media Files 404
Symptom: Product images return 404 errors.
Fix: The saleor-media volume must be shared between the api and worker services (already configured in the compose file above). If using object storage (S3/MinIO), configure:
environment:
AWS_MEDIA_BUCKET_NAME: "saleor-media"
AWS_STORAGE_BUCKET_NAME: "saleor-static"
AWS_S3_ENDPOINT_URL: "http://minio:9000"
Database Migration Failures
Symptom: API container restarts with migration errors.
Fix: Run migrations manually:
docker exec -it saleor-api python manage.py migrate
If upgrading between Saleor versions, always read the release notes — some upgrades require data migration steps.
Resource Requirements
| Metric | Value |
|---|---|
| RAM (idle) | ~800 MB (API + worker + PostgreSQL + Valkey + dashboard) |
| RAM (active, with traffic) | 2-4 GB |
| CPU | Medium-High (Python uvicorn + Celery workers) |
| Disk | ~1.5 GB for application + media storage growth |
Verdict
Saleor is the most feature-complete self-hosted headless e-commerce platform available. The GraphQL API is well-designed, the admin dashboard is polished, and the multi-channel/multi-warehouse support is genuinely enterprise-grade. If you’re building a custom storefront and want full control over the commerce backend, Saleor is the right choice.
The trade-off is complexity. Five Docker services, a separate storefront deployment, and Python/Celery to maintain — this is not a weekend project. If you want something simpler with a built-in storefront, self-host WordPress with WooCommerce instead. If you want headless but prefer Node.js, look at Medusa.
Related
Get self-hosting tips in your inbox
Get the Docker Compose configs, hardware picks, and setup shortcuts we don't put in articles. Weekly. No spam.
Comments