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:

ComponentIncluded?Notes
Admin dashboardYesManage products, orders, customers
GraphQL APIYesFull commerce API
Customer storefrontNoYou need to build or deploy one separately
Payment processingPlugin-basedStripe, Braintree, Adyen via apps
Email templatesBasicCustomizable 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

  1. Open the dashboard at http://your-server-ip:9000
  2. The dashboard will ask for the API URL — enter http://your-server-ip:8000/graphql/
  3. Create your admin account through the API first:
docker exec -it saleor-api python manage.py createsuperuser
  1. Log in to the dashboard with your admin credentials
  2. Set up your first sales channel, warehouse, and shipping zone

First Steps in the Dashboard

StepWhereWhat to Configure
1Configuration → ChannelsCreate a sales channel (currency, country)
2Configuration → WarehousesAdd a warehouse with shipping zones
3Configuration → ShippingDefine shipping methods and rates
4Catalog → ProductsAdd your first product with variants
5Configuration → TaxesSet 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

MetricValue
RAM (idle)~800 MB (API + worker + PostgreSQL + Valkey + dashboard)
RAM (active, with traffic)2-4 GB
CPUMedium-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.

Comments