Skip to content

Docker Deployment

Deploy Capyshop in production using Docker Compose with the pre-built container image and PostgreSQL with pgvector.

Prerequisites

  • Docker Engine 20+
  • Docker Compose v2+

1. Create a Docker Compose File

Create a docker-compose.yml in your deployment directory:

yaml
services:
  postgres:
    image: pgvector/pgvector:pg17
    restart: always
    environment:
      POSTGRES_USER: capyshop
      POSTGRES_PASSWORD: changeme
      POSTGRES_DB: capyshop
    volumes:
      - postgres_data:/var/lib/postgresql/data

  app:
    image: capyshop/capyshop:latest
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://capyshop:changeme@postgres:5432/capyshop
      - BETTER_AUTH_SECRET=<generate-with-openssl-rand-base64-32>
      - MASTER_SECRET=<generate-with-openssl-rand-base64-32>
      - TRUSTED_ORIGINS=https://mystore.com
      - BASE_URL=https://mystore.com
    volumes:
      - ./data:/app/data
    depends_on:
      - postgres

volumes:
  postgres_data:

Replace the placeholder values:

VariableDescription
BETTER_AUTH_SECRETSecret key for session signing. Generate with openssl rand -base64 32.
MASTER_SECRETApplication master secret for encryption. Generate with openssl rand -base64 32.
POSTGRES_USERPostgreSQL username (must match in both postgres and DATABASE_URL).
POSTGRES_PASSWORDPostgreSQL password (must match in both postgres and DATABASE_URL).
POSTGRES_DBPostgreSQL database name (must match in both postgres and DATABASE_URL).
TRUSTED_ORIGINSComma-separated list of trusted origins for CSRF protection and OAuth redirects (e.g., https://mystore.com).
BASE_URLPublic URL of the store, used in emails, SEO, sitemaps (e.g., https://mystore.com).

Optional: SMTP Configuration via Environment Variables

By default, SMTP credentials are managed through Settings → Email in the admin panel. If you prefer to configure SMTP at deploy time instead, you can set the following environment variables. When set, they override the admin-panel values and the corresponding fields in the Email settings form become disabled.

yaml
environment:
  # ...other vars
  - SMTP_HOST=smtp.sendgrid.net
  - SMTP_PORT=587
  - SMTP_USER=apikey
  - SMTP_PASSWORD=your-smtp-password
VariableDescription
SMTP_HOSTMail server hostname (e.g., smtp.sendgrid.net).
SMTP_PORTMail server port. Usually 587 for TLS or 465 for SSL. Must be an integer in [1, 65535].
SMTP_USERSMTP login username.
SMTP_PASSWORDSMTP login password. Stored in plaintext in the environment — never passed through the database encryption layer.

Each variable is independent. If only SMTP_HOST is set, the other three SMTP fields remain editable in the admin UI and are read from the database.

Optional: S3 Asset Storage

By default, uploaded images and other assets are stored on the local disk under data/files/. To push uploads to an S3-compatible bucket and serve them from a CDN, set the storage mode to s3 and configure the bucket credentials.

yaml
environment:
  # ...other vars
  - ASSETS_STORAGE_MODE=s3
  - ASSETS_S3_ENDPOINT=https://s3.amazonaws.com
  - ASSETS_S3_REGION=us-east-1
  - ASSETS_S3_BUCKET=my-store-assets
  - ASSETS_S3_ACCESS_KEY_ID=AKIA...
  - ASSETS_S3_SECRET_ACCESS_KEY=...
  - ASSETS_PUBLIC_BASE_URL=https://cdn.mystore.com
  - ASSETS_MAX_BYTES=10gb
VariableDescription
ASSETS_STORAGE_MODElocal (default) or s3. When s3, the six ASSETS_S3_* vars and ASSETS_PUBLIC_BASE_URL are required.
ASSETS_S3_ENDPOINTS3 API endpoint (e.g. https://s3.amazonaws.com, or your provider's URL for Cloudflare R2, Backblaze B2, MinIO, etc.).
ASSETS_S3_REGIONBucket region (e.g. us-east-1, auto for R2).
ASSETS_S3_BUCKETBucket name.
ASSETS_S3_ACCESS_KEY_IDAccess key with read/write permissions on the bucket.
ASSETS_S3_SECRET_ACCESS_KEYSecret access key.
ASSETS_PUBLIC_BASE_URLPublic base URL the storefront uses to load assets (e.g. your CDN domain pointed at the bucket).
ASSETS_MAX_BYTESOptional cumulative storage cap across all files. Accepts 10gb, 500mb, or a raw byte count. Applies in both local and s3 modes. Per-upload caps (5 MB image / 50 MB video) are unchanged.

When switching an existing store from local to s3, run the bundled migration script once inside a deployed container to upload the existing files to the bucket and pre-generate WebP variants:

bash
docker exec -it <container> node build/scripts/migrate-assets-to-s3.mjs

The script is idempotent — re-running it is safe, and it skips work that's already been done. If your existing originals live on the host (e.g. /docker/<store>/app_data/files/), bind-mount that path to /app/data/files inside the container so the script can pick them up; otherwise it will fall back to whatever is already in the bucket.

2. Start the Application

bash
docker compose up -d

This starts two services:

  • postgres — PostgreSQL 17 with pgvector (pgvector/pgvector:pg17), listening on port 5432.
  • app — The application container (capyshop/capyshop:latest), exposed on port 3000.

On startup, the application container automatically runs database migrations (prisma migrate deploy) before starting the server.

3. Verify

bash
# Check both containers are running
docker compose ps

# Check application logs
docker compose logs app

The application should be accessible at http://<your-host>:3000.

Reverse Proxy

For production deployments with HTTPS, place a reverse proxy (such as Traefik, Caddy, or nginx) in front of the application. The reverse proxy handles TLS termination and forwards traffic to port 3000.

Enabling Gzip Compression with Traefik

If you use Traefik as your reverse proxy, enable gzip compression to reduce response sizes and improve page load speed — a factor in search-engine ranking. Add the following labels to the app service in your docker-compose.yml:

yaml
labels:
  - "traefik.http.middlewares.compress.compress.minresponsebodybytes=256"
  - "traefik.http.routers.STORE_NAME-app.middlewares=compress"

Replace STORE_NAME with the name of your store's Traefik router. The first label defines a compress middleware that gzips every response larger than 256 bytes. The second label attaches that middleware to your store's router.

Volumes

Volume / MountPurpose
postgres_dataPersists PostgreSQL data across container restarts.
./data:/app/dataPersists uploaded files and application data across restarts.

Building from Source

If you prefer to build the image yourself instead of using the pre-built one:

bash
docker build -t capyshop .

The Dockerfile uses a multi-stage build:

  1. Installs all dependencies and builds the application.
  2. Copies only production dependencies and the build output into the final image.
  3. Runs prisma migrate deploy then starts the server on port 3000.

To use your custom image, update the image field in your docker-compose.yml, or run standalone:

bash
docker run -p 3000:3000 \
  -e DATABASE_URL=postgresql://user:pass@host:5432/db \
  -e BETTER_AUTH_SECRET=your-secret \
  -e MASTER_SECRET=your-secret \
  capyshop

Troubleshooting

ErrorCauseFix
app exits immediatelyMissing or invalid environment variablesCheck docker compose logs app and verify all required environment variables
ECONNREFUSED to postgresApp started before database was readyRestart the app: docker compose restart app
P3009 - failed migrationsA previous migration left dirty stateDROP TABLE IF EXISTS _prisma_migrations CASCADE; in the database, then restart
type "vector" does not existpgvector extension not createdThe pgvector/pgvector:pg17 image includes it, but run CREATE EXTENSION IF NOT EXISTS vector; if using a different image
Port 3000 already in useAnother process is using the portStop the conflicting process or change the port mapping in docker-compose.yml
Uploaded files lost after restart./data volume not mountedEnsure the volumes section includes ./data:/app/data