Search by

fyrst / shopware-cd

robjke

Thin Packagist library for fyrst.dev Shopware continuous deploy. Provides fyrst:sales-channel:rewrite-urls (sales_channel_domain.url after a non-live DB restore). Overlay files (CI, deploy Compose, .env.example) ship in overlay/ for Symfony Flex copy-from-package. The Flex recipe (fyrst-dev/recipes)

1.1.0 2026-09-14 15:38 UTC

README

Packagist library for fyrst.dev Shopware continuous deploy: the PHP rewrite command.

This repository is not a Shopware installation. It does not vendor Shopware core. The Packagist package fyrst/shopware-cd is a thin library that ships bin/console fyrst:sales-channel:rewrite-urls (rewrite sales_channel_domain.url after a non-live DB restore). It does not own deploy, sync, backup, or env-init pipeline logic.

Operator path: fyrst-cli 0.1.0+ only. Install it on each VPS and laptop. Recipe deploy/*.sh wrappers are removed — CI and operators call fyrst-cli shopware … only. Dump stays shopware-cli project dump forever — fyrst-cli never dumps.

Shop overlay files live in this package under overlay/. Symfony Flex copy-from-package copies CI (.github/workflows/cd.yaml, .gitlab-ci.yaml), .dockerignore, .env.example, and deploy/ (CD Compose: deploy/compose.yaml, deploy/compose.prod.yaml, deploy/compose.vps.yaml, plus edge/managed docs) from vendor/fyrst/shopware-cd/overlay/ into the shop. It does not copy deploy/*.sh wrappers, and it does not copy compose.yaml, .gitignore, or .shopware-project.yamlshopware-cli project create owns those (create writes .shopware-project.yml; .yaml is also accepted — do not rename). Flex env may append a ###> fyrst/shopware-cd ### SoT block to .env (empty shop id; no secrets). It does not overwrite create’s whole .env. Then run fyrst-cli shopware env init --shop-id <slug>. The image build file is always shopware/docker’s docker/Dockerfile — shops must composer require shopware/docker on the same line as this package. Each VPS and laptop needs fyrst-cli 0.1.0+ on PATH. The Flex recipe (fyrst-dev/recipes, later recipes-contrib) is metadata only (copy-from-package + bundles + env + post-install toast) — not a second overlay source.

Process (locked standard): Shopware Create & Continuous Deploy

Build once, run everywhere. The image is identical for every target. The supported last mile is Docker Compose on a VPS. A managed container host is planned / not implemented.

There is no git submodule, no custom fyrst-shopware-cd CLI in this package, no Composer plugin that copies files, and no Composer dependency on fyrst-dev/recipes. Operators install fyrst-cli from fyrst-dev/cli (0.1.0+), not from this repo.

Architecture

Piece Role
This repo (fyrst-dev/shopware-cd) Packagist package fyrst/shopware-cd: thin library + Symfony bundle (fyrst:sales-channel:rewrite-urls) + overlay/ (Flex copy-from-package → shop root). No deploy/sync/backup pipeline.
fyrst-cli 0.1.0+ Operator CLI: shopware env init, deploy {release|rollback}, sync {capture|apply|pull|local}, backup {create|prune|recover}, db import. Never dumps.
shopware-cli project create owns compose.yaml, .gitignore, .shopware-project.yml (create’s default; .yaml also accepted — do not rename), and local Docker (shopware-cli project dev). project dump is the only dump. The fyrst Flex recipe does not copy those create files.
fyrst-dev/recipes Flex metadata only: copy-from-package of this package’s overlay/ + existing bundles + env + post-install toast. Compiles flex/main/index.json. Not a second overlay source. Does not ship deploy/*.sh wrappers.

Flex maps the Packagist package name fyrst/shopware-cd to a recipe only via the shop’s extra.symfony.endpoint list — not via a require of the recipes repo.

Primary path (only)

Configure the Flex endpoint before composer require. Shopware already writes extra.symfony.endpoint; replace it so fyrst is first, then Shopware, then defaults:

{
    "extra": {
        "symfony": {
            "allow-contrib": true,
            "endpoint": [
                "https://raw.githubusercontent.com/fyrst-dev/recipes/flex/main/index.json",
                "https://raw.githubusercontent.com/shopware/recipes/flex/main/index.json",
                "flex://defaults"
            ]
        }
    }
}
shopware-cli project create <shop-name>
# or: npx @shopware-ag/shopware-cli project create <shop-name>
# optional version pin: shopware-cli project create <shop-name> 6.6.x.x
cd <shop-name>
composer config extra.symfony.allow-contrib true
composer config --json extra.symfony.endpoint '["https://raw.githubusercontent.com/fyrst-dev/recipes/flex/main/index.json","https://raw.githubusercontent.com/shopware/recipes/flex/main/index.json","flex://defaults"]'
composer require shopware/docker shopware/deployment-helper fyrst/shopware-cd
# each VPS / laptop needs fyrst-cli 0.1.0+
curl -fsSL https://raw.githubusercontent.com/fyrst-dev/cli/main/scripts/install.sh | bash
# pin: FYRST_CLI_VERSION=0.1.0
fyrst-cli shopware env init --shop-id <slug>
# VPS: fyrst-cli shopware env init --shop-id <slug> --vps

Run Composer inside Docker when the web container is up (docker compose exec web composer …). Host PHP is often under-provisioned.

Flex then:

  • shopware/dockerrequired. Flex copies official docker/Dockerfile. Image builds always use that file. CI DOCKERFILE=docker/Dockerfile (or default to that). The fyrst recipe does not ship a shop-root Dockerfile.
  • shopware/deployment-helper → install/update at deploy time
  • fyrst/shopware-cd.github/workflows/cd.yaml, .gitlab-ci.yaml, .dockerignore, .env.example, deploy/ including deploy/compose.yaml, deploy/compose.prod.yaml, deploy/compose.vps.yaml — from this package’s overlay/ via Flex copy-from-package. Does not copy deploy/*.sh wrappers. Does not copy compose.yaml, .gitignore, or .shopware-project.yaml / .shopware-project.yml. Flex env appends a ###> fyrst/shopware-cd ### block (safe defaults). Finish values with fyrst-cli shopware env init --shop-id <slug>.

Wizard defaults for fyrst: current stable Shopware, Docker = yes.

Commit the files Flex copied. vendor/ is gitignored; CI and the VPS checkout need those paths in git.

If a file already exists, Flex skips or prompts. Flex env appends to .env; it does not overwrite create’s whole .env. Shop-specific local Compose tweaks belong in compose.override.yaml next to the CLI-owned root compose.yaml.

Without the fyrst-dev/recipes endpoint, Flex copies no overlay files. The rewrite command still ships in this package; Flex still needs to register FyrstShopwareCdBundle (see Sales-channel URL rewrite).

How to change the overlay

Overlay files live in this repo under overlay/. Flex copy-from-package maps overlay/ → shop root.

  1. Change files under overlay/ here. Do not add compose.yaml, .gitignore, or .shopware-project.yaml / .shopware-project.yml — those stay CLI-owned. Do not add deploy/*.sh.
  2. Merge to main and wait for Packagist (or point shops at dev-main).
  3. In each shop:
composer update fyrst/shopware-cd
composer recipes:update fyrst/shopware-cd

The Flex recipe (fyrst-dev/recipes, later recipes-contrib) must stay a thin copy-from-package of overlay/. Do not keep a second root/ copy in the recipe. See the recipes README for endpoint details.

Locked standard (do not fork locally)

Topic Decision
Create shopware-cli project create / npx @shopware-ag/shopware-cli
CLI-owned files compose.yaml, .gitignore, .shopware-project.yml (create’s default; .yaml also accepted — do not rename) and local Docker via CLI. Flex does not copy these.
Overlay extra.symfony.endpoint (fyrst-dev/recipes first) then composer require shopware/docker shopware/deployment-helper fyrst/shopware-cd + Symfony Flex
Overlay files Shipped in this package’s overlay/. Flex copies CI (.github/workflows/cd.yaml, .gitlab-ci.yaml), .dockerignore, .env.example, deploy/ (deploy/compose.yaml, deploy/compose.prod.yaml, deploy/compose.vps.yaml). No deploy/*.sh wrappers.
Operator CLI fyrst-cli 0.1.0+ on each VPS / laptop. Call fyrst-cli shopware … only. Dump stays shopware-cli project dump.
Local Docker via shopware-cli project dev (CLI-managed root compose.yaml). Live media/files: fyrst-cli shopware sync local (rsync path remap into the project tree; remote auto /var/lib/shopware/data/${SHOPWARE_SHOP_ID}/live — not VPS sync {capture|apply|pull}; fyrst-cli refuses --data all on sync local — use the default volume list)
Runtime App always runs in Docker (shopware/docker / ghcr.io/shopware/docker-base)
Image Always docker/Dockerfile from required shopware/docker. CI DOCKERFILE=docker/Dockerfile (or default to that).
Build shopware-cli project ci inside that multi-stage docker/Dockerfile
CI GitHub Actions and GitLab CI, same stages
Identity (hybrid) Required SoT: SHOPWARE_SHOP_ID + SHOPWARE_DEPLOY_ENV. Optional SHOPWARE_DATA_BASE. COMPOSE_PROJECT_NAME and SHOPWARE_DATA_ROOT are optional — Docker Compose and sync derive COMPOSE_PROJECT_NAME=${SHOPWARE_SHOP_ID}-${SHOPWARE_DEPLOY_ENV} and SHOPWARE_DATA_ROOT=${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}. Compose uses those SoT vars directly. No hardcoded Compose project name shopware.
Primary deploy Docker Compose on a VPS using deploy/compose.yaml + deploy/compose.prod.yaml + deploy/compose.vps.yaml (not root compose.yaml)
Runtime data Out of git and out of the image. VPS bind mounts under the derived shop/env root ({files,media,thumbnail,theme,sitemap}). Named volumes remain only for mysql_data / redis_data (scoped by the derived Compose project name). VPS: fyrst-cli shopware sync {capture|apply|pull} live → staging/playground/dev (SSH + shopware-cli project dump + rsync of those host dirs; no S3; cron on the consumer; paths from shop id + deploy env). Local shopware-cli project dev: fyrst-cli shopware sync local (rsync path remap into files/ and public/{media,thumbnail,theme,sitemap}; remote default /var/lib/shopware/data/${SHOPWARE_SHOP_ID}/live)
Managed deploy Planned / not implemented (same image contract; no CI job). Compose/VPS is the only supported last mile.
Deploy-time tasks vendor/bin/shopware-deployment-helper run --skip-theme-compile --skip-assets-install

Out of scope: bare Deployer/SSH without containers, Shopware PaaS as default, compiling assets on the production host, git submodules, custom create wrappers, S3 as the default VPS env-to-env copy.

Identity (locked hybrid)

Several shops, and live + staging of the same shop, can share one VPS. Isolate them with shop id + deploy env — not a single global data directory and not a hardcoded Compose project name shopware.

Shop-root files only. fyrst-cli loads process env, then .env, then .env.local if present (laptop SSH / extras), then .env.prod if present (VPS). Later file wins. Identity keys (SHOPWARE_SHOP_ID, SHOPWARE_DEPLOY_ENV) cannot be overridden by later files. CI VPS_* / SSH_PRIVATE_KEY stay GitHub/GitLab secrets — they are not shop .env.

Required source of truth in shop-root .env / .env.local / .env.prod:

Variable Meaning Example
SHOPWARE_SHOP_ID Stable shop slug (same on live, staging, and laptop) acme
SHOPWARE_DEPLOY_ENV This stack’s role live / staging / playground / …

Optional (Compose and sync derive these when unset):

Variable Meaning When unset
SHOPWARE_DATA_BASE Host prefix for bind-mount trees /var/lib/shopware/data
COMPOSE_PROJECT_NAME Docker project name override ${SHOPWARE_SHOP_ID}-${SHOPWARE_DEPLOY_ENV}acme-live
SHOPWARE_DATA_ROOT Bind-mount root override ${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}

COMPOSE_PROJECT_NAME and SHOPWARE_DATA_ROOT are optional. deploy/compose.yaml interpolates SHOPWARE_SHOP_ID, SHOPWARE_DEPLOY_ENV, and SHOPWARE_DATA_BASE directly (project name + bind-mount paths). You do not set expanded COMPOSE_PROJECT_NAME / SHOPWARE_DATA_ROOT for Compose to work.

VPS warning: shopware-cli project create writes COMPOSE_PROJECT_NAME=sw-shop-… into shop-root .env for local project dev. That env var overrides Compose name: (${SHOPWARE_SHOP_ID}-${SHOPWARE_DEPLOY_ENV}). On the VPS, comment out that line with fyrst-cli shopware env init --vps (or by hand). Flex does not delete it on composer require (create owns the local flow).

After create + composer require, finish shop-specific .env values with fyrst-cli shopware env init:

fyrst-cli shopware env init --shop-id acme
# VPS: add --vps --env live --image ghcr.io/example/acme

--shop-id is required unless already non-empty. fyrst-cli merges missing keys from .env.example and does not invent MYSQL passwords or APP_URL. See deploy/README.md after Flex copies it, and fyrst-cli. Same loader for every fyrst-cli shopware verb.

Formula:

COMPOSE_PROJECT_NAME=${SHOPWARE_SHOP_ID}-${SHOPWARE_DEPLOY_ENV}
SHOPWARE_DATA_ROOT=${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}
# /var/lib/shopware/data/acme/live/{files,media,thumbnail,theme,sitemap}
# /var/lib/shopware/data/acme/staging/…

Set SHOPWARE_SHOP_ID and SHOPWARE_DEPLOY_ENV in shop-root .env / .env.local / .env.prod. Set SHOPWARE_DATA_BASE only when the prefix is not /var/lib/shopware/data. fyrst-cli shopware sync derives this host from SHOPWARE_SHOP_ID + SHOPWARE_DEPLOY_ENV (and SHOPWARE_DATA_BASE) and the SSH source from shop id + live when roots are unset. fyrst-cli shopware sync local auto-derives remote /var/lib/shopware/data/${SHOPWARE_SHOP_ID}/live. Override with SHOPWARE_DATA_ROOT / SHOPWARE_REMOTE_DATA_ROOT when needed.

Named volumes mysql_data / redis_data are scoped by the derived Compose project name (COMPOSE_PROJECT_NAME=${SHOPWARE_SHOP_ID}-${SHOPWARE_DEPLOY_ENV}).

Secrets and hosts (CI + runtime)

Build-time (CI variables / GitHub secrets — never in git):

Name Purpose
SHOPWARE_PACKAGES_TOKEN Optional. Set only if the shop uses packages.shopware.com. Empty is fine.
COMPOSER_AUTH Optional JSON for private Composer repos (auth.json)
REGISTRY_* Push the image (REGISTRY_USERNAME / REGISTRY_PASSWORD, or GitHub GITHUB_TOKEN / GitLab CI_REGISTRY_*)
REGISTRY_IMAGE Optional override of the image name
DOCKERFILE Image build file. Always docker/Dockerfile (or default to that).

Runtime (VPS .env mode 0600, or managed-host env):

Name Purpose
APP_URL / SALES_CHANNEL_URL Public shop URL. Rewrite uses APP_URL only
APP_SECRET Persistent secret (openssl rand -hex 32)
DATABASE_URL MySQL/MariaDB DSN
SHOPWARE_SHOP_ID Stable shop slug (same on every stack of this shop). Required.
SHOPWARE_DEPLOY_ENV This stack’s role (live / staging / …). Required.
SHOPWARE_DATA_BASE Optional prefix for bind-mount trees (/var/lib/shopware/data when unset)
COMPOSE_PROJECT_NAME Optional. Derived ${SHOPWARE_SHOP_ID}-${SHOPWARE_DEPLOY_ENV} — unique on the host. Compose uses the SoT vars directly.
SHOPWARE_DATA_ROOT Optional bind-mount root override. Derived ${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}
SHOPWARE_SSH_HOST / SHOPWARE_SSH_USER / SHOPWARE_SSH_KEY Laptop → live (and any host SSH). Put in .env.local. Host defaults to the sync alias (live~/.ssh/config Host live) when unset
SHOPWARE_REMOTE_DATA_ROOT Optional live bind-mount root. Else /var/lib/shopware/data/${SHOPWARE_SHOP_ID}/live
SHOPWARE_ALLOW_LIVE_RESTORE Live gate for sync apply and backup recover (plus --allow-live)
INSTALL_ADMIN_* First-install admin user only
Store / app licence vars Only if you ship licensed apps

Deploy transport (CI secrets — not shop .env):

Name Purpose
SSH_PRIVATE_KEY CI → VPS
VPS_HOST / VPS_USER / VPS_PATH SSH target and checkout path
SSH_KNOWN_HOSTS Recommended instead of blindly accepting host keys

Placeholders live in the Flex-copied .github/workflows/cd.yaml, .gitlab-ci.yaml, and .env.example. Flex may append SoT keys to .env; then run fyrst-cli shopware env init --shop-id <slug> (VPS: --vps). Put consumer identity and APP_URL in .env / .env.prod. Put laptop SSH in .env.local.

GitLab still looks for .gitlab-ci.yml by default — set Settings → CI/CD → CI/CD configuration file to .gitlab-ci.yaml. GitHub Actions and shopware-cli load the .yaml names directly.

CD pipeline

Push to GitHub and/or GitLab on main (or a v* tag):

  1. Builddocker buildx with BuildKit secrets; shopware-cli project ci inside the shopware-cli image.
  2. Push:git-sha always; :latest on default branch; :semver on version tags.
  3. Deploy — SSH to the VPS, then fyrst-cli shopware deploy release (existing IMAGE / IMAGE_TAG / COMPOSE_DIR): pull image, Compose up, one-shot setup. Managed host (DEPLOY_TARGET=managed) is planned / not implemented and is not a supported CI switch.

Local day-to-day:

shopware-cli project dev

To pull live media/files into that checkout, use fyrst-cli shopware sync local (rsync path remap; do not pass --data all — fyrst-cli refuses it; use the default volume list). Do not run fyrst-cli shopware sync {capture|apply|pull} on a laptop — that is VPS-only.

Primary vs planned deploy

                    ┌─ shopware-cli project ci ─┐
  git push  ──────│     multi-stage image    │  ──│ registry (:sha / :latest / :semver)
                    └──────────────────────────────┘
                                   │
            ┌───────────────────────┴──────────────────────┐
            ▼                                             ▼
   Primary: Compose / VPS                      Planned: managed host
   SSH → pull → compose up web                 Same image, different job
   + setup one-shot                             (not implemented; no CI job)
  • Primary (Compose / VPS): deploy/README.md after Flex copies it. Live recommendation: uncomment COMPOSE_PROFILES=redis,worker,scheduler in .env. Staging leaves profiles unset unless you intentionally need worker/scheduler.
  • Planned (managed host): deploy/managed/README.md. Not implemented. DEPLOY_TARGET=managed is not a supported switch.

The only image build file is docker/Dockerfile. Do not maintain a second Dockerfile per target or per CI system.

Image contract

Trigger Tags
Every successful build (non-PR) :<git-sha> (full SHA)
Default branch (main) also :latest
Git tag v* also :semver (1.2.3, 1.2)

PHP is pinned to 8.3 via PHP_VERSION in docker/Dockerfile / deploy Compose build args.

How Flex finds the recipe

Packagist is done. fyrst/shopware-cd is the published thin library. Flex copies nothing unless a configured endpoint lists a recipe for that package name. That recipe’s job is copy-from-package of this package’s overlay/ plus bundles / env — it does not carry a root/ tree.

There is no Composer dependency from this package on fyrst-dev/recipes. Association is package name → recipe via extra.symfony.endpoint.

Shops must configure the endpoint before composer require (see Primary path).

symfony/recipes-contrib is not the primary path. An optional contrib PR may exist; do not wait on it. Keep using the fyrst-dev/recipes endpoint.

Compose layout

Local (CLI-owned, not Flex):

  • compose.yaml — from shopware-cli project create. Local Docker via shopware-cli project dev. Shop-specific tweaks belong in compose.override.yaml.
  • .gitignore / .shopware-project.yml (create’s default; .yaml also accepted — do not rename) — also from shopware-cli project create

CD / VPS (Flex-copied under deploy/):

  • deploy/compose.yaml — CD services: web, bundled mysql, optional redis / setup / worker / scheduler profiles; interpolates SHOPWARE_SHOP_ID / SHOPWARE_DEPLOY_ENV / SHOPWARE_DATA_BASE directly; bind mounts under the derived shop/env root (named volumes only mysql_data / redis_data, scoped by the derived Compose project name)
  • deploy/compose.prod.yaml — VPS/prod overrides
  • deploy/compose.vps.yamlpull_policy: ${PULL_POLICY:-always} (CI/VPS default after a registry push). Same-host tag-and-load / air-gap: PULL_POLICY=never and SKIP_PULL=1 (or fyrst-cli shopware deploy release --skip-pull)

Operators call fyrst-cli 0.1.0+ (recipe deploy/*.sh wrappers are removed):

  • fyrst-cli shopware env init — Flex env appends safe SoT keys; fyrst-cli fills shop id / env, optional IMAGE / APP_SECRET, and --vps comments create’s COMPOSE_PROJECT_NAME=sw-shop-…
  • fyrst-cli shopware deploy release — also invoked from CI (existing IMAGE / IMAGE_TAG / COMPOSE_DIR). compose run uses --pull never (Compose v5 dropped --no-build from the run subcommand). up uses --no-build.
  • fyrst-cli shopware deploy rollback
  • fyrst-cli shopware sync {capture\|apply\|pull} — VPS only: live → staging/playground/dev copy of DB + host dirs (SSH + shopware-cli project dump + rsync; no S3; paths from SHOPWARE_SHOP_ID + SHOPWARE_DEPLOY_ENV, optional SHOPWARE_DATA_BASE / SHOPWARE_DATA_ROOT)
  • fyrst-cli shopware sync local — laptop only: live media/files into shopware-cli project dev (rsync path remap; remote default /var/lib/shopware/data/${SHOPWARE_SHOP_ID}/live; not VPS sync {capture\|apply\|pull}; fyrst-cli refuses --data all)
  • fyrst-cli shopware backup {create\|prune\|recover} — off-host DR; not sync; dump is still shopware-cli project dump

The VPS uses those deploy Compose files. It does not use the CLI-managed root compose.yaml.

Setup always:

vendor/bin/shopware-deployment-helper run \
  --skip-theme-compile \
  --skip-assets-install

Runtime data sync (VPS, no S3)

DB, media, documents, thumbnails, and related uploads stay out of git and out of the image. .dockerignore excludes those paths. On the VPS they live in bind mounts under the derived shop/env root (default /var/lib/shopware/data/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/{files,media,thumbnail,theme,sitemap}) plus the database (mysql_data named volume or DBaaS). Named Docker volumes remain only for mysql_data / redis_data — not for those media/files paths. A new image pull keeps the same host directories mounted.

One-time on each VPS stack (shop id + deploy env):

# .env: SHOPWARE_SHOP_ID=acme SHOPWARE_DEPLOY_ENV=staging
# Compose/sync derive COMPOSE_PROJECT_NAME=acme-staging and
# SHOPWARE_DATA_ROOT=/var/lib/shopware/data/acme/staging
sudo mkdir -p "$SHOPWARE_DATA_ROOT"/{files,media,thumbnail,theme,sitemap}
sudo chown -R 82:82 "$SHOPWARE_DATA_ROOT"

No S3. Copying live → staging / playground / dev is SSH + shopware-cli project dump (operator-provided; fyrst-cli never dumps) + rsync of those host dirs, not object storage. Restore is fyrst-cli shopware db import.

fyrst-cli shopware sync {capture|apply|pull} rsyncs those host dirs. Paths use shop id + deploy env: this host from SHOPWARE_SHOP_ID + SHOPWARE_DEPLOY_ENV (and optional SHOPWARE_DATA_BASE); the SSH source defaults to the same shop id + live. Override with SHOPWARE_DATA_ROOT / SHOPWARE_REMOTE_DATA_ROOT when needed. Run it on the consumer (cron on staging/playground/dev). Pull from live; never auto-push into live. VPS sync is not for local shopware-cli project dev.

# on staging / playground / dev
cd /opt/shopware/acme-staging
# .env / .env.prod: SHOPWARE_SHOP_ID=acme SHOPWARE_DEPLOY_ENV=staging
# APP_URL=https://staging.example.com
# COMPOSE_PROJECT_NAME and SHOPWARE_DATA_ROOT stay unset (derived)
# fyrst-cli uses APP_URL for sales-channel rewrite
# (shops need composer update fyrst/shopware-cd).
fyrst-cli shopware sync pull --from live --data all
15 2 * * * cd /opt/shopware/acme-staging && fyrst-cli shopware sync pull --from live --data all

See deploy/README.md after Flex. Overlay copies live in this package’s overlay/.

Runtime files into local shopware-cli project dev

Local and VPS use different paths. shopware-cli project dev bind-mounts the project tree, not SHOPWARE_DATA_ROOT. Do not run fyrst-cli shopware sync {capture|apply|pull} on a laptop.

Laptop .env needs at least SHOPWARE_SHOP_ID (same slug as live). Put SHOPWARE_SSH_HOST / SHOPWARE_SSH_USER / SHOPWARE_SSH_KEY in .env.local (host defaults to alias live when unset). fyrst-cli shopware sync local auto-derives remote /var/lib/shopware/data/${SHOPWARE_SHOP_ID}/live unless you override it (--remote-data-root / SHOPWARE_REMOTE_DATA_ROOT). From the shop root it rsyncs live bind-mount trees into local project paths (rsync path remap). fyrst-cli refuses --data all on sync local; use the default volume list (volumes only, never DB):

Live (/var/lib/shopware/data/${SHOPWARE_SHOP_ID}/live) Local project
.../files files/
.../media public/media/
.../thumbnail public/thumbnail/
.../theme public/theme/
.../sitemap public/sitemap/
cd /path/to/your-shop
# .env: SHOPWARE_SHOP_ID=acme   # same as live; remote → /var/lib/shopware/data/acme/live
# .env.local: SHOPWARE_SSH_* (or rely on ~/.ssh/config Host live)
fyrst-cli shopware sync local --from live
# optional: --data media,files  --delete  --dry-run
shopware-cli project console cache:clear

Needs SSH to live (SHOPWARE_SSH_* in .env.local, or Host live in ~/.ssh/config) and rsync on the laptop. These dirs stay gitignored — never commit them.

Pulling media/files does not copy the database. For a full content match, dump live into the local CLI DB separately, then rewrite sales-channel URLs for http://127.0.0.1:8000 (see Sales-channel URL rewrite). Do not point local at the live database.

Sales-channel URL rewrite (console)

This package registers Symfony bundle Fyrst\ShopwareCd\FyrstShopwareCdBundle (extra.symfony.bundle) and command fyrst:sales-channel:rewrite-urls. Shops need a Composer package update (composer update fyrst/shopware-cd) — this is not a Flex overlay copy. Pipeline verbs (env init, deploy, sync, backup) stay in fyrst-cli 0.1.0; they are not console commands in this package.

Discoverable in a Shopware app after composer require fyrst/shopware-cd (or update) when Flex adds the bundle to config/bundles.php:

  1. Flex reads extra.symfony.bundle / the Bundle class heuristic, or
  2. The fyrst-dev/recipes recipe lists the bundle in manifest.json ("Fyrst\\ShopwareCd\\FyrstShopwareCdBundle": ["all"]) so composer recipes:update fyrst/shopware-cd writes config/bundles.php. If that line is missing, add:
Fyrst\ShopwareCd\FyrstShopwareCdBundle::class => ['all' => true],

Then:

bin/console list fyrst
# fyrst:sales-channel:rewrite-urls

Rewrite uses APP_URL (or --app-url / --map). Requires at least one rewrite option when invoked by hand. Updates sales_channel_domain.url and updated_at = NOW(3) only. Does not touch media CDN, plugin configs, APP_URL in .env, or payment/shipping webhooks. Unique url collisions abort. Hard-refused when SHOPWARE_DEPLOY_ENV, --checkout-basename, or hostname is live. SHOPWARE_ALLOW_LIVE_RESTORE=1 does not bypass that refuse.

fyrst-cli sync apply / sync pull already call it from the shop checkout using APP_URL from .env / .env.local / .env.prod (same compose run style as cache:clear):

# origin replace (path/query/hash kept); staging / playground / dev only
docker compose --env-file .env \
  -f deploy/compose.yaml -f deploy/compose.prod.yaml -f deploy/compose.vps.yaml \
  run --rm --pull never --entrypoint php web bin/console fyrst:sales-channel:rewrite-urls \
  --app-url=https://staging.example.com \
  --deploy-env="${SHOPWARE_DEPLOY_ENV}" \
  --checkout-basename="$(basename "$PWD")"

# 1:1 prefix map (longest old prefix first) when one origin is not enough
docker compose --env-file .env \
  -f deploy/compose.yaml -f deploy/compose.prod.yaml -f deploy/compose.vps.yaml \
  run --rm --pull never --entrypoint php web bin/console fyrst:sales-channel:rewrite-urls \
  --map='https://shop.example.com=https://staging.example.com,https://b2b.example.com=https://b2b.staging.example.com' \
  --deploy-env="${SHOPWARE_DEPLOY_ENV}" \
  --checkout-basename="$(basename "$PWD")"

# plan only
docker compose --env-file .env \
  -f deploy/compose.yaml -f deploy/compose.prod.yaml -f deploy/compose.vps.yaml \
  run --rm --pull never --entrypoint php web bin/console fyrst:sales-channel:rewrite-urls \
  --app-url=https://staging.example.com \
  --deploy-env="${SHOPWARE_DEPLOY_ENV}" \
  --checkout-basename="$(basename "$PWD")" \
  --dry-run

Local shopware-cli project dev after a live DB dump:

shopware-cli project console fyrst:sales-channel:rewrite-urls \
  --app-url=http://127.0.0.1:8000 \
  --deploy-env=dev \
  --checkout-basename="$(basename "$PWD")"

File tree (this package)

.
├── README.md
├── CREATE.md
├── LICENSE
├── composer.json                  # Packagist: fyrst/shopware-cd (library + Symfony bundle)
├── phpunit.xml.dist
├── overlay/                       # Flex copy-from-package → shop root (kept in the Composer dist)
│   ├── .dockerignore
│   ├── .env.example
│   ├── .github/workflows/cd.yaml
│   ├── .gitlab-ci.yaml
│   └── deploy/                    # CD Compose, edge, managed docs — no *.sh
├── src/                           # FyrstShopwareCdBundle + fyrst:sales-channel:rewrite-urls
└── tests/

This package’s GitHub workflow is package tests only (.github/workflows/ci.yml). Shop CD YAML lives in overlay/.github/workflows/cd.yaml so it is not run here.

Anti-patterns

  • Compiling assets or themes on the VPS after the image is built
  • Skipping shopware/docker on the composer require line
  • Building from a shop-root Dockerfile (the fyrst recipe does not provide one)
  • Different Dockerfiles per CI system or per deploy target (always docker/Dockerfile)
  • Manual FTP/rsync of vendor/
  • Git submodules for this overlay
  • Custom create / apply CLIs or Composer plugins that copy files
  • Putting deploy / sync / backup pipeline logic into this PHP package (operators use fyrst-cli 0.1.0 only)
  • Skipping fyrst-cli 0.1.0+ on the VPS or laptop
  • Dumping with fyrst-cli (Dump stays shopware-cli project dump)
  • Treating recipe deploy/*.sh wrappers as the operator path (they are removed; call fyrst-cli shopware …)
  • Requiring fyrst-dev/recipes as a Composer package (Flex uses extra.symfony.endpoint)
  • Skipping the fyrst-dev/recipes endpoint before composer require (Flex copies nothing)
  • Letting the fyrst Flex recipe copy or overwrite compose.yaml, .gitignore, or .shopware-project.yaml / .shopware-project.yml
  • Renaming create’s .shopware-project.yml to .yaml (shopware-cli accepts both)
  • Leaving create’s COMPOSE_PROJECT_NAME=sw-shop-… in the VPS .env (it overrides Compose name:; use fyrst-cli shopware env init --vps)
  • Putting real secrets in the Flex env block (empty shop id only; operators fill via fyrst-cli shopware env init or by hand)
  • Forcing a registry pull on a same-host tag-and-load (pull_policy: always without PULL_POLICY=never / SKIP_PULL=1)
  • Deploying the VPS from the CLI-managed root compose.yaml (use deploy/compose.yaml + deploy/compose.prod.yaml + deploy/compose.vps.yaml)
  • Editing overlay files in fyrst-dev/recipes root/ (this package’s overlay/ is the source of truth; the recipe is metadata only)
  • Skipping the Deployment Helper
  • Committing .env, .env.local, .env.prod, auth.json, leftover deploy/*.env, or real hostnames
  • Baking media, uploads, or DB dumps into git or the image (they stay on VPS bind mounts under the derived shop/env root, or in the local project tree)
  • Using a single global /var/lib/shopware/data without SHOPWARE_SHOP_ID / SHOPWARE_DEPLOY_ENV segments
  • Hardcoding Compose project name shopware (Compose derives COMPOSE_PROJECT_NAME=${SHOPWARE_SHOP_ID}-${SHOPWARE_DEPLOY_ENV})
  • Requiring COMPOSE_PROJECT_NAME or SHOPWARE_DATA_ROOT in .env (they are optional; Compose uses those SoT vars directly)
  • Using S3/MinIO as the default way to copy live data to staging (use fyrst-cli shopware sync: SSH + shopware-cli project dump + rsync of those host dirs)
  • Running VPS fyrst-cli shopware sync {capture|apply|pull} against local shopware-cli project dev (use fyrst-cli shopware sync local)
  • Cron that pushes into live (the consumer pulls from live)
  • Treating managed-host deploy as a supported CI path (DEPLOY_TARGET=managed is planned / not implemented)

Roadmap (ops)

Shared pipeline logic lives in fyrst-cli 0.1.0. Compose/CI templates live in this package’s overlay/. The Flex recipe stays metadata. This package also stays the rewrite command. Package docs stay aligned with those epics: