diff --git a/.env.docker.production.example b/.env.docker.production.example index fdf1bae..42f3e7f 100644 --- a/.env.docker.production.example +++ b/.env.docker.production.example @@ -1,26 +1,26 @@ # ── Traefik domains — used in docker-compose labels ────────────────────────── -ACME_EMAIL=rentaldrivego@gmail.com -API_DOMAIN=api.rentaldrivego.ma -PUBLIC_SITE_DOMAIN=rentaldrivego.ma -PGMANAGE_DOMAIN=pgmanage.rentaldrivego.ma -PORTAINER_DOMAIN=portainer.rentaldrivego.ma +ACME_EMAIL=ops@example.com +API_DOMAIN=api.example.com +PUBLIC_SITE_DOMAIN=example.com +PGMANAGE_DOMAIN=pgmanage.example.com +PORTAINER_DOMAIN=portainer.example.com # ── Optional prebuilt image source ──────────────────────────────────────────── # Used for pull-based deployments. CI injects APP_IMAGE and APP_VERSION # automatically during registry-backed deploys, so these can stay commented out. -# APP_IMAGE=gitlab.rentaldrivego.ma:5050/rental_car/car_management_system +# APP_IMAGE=registry.example.com/rentaldrivego/car_management_system # APP_VERSION=latest # ── Database ────────────────────────────────────────────────────────────────── # Full connection URL (used when DATABASE_URL_FROM_POSTGRES=false) -DATABASE_URL=postgresql://postgres:24DY&1u5FLCkNMeiO_p@postgres:5432/rentaldrivego +DATABASE_URL=postgresql://postgres:change-me@postgres:5432/rentaldrivego # Build the URL from individual vars (set to true to use POSTGRES_* vars below) DATABASE_URL_FROM_POSTGRES=true POSTGRES_HOST=postgres POSTGRES_PORT=5432 POSTGRES_DB=rentaldrivego POSTGRES_USER=postgres -POSTGRES_PASSWORD=24DY&1u5FLCkNMeiO_p +POSTGRES_PASSWORD=change-me # ── Cache ───────────────────────────────────────────────────────────────────── REDIS_URL=redis://redis:6379 @@ -30,44 +30,44 @@ API_PORT=4000 # SSR server-side calls go via the internal Docker network API_INTERNAL_URL=http://api:4000/api/v1 # Public-facing API URL visible to browsers — MUST be your real domain, not localhost -API_URL=https://api.rentaldrivego.ma +API_URL=https://api.example.com # Baked into Next.js bundles at build time — MUST be set before running `npm run build` -NEXT_PUBLIC_API_URL=https://api.rentaldrivego.ma/api/v1 +NEXT_PUBLIC_API_URL=https://api.example.com/api/v1 # ── Frontend public URLs ────────────────────────────────────────────────────── # Marketplace sits at the domain root (Traefik routes /dashboard and /admin to their apps) -NEXT_PUBLIC_MARKETING_URL=https://rentaldrivego.ma -NEXT_PUBLIC_MARKETPLACE_URL=https://rentaldrivego.ma -NEXT_PUBLIC_DASHBOARD_URL=https://rentaldrivego.ma/dashboard -NEXT_PUBLIC_ADMIN_URL=https://rentaldrivego.ma/admin -NEXT_PUBLIC_PUBLIC_SITE_DOMAIN=rentaldrivego.ma -DASHBOARD_URL=https://rentaldrivego.ma/dashboard -ADMIN_URL=https://rentaldrivego.ma/admin +NEXT_PUBLIC_MARKETING_URL=https://example.com +NEXT_PUBLIC_MARKETPLACE_URL=https://example.com +NEXT_PUBLIC_DASHBOARD_URL=https://example.com/dashboard +NEXT_PUBLIC_ADMIN_URL=https://example.com/admin +NEXT_PUBLIC_PUBLIC_SITE_DOMAIN=example.com +DASHBOARD_URL=https://example.com/dashboard +ADMIN_URL=https://example.com/admin # ── CORS ────────────────────────────────────────────────────────────────────── # Comma-separated list of allowed browser origins. REQUIRED in production. -CORS_ORIGINS=https://rentaldrivego.ma +CORS_ORIGINS=https://example.com,https://www.example.com # ── Auth ────────────────────────────────────────────────────────────────────── -JWT_SECRET=PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK +JWT_SECRET=replace-with-a-long-random-secret JWT_EXPIRY=8h RENTER_JWT_EXPIRY=7d NODE_ENV=production # ── Email ───────────────────────────────────────────────────────────────────── -EMAIL_FROM=rentaldrivego@gmail.com +EMAIL_FROM=noreply@example.com EMAIL_FROM_NAME=RentalDriveGo # Option A — Resend -#RESEND_API_KEY=C8qPDuFwsv5l@KsGhL/V +#RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx # Option B — SMTP (Gmail) MAIL_HOST=smtp.gmail.com MAIL_PORT=587 MAIL_SCHEME=smtp -MAIL_USERNAME=rentaldrivego@gmail.com -MAIL_PASSWORD="xmhg ibqy muoc rntc" -MAIL_FROM_ADDRESS=rentaldrivego@gmail.com +MAIL_USERNAME=your-smtp-user@example.com +MAIL_PASSWORD=replace-with-an-app-password +MAIL_FROM_ADDRESS=noreply@example.com MAIL_FROM_NAME=RentalDriveGo -MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com +MAIL_REPLY_TO_ADDRESS=support@example.com MAIL_REPLY_TO_NAME=RentalDriveGo # ── Firebase push notifications (optional) ──────────────────────────────────── diff --git a/.env.docker.production.generated b/.env.docker.production.generated index be28b61..2a229b6 100644 --- a/.env.docker.production.generated +++ b/.env.docker.production.generated @@ -1,9 +1,9 @@ # Traefik domains -ACME_EMAIL=rentaldrivego@gmail.com -API_DOMAIN=api.rentaldrivego.ma -PUBLIC_SITE_DOMAIN=rentaldrivego.ma -PGMANAGE_DOMAIN=pgmanage.rentaldrivego.ma -PORTAINER_DOMAIN=portainer.rentaldrivego.ma +ACME_EMAIL=ops@example.com +API_DOMAIN=api.example.com +PUBLIC_SITE_DOMAIN=example.com +PGMANAGE_DOMAIN=pgmanage.example.com +PORTAINER_DOMAIN=portainer.example.com # Database DATABASE_URL_FROM_POSTGRES=true @@ -11,8 +11,8 @@ POSTGRES_HOST=postgres POSTGRES_PORT=5432 POSTGRES_DB=rentaldrivego POSTGRES_USER=postgres -POSTGRES_PASSWORD=24DY&1u5FLCkNMeiO_p -DATABASE_URL=postgresql://postgres:24DY&1u5FLCkNMeiO_p@postgres:5432/rentaldrivego +POSTGRES_PASSWORD=change-me +DATABASE_URL=postgresql://postgres:change-me@postgres:5432/rentaldrivego # Cache REDIS_URL=redis://redis:6379 @@ -20,23 +20,23 @@ REDIS_URL=redis://redis:6379 # API API_PORT=4000 API_INTERNAL_URL=http://api:4000/api/v1 -API_URL=https://api.rentaldrivego.ma -NEXT_PUBLIC_API_URL=https://api.rentaldrivego.ma/api/v1 +API_URL=https://api.example.com +NEXT_PUBLIC_API_URL=https://api.example.com/api/v1 # Frontend public URLs -NEXT_PUBLIC_MARKETING_URL=https://rentaldrivego.ma -NEXT_PUBLIC_MARKETPLACE_URL=https://rentaldrivego.ma -NEXT_PUBLIC_DASHBOARD_URL=https://rentaldrivego.ma/dashboard -NEXT_PUBLIC_ADMIN_URL=https://rentaldrivego.ma/admin -NEXT_PUBLIC_PUBLIC_SITE_DOMAIN=rentaldrivego.ma -DASHBOARD_URL=https://rentaldrivego.ma/dashboard -ADMIN_URL=https://rentaldrivego.ma/admin +NEXT_PUBLIC_MARKETING_URL=https://example.com +NEXT_PUBLIC_MARKETPLACE_URL=https://example.com +NEXT_PUBLIC_DASHBOARD_URL=https://example.com/dashboard +NEXT_PUBLIC_ADMIN_URL=https://example.com/admin +NEXT_PUBLIC_PUBLIC_SITE_DOMAIN=example.com +DASHBOARD_URL=https://example.com/dashboard +ADMIN_URL=https://example.com/admin # CORS -CORS_ORIGINS=https://rentaldrivego.ma,https://www.rentaldrivego.ma +CORS_ORIGINS=https://example.com,https://www.example.com # Auth -JWT_SECRET=PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK +JWT_SECRET=replace-with-a-long-random-secret JWT_EXPIRY=8h RENTER_JWT_EXPIRY=7d NODE_ENV=production @@ -46,17 +46,17 @@ FILE_STORAGE_ROOT=/var/lib/rentaldrivego/storage # PgManage PGMANAGE_DEFAULT_USERNAME=admin -PGMANAGE_DEFAULT_PASSWORD=24DY&1u5FLCkNMeiO_p +PGMANAGE_DEFAULT_PASSWORD=change-me # Email -EMAIL_FROM=rentaldrivego@gmail.com +EMAIL_FROM=noreply@example.com EMAIL_FROM_NAME=RentalDriveGo MAIL_HOST=smtp.gmail.com MAIL_PORT=587 MAIL_SCHEME=smtp -MAIL_USERNAME=rentaldrivego@gmail.com -MAIL_PASSWORD=xmhg ibqy muoc rntc -MAIL_FROM_ADDRESS=rentaldrivego@gmail.com +MAIL_USERNAME=your-smtp-user@example.com +MAIL_PASSWORD=replace-with-an-app-password +MAIL_FROM_ADDRESS=noreply@example.com MAIL_FROM_NAME=RentalDriveGo -MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com +MAIL_REPLY_TO_ADDRESS=support@example.com MAIL_REPLY_TO_NAME=RentalDriveGo diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 63bc74b..acdf840 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,6 +5,7 @@ stages: variables: DOCKERFILE_PATH: Dockerfile.production + DEPLOY_ROOT: /opt/rentaldrivego # Required: disable TLS so the docker client can reach the dind daemon DOCKER_TLS_CERTDIR: "" DOCKER_HOST: tcp://docker:2375 @@ -141,35 +142,27 @@ deploy_to_vps: script: - echo "--- Deploying $DOCKER_IMAGE to VPS ---" - | + REGISTRY_PASSWORD_B64="$(printf '%s' "$REGISTRY_PASSWORD" | base64 | tr -d '\n')" + + echo "-- Syncing deployment assets --" ssh $VPS_USER@$VPS_IP " set -e - cd /opt/rentaldrivego + mkdir -p '$DEPLOY_ROOT/scripts' '$DEPLOY_ROOT/docker/pgmanage' + " + scp docker-compose.production.yml docker-compose.portainer.production.yml traefik.yaml $VPS_USER@$VPS_IP:$DEPLOY_ROOT/ + scp scripts/docker-prod-common.sh scripts/docker-prod-deploy.sh $VPS_USER@$VPS_IP:$DEPLOY_ROOT/scripts/ + scp docker/pgmanage/override.py $VPS_USER@$VPS_IP:$DEPLOY_ROOT/docker/pgmanage/ - echo '-- Pulling latest code --' - git pull - - echo '-- Logging into registry --' - test -n '$REGISTRY_HOST' || { echo 'Registry host is not set.'; exit 1; } - test -n '$REGISTRY_USER' || { echo 'Registry user is not set.'; exit 1; } - test -n '$REGISTRY_PASSWORD' || { echo 'Registry password is not set.'; exit 1; } - echo '$REGISTRY_PASSWORD' | docker login '$REGISTRY_HOST' -u '$REGISTRY_USER' --password-stdin - - echo '-- Pulling pre-built image --' - docker pull $DOCKER_IMAGE - - echo '-- Ensuring uploads volume exists --' - docker volume create rentaldrivego-prod_api_uploads >/dev/null - - echo '-- Restarting services --' - APP_IMAGE=$IMAGE_REPOSITORY \ - APP_VERSION=$CI_COMMIT_SHORT_SHA \ - docker compose -p rentaldrivego-prod \ - --env-file .env.docker.production \ - -f docker-compose.production.yml \ - up -d --no-build - - echo '-- Pruning old images --' - docker image prune -f + echo "-- Running deploy script on VPS --" + ssh $VPS_USER@$VPS_IP " + set -e + cd '$DEPLOY_ROOT' + APP_IMAGE='$IMAGE_REPOSITORY' \ + APP_VERSION='$CI_COMMIT_SHORT_SHA' \ + REGISTRY_HOST='$REGISTRY_HOST' \ + REGISTRY_USER='$REGISTRY_USER' \ + REGISTRY_PASSWORD=\$(printf '%s' '$REGISTRY_PASSWORD_B64' | base64 -d) \ + bash scripts/docker-prod-deploy.sh " rules: # Deploy only when both registry access and VPS credentials are available. diff --git a/docker-compose.production.yml b/docker-compose.production.yml index 8867b80..9b9a03b 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -26,6 +26,32 @@ services: volumes: - redis_prod_data:/data + migrate: + image: ${APP_IMAGE:-rentaldrivego-prod-app}:${APP_VERSION:-latest} + build: + context: . + dockerfile: Dockerfile.production + args: + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL} + NEXT_PUBLIC_MARKETPLACE_URL: ${NEXT_PUBLIC_MARKETPLACE_URL} + NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL} + NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL} + command: ["npm", "run", "db:deploy"] + networks: + - internal + depends_on: + postgres: + condition: service_healthy + env_file: + - .env.docker.production + environment: + DATABASE_URL_FROM_POSTGRES: ${DATABASE_URL_FROM_POSTGRES:-true} + POSTGRES_HOST: ${POSTGRES_HOST:-postgres} + POSTGRES_PORT: ${POSTGRES_PORT:-5432} + POSTGRES_DB: ${POSTGRES_DB:-rentaldrivego} + POSTGRES_USER: ${POSTGRES_USER:-postgres} + restart: "no" + api: image: ${APP_IMAGE:-rentaldrivego-prod-app}:${APP_VERSION:-latest} build: @@ -37,12 +63,7 @@ services: timeout: 10s retries: 5 start_period: 60s - command: - - bash - - -c - - | - npm run db:deploy && - npm run start --workspace @rentaldrivego/api + command: ["npm", "run", "start", "--workspace", "@rentaldrivego/api"] networks: - internal - traefik-proxy @@ -92,7 +113,8 @@ services: - internal - traefik-proxy depends_on: - - api + api: + condition: service_healthy env_file: - .env.docker.production restart: unless-stopped @@ -127,7 +149,8 @@ services: - internal - traefik-proxy depends_on: - - api + api: + condition: service_healthy env_file: - .env.docker.production restart: unless-stopped @@ -162,7 +185,8 @@ services: - internal - traefik-proxy depends_on: - - api + api: + condition: service_healthy env_file: - .env.docker.production restart: unless-stopped diff --git a/docs/DOCKER.md b/docs/DOCKER.md index 2388014..9cb4fd7 100644 --- a/docs/DOCKER.md +++ b/docs/DOCKER.md @@ -182,27 +182,27 @@ REGISTRY_USER= REGISTRY_PASSWORD= ``` -The production compose file now reads `APP_IMAGE` and `APP_VERSION` for pull-based deploys. The GitLab deploy job injects those values automatically. If you want to pull manually on the server, you can also set `APP_IMAGE` in `.env.docker.production`. +The production compose file reads `APP_IMAGE` and `APP_VERSION` for pull-based deploys. The GitLab deploy job injects those values automatically and now syncs the deployment assets to the VPS before running the server-side deploy script. Production no longer depends on `git pull` during release. The CI pipeline publishes and deploys from the GitLab default branch (`$CI_DEFAULT_BRANCH`). If you change your release branch, update the repository default branch in GitLab instead of hard-coding branch names in `.gitlab-ci.yml`. If you use `docker:dind` on a self-hosted GitLab runner, the runner's Docker executor must be started with `privileged = true`. Without that, `docker:dind` often logs AppArmor or `/sys/kernel/security` mount errors and can become unreliable even when the job container still starts. -#### 5. Start Traefik +#### 5. Bootstrap the server once -Traefik must be running before the app stack so it can wire up routes at startup. +Traefik must be running before the app stack so it can wire up routes at startup. This bootstrap path is for the first server setup or for deliberate source-based rebuilds. Normal production releases should go through GitLab CI. ```bash -docker compose -f traefik.yaml up -d +bash scripts/docker-prod-up-traefik.sh ``` -#### 6. Build and start the app stack +If you intentionally want to build from source on the server: ```bash npm run docker:prod:up ``` -Or use the new helper scripts if you want to start one production container at a time: +Or use the helper scripts if you want to start one production container at a time: ```bash npm run docker:prod:start:traefik @@ -220,11 +220,22 @@ npm run docker:prod:start:api && npm run docker:prod:start:frontends ``` Docker will: -1. Build the monorepo image +1. Build the monorepo image on the server 2. Start all app services (`api`, `marketplace`, `dashboard`, `admin`, `pgmanage`) Traefik automatically picks up the containers and provisions TLS certificates. Services are live at their `https://` URLs within ~30 seconds. +#### 6. Standard release flow + +Normal production releases should use GitLab CI: + +1. CI builds and pushes a single versioned image. +2. CI copies the current deployment assets (`docker-compose.production.yml`, Traefik config, deploy scripts, pgManage override) to the VPS. +3. CI runs `bash scripts/docker-prod-deploy.sh` on the VPS with `APP_IMAGE` and `APP_VERSION` pinned to the commit being released. +4. The deploy script pulls the release image, starts Postgres and Redis, runs `npm run db:deploy` as a one-shot migration job, then starts the API and frontends and waits for their health checks. + +This avoids the old drift where the VPS checkout changed independently through `git pull`, or where API startup silently mixed migrations with application boot. + #### 6a. Deploy Portainer Portainer is deployed separately from the main app stack and reuses the shared `traefik-proxy` network managed by Traefik. @@ -245,25 +256,25 @@ The production helper scripts and the GitLab deploy job now explicitly create th #### Updating after a code change -Pull the latest code and rebuild only the changed service: +Push to the GitLab default branch and let the pipeline deploy the pinned image. That is the supported production release path. + +If CI is unavailable and you need to redeploy the image already published to the registry from the server: ```bash -git pull -docker compose -p rentaldrivego-prod --env-file .env.docker.production -f docker-compose.production.yml up --build -d --no-deps -# e.g. to redeploy only the API: -docker compose -p rentaldrivego-prod --env-file .env.docker.production -f docker-compose.production.yml up --build -d --no-deps api +APP_IMAGE=registry.example.com/rentaldrivego/car_management_system \ +APP_VERSION= \ +REGISTRY_HOST=registry.example.com \ +REGISTRY_USER= \ +REGISTRY_PASSWORD= \ +bash scripts/docker-prod-deploy.sh ``` -To rebuild everything: - -```bash -npm run docker:prod:up -``` +Only use `npm run docker:prod:up` when you intentionally want a source build on the server. #### Apply database migrations without downtime ```bash -docker compose -p rentaldrivego-prod --env-file .env.docker.production -f docker-compose.production.yml run --rm api npm run db:deploy +docker compose -p rentaldrivego-prod --env-file .env.docker.production -f docker-compose.production.yml run --rm migrate ``` #### Create the first production admin diff --git a/package.json b/package.json index 5266e49..f079d96 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "docker:prod:start:frontends": "bash scripts/docker-prod-up-frontends.sh", "docker:prod:start:pgmanage": "bash scripts/docker-prod-up-pgmanage.sh", "docker:prod:start:portainer": "bash scripts/docker-prod-up-portainer.sh", - "docker:prod:up": "docker compose -p rentaldrivego-prod --env-file .env.docker.production -f docker-compose.production.yml up --build -d", + "docker:prod:deploy": "bash scripts/docker-prod-deploy.sh", + "docker:prod:up": "bash scripts/docker-prod-up-all.sh", "docker:prod:down": "docker compose -p rentaldrivego-prod --env-file .env.docker.production -f docker-compose.production.yml down", "docker:prod:backup": "bash scripts/docker-prod-backup.sh", "docker:prod:restore": "bash scripts/docker-prod-restore.sh", diff --git a/scripts/docker-prod-common.sh b/scripts/docker-prod-common.sh index 54e51be..e36c512 100755 --- a/scripts/docker-prod-common.sh +++ b/scripts/docker-prod-common.sh @@ -50,9 +50,63 @@ traefik_compose() { start_prod_services() { ensure_env_file ensure_api_uploads_volume + + local services=("$@") + local needs_migrations=0 + + for service in "${services[@]}"; do + case "${service}" in + api|marketplace|dashboard|admin) + needs_migrations=1 + break + ;; + esac + done + + if [[ ${needs_migrations} -eq 1 ]]; then + prod_compose up --build -d postgres redis + wait_for_healthy postgres 120 + prod_compose run --rm --build migrate + fi + prod_compose up --build -d "$@" } +require_release_image() { + if [[ -z "${APP_IMAGE:-}" || -z "${APP_VERSION:-}" ]]; then + echo "APP_IMAGE and APP_VERSION must be set for pull-based production deploys." >&2 + exit 1 + fi +} + +login_registry_if_configured() { + local configured=0 + + [[ -n "${REGISTRY_HOST:-}" ]] && configured=$((configured + 1)) + [[ -n "${REGISTRY_USER:-}" ]] && configured=$((configured + 1)) + [[ -n "${REGISTRY_PASSWORD:-}" ]] && configured=$((configured + 1)) + + if [[ ${configured} -eq 0 ]]; then + return 0 + fi + + if [[ ${configured} -ne 3 ]]; then + echo "REGISTRY_HOST, REGISTRY_USER, and REGISTRY_PASSWORD must either all be set or all be unset." >&2 + exit 1 + fi + + echo "${REGISTRY_PASSWORD}" | docker login "${REGISTRY_HOST}" -u "${REGISTRY_USER}" --password-stdin +} + +pull_prod_release_images() { + require_release_image + prod_compose pull migrate api marketplace dashboard admin +} + +run_prod_migrations() { + prod_compose run --rm migrate +} + wait_for_healthy() { local service="$1" local timeout="${2:-120}" diff --git a/scripts/docker-prod-deploy.sh b/scripts/docker-prod-deploy.sh new file mode 100644 index 0000000..3692a9a --- /dev/null +++ b/scripts/docker-prod-deploy.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${ROOT_DIR}/scripts/docker-prod-common.sh" + +echo "Deploying production release ${APP_IMAGE:-}:${APP_VERSION:-}" + +ensure_env_file +ensure_traefik_network +ensure_api_uploads_volume +require_release_image +login_registry_if_configured + +echo "Pulling release images" +pull_prod_release_images + +echo "Starting shared infrastructure" +start_traefik +prod_compose up -d postgres redis +wait_for_healthy postgres 120 + +echo "Running database migrations" +run_prod_migrations + +echo "Starting application services" +prod_compose up -d api marketplace dashboard admin pgmanage +wait_for_healthy api 180 +wait_for_healthy marketplace 180 +wait_for_healthy dashboard 180 +wait_for_healthy admin 180 + +echo "Pruning old images" +docker image prune -f >/dev/null + +echo "Production release ${APP_IMAGE}:${APP_VERSION} is up and healthy." diff --git a/scripts/docker-prod-up-all.sh b/scripts/docker-prod-up-all.sh index fa8d003..85c346d 100755 --- a/scripts/docker-prod-up-all.sh +++ b/scripts/docker-prod-up-all.sh @@ -6,7 +6,7 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" source "${ROOT_DIR}/scripts/docker-prod-common.sh" start_traefik -start_prod_services +start_prod_services postgres redis api marketplace dashboard admin pgmanage wait_for_healthy api wait_for_healthy marketplace wait_for_healthy dashboard