Shopware 6 mit Docker auf VPS installieren - Schritt für Schritt für Anfänger und Profis

Hallo zusammen,

Stunden mühevoller Recherche und Arbeit stecken in dieser Anleitung. In der Hoffnung, dass sie dem einen oder anderem Zeit verschafft, die lieber in der Natur verbracht werden kann.
Mit dieser Anleitung sollten selbst Anfänger in der Lage sein, Shopware 6 mit Docker auf einem VPS zu installieren. Keine Garantie auf Vollständig- und Richtigkeit.
Konstruktiv kritische Kommentare und Verbesserungsvorschläge sehr gerne.
Viel Spaß und vor allem Erfolg!

Shopware 6 auf einem VPS mit Docker – Schritt für Schritt

Eine Anleitung für absolute Anfänger

Empfohlene Mindestausstattung: 4 vCores CPU · 4 GB RAM · 120 GB NVMe SSD · Debian 13 / PHP 8.4

:information_source: HINWEIS Alle Befehle werden der Reihe nach in einem Terminal (SSH-Verbindung) eingegeben.


Platzhalter-Übersicht

Ersetze vor Beginn alle Platzhalter. Nutze „Alle ersetzen" (Strg+H / Cmd+H). Kopiere den Platzhalter aus der linken Spalte und ersetze ihn durch deinen Wert.

Server & Netzwerk

Platzhalter Beschreibung Beispiel / Hinweis
DEINE_SERVER_IP Öffentliche IP-Adresse des VPS z. B. 203.0.113.10
DEINE_DOMAIN Domain des Shops (ohne https://) z. B. mein-shop.de
DEINE_MAIL E-Mail-Adresse (für SSH-Key & Certbot) z. B. admin@mein-shop.de

Linux-Benutzer

Platzhalter Beschreibung Beispiel / Hinweis
DEIN_LINUX_USER Neuer sudo-Benutzer auf dem Server z. B. shopuser

Datenbank

Platzhalter Beschreibung Beispiel / Hinweis
DEIN_DB_NAME Name der MariaDB-Datenbank z. B. shopware_db
DEIN_DB_USER MariaDB-Benutzername (nicht root) z. B. sw_user
DEIN_DB_PASSWORD Passwort für den DB-Benutzer openssl rand -hex 32
DEIN_DB_ROOT_PW MariaDB root-Passwort openssl rand -hex 32

Shopware Admin

Platzhalter Beschreibung Beispiel / Hinweis
DEIN_ADMIN_USER Benutzername des Shopware-Admins z. B. shop-admin
DEIN_ADMIN_PASSWORD Passwort des Shopware-Admins min. 8 Zeichen, Groß/Klein/Zahl/Sonderz.

:light_bulb: TIPP openssl rand -hex 32 erzeugt ein sicheres Zufallspasswort. Die .env-Datei darf niemals in ein Git-Repository eingecheckt oder veröffentlicht werden!


Begriffserklärung

Für Einsteiger folgt eine kurze Übersicht der verwendeten Begriffe:

  • VPS (Virtual Private Server): Ein gemieteter virtueller Computer im Rechenzentrum. Verbindung per Internet.

  • SSH: Sichere verschlüsselte Verbindung vom eigenen PC zum Server (Fernzugriff übers Terminal).

  • Docker: Programm, das Anwendungen in abgeschlossenen Containern laufen lässt – sauber, reproduzierbar und leicht zu starten/stoppen.

  • Docker Compose: Werkzeug, das mehrere Container gleichzeitig steuert. Unser Shop besteht aus: Web-App, Datenbank, Worker (Hintergrundprozess) und Scheduler (Aufgabenplaner).

  • Shopware 6: Das Shop-System, das wir installieren. Läuft komplett im Docker-Container.

  • Nginx: Webserver, der Anfragen aus dem Internet entgegennimmt und an Shopware weiterleitet. Stellt auch das SSL-Zertifikat (https://) bereit.

  • Domain: Deine Internetadresse, z. B. mein-shop.de. Ohne Domain ist der Shop zunächst nur über die Server-IP erreichbar.

  • Termius: SSH-Client (App), mit der du dich von deinem lokalen Gerät auf den Server verbindest. Die kostenlose Version reicht aus.


Teil 0 – VPS mit Debian 13 installieren

Installiere Debian 13 auf deinem VPS über die Oberfläche deines Anbieters.

:warning: WARNUNG Port 22 muss zu Beginn freigeschaltet sein, damit der erste SSH-Zugang funktioniert.


Teil 1 – VPS absichern

Bevor wir irgendetwas installieren, machen wir den Server sicher. Ein frischer VPS ist im Internet sofort sichtbar und wird binnen Minuten von automatisierten Angriffen gescannt. Die folgenden Schritte schützen ihn auf ein solides Grundniveau.


Schritt 1 – System aktualisieren & Benutzer anlegen

Als erstes melden wir uns als root-User am Server an und aktualisieren alle vorinstallierten Programme. Anschließend legen wir einen eigenen Benutzer an – der root-Account hat unbegrenzte Rechte und sollte nicht für die tägliche Arbeit genutzt werden.

Der Benutzername in dieser Anleitung lautet DEIN_LINUX_USER. Tausche überall in der Anleitung diesen Platzhalter gegen deinen gewählten Namen aus.

apt update && apt upgrade -y \
&& adduser DEIN_LINUX_USER \
&& usermod -aG sudo DEIN_LINUX_USER

:light_bulb: TIPP Du wirst nach einem Passwort für DEIN_LINUX_USER gefragt. Wähle ein starkes Passwort und notiere es sicher. Die weiteren Fragen (Name, Telefon etc.) kannst du mit Enter überspringen.


Schritt 2 – SSH-Key-Authentifizierung einrichten

SSH-Keys sind sicherer als Passwort-Login: Du erzeugst ein Schlüsselpaar auf deinem lokalen PC, legst den öffentlichen Teil auf dem Server ab – und ab dann kommst nur du mit deinem privaten Schlüssel rein.

Auf Windows (PowerShell 7):

ssh-keygen -t ed25519 -C DEINE_MAIL

ssh-keygen -R DEINE_SERVER_IP; type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh DEIN_LINUX_USER@DEINE_SERVER_IP "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

Verbindung testen:

ssh DEIN_LINUX_USER@DEINE_SERVER_IP


Schritt 3 – SSH-Konfiguration absichern

Jetzt deaktivieren wir den root-Login, schalten die Passwort-Anmeldung ab und verlegen SSH auf Port 2222 statt Standard 22. Das macht automatisierte Angriffe erheblich schwerer. Port 2222 muss ggf. noch in der Oberfläche deines VPS-Anbieters freigeschaltet werden.

:warning: WARNUNG WICHTIG: Lass deine aktuelle SSH-Verbindung (Termius-Session) offen! Öffne danach eine NEUE Verbindung in Termius, um zu prüfen, ob der Login noch klappt. Erst wenn der neue Login funktioniert, kannst du die alte Session schließen. Andernfalls sperrst du dich aus und musst den VPS zurücksetzen.

sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config \
&& sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config \
&& sudo sed -i 's/^#\?PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config \
&& sudo sed -i 's/^#\?Port .*/Port 2222/' /etc/ssh/sshd_config \
&& echo 'AllowUsers DEIN_LINUX_USER' | sudo tee -a /etc/ssh/sshd_config \
&& sudo systemctl restart sshd

Neue Verbindung im zweiten Termius-Tab testen (Port 2222 beachten!):

ssh -p 2222 DEIN_LINUX_USER@DEINE_SERVER_IP

:light_bulb: TIPP Ab jetzt lautet dein SSH-Befehl immer: ssh -p 2222 DEIN_LINUX_USER@DEINE_SERVER_IP – vergiss den Port nicht!


Schritt 4 – UFW-Firewall einrichten

UFW (Uncomplicated Firewall) erlaubt nur die Ports, die wir wirklich brauchen: 2222 (SSH), 80 (HTTP) und 443 (HTTPS). Alles andere wird geblockt.

sudo apt install ufw -y \
&& sudo ufw default deny incoming \
&& sudo ufw default allow outgoing \
&& sudo ufw allow 2222/tcp \
&& sudo ufw allow 80/tcp \
&& sudo ufw allow 443/tcp \
&& yes | sudo ufw enable \
&& sudo ufw status verbose

:information_source: HINWEIS yes | sudo ufw enable bestätigt die Aktivierung automatisch. ufw status verbose zeigt danach die aktiven Regeln – dort sollten die drei Ports 2222, 80 und 443 erscheinen.

:warning: WARNUNG Docker umgeht UFW standardmäßig über iptables direkt. Wenn MariaDB jemals mit ports: 3306:3306 in der compose.yaml exposed wird, ist sie von außen erreichbar – UFW sieht das nicht! Deshalb niemals den Datenbankport nach außen exponieren. In unserer Konfiguration ist das korrekt gelöst: Port 3306 ist in der compose.yaml bewusst nicht unter ports: aufgeführt.


Schritt 5 – Fail2Ban installieren (Brute-Force-Schutz)

Fail2Ban überwacht die Login-Versuche auf dem Server. Nach 3 Fehlversuchen innerhalb von 10 Minuten wird die angreifende IP-Adresse für 1 Stunde gesperrt.

sudo apt install fail2ban -y \
&& sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local \
&& sudo tee -a /etc/fail2ban/jail.local > /dev/null << 'EOF'
[sshd]
enabled = true
port = 2222
maxretry = 3
bantime = 3600
findtime = 600
EOF
sudo systemctl enable fail2ban && sudo systemctl start fail2ban


Teil 2 – Docker installieren

Docker ist die Grundlage für unsere Shopware-Installation. Es ermöglicht, Shopware und die dazugehörige Datenbank in isolierten Containern zu betreiben – sauber, wiederherstellbar und einfach zu aktualisieren.


Schritt 1 – Automatische Sicherheitsupdates aktivieren

sudo apt install unattended-upgrades -y \
&& sudo dpkg-reconfigure -plow unattended-upgrades

:information_source: HINWEIS Du wirst gefragt, ob automatische Updates aktiviert werden sollen – wähle Ja.


Schritt 2 – Docker installieren

Docker ist nicht in den Standard-Paketquellen von Debian enthalten. Wir fügen die offizielle Docker-Paketquelle hinzu und installieren Docker darüber.

sudo apt install -y apt-transport-https ca-certificates curl gnupg lsb-release \
&& sudo install -m 0755 -d /etc/apt/keyrings \
&& curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
&& sudo chmod a+r /etc/apt/keyrings/docker.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/debian $(lsb_release -cs) stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null \
&& sudo apt update \
&& sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \
&& sudo systemctl enable docker && sudo systemctl start docker \
&& sudo usermod -aG docker DEIN_LINUX_USER

:information_source: HINWEIS Der letzte Befehl (usermod) gibt DEIN_LINUX_USER die Berechtigung, Docker ohne sudo zu nutzen. Diese Änderung wird erst nach dem nächsten Ein- und Ausloggen wirksam.


Schritt 3 – Zeitzone, Dienste und Logwatch einrichten

sudo timedatectl set-timezone Europe/Berlin \
&& sudo systemctl disable bluetooth cups avahi-daemon 2>/dev/null; \
sudo apt install logwatch -y

:light_bulb: TIPP Logwatch schickt täglich eine Zusammenfassung der Server-Aktivitäten per E-Mail – sofern E-Mail-Versand auf dem Server eingerichtet ist. Ohne E-Mail-Konfiguration speichert es die Berichte lokal.


Schritt 4 – Installation prüfen

Jetzt ausloggen und neu einloggen (damit die Docker-Gruppe wirksam wird), dann prüfen:

# Neu einloggen:
exit
ssh -p 2222 DEIN_LINUX_USER@DEINE_SERVER_IP

# Alles auf einmal prüfen:
sudo ufw status && sudo fail2ban-client status sshd && docker --version && docker compose version

:light_bulb: TIPP Wenn alle vier Befehle ohne Fehlermeldung durchlaufen, ist die Basis fertig. Du siehst z. B.: Docker version 27.x.x und Docker Compose version v2.x.x


Teil 3 – Shopware 6 installieren

Jetzt kommt der eigentliche Shop. Wir erstellen ein Shopware-Projekt mit Composer, bauen ein Docker-Image und starten alle nötigen Dienste mit Docker Compose.

:information_source: HINWEIS Was ist Docker Compose? Docker Compose steuert mehrere Container gleichzeitig. Unser Shop besteht aus: der Web-App, der Datenbank, einem Worker (Hintergrundprozess) und einem Scheduler (Aufgabenplaner). Compose startet und verwaltet alle auf einmal.


Schritt 1 – Shopware-Projekt anlegen

Zuerst installieren wir Composer (PHP-Paketverwaltung) und die benötigten PHP-Erweiterungen. Dann erstellen wir das Shopware-Projekt.

sudo apt install -y php-cli unzip \
&& sudo apt install -y \
php8.4-curl php8.4-gd php8.4-xml php8.4-mbstring php8.4-zip \
php8.4-intl php8.4-mysql php8.4-pdo php8.4-opcache

# 1. Installer herunterladen
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"

# 2. Erwarteten Hash von der offiziellen Quelle holen
EXPECTED_HASH=$(curl -s https://composer.github.io/installer.sig)

# 3. Tatsächlichen Hash der heruntergeladenen Datei berechnen
ACTUAL_HASH=$(php -r "echo hash_file('sha384', 'composer-setup.php');")

# 4. Vergleichen -- bei Abweichung Abbruch
if [ "$EXPECTED_HASH" != "$ACTUAL_HASH" ]; then
  echo "FEHLER: Hash stimmt nicht überein! Die Datei könnte manipuliert sein. Abbruch."
  rm composer-setup.php
  exit 1
fi

echo "Hash verifiziert -- Composer wird installiert..."

# 5. Erst jetzt installieren
sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer

# 6. Installer aufräumen
rm composer-setup.php

# 7. Prüfen
composer --version

Die folgenden Befehle abschnittsweise eingeben!

# Verzeichnis erstellen und Rechte setzen
sudo mkdir -p /opt/shopware
sudo chown DEIN_LINUX_USER:DEIN_LINUX_USER /opt/shopware

# Zum Benutzer wechseln
su - DEIN_LINUX_USER

# Shopware-Projekt erstellen
cd /opt/shopware

# nächster Abschnitt
composer create-project shopware/production:^6.7.8.1 .
composer require shopware/docker

:information_source: HINWEIS su - DEIN_LINUX_USER wechselt in den Benutzer-Account. Mit exit gelangst du zurück. Die Composer-Befehle dauern einige Minuten – das ist normal.

Da wir einen dedizierten Worker-Container verwenden, deaktivieren wir den Admin Worker direkt. Die Datei wird beim Docker-Build automatisch mit ins Image kopiert.

update_mail_variables_on_send: false – Deaktiviert die Aktualisierung von Nutzungsstatistiken in Mail-Templates nach jedem Versand. Das reduziert unnötige Datenbankschreiboperationen. Nur relevant wenn du auswerten möchtest wie oft ein bestimmtes Mail-Template verwendet wurde – für den normalen Shopbetrieb ohne Auswirkung.

mkdir -p /opt/shopware/config/packages
cat > /opt/shopware/config/packages/z-shopware.yaml << 'EOF'
shopware:
  admin_worker:
    enable_admin_worker: false
  mail:
    update_mail_variables_on_send: false
EOF


Schritt 2 – Dockerfile erstellen

Das Dockerfile beschreibt, wie das Docker-Image für Shopware gebaut wird. Es basiert auf einem fertigen Shopware-Basis-Image und ergänzt unseren Shop-Code.

Zuerst in das richtige Verzeichnis wechseln:

cd /opt/shopware

cat > Dockerfile << 'EOF'
#syntax=docker/dockerfile:1

ARG PHP_VERSION=8.4

FROM ghcr.io/shopware/docker-base:${PHP_VERSION}-frankenphp AS base-image

FROM ghcr.io/shopware/shopware-cli:latest-php-${PHP_VERSION} AS shopware-cli

FROM shopware-cli AS build

ADD . /src

WORKDIR /src

RUN --mount=type=cache,target=/root/.composer \
    --mount=type=cache,target=/root/.npm \
    /usr/local/bin/entrypoint.sh shopware-cli project ci /src

FROM base-image

COPY --from=build --chown=82 --link /src /var/www/html

EOF


Schritt 3 – Docker Compose Konfiguration erstellen

Die von Composer generierte compose.yaml ist nur für lokale Entwicklung gedacht. Wir ersetzen sie durch eine Production-Konfiguration mit folgenden Diensten:

  • database: MariaDB-Datenbank, in der alle Shop-Daten gespeichert werden.

  • init-perm: Setzt einmalig die Dateirechte für geteilte Ordner.

  • init: Richtet Shopware ein (Datenbank-Migrationen, erste Installation).

  • web: Die eigentliche Shopware-Webanwendung, erreichbar auf Port 8000.

  • worker: Verarbeitet Hintergrundaufgaben (z. B. E-Mails, Importe, fehlgeschlagene Nachrichten).

  • scheduler: Führt regelmäßige Aufgaben aus (z. B. Preis-Updates, Cache-Bereinigung).

:warning: WARNUNG Bitte ersetze alle Platzhalter (DEIN_DB_NAME, DEIN_DB_USER, etc.) gemäß der Platzhalter-Übersicht am Anfang des Dokuments!

rm -f compose.yaml compose.override.yaml \
&& cat > compose.yml << 'EOF'
x-shopware-environment: &shopware
  environment:
    APP_ENV: prod
    APP_SECRET: "${APP_SECRET}"
    APP_URL: "${APP_URL:-http://localhost:8000}"
    DATABASE_URL: "mysql://DEIN_DB_USER:${MYSQL_PASSWORD:-DEIN_DB_PASSWORD}@database/DEIN_DB_NAME"
    PHP_UPLOAD_MAX_FILESIZE: "64M"
    PHP_POST_MAX_SIZE: "64M"
    PHP_MAX_EXECUTION_TIME: "350"
    PHP_MAX_INPUT_TIME: "350"
    PHP_MEMORY_LIMIT: "512M"
    SYMFONY_TRUSTED_PROXIES: "172.16.0.0/12"
    DATABASE_HOST: "database"
    JWT_PRIVATE_KEY: "${JWT_PRIVATE_KEY}"
    JWT_PUBLIC_KEY: "${JWT_PUBLIC_KEY}"
    SHOPWARE_SKIP_WEBINSTALLER: 1
    INSTALL_LOCALE: "${INSTALL_LOCALE:-de-DE}"
    INSTALL_CURRENCY: "${INSTALL_CURRENCY:-EUR}"
    INSTALL_ADMIN_USERNAME: "${INSTALL_ADMIN_USERNAME:-DEIN_ADMIN_USER}"
    INSTALL_ADMIN_PASSWORD: "${INSTALL_ADMIN_PASSWORD:-DEIN_ADMIN_PASSWORD}"
  volumes:
    - plugins:/var/www/html/custom/plugins
    - files:/var/www/html/files
    - theme:/var/www/html/public/theme
    - media:/var/www/html/public/media
    - thumbnail:/var/www/html/public/thumbnail
    - sitemap:/var/www/html/public/sitemap

services:
  database:
    image: mariadb:11.4
    restart: unless-stopped
    environment:
      MARIADB_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:-DEIN_DB_ROOT_PW}"
      MARIADB_USER: DEIN_DB_USER
      MARIADB_PASSWORD: "${MYSQL_PASSWORD:-DEIN_DB_PASSWORD}"
      MARIADB_DATABASE: DEIN_DB_NAME
    volumes:
      - mysql-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mariadb-admin", "ping", "-h", "localhost", "-p${MYSQL_ROOT_PASSWORD:-DEIN_DB_ROOT_PW}"]
      interval: 10s
      timeout: 5s
      retries: 5

  init-perm:
    image: alpine
    <<: *shopware
    command: >
      chown 82:82
      /var/www/html/files
      /var/www/html/public/theme
      /var/www/html/public/media
      /var/www/html/public/thumbnail
      /var/www/html/public/sitemap

  init:
    image: shopware-local
    build:
      context: .
    <<: *shopware
    entrypoint: ["php", "vendor/bin/shopware-deployment-helper", "run"]
    depends_on:
      database:
        condition: service_healthy
      init-perm:
        condition: service_completed_successfully

  web:
    image: shopware-local
    build:
      context: .
    <<: *shopware
    restart: unless-stopped
    depends_on:
      init:
        condition: service_completed_successfully
    ports:
      - "127.0.0.1:8000:8000"

  worker:
    image: shopware-local
    build:
      context: .
    <<: *shopware
    restart: unless-stopped
    stop_signal: SIGTERM
    stop_grace_period: 60s
    depends_on:
      init:
        condition: service_completed_successfully
    entrypoint: ["php", "bin/console", "messenger:consume",
      "async", "low_priority", "failed",
      "--time-limit=3600", "--memory-limit=512M"]
    healthcheck:
      test: ["CMD", "php", "bin/console", "messenger:stats"]
      interval: 60s
      timeout: 15s
      retries: 3

  scheduler:
    image: shopware-local
    build:
      context: .
    <<: *shopware
    restart: unless-stopped
    depends_on:
      init:
        condition: service_completed_successfully
    entrypoint: ["php", "bin/console", "scheduled-task:run", "--time-limit=3600"]
    healthcheck:
      test: ["CMD", "php", "bin/console", "messenger:stats"]
      interval: 60s
      timeout: 15s
      retries: 3

volumes:
  plugins:
  mysql-data:
  files:
  theme:
  media:
  thumbnail:
  sitemap:
EOF

:information_source: HINWEIS Worker-Konfiguration: Die failed-Queue sorgt dafür, dass fehlgeschlagene Nachrichten erneut verarbeitet werden. --time-limit=3600 lässt den Worker-Prozess bis zu 1 Stunde laufen, bevor Docker ihn sauber neu startet. Der Scheduler erhält ebenfalls --time-limit=3600, damit er bei einem Fehler nicht endlos hängt.


Schritt 4 – Konfiguration & Passwörter (.env-Datei erstellen)

Die .env-Datei ist die zentrale Konfigurationsdatei mit Shop-URL, Datenbankpasswörtern und kryptografischen Schlüsseln. Das folgende Skript erzeugt sie automatisch.

:warning: WARNUNG WICHTIG: INSTALL_LOCALE legt die Systemsprache fest. Diese Einstellung kann nach der ersten Installation NICHT mehr geändert werden! Setze sie daher jetzt korrekt, bevor du in Schritt 5 mit docker compose up startest.

APP_SECRET=$(openssl rand -hex 32)
eval "$(docker run --rm ghcr.io/shopware/shopware-cli:latest project generate-jwt --env)"

cat > .env << EOF
APP_ENV=prod
APP_SECRET=${APP_SECRET}
APP_URL=http://DEINE_DOMAIN
MYSQL_ROOT_PASSWORD=DEIN_DB_ROOT_PW
MYSQL_PASSWORD=DEIN_DB_PASSWORD
JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY}
JWT_PUBLIC_KEY=${JWT_PUBLIC_KEY}
INSTALL_LOCALE=de-DE
INSTALL_CURRENCY=EUR
INSTALL_ADMIN_USERNAME=DEIN_ADMIN_USER
INSTALL_ADMIN_PASSWORD=DEIN_ADMIN_PASSWORD
EOF

echo "Die .env-Datei wurde erfolgreich erstellt."

:warning: WARNUNG Sicherheit: Öffne die .env jetzt mit einem Texteditor (z. B. nano .env) und prüfe alle Werte. Tipp: openssl rand -hex 32 generiert ein sicheres Zufallspasswort. Diese Datei darf niemals in ein Git-Repository eingecheckt oder veröffentlicht werden!

:light_bulb: TIPP Was ist APP_URL? Das ist die Adresse, unter der dein Shop erreichbar sein soll. Hast du noch keine Domain, trage zunächst http://DEINE_SERVER_IP:8000 ein. Sobald eine Domain vorhanden ist und Nginx mit SSL läuft (Schritt 6), änderst du sie auf https://DEINE_DOMAIN.

framework.yaml erstellen, damit Admin-IP-Adressen im Wartungsmodus richtig funktionieren:

cat > /opt/shopware/config/packages/framework.yaml << 'EOF'
framework:
  trusted_proxies: '%env(default::SYMFONY_TRUSTED_PROXIES)%'
  trusted_headers:
    - 'x-forwarded-for'
    - 'x-forwarded-host'
    - 'x-forwarded-proto'
    - 'x-forwarded-port'
EOF


Schritt 5 – Shop bauen und starten

Jetzt bauen wir das Docker-Image und starten alle Dienste. Dieser Schritt dauert beim ersten Mal je nach Internetverbindung und Server-Leistung zwischen 5 und 20 Minuten – das ist normal!

Zuerst in das richtige Verzeichnis wechseln:

cd /opt/shopware

# Image bauen und alle Dienste starten
docker compose build --no-cache \
&& docker compose up -d \
&& docker compose ps

:information_source: HINWEIS --no-cache sorgt dafür, dass das Image komplett neu gebaut wird, ohne auf zwischengespeicherte Schichten zurückzugreifen. Das ist beim ersten Mal sinnvoll, dauert aber länger.

Logs beobachten (Strg+C zum Beenden):

docker compose logs -f

Sobald der init-Container mit „exit 0" abgeschlossen ist und der web-Container läuft, ist der Shop erreichbar:

  • Frontend: http://DEINE_SERVER_IP:8000 (oder https://DEINE_DOMAIN nach Schritt 6)

  • Admin-Backend: http://DEINE_SERVER_IP:8000/admin

:light_bulb: TIPP Falls der init-Container mit einem Fehler abbricht (Exit-Code 1): docker compose logs init zeigt die genaue Fehlermeldung. Häufige Ursache: ein Fehler in der .env-Datei (z. B. fehlendes Passwort oder falsche APP_URL).


Schritt 6 – Nginx als Reverse Proxy mit SSL einrichten

Im Produktivbetrieb soll der Shop über eine Domain mit https:// erreichbar sein. Dafür brauchen wir Nginx als Reverse Proxy und ein kostenloses SSL-Zertifikat von Let’s Encrypt (Certbot).

:information_source: HINWEIS Was macht Nginx hier? Shopware läuft intern auf Port 8000. Nginx nimmt Anfragen auf Port 80 (HTTP) und 443 (HTTPS) entgegen und leitet sie an Shopware weiter. Certbot besorgt automatisch ein SSL-Zertifikat und erneuert es alle 90 Tage selbst.

Ersetze DEINE_DOMAIN in den folgenden Befehlen durch deine echte Domain.

:warning: WARNUNG Deine Domain muss bereits auf die IP-Adresse des Servers zeigen (DNS-Eintrag), bevor Certbot das Zertifikat ausstellen kann! Überprüfe das vorher mit: ping DEINE_DOMAIN

sudo apt install -y nginx certbot python3-certbot-nginx

1. Zuerst mit HTTP:

sudo tee /etc/nginx/sites-available/shopware << 'EOF'
server {
    listen 80;
    server_name DEINE_DOMAIN;

    client_max_body_size 64M;

    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
    # Kein HSTS hier!

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_buffer_size 128k;
        proxy_buffers 4 256k;
        proxy_busy_buffers_size 256k;
    }
}
EOF

sudo ln -s /etc/nginx/sites-available/shopware /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

2. Certbot:

sudo certbot --nginx -d DEINE_DOMAIN

3. Certbot schreibt die Datei um, aber HSTS trägt es nicht ein. Das machst du danach manuell:

sudo nano /etc/nginx/sites-available/shopware

4. Die Datei sieht nach Certbot ungefähr so aus – du fügst HSTS nur im 443-Block ein:

server {
    listen 80;
    server_name DEINE_DOMAIN;
    return 301 https://$host$request_uri; # von Certbot gesetzt
}

server {
    listen 443 ssl;
    server_name DEINE_DOMAIN;

    ssl_certificate /etc/letsencrypt/live/DEINE_DOMAIN/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/DEINE_DOMAIN/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    client_max_body_size 64M;

    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # ← jetzt eintragen

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_buffer_size 128k;
        proxy_buffers 4 256k;
        proxy_busy_buffers_size 256k;
    }
}

Nach dem Speichern Nginx neu laden und APP_URL in der .env aktualisieren:

sudo nginx -t && sudo systemctl reload nginx

# APP_URL in .env auf HTTPS umstellen
sed -i 's|APP_URL=.*|APP_URL=https://DEINE_DOMAIN|' /opt/shopware/.env

docker compose down && docker compose up -d


Teil: UPDATE von Shopware

Hier die korrekte Version eintragen:

composer require shopware/production:6.7.8.2 shopware/core:6.7.8.2 --with-all-dependencies

docker compose down

docker compose build --no-cache

docker compose up init

docker compose up -d

docker compose exec web php bin/console system:update:finish

docker compose exec web php bin/console cache:clear


Teil: Backup Cronjob

Backup-Ordner mit korrekter Berechtigung erstellen:

# Verzeichnis anlegen, DEIN_LINUX_USER als Eigentümer
sudo mkdir -p /opt/backups
sudo chown DEIN_LINUX_USER:DEIN_LINUX_USER /opt/backups
sudo chmod 750 /opt/backups

Backup-Skript erstellen (speichert für 3 Monate – selbst auf die eigenen Wünsche anpassen!!):

sudo bash -c 'cat > /opt/backups/backup.sh << '"'"'EOF'"'"'
#!/bin/bash

# === Konfiguration ===
BACKUP_DIR="/opt/backups"
DATE=$(date +%Y%m%d)
APP_DIR="/opt/shopware"
RETENTION_DAYS=90
LOG_FILE="/opt/backups/backup.log"

# === Logging ===
exec > >(tee -a "$LOG_FILE") 2>&1

echo "=============================="
echo "Backup gestartet: $(date)"
echo "=============================="

# === Verzeichnis erstellen ===
mkdir -p "$BACKUP_DIR"

# === Datenbank-Backup ===
echo "[DB] Starte MariaDB-Dump..."

cd "$APP_DIR" || { echo "APP_DIR nicht gefunden: $APP_DIR"; exit 1; }

DB_PASS=$(grep -oP '^MYSQL_PASSWORD=\K.*' .env)

docker compose exec -T database mariadb-dump \
  -u DEIN_DB_USER -p"${DB_PASS}" DEIN_DB_NAME \
  > "${BACKUP_DIR}/backup_${DATE}.sql"

if [ $? -eq 0 ]; then
  echo "[DB] Datenbank-Backup erfolgreich: backup_${DATE}.sql"
else
  echo "[DB] FEHLER beim Datenbank-Backup!"
fi

# === Datei-Backup (Volumes) ===
echo "[Files] Starte Volume-Backup..."

docker run --rm \
  -v shopware_files:/data/files \
  -v shopware_media:/data/media \
  -v shopware_theme:/data/theme \
  -v shopware_plugins:/data/plugins \
  -v "${BACKUP_DIR}:/backup" \
  alpine tar czf "/backup/shopware_files_${DATE}.tar.gz" /data

if [ $? -eq 0 ]; then
  echo "[Files] Datei-Backup erfolgreich: shopware_files_${DATE}.tar.gz"
else
  echo "[Files] FEHLER beim Datei-Backup!"
fi

# Eigentümer der neuen Backup-Dateien auf deinen Benutzer setzen
chown DEIN_LINUX_USER:DEIN_LINUX_USER "${BACKUP_DIR}/backup_${DATE}.sql"
chown DEIN_LINUX_USER:DEIN_LINUX_USER "${BACKUP_DIR}/shopware_files_${DATE}.tar.gz"
chmod 640 "${BACKUP_DIR}/backup_${DATE}.sql"
chmod 640 "${BACKUP_DIR}/shopware_files_${DATE}.tar.gz"

# === Alte Backups löschen ===
echo "[Cleanup] Lösche Backups älter als ${RETENTION_DAYS} Tage..."

find "$BACKUP_DIR" -name "backup_*.sql" -mtime +${RETENTION_DAYS} -delete
find "$BACKUP_DIR" -name "shopware_files_*.tar.gz" -mtime +${RETENTION_DAYS} -delete

echo "Backup abgeschlossen: $(date)"
echo ""
EOF
chmod +x /opt/backups/backup.sh'

Backup testen:

sudo bash /opt/backups/backup.sh

Cronjob aktivieren (jeden Sonntag um 2 Uhr):

sudo apt install cron -y \
&& sudo systemctl enable cron \
&& sudo systemctl start cron

(sudo crontab -l 2>/dev/null; echo "0 2 * * 0 /opt/backups/backup.sh") | sudo crontab -

Prüfen ob es geklappt hat:

sudo crontab -l


Teil 4 – Referenz & Wartung

Nützliche Docker-Befehle

Befehl Beschreibung
docker compose up -d Alle Dienste im Hintergrund starten
docker compose stop Alle Dienste stoppen
docker compose ps Status aller Container anzeigen
docker compose logs -f Logs aller Dienste live anzeigen
docker compose logs -f web Nur Logs des Web-Containers
docker compose exec web bash Shell im Web-Container öffnen
docker compose build --no-cache Image komplett neu bauen
docker compose pull Neueste Basis-Images herunterladen

Häufige Shopware CLI-Befehle

Diese Befehle werden alle im laufenden web-Container ausgeführt:

Befehl Beschreibung
docker compose exec web php bin/console cache:clear Cache leeren
docker compose exec web php bin/console theme:compile Theme neu kompilieren
docker compose exec web php bin/console database:migrate --all Datenbank-Migrationen ausführen
docker compose exec web php bin/console user:create admin --admin Admin-User erstellen
docker compose exec web php bin/console system:update:finish Shopware-Update abschließen
docker compose exec web php bin/console list Alle verfügbaren Befehle anzeigen

Troubleshooting

Problem Lösung
Container startet nicht docker compose logs init – zeigt den Fehler
Datenbankverbindung schlägt fehl docker compose ps – ist der database-Container healthy?
Permission denied Fehler docker compose up init-perm – Rechte neu setzen
Out of Memory PHP_MEMORY_LIMIT in compose.yml erhöhen
502 Bad Gateway (Nginx) Ist der web-Container wirklich gestartet?
Langsame Performance Redis als Cache/Session-Speicher ergänzen

Security Checklist

Vor dem Go-Live – diese Punkte abhaken:

  • [ ] Starke, individuelle Passwörter in der .env gesetzt

  • [ ] SSL/TLS mit gültigem Zertifikat aktiv (https://)

  • [ ] Datenbankport 3306 nicht nach außen offen (UFW)

  • [ ] Docker und Images regelmäßig aktualisieren

  • [ ] Regelmäßige Backups eingerichtet

  • [ ] UFW-Firewall aktiv und korrekt konfiguriert

  • [ ] APP_ENV=prod in der .env gesetzt

  • [ ] Standard-Admin-Passwort geändert

  • [ ] SSH-Key-Login funktioniert, Passwort-Login deaktiviert

2 „Gefällt mir“

Das ist eine ganz wundervolle Anleitung. Danke dafür @robertschoenfeld_1 !

Ich habe dazu ein paar kleine Anmerkungen:

  • Die ganz oben beschriebenen Serverparameter gibt’s im ganz kleinen Paket, oder? Ich bin mir nicht sicher, ob das wirklich dem Produktivumfeld standhält - kommt natürlich auch auf den Traffic und die Shopkonfiguration an (Anzahl Artikel, Kategorien etc). Das größte Problem ist dabei, dass die genannten Serverparameter bei verschiedenen Anbietern nicht fix sind und garantiert werden können, je nachdem, was sonst noch auf dem “Blech” passiert.
  • Wenn wir über den gleichen Anbieter sprechen, kann die Firewall durchaus auch über konsoleh aktiviert werden :wink:
  • Man könnte soweit gehen, die Anmeldung nur noch per SSH-Key zuzulassen. Dazu muss in /etc/ssh/sshd_config der Parameter PasswordAuthentication auf “no” gesetzt werden.
  • Last but not least: Mit dieser Servereinrichtung können natürlich auch andere Systeme mittels composer aufgesetzt werden.

Ich werfe diese Anleitung mal mit in https://hub.shopware.com :wink:

1 „Gefällt mir“

Gerne, freue mich für jeden, der dadurch Zeit sparen kann.

Welche anderen Systeme meinst du?

Ich würde mich noch freuen, wenn irgendjemand eine detaillierte Anleitung für eine Migration durchführt von einem zum anderen oder eine 1:1 Kopie macht.

Am besten von einer Shopware 6 Installation ohne Docker zu einer mit Docker. Und am liebsten wenn beide 6.7.8 haben :smiley:

Mit der Anleitung kann man jeden X-beliebigen Docker-Container auf dem Zielsystem hochfahren. Ich habe das auf dem gleichen Server mit Stirling PDF, DocuSeal und SnipeIT gemacht. Das geht sicher genauso mit Typo, Drupal, WP, Contao or whatever :wink:

(Beitrag vom Verfasser gelöscht)

Update: Umfangreich aktualisiert am 19.03.2026
Anmerkungen und konstruktive Kritik wie immer sehr gerne!