- Symfony 7.3 backend running on PHP 8.3 FPM.
- React with Redux Toolkit on Node.js 20, built via Vite, Vitest, ESLint, and the i18n tooling.
- Docker Compose orchestrates the PHP backend, PostgreSQL 15, the static frontend, and the Traefik-based ingress/ACME stack used in production.
- Live status & monitoring: https://status.silentoakranch.de
Composer packages are pinned to stable versions for reproducible builds. Notable constraints include endroid/qr-code-bundle (^6.0) and stripe/stripe-php (^14.0).
After cloning, align your local tooling with the containers and CI pipeline (PHP 8.3 and Node.js 20) before installing dependencies:
cd backend
composer install --ignore-platform-req=ext-sodium
cd ../frontend
npm ci
npm run buildIf you want Composer to run inside the PHP 8.3 container instead of installing PHP locally, execute:
docker compose run --rm backend composer install --ignore-platform-req=ext-sodiumCreate a project-wide .env file from the template before starting Docker Compose or running Symfony commands:
cp .env.example .envPopulate every mandatory entry from .env.example:
POSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_DB,DATABASE_URL– configure the PostgreSQL 15 service that the backend reaches via the internal hostdb:5432.APP_ENV,APP_SECRET– choose the Symfony environment (devfor local,prodfor deployments) and generate a 64-character secret, e.g.php -r 'echo bin2hex(random_bytes(32));'.DOMAIN,BOOKING_DOMAIN,LETSENCRYPT_EMAIL,TRUSTED_PROXIES,TRUSTED_HOSTS– define the public hostnames and proxy settings consumed by Traefik and the application containers.STATUS_DASH_AUTH_USERS,OAUTH2_PROXY_CLIENT_ID,OAUTH2_PROXY_CLIENT_SECRET,OAUTH2_PROXY_COOKIE_SECRET,OAUTH2_PROXY_OIDC_ISSUER_URL– protect the monitoring dashboards (basic auth for Uptime Kuma, OAuth2 Proxy in front of Grafana).ALERTMANAGER_SMTP_*,ALERTMANAGER_EMAIL_TO,ALERTMANAGER_TELEGRAM_*– configure alert delivery targets.STORAGE_BOX_REMOTE– rclone remote name for Hetzner Storage Box uploads used byscripts/backup.sh.STRIPE_SECRET_KEY,VITE_STRIPE_PUBLISHABLE_KEY– supply the backend secret key and frontend publishable key used during Stripe Checkout. The publishable key must be available when the frontend builds; export it before runningnpm run buildor provide it via.envsodocker compose build frontendreceives the same value. Otherwise Vite renders the Stripe button in a disabled state. Mirror the value in theVITE_STRIPE_PUBLISHABLE_KEYGitHub secret so the CI and deployment workflows inject the key as a Docker build argument.JWT_SECRET_KEY,JWT_PUBLIC_KEY,JWT_PASSPHRASE– point to the LexikJWT key pair and provide the matching passphrase (generate the keys withdocker compose run --rm backend php bin/console lexik:jwt:generate-keypair --overwrite).AGREEMENT_SIGNATURE_CERTIFICATE_PATH,AGREEMENT_SIGNATURE_PRIVATE_KEY_PATH,AGREEMENT_SIGNATURE_PRIVATE_KEY_PASSPHRASE– configure the X.509 certificate and encrypted private key used to sign digitally generated agreements. Keep the files outside the Git repository (e.g../shared/agreements/signing/). Docker Compose mounts that directory into the backend container at/var/www/backend/config/agreements/, so dropping the certificate and key there satisfies the paths.SIGNATURE_API_URL,SIGNATURE_TTL_DAYS– configure the optional external PKCS7 validation endpoint and signature validity window for the contract verification API.MESSENGER_TRANSPORT_DSN,WHATSAPP_DSN,SMS_DSN– configure the messenger transports for asynchronous processing and notifications.SMTP_HOST,SMTP_PORT,SMTP_USERNAME,SMTP_PASSWORD– configure outbound mail delivery.
Create ./shared/backend/var on the host (the repository provides the directory and .gitkeep files) so the bind-mounted var/ path used by the backend remains writable between deployments. Agreements and invoices generated by the Symfony services are stored there alongside their signature metadata.
Create ./shared/audit and keep it writable by the backend container; the append-only WORM replica for audit events is written there via the audit-log volume.
Optional development helpers such as VAR_DUMPER_SERVER can stay commented out or be set as needed. Docker Compose loads .env via env_file so every container receives the same credentials.
cd backend && composer install --ignore-platform-req=ext-sodiumcd frontend && npm ci- React Router is configured with
futureflags (v7_startTransitionandv7_relativeSplatPath) preparing the project for React Router v7. - The frontend
package.jsonuses"type": "module"so tooling like PostCSS runs in ES module mode.
composer install
vendor/bin/phpunit
npm run lint
npm test
npm run buildUse the bundled repo helper to discover the major components that make up Silent
Oak Ranch. The command reads codex.json and renders a short table so you can
quickly jump to the relevant directory:
./repo list --allPassing --all expands the table with supporting directories such as
docs/, monitoring/, proxy/, scripts/, and the shared bind mounts that
power the Docker services.
The backend uses PHPUnit 12. To run the test suite without deprecations or skipped tests:
cd backend
./bin/phpunit --testdoxTests that depend on external services should be tagged with @group external and are excluded from the core suite by default.
Static analysis is handled by PHPStan. Run it from the backend directory so the bundled configuration is picked up automatically:
cd backend
vendor/bin/phpstan analyseThe default memory limit is --memory-limit=512M. Override it locally if needed:
cd backend
vendor/bin/phpstan analyse --memory-limit=1G- PHPStan-Level 8 prüft den MeController auf korrekte Typen; Fehler durch falsche Signatur wurden korrigiert.
The Horse ↔ StallUnit relationship now stores a nullable stall_unit_id foreign key on the horse table. Horses reference stall units through a ManyToOne association, and each stall unit provides the inverse side with OneToMany or OneToOne mapping depending on configuration. Removing a stall unit automatically sets horse.stall_unit_id to NULL instead of deleting the horse. After pulling these changes, regenerate and apply migrations:
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrateOnce migrations are applied, validate the ORM mapping and run the test suite to check for regressions:
php bin/console doctrine:schema:validate
./bin/phpunit --testdoxAll npm vulnerabilities have been resolved. Keep dependencies current and check for new issues regularly:
# PHP dependencies
composer update
composer audit
# JavaScript dependencies
npm update
npm audit
npm audit fixSome environments lack the ext-sodium PHP extension. Use
composer install --ignore-platform-req=ext-sodium to bypass the requirement during installation.
The Pferdewaage service guides horse owners from reservation to result:
- Booking - reserve a timeslot online.
- Confirmation - receive a confirmation containing a QR code.
- Payment - complete payment to secure the slot.
- Weighing - on site, scan the QR code for check-in and automatic weight capture.
Flow: booking → confirmation → payment → weighing.
git remote add origin <REMOTE_URL> git push -u origin main git tag v1.0.0 git push origin v1.0.0
GitHub Actions automates the Docker workflows:
.github/workflows/ci.ymlbuilds the PHP 8.3 backend image, runs PHPStan and PHPUnit inside it, and lints/tests the frontend with Node.js 20..github/workflows/deploy.ymluses Docker Buildx to build the backend and frontend images, exports them as artifacts, and synchronises them to the target host before starting the stack and applying Doctrine migrations during deployment.
ℹ️ Für einen Gesamtüberblick über das hybride WordPress/Symfony-Setup inklusive Compose-Services, Plugin-Konfiguration und HMAC-Fluss siehe
docs/hybrid-setup.md.
ℹ️ Details zur Audit- und Signatur-Compliance inklusive WORM-Speicherstrategie finden sich in
docs/audit-compliance.md.
To deploy with Docker Compose manually:
-
Prepare
.envas described in Environment configuration. For production ensure PostgreSQL credentials remain pointed at the internaldbservice, replace the secret/API placeholders (Stripe, SMTP, messenger DSNs, domain settings), and generate the LexikJWT key pair inside the backend container:docker compose run --rm backend php bin/console lexik:jwt:generate-keypair --overwrite
The keys are written to
/var/www/backend/config/jwt/private.pemand/var/www/backend/config/jwt/public.pem; keep the corresponding paths andJWT_PASSPHRASEin sync inside.env. -
Start the stack (image builds complete without a database connection because migrations now run at deploy time):
docker compose up -d --build
-
Apply database migrations once the containers are online:
docker compose exec backend php bin/console doctrine:migrations:migrate --no-interaction -
Verify that Symfony can connect to PostgreSQL from inside the backend container:
docker compose run --rm backend php bin/console doctrine:query:sql 'SELECT 1'The command executes a lightweight SQL query via Doctrine and confirms that the configured credentials work end to end.
- All Composer and NPM dependencies are installed automatically during the image build.
- The frontend is served as an Nginx static server.
The nginx-proxy and acme-companion services share the same certificate, vhost, and webroot volumes. The companion container
requests and renews Let's Encrypt certificates automatically. To confirm the proxy is managing certificates correctly, check the
logs after bringing the stack online:
docker compose logs proxyProduction hosts typically wrap docker compose up -d in a dedicated systemd unit with Restart=always so systemctl restart silentoakranch.service maps to a full container restart (docker compose restart). Even without systemd you can refresh a single
service with docker restart <container_name> during maintenance windows. Pair restart automation with routine health checks, for
example:
docker compose ps --format 'table {{.Name}}\t{{.State}}\t{{.Health}}'
docker inspect --format '{{json .State.Health}}' silentoakranch-backend-1The first command highlights failing health checks at a glance, while the second prints the raw probe history for detailed troubleshooting.
Successful output shows the generated certificates under /etc/nginx/certs and that renewal jobs run without errors.
- CI status – confirm the latest runs of
ci.ymlanddeploy.ymlin GitHub Actions are green before promoting a build. - SSL renewal – review the
acme-companionlogs weekly to ensure Let's Encrypt renewals finish without rate-limit warnings. - Restart policy – verify systemd units and Docker Compose services use
restart: always/unless-stoppedto survive host reboots. - Backup routine – schedule nightly PostgreSQL dumps (e.g.
pg_dumpto./shared/backups/) and copy them off-site alongside uploaded agreements.
PostgreSQL stores its data in the named Docker volume db-data declared in docker-compose.yml. The former ./db/data bind
mount is no longer used; remove any legacy db/ directory after switching to the volume so stale files do not confuse local
setups. Named volumes are managed by Docker and survive docker compose down, meaning database contents persist across
redeployments. Only prune them intentionally—for example with docker volume prune or docker volume rm db-data—after
creating a backup (e.g. via pg_dump) to avoid losing production data.
./shared/backend/var→/var/www/backend/var: bind-mounted storage that keeps the PDF agreements and invoices produced byAgreementServiceandInvoiceService. Subdirectories (agreements/per-user folders andinvoices/) are committed with.gitkeepfiles so the structure exists before the container starts. Ensure the host path is writable by the container user (typicallywww-data) or relax permissions accordingly../shared/agreements/signing→/var/www/backend/config/agreements: stores the X.509 certificate and private key referenced byAGREEMENT_SIGNATURE_CERTIFICATE_PATHandAGREEMENT_SIGNATURE_PRIVATE_KEY_PATH. Keep the real files out of Git and copy them into this directory prior to deploying.
Existing environments that previously stored agreements and invoices inside the container should back up /var/www/backend/var before upgrading. After pulling the update, stop the stack, copy the files into ./shared/backend/var on the host, and redeploy. The new bind-mount ensures the historical documents remain accessible across future container rebuilds.
The included nginx-proxy and acme-companion automatically request and renew TLS certificates via Let's Encrypt. Ensure the companion sees the proxy by exporting NGINX_PROXY_CONTAINER=proxy (as done in docker-compose.yml) or by giving the proxy container that name before starting the stack; otherwise certificate discovery fails. The proxy routes requests based on the path:
- https://app.silent-oak-ranch.de → Frontend (Port 80)
- https://app.silent-oak-ranch.de/api → Backend (Port 8080)
The backend expects the LexikJWT keys in config/jwt. To keep them outside of the Docker image the directory is bind-mounted from the host (./shared/jwt/backend ↔ /var/www/backend/config/jwt). The mount point is ignored by Git so generated keys never end up in the repository.
- Ensure that
.envcontains aJWT_PASSPHRASE. The default value is only meant for local testing – change it before exposing the stack. - Start the containers with
docker compose up -d --build. The runtime entrypoint checks forconfig/jwt/private.pemandconfig/jwt/public.pem. If either file is missing,php bin/console lexik:jwt:generate-keypair --overwrite --no-interactionis executed automatically and the freshly generated pair is stored on the mounted volume. - To rotate keys manually run
docker compose run --rm backend php bin/console lexik:jwt:generate-keypair --overwrite --no-interactionand restart the backend container.
-
Keep the
shared/jwt/backendmount (or a similar host path) persistent so that redeployments reuse the existing key pair. Adjust ownership/permissions on the host to restrict access to the files. -
Alternatively, load the keys via container secrets. For example:
docker secret create backend_jwt_private config/jwt/private.pem docker secret create backend_jwt_public config/jwt/public.pem
Mount the secrets into the container and point
JWT_SECRET_KEY/JWT_PUBLIC_KEYto the secret paths before starting the stack. -
Whenever the passphrase or keys change, restart the backend service so Symfony reloads the credentials.
CI builds artifacts for each successful workflow run. After verifying the build in CI, deploy the artifact manually:
scripts/deploy.sh <build-id> <target-dir>Use a dry run to test the commands without making changes:
bash scripts/deploy.sh --dry-run 123 /tmp/deployIn dry-run mode the script logs each command instead of executing it. It also creates dummy artifact and target directories, making it safe for testing. Real deployments run the commands and apply the downloaded artifacts to the target directory. Full automation with Docker or Kubernetes may be added later.
Der Rechnungsprozess umfasst folgende Schritte:
- Payment – Nach erfolgreicher Zahlung wird eine Rechnung erzeugt.
- PDF-Erstellung – Das System erstellt eine gebrandete PDF-Datei mit ausgewiesener Mehrwertsteuer.
- E-Mail-Versand – Die Rechnung wird an die hinterlegte Adresse versendet.
- Portal-Anzeige – Im Kundenportal steht die Rechnung zusätzlich zum Download bereit.
Jede Rechnung muss konsistentes Branding tragen und die gesetzliche Umsatzsteuer klar anzeigen.
Die Reko-Dokumentation unterteilt sich in drei Pakete:
- BASIS – einfache Erstellung und Verwaltung von Reko-Einträgen.
- STANDARD – alle BASIS-Funktionen plus Kategorisierung und Filter.
- PREMIUM – kompletter Funktionsumfang inklusive Export.
- Im Bereich Reko-Dokumentation „Neuer Eintrag“ wählen.
- Pflichtfelder wie Datum, Kategorie und Beschreibung ausfüllen.
- Speichern.
Premium-Nutzende können ihre Reko-Einträge als CSV oder Excel exportieren.
Der Terminprozess begleitet Nutzer*innen von der Anfrage bis zur optionalen Rechnungsstellung:
Anfrage -> Bestätigung -> Erinnerungen -> Durchführung -> (Rechnung)
Erinnerungen werden zwingend per E-Mail versendet. Optional können zusätzliche Hinweise per WhatsApp oder SMS erfolgen.
Bestätigungs-, Erinnerungs- und Absage-E-Mails werden mit deutschen und englischen Texten versendet.
Der Vertragsbereich unterstützt die komplette Verwaltung von Vertragsdokumenten:
- Anlage – Neue Verträge werden im System erfasst.
- Verwaltung – Bestehende Verträge lassen sich prüfen, bearbeiten und archivieren.
- Signatur – Optional können Verträge digital signiert werden.