fix prod deployment
Build & Deploy / Build & Push Docker Image (push) Failing after 34s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 57s
Test / API Unit Tests (push) Failing after 52s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Storefront Unit Tests (push) Successful in 44s
Test / Admin Unit Tests (push) Successful in 41s
Test / Dashboard Unit Tests (push) Successful in 43s
Test / API Integration Tests (push) Has been cancelled
Build & Deploy / Build & Push Docker Image (push) Failing after 34s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 57s
Test / API Unit Tests (push) Failing after 52s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Storefront Unit Tests (push) Successful in 44s
Test / Admin Unit Tests (push) Successful in 41s
Test / Dashboard Unit Tests (push) Successful in 43s
Test / API Integration Tests (push) Has been cancelled
This commit is contained in:
@@ -4,34 +4,32 @@ API_DOMAIN=api.rentaldrivego.ma
|
|||||||
PUBLIC_SITE_DOMAIN=rentaldrivego.ma
|
PUBLIC_SITE_DOMAIN=rentaldrivego.ma
|
||||||
PGMANAGE_DOMAIN=pgmanage.rentaldrivego.ma
|
PGMANAGE_DOMAIN=pgmanage.rentaldrivego.ma
|
||||||
PORTAINER_DOMAIN=portainer.rentaldrivego.ma
|
PORTAINER_DOMAIN=portainer.rentaldrivego.ma
|
||||||
REGISTRY_DOMAIN=registry.rentaldrivego.ma
|
|
||||||
REGISTRY_UPSTREAM_URL=http://10.0.0.10:5000
|
|
||||||
|
|
||||||
# ── Optional prebuilt image source ────────────────────────────────────────────
|
# ── Optional prebuilt image source ────────────────────────────────────────────
|
||||||
# Used for pull-based deployments. CI injects APP_IMAGE and APP_VERSION
|
# Used for production deployments. CI injects IMAGE_TAG automatically during
|
||||||
# automatically during registry-backed deploys, so these can stay commented out.
|
# registry-backed deploys; set it manually when running docker compose directly.
|
||||||
|
IMAGE_TAG=latest
|
||||||
|
# APP_IMAGE and APP_VERSION are still accepted by helper scripts for compatibility.
|
||||||
# APP_IMAGE=registry.example.com/rentaldrivego/car_management_system
|
# APP_IMAGE=registry.example.com/rentaldrivego/car_management_system
|
||||||
# APP_VERSION=latest
|
# APP_VERSION=latest
|
||||||
REGISTRY_HOST=registry.rentaldrivego.ma
|
|
||||||
REGISTRY_USER=rentaldrivego_registry
|
|
||||||
REGISTRY_PASSWORD=placeholder
|
|
||||||
REGISTRY_IMAGE=registry.rentaldrivego.ma/rentaldrivego/car_management_system
|
|
||||||
REGISTRY_HTTP_SECRET=placeholder
|
|
||||||
|
|
||||||
# ── Database ──────────────────────────────────────────────────────────────────
|
# ── Database ──────────────────────────────────────────────────────────────────
|
||||||
# Full connection URL (used when DATABASE_URL_FROM_POSTGRES=false)
|
# Full connection URL (used when DATABASE_URL_FROM_POSTGRES=false)
|
||||||
DATABASE_URL=postgresql://placeholder:placeholder@placeholder:5432/placeholder
|
|
||||||
# Build the URL from individual vars (set to true to use POSTGRES_* vars below)
|
# Build the URL from individual vars (set to true to use POSTGRES_* vars below)
|
||||||
|
DATABASE_URL=postgresql://dbadmin:PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK@localhost:5432/rentaldrivego
|
||||||
DATABASE_URL_FROM_POSTGRES=true
|
DATABASE_URL_FROM_POSTGRES=true
|
||||||
POSTGRES_HOST=postgres
|
POSTGRES_HOST=postgres
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
POSTGRES_DB=rentaldrivego
|
POSTGRES_DB=rentaldrivego
|
||||||
POSTGRES_USER=postgres
|
POSTGRES_USER=dbadmin
|
||||||
POSTGRES_PASSWORD=placeholder
|
POSTGRES_PASSWORD=PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK
|
||||||
|
|
||||||
|
|
||||||
# ── Cache ─────────────────────────────────────────────────────────────────────
|
# ── Cache ─────────────────────────────────────────────────────────────────────
|
||||||
REDIS_URL=redis://redis:6379
|
REDIS_PASSWORD=PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK
|
||||||
|
REDIS_URL=redis://:PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK@redis:6379
|
||||||
|
|
||||||
# ── API ───────────────────────────────────────────────────────────────────────
|
# ── API ───────────────────────────────────────────────────────────────────────
|
||||||
API_PORT=4000
|
API_PORT=4000
|
||||||
@@ -53,6 +51,7 @@ SITE_ORIGIN=https://rentaldrivego.ma
|
|||||||
|
|
||||||
# Marketplace sits at the domain root (Traefik routes /dashboard and /admin to their apps)
|
# Marketplace sits at the domain root (Traefik routes /dashboard and /admin to their apps)
|
||||||
NEXT_PUBLIC_MARKETING_URL=https://rentaldrivego.ma
|
NEXT_PUBLIC_MARKETING_URL=https://rentaldrivego.ma
|
||||||
|
NEXT_PUBLIC_MARKETPLACE_URL=https://rentaldrivego.ma
|
||||||
NEXT_PUBLIC_STOREFRONT_URL=https://rentaldrivego.ma/storefront
|
NEXT_PUBLIC_STOREFRONT_URL=https://rentaldrivego.ma/storefront
|
||||||
NEXT_PUBLIC_DASHBOARD_URL=https://rentaldrivego.ma/dashboard
|
NEXT_PUBLIC_DASHBOARD_URL=https://rentaldrivego.ma/dashboard
|
||||||
NEXT_PUBLIC_ADMIN_URL=https://rentaldrivego.ma/admin
|
NEXT_PUBLIC_ADMIN_URL=https://rentaldrivego.ma/admin
|
||||||
@@ -67,7 +66,7 @@ ADMIN_URL=https://rentaldrivego.ma/admin
|
|||||||
CORS_ORIGINS=https://rentaldrivego.ma,https://www.rentaldrivego.ma
|
CORS_ORIGINS=https://rentaldrivego.ma,https://www.rentaldrivego.ma
|
||||||
|
|
||||||
# ── Auth ──────────────────────────────────────────────────────────────────────
|
# ── Auth ──────────────────────────────────────────────────────────────────────
|
||||||
JWT_SECRET=placeholder
|
JWT_SECRET=eb9ab3eb5d6648bdb82cbef29afde498c58f089829a037dfea6cb5e98ef2aae0
|
||||||
JWT_EXPIRY=8h
|
JWT_EXPIRY=8h
|
||||||
RENTER_JWT_EXPIRY=7d
|
RENTER_JWT_EXPIRY=7d
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
@@ -79,11 +78,12 @@ EMAIL_FROM_NAME=RentalDriveGo
|
|||||||
# Option A — Resend
|
# Option A — Resend
|
||||||
#RESEND_API_KEY=C8qPDuFwsv5l@KsGhL/V
|
#RESEND_API_KEY=C8qPDuFwsv5l@KsGhL/V
|
||||||
# Option B — SMTP (Gmail)
|
# Option B — SMTP (Gmail)
|
||||||
|
# SMTP fallback (only used if Resend fails or is unconfigured)
|
||||||
MAIL_HOST=smtp.gmail.com
|
MAIL_HOST=smtp.gmail.com
|
||||||
MAIL_PORT=587
|
MAIL_PORT=587
|
||||||
MAIL_SCHEME=smtp
|
MAIL_SCHEME=smtp
|
||||||
MAIL_USERNAME=rentaldrivego@gmail.com
|
MAIL_USERNAME=rentaldrivego@gmail.com
|
||||||
MAIL_PASSWORD=placeholder
|
MAIL_PASSWORD=kfahihfzbcvkczew
|
||||||
MAIL_FROM_ADDRESS=rentaldrivego@gmail.com
|
MAIL_FROM_ADDRESS=rentaldrivego@gmail.com
|
||||||
MAIL_FROM_NAME=RentalDriveGo
|
MAIL_FROM_NAME=RentalDriveGo
|
||||||
MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com
|
MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ PORTAINER_DOMAIN=portainer.example.com
|
|||||||
REGISTRY_DOMAIN=registry.example.com
|
REGISTRY_DOMAIN=registry.example.com
|
||||||
REGISTRY_UPSTREAM_URL=http://10.0.0.10:5000
|
REGISTRY_UPSTREAM_URL=http://10.0.0.10:5000
|
||||||
|
|
||||||
|
# Image tag
|
||||||
|
IMAGE_TAG=latest
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
DATABASE_URL_FROM_POSTGRES=true
|
DATABASE_URL_FROM_POSTGRES=true
|
||||||
POSTGRES_HOST=postgres
|
POSTGRES_HOST=postgres
|
||||||
@@ -17,6 +20,7 @@ POSTGRES_PASSWORD=placeholder
|
|||||||
DATABASE_URL=postgresql://placeholder:placeholder@placeholder:5432/placeholder
|
DATABASE_URL=postgresql://placeholder:placeholder@placeholder:5432/placeholder
|
||||||
|
|
||||||
# Cache
|
# Cache
|
||||||
|
REDIS_PASSWORD=placeholder
|
||||||
REDIS_URL=redis://redis:6379
|
REDIS_URL=redis://redis:6379
|
||||||
|
|
||||||
# API
|
# API
|
||||||
@@ -27,6 +31,7 @@ NEXT_PUBLIC_API_URL=https://api.example.com/api/v1
|
|||||||
|
|
||||||
# Frontend public URLs
|
# Frontend public URLs
|
||||||
NEXT_PUBLIC_MARKETING_URL=https://example.com
|
NEXT_PUBLIC_MARKETING_URL=https://example.com
|
||||||
|
NEXT_PUBLIC_MARKETPLACE_URL=https://example.com
|
||||||
NEXT_PUBLIC_HOMEPAGE_URL=https://example.com
|
NEXT_PUBLIC_HOMEPAGE_URL=https://example.com
|
||||||
NEXT_PUBLIC_STOREFRONT_URL=https://example.com/storefront
|
NEXT_PUBLIC_STOREFRONT_URL=https://example.com/storefront
|
||||||
NEXT_PUBLIC_DASHBOARD_URL=https://example.com/dashboard
|
NEXT_PUBLIC_DASHBOARD_URL=https://example.com/dashboard
|
||||||
|
|||||||
+28
-4
@@ -4,7 +4,13 @@
|
|||||||
# ═══════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
# ─── Database ──────────────────────────────────────────────────
|
# ─── Database ──────────────────────────────────────────────────
|
||||||
DATABASE_URL=postgresql://placeholder:placeholder@placeholder:5432/placeholder
|
DATABASE_URL=postgresql://dbadmin:PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK@localhost:5432/rentaldrivego
|
||||||
|
DATABASE_URL_FROM_POSTGRES=true
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_DB=rentaldrivego
|
||||||
|
POSTGRES_USER=dbadmin
|
||||||
|
POSTGRES_PASSWORD=PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK
|
||||||
|
|
||||||
# ─── API ───────────────────────────────────────────────────────
|
# ─── API ───────────────────────────────────────────────────────
|
||||||
API_PORT=4000
|
API_PORT=4000
|
||||||
@@ -12,7 +18,7 @@ API_URL=http://localhost:4000
|
|||||||
NEXT_PUBLIC_API_URL=http://localhost:4000/api/v1
|
NEXT_PUBLIC_API_URL=http://localhost:4000/api/v1
|
||||||
|
|
||||||
# ─── JWT (Renter auth + Admin auth) ───────────────────────────
|
# ─── JWT (Renter auth + Admin auth) ───────────────────────────
|
||||||
JWT_SECRET=placeholder
|
JWT_SECRET=8bf96de20297c2c295a60e4040e937a7eb8ec7d7d2b83e79a1fc98463322a97c
|
||||||
JWT_EXPIRY=8h
|
JWT_EXPIRY=8h
|
||||||
RENTER_JWT_EXPIRY=7d
|
RENTER_JWT_EXPIRY=7d
|
||||||
|
|
||||||
@@ -71,6 +77,7 @@ NEXT_PUBLIC_FIREBASE_APP_ID=1:123456789:web:abc123
|
|||||||
# MAIL_* SMTP variables are intentionally omitted here.
|
# MAIL_* SMTP variables are intentionally omitted here.
|
||||||
|
|
||||||
# ─── Redis (Real-time / Socket.io) ────────────────────────────
|
# ─── Redis (Real-time / Socket.io) ────────────────────────────
|
||||||
|
REDIS_PASSWORD=replace-with-production-redis-password
|
||||||
REDIS_URL=redis://localhost:6379
|
REDIS_URL=redis://localhost:6379
|
||||||
|
|
||||||
# ─── App URLs ──────────────────────────────────────────────────
|
# ─── App URLs ──────────────────────────────────────────────────
|
||||||
@@ -80,19 +87,36 @@ REDIS_URL=redis://localhost:6379
|
|||||||
|
|
||||||
|
|
||||||
NEXT_PUBLIC_MARKETING_URL=http://localhost:3000
|
NEXT_PUBLIC_MARKETING_URL=http://localhost:3000
|
||||||
|
NEXT_PUBLIC_MARKETPLACE_URL=http://localhost:3000
|
||||||
NEXT_PUBLIC_DASHBOARD_URL=http://localhost:3000/dashboard
|
NEXT_PUBLIC_DASHBOARD_URL=http://localhost:3000/dashboard
|
||||||
NEXT_PUBLIC_ADMIN_URL=http://localhost:3000/admin
|
NEXT_PUBLIC_ADMIN_URL=http://localhost:3000/admin
|
||||||
NEXT_PUBLIC_STOREFRONT_URL=http://localhost:3000/storefront
|
NEXT_PUBLIC_STOREFRONT_URL=http://localhost:3000/storefront
|
||||||
NEXT_PUBLIC_HOMEPAGE_URL=http://localhost:3000
|
NEXT_PUBLIC_HOMEPAGE_URL=http://localhost:3000
|
||||||
# Public site is subdomain-based; use this for local dev:
|
# Public site is subdomain-based; use this for local dev:
|
||||||
NEXT_PUBLIC_PUBLIC_SITE_DOMAIN=localhost:3003
|
NEXT_PUBLIC_PUBLIC_SITE_DOMAIN=localhost:3003
|
||||||
|
API_DOMAIN=api.rentaldrivego.ma
|
||||||
|
PUBLIC_SITE_DOMAIN=rentaldrivego.ma
|
||||||
DASHBOARD_URL=http://localhost:3000/dashboard
|
DASHBOARD_URL=http://localhost:3000/dashboard
|
||||||
|
|
||||||
# ─── Admin seed (first SUPER_ADMIN created on db:seed) ────────
|
# ─── Admin seed (first SUPER_ADMIN created on db:seed) ────────
|
||||||
ADMIN_SEED_EMAIL=admin@rentaldrivego.ma
|
ADMIN_SEED_EMAIL=admin@rentaldrivego.ma
|
||||||
ADMIN_SEED_PASSWORD=placeholder
|
ADMIN_SEED_PASSWORD=PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK
|
||||||
ADMIN_SEED_FIRST_NAME=Super
|
ADMIN_SEED_FIRST_NAME=Super
|
||||||
ADMIN_SEED_LAST_NAME=Admin
|
ADMIN_SEED_LAST_NAME=Admin
|
||||||
|
|
||||||
|
# SMTP fallback (only used if Resend fails or is unconfigured)
|
||||||
|
MAIL_HOST=smtp.gmail.com
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_SCHEME=smtp
|
||||||
|
MAIL_USERNAME=rentaldrivego@gmail.com
|
||||||
|
MAIL_PASSWORD=kfahihfzbcvkczew
|
||||||
|
MAIL_FROM_ADDRESS=rentaldrivego@gmail.com
|
||||||
|
MAIL_FROM_NAME=RentalDriveGo
|
||||||
|
MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com
|
||||||
|
MAIL_REPLY_TO_NAME=RentalDriveGo
|
||||||
|
|
||||||
|
|
||||||
|
IMAGE_TAG=13d0512048d86e9fe93e9395459d04663c2b7eb0
|
||||||
|
|
||||||
# ─── Misc ──────────────────────────────────────────────────────
|
# ─── Misc ──────────────────────────────────────────────────────
|
||||||
NODE_ENV=development
|
NODE_ENV=production
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ env:
|
|||||||
NODE_VERSION: "20"
|
NODE_VERSION: "20"
|
||||||
# TODO: Remove after installing internal CA certificate on the runner
|
# TODO: Remove after installing internal CA certificate on the runner
|
||||||
GIT_SSL_NO_VERIFY: "true"
|
GIT_SSL_NO_VERIFY: "true"
|
||||||
REGISTRY_HOST: 192.168.3.80
|
REGISTRY_HOST: 10.0.0.4
|
||||||
DOCKERFILE_PATH: Dockerfile.production
|
DOCKERFILE_PATH: Dockerfile.production
|
||||||
DOCKER_PLATFORM: linux/amd64
|
DOCKER_PLATFORM: linux/amd64
|
||||||
DEPLOY_ROOT: /opt/rentaldrivego
|
DEPLOY_ROOT: /opt/rentaldrivego
|
||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
buildkitd-config-inline: |
|
buildkitd-config-inline: |
|
||||||
[registry."192.168.3.80"]
|
[registry."10.0.0.4"]
|
||||||
insecure = true
|
insecure = true
|
||||||
|
|
||||||
- name: Docker image metadata
|
- name: Docker image metadata
|
||||||
@@ -190,6 +190,7 @@ jobs:
|
|||||||
cd '$DEPLOY_ROOT'
|
cd '$DEPLOY_ROOT'
|
||||||
APP_IMAGE='$IMAGE_REPOSITORY' \
|
APP_IMAGE='$IMAGE_REPOSITORY' \
|
||||||
APP_VERSION='${{ gitea.sha }}' \
|
APP_VERSION='${{ gitea.sha }}' \
|
||||||
|
IMAGE_TAG='${{ gitea.sha }}' \
|
||||||
REGISTRY_HOST='$REGISTRY_HOST' \
|
REGISTRY_HOST='$REGISTRY_HOST' \
|
||||||
REGISTRY_USER='${{ secrets.REGISTRY_USERNAME }}' \
|
REGISTRY_USER='${{ secrets.REGISTRY_USERNAME }}' \
|
||||||
REGISTRY_PASSWORD=\$(printf '%s' '$REGISTRY_PASSWORD_B64' | base64 -d) \
|
REGISTRY_PASSWORD=\$(printf '%s' '$REGISTRY_PASSWORD_B64' | base64 -d) \
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ apps/api/storage/
|
|||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
.env.docker.production
|
||||||
|
production/.env.docker.production
|
||||||
|
|
||||||
# Turbo
|
# Turbo
|
||||||
.turbo
|
.turbo
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ FROM node:20-bookworm AS runner
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production \
|
||||||
|
NPM_CONFIG_UPDATE_NOTIFIER=false
|
||||||
|
|
||||||
RUN corepack enable && corepack prepare npm@10.5.0 --activate \
|
RUN corepack enable && corepack prepare npm@10.5.0 --activate \
|
||||||
&& groupadd --system --gid 10001 app \
|
&& groupadd --system --gid 10001 app \
|
||||||
@@ -56,6 +57,7 @@ COPY --from=builder --chown=app:app /app/package.json /app/package-lock.json /ap
|
|||||||
COPY --from=builder --chown=app:app /app/node_modules ./node_modules
|
COPY --from=builder --chown=app:app /app/node_modules ./node_modules
|
||||||
COPY --from=builder --chown=app:app /app/apps ./apps
|
COPY --from=builder --chown=app:app /app/apps ./apps
|
||||||
COPY --from=builder --chown=app:app /app/packages ./packages
|
COPY --from=builder --chown=app:app /app/packages ./packages
|
||||||
|
COPY --from=builder --chown=app:app /app/config ./config
|
||||||
COPY --chown=root:root docker/entrypoint.production.sh /usr/local/bin/rdg-entrypoint.sh
|
COPY --chown=root:root docker/entrypoint.production.sh /usr/local/bin/rdg-entrypoint.sh
|
||||||
|
|
||||||
RUN chmod 755 /usr/local/bin/rdg-entrypoint.sh
|
RUN chmod 755 /usr/local/bin/rdg-entrypoint.sh
|
||||||
|
|||||||
Binary file not shown.
@@ -2,6 +2,8 @@ name: rentaldrivego-prod
|
|||||||
|
|
||||||
x-node-runtime-hardening: &node-runtime-hardening
|
x-node-runtime-hardening: &node-runtime-hardening
|
||||||
init: true
|
init: true
|
||||||
|
user: "10001:10001"
|
||||||
|
entrypoint: []
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
tmpfs:
|
tmpfs:
|
||||||
@@ -14,11 +16,11 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: rentaldrivego
|
POSTGRES_DB: ${POSTGRES_DB:-rentaldrivego}
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U postgres -d rentaldrivego"]
|
test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
@@ -44,9 +46,7 @@ services:
|
|||||||
|
|
||||||
migrate:
|
migrate:
|
||||||
<<: *node-runtime-hardening
|
<<: *node-runtime-hardening
|
||||||
cap_drop:
|
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||||
- ALL
|
|
||||||
image: ${APP_IMAGE:-rentaldrivego-prod-app}:${APP_VERSION:-latest}
|
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.production
|
dockerfile: Dockerfile.production
|
||||||
@@ -56,8 +56,11 @@ services:
|
|||||||
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
||||||
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
||||||
NEXT_PUBLIC_MARKETPLACE_URL: ${NEXT_PUBLIC_MARKETPLACE_URL}
|
NEXT_PUBLIC_MARKETPLACE_URL: ${NEXT_PUBLIC_MARKETPLACE_URL}
|
||||||
|
NEXT_PUBLIC_HOMEPAGE_URL: ${NEXT_PUBLIC_HOMEPAGE_URL}
|
||||||
|
NEXT_PUBLIC_STOREFRONT_URL: ${NEXT_PUBLIC_STOREFRONT_URL}
|
||||||
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
||||||
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
||||||
|
SITE_ORIGIN: ${SITE_ORIGIN}
|
||||||
command: ["npm", "run", "db:deploy"]
|
command: ["npm", "run", "db:deploy"]
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
@@ -65,7 +68,7 @@ services:
|
|||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
env_file:
|
env_file:
|
||||||
- .env.docker.production
|
- ${APP_ENV_FILE:-.env.production}
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL_FROM_POSTGRES: ${DATABASE_URL_FROM_POSTGRES:-true}
|
DATABASE_URL_FROM_POSTGRES: ${DATABASE_URL_FROM_POSTGRES:-true}
|
||||||
POSTGRES_HOST: ${POSTGRES_HOST:-postgres}
|
POSTGRES_HOST: ${POSTGRES_HOST:-postgres}
|
||||||
@@ -76,7 +79,7 @@ services:
|
|||||||
|
|
||||||
api:
|
api:
|
||||||
<<: *node-runtime-hardening
|
<<: *node-runtime-hardening
|
||||||
image: ${APP_IMAGE:-rentaldrivego-prod-app}:${APP_VERSION:-latest}
|
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.production
|
dockerfile: Dockerfile.production
|
||||||
@@ -86,8 +89,11 @@ services:
|
|||||||
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
||||||
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
||||||
NEXT_PUBLIC_MARKETPLACE_URL: ${NEXT_PUBLIC_MARKETPLACE_URL}
|
NEXT_PUBLIC_MARKETPLACE_URL: ${NEXT_PUBLIC_MARKETPLACE_URL}
|
||||||
|
NEXT_PUBLIC_HOMEPAGE_URL: ${NEXT_PUBLIC_HOMEPAGE_URL}
|
||||||
|
NEXT_PUBLIC_STOREFRONT_URL: ${NEXT_PUBLIC_STOREFRONT_URL}
|
||||||
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
||||||
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
||||||
|
SITE_ORIGIN: ${SITE_ORIGIN}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:4000/health', (r) => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))\""]
|
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:4000/health', (r) => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))\""]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
@@ -104,7 +110,7 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
env_file:
|
env_file:
|
||||||
- .env.docker.production
|
- ${APP_ENV_FILE:-.env.production}
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL_FROM_POSTGRES: ${DATABASE_URL_FROM_POSTGRES:-true}
|
DATABASE_URL_FROM_POSTGRES: ${DATABASE_URL_FROM_POSTGRES:-true}
|
||||||
FILE_STORAGE_ROOT: /var/lib/rentaldrivego/storage
|
FILE_STORAGE_ROOT: /var/lib/rentaldrivego/storage
|
||||||
@@ -132,9 +138,7 @@ services:
|
|||||||
|
|
||||||
homepage:
|
homepage:
|
||||||
<<: *node-runtime-hardening
|
<<: *node-runtime-hardening
|
||||||
cap_drop:
|
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||||
- ALL
|
|
||||||
image: ${APP_IMAGE:-rentaldrivego-prod-app}:${APP_VERSION:-latest}
|
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.production
|
dockerfile: Dockerfile.production
|
||||||
@@ -144,8 +148,11 @@ services:
|
|||||||
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
||||||
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
||||||
NEXT_PUBLIC_HOMEPAGE_URL: ${NEXT_PUBLIC_HOMEPAGE_URL}
|
NEXT_PUBLIC_HOMEPAGE_URL: ${NEXT_PUBLIC_HOMEPAGE_URL}
|
||||||
|
NEXT_PUBLIC_MARKETPLACE_URL: ${NEXT_PUBLIC_MARKETPLACE_URL}
|
||||||
|
NEXT_PUBLIC_STOREFRONT_URL: ${NEXT_PUBLIC_STOREFRONT_URL}
|
||||||
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
||||||
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
||||||
|
SITE_ORIGIN: ${SITE_ORIGIN}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3000/', (r) => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))\""]
|
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3000/', (r) => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))\""]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
@@ -160,7 +167,7 @@ services:
|
|||||||
api:
|
api:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
env_file:
|
env_file:
|
||||||
- .env.docker.production
|
- ${APP_ENV_FILE:-.env.production}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
@@ -175,9 +182,7 @@ services:
|
|||||||
|
|
||||||
storefront:
|
storefront:
|
||||||
<<: *node-runtime-hardening
|
<<: *node-runtime-hardening
|
||||||
cap_drop:
|
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||||
- ALL
|
|
||||||
image: ${APP_IMAGE:-rentaldrivego-prod-app}:${APP_VERSION:-latest}
|
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.production
|
dockerfile: Dockerfile.production
|
||||||
@@ -186,9 +191,12 @@ services:
|
|||||||
DASHBOARD_INTERNAL_URL: ${DASHBOARD_INTERNAL_URL:-http://dashboard:3001}
|
DASHBOARD_INTERNAL_URL: ${DASHBOARD_INTERNAL_URL:-http://dashboard:3001}
|
||||||
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
||||||
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
||||||
|
NEXT_PUBLIC_MARKETPLACE_URL: ${NEXT_PUBLIC_MARKETPLACE_URL}
|
||||||
|
NEXT_PUBLIC_HOMEPAGE_URL: ${NEXT_PUBLIC_HOMEPAGE_URL}
|
||||||
NEXT_PUBLIC_STOREFRONT_URL: ${NEXT_PUBLIC_STOREFRONT_URL}
|
NEXT_PUBLIC_STOREFRONT_URL: ${NEXT_PUBLIC_STOREFRONT_URL}
|
||||||
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
||||||
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
||||||
|
SITE_ORIGIN: ${SITE_ORIGIN}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3004/', (r) => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))\""]
|
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3004/', (r) => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))\""]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
@@ -203,7 +211,7 @@ services:
|
|||||||
api:
|
api:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
env_file:
|
env_file:
|
||||||
- .env.docker.production
|
- ${APP_ENV_FILE:-.env.production}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
@@ -218,9 +226,7 @@ services:
|
|||||||
|
|
||||||
dashboard:
|
dashboard:
|
||||||
<<: *node-runtime-hardening
|
<<: *node-runtime-hardening
|
||||||
cap_drop:
|
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||||
- ALL
|
|
||||||
image: ${APP_IMAGE:-rentaldrivego-prod-app}:${APP_VERSION:-latest}
|
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.production
|
dockerfile: Dockerfile.production
|
||||||
@@ -230,8 +236,11 @@ services:
|
|||||||
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
||||||
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
||||||
NEXT_PUBLIC_MARKETPLACE_URL: ${NEXT_PUBLIC_MARKETPLACE_URL}
|
NEXT_PUBLIC_MARKETPLACE_URL: ${NEXT_PUBLIC_MARKETPLACE_URL}
|
||||||
|
NEXT_PUBLIC_HOMEPAGE_URL: ${NEXT_PUBLIC_HOMEPAGE_URL}
|
||||||
|
NEXT_PUBLIC_STOREFRONT_URL: ${NEXT_PUBLIC_STOREFRONT_URL}
|
||||||
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
||||||
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
||||||
|
SITE_ORIGIN: ${SITE_ORIGIN}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3001/dashboard', (r) => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))\""]
|
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3001/dashboard', (r) => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))\""]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
@@ -246,7 +255,7 @@ services:
|
|||||||
api:
|
api:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
env_file:
|
env_file:
|
||||||
- .env.docker.production
|
- ${APP_ENV_FILE:-.env.production}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
@@ -261,9 +270,7 @@ services:
|
|||||||
|
|
||||||
admin:
|
admin:
|
||||||
<<: *node-runtime-hardening
|
<<: *node-runtime-hardening
|
||||||
cap_drop:
|
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||||
- ALL
|
|
||||||
image: ${APP_IMAGE:-rentaldrivego-prod-app}:${APP_VERSION:-latest}
|
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.production
|
dockerfile: Dockerfile.production
|
||||||
@@ -273,8 +280,11 @@ services:
|
|||||||
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
||||||
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
||||||
NEXT_PUBLIC_MARKETPLACE_URL: ${NEXT_PUBLIC_MARKETPLACE_URL}
|
NEXT_PUBLIC_MARKETPLACE_URL: ${NEXT_PUBLIC_MARKETPLACE_URL}
|
||||||
|
NEXT_PUBLIC_HOMEPAGE_URL: ${NEXT_PUBLIC_HOMEPAGE_URL}
|
||||||
|
NEXT_PUBLIC_STOREFRONT_URL: ${NEXT_PUBLIC_STOREFRONT_URL}
|
||||||
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
||||||
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
||||||
|
SITE_ORIGIN: ${SITE_ORIGIN}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3002/admin', (r) => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))\""]
|
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3002/admin', (r) => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))\""]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
@@ -289,7 +299,7 @@ services:
|
|||||||
api:
|
api:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
env_file:
|
env_file:
|
||||||
- .env.docker.production
|
- ${APP_ENV_FILE:-.env.production}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
@@ -333,3 +343,5 @@ volumes:
|
|||||||
redis_prod_data:
|
redis_prod_data:
|
||||||
pgmanage_prod_data:
|
pgmanage_prod_data:
|
||||||
api_uploads:
|
api_uploads:
|
||||||
|
external: true
|
||||||
|
name: ${DOCKER_PROD_API_UPLOADS_VOLUME:-rentaldrivego-prod_api_uploads}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Traefik domains
|
||||||
|
ACME_EMAIL=ops@example.com
|
||||||
|
API_DOMAIN=api.rentaldrivego.ma
|
||||||
|
PUBLIC_SITE_DOMAIN=rentaldrivego.ma
|
||||||
|
PGMANAGE_DOMAIN=pgmanage.rentaldrivego.ma
|
||||||
|
|
||||||
|
# Image tag
|
||||||
|
IMAGE_TAG=13d0512048d86e9fe93e9395459d04663c2b7eb0
|
||||||
|
# Optional prebuilt image source. Set this when deploying from a private registry.
|
||||||
|
APP_IMAGE=10.0.0.4/melabidi/carmanagement
|
||||||
|
# APP_VERSION is accepted by helper scripts for compatibility, but IMAGE_TAG is preferred.
|
||||||
|
# APP_VERSION=latest
|
||||||
|
|
||||||
|
# External image registry host. REGISTRY_USER and REGISTRY_PASSWORD are only needed when docker login is required.
|
||||||
|
REGISTRY_HOST=10.0.0.4
|
||||||
|
REGISTRY_USER=melabidi
|
||||||
|
REGISTRY_PASSWORD=
|
||||||
|
|
||||||
|
# Optional shared folder for image archives created by the VPN/registry helper container.
|
||||||
|
# The VPS cannot reach the VPN-only registry, so archive loading is required.
|
||||||
|
IMAGE_ARCHIVE_DIR=/opt/docker-image-transfer
|
||||||
|
IMAGE_ARCHIVE_REQUIRED=true
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASE_URL_FROM_POSTGRES=true
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_DB=rentaldrivego
|
||||||
|
POSTGRES_USER=dbadmin
|
||||||
|
POSTGRES_PASSWORD=placeholder
|
||||||
|
DATABASE_URL=postgresql://placeholder:placeholder@placeholder:5432/placeholder
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
REDIS_PASSWORD=placeholder
|
||||||
|
REDIS_URL=redis://:placeholder@redis:6379
|
||||||
|
|
||||||
|
# API
|
||||||
|
API_PORT=4000
|
||||||
|
API_INTERNAL_URL=http://api:4000/api/v1
|
||||||
|
DASHBOARD_INTERNAL_URL=http://dashboard:3001
|
||||||
|
ADMIN_INTERNAL_URL=http://admin:3002
|
||||||
|
API_URL=https://api.rentaldrivego.ma
|
||||||
|
NEXT_PUBLIC_API_URL=https://api.rentaldrivego.ma/api/v1
|
||||||
|
|
||||||
|
# Frontend public URLs
|
||||||
|
SITE_ORIGIN=https://rentaldrivego.ma
|
||||||
|
NEXT_PUBLIC_MARKETING_URL=https://rentaldrivego.ma
|
||||||
|
NEXT_PUBLIC_MARKETPLACE_URL=https://rentaldrivego.ma
|
||||||
|
NEXT_PUBLIC_HOMEPAGE_URL=https://rentaldrivego.ma
|
||||||
|
NEXT_PUBLIC_STOREFRONT_URL=https://rentaldrivego.ma/storefront
|
||||||
|
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
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ORIGINS=https://rentaldrivego.ma,https://www.rentaldrivego.ma
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
JWT_SECRET=placeholder
|
||||||
|
JWT_EXPIRY=8h
|
||||||
|
RENTER_JWT_EXPIRY=7d
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# File storage
|
||||||
|
FILE_STORAGE_ROOT=/var/lib/rentaldrivego/storage
|
||||||
|
|
||||||
|
# PgManage
|
||||||
|
PGMANAGE_DEFAULT_USERNAME=admin
|
||||||
|
PGMANAGE_DEFAULT_PASSWORD=change-me
|
||||||
|
|
||||||
|
# Email
|
||||||
|
EMAIL_FROM=rentaldrivego@gmail.com
|
||||||
|
EMAIL_FROM_NAME=RentalDriveGo
|
||||||
|
MAIL_HOST=smtp.gmail.com
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_SCHEME=smtp
|
||||||
|
MAIL_USERNAME=rentaldrivego@gmail.com
|
||||||
|
MAIL_PASSWORD=placeholder
|
||||||
|
MAIL_FROM_ADDRESS=rentaldrivego@gmail.com
|
||||||
|
MAIL_FROM_NAME=RentalDriveGo
|
||||||
|
MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com
|
||||||
|
MAIL_REPLY_TO_NAME=RentalDriveGo
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
# RentalDriveGo Production Bundle
|
||||||
|
|
||||||
|
This folder contains the files needed to run a production deployment from prebuilt Docker images.
|
||||||
|
|
||||||
|
It is intended for a VPS/deploy directory such as `/opt/rentaldrivego` or `~/prodfolder`.
|
||||||
|
Do not run `docker compose up --build` from this folder; building from source requires the full repository.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `docker-compose.production.yml` - production app stack.
|
||||||
|
- `traefik.yaml` - Traefik reverse proxy stack.
|
||||||
|
- `.env.docker.production.example` - sanitized environment template.
|
||||||
|
- `scripts/` - production deploy, startup, backup, and restore helpers.
|
||||||
|
- `docker/pgmanage/override.py` - PgManage runtime override.
|
||||||
|
- `dynamic/` - generated Traefik dynamic config location.
|
||||||
|
|
||||||
|
## First-Time Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/rentaldrivego
|
||||||
|
chmod +x scripts/*.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This bundle includes `.env.docker.production` for the current `rentaldrivego.ma` deployment. Review it before starting services:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano .env.docker.production
|
||||||
|
```
|
||||||
|
|
||||||
|
At minimum, verify or rotate:
|
||||||
|
|
||||||
|
- `POSTGRES_PASSWORD`
|
||||||
|
- `REDIS_PASSWORD`
|
||||||
|
- `JWT_SECRET`
|
||||||
|
- `IMAGE_TAG`
|
||||||
|
- `APP_IMAGE`
|
||||||
|
- `MAIL_PASSWORD`
|
||||||
|
- `PGMANAGE_DEFAULT_PASSWORD`
|
||||||
|
|
||||||
|
If Docker needs to log in before pulling the app image, also set:
|
||||||
|
|
||||||
|
- `REGISTRY_USER`
|
||||||
|
- `REGISTRY_PASSWORD`
|
||||||
|
|
||||||
|
For the current VPN-reachable image registry, use:
|
||||||
|
|
||||||
|
```env
|
||||||
|
APP_IMAGE=10.0.0.4/melabidi/carmanagement
|
||||||
|
REGISTRY_HOST=10.0.0.4
|
||||||
|
```
|
||||||
|
|
||||||
|
## VPN Container Image Handoff
|
||||||
|
|
||||||
|
If the registry is only reachable inside a VPN container, use the shared archive folder:
|
||||||
|
|
||||||
|
```env
|
||||||
|
IMAGE_ARCHIVE_DIR=/opt/docker-image-transfer
|
||||||
|
IMAGE_ARCHIVE_REQUIRED=true
|
||||||
|
APP_IMAGE=10.0.0.4/melabidi/carmanagement
|
||||||
|
IMAGE_TAG=13d0512048d86e9fe93e9395459d04663c2b7eb0
|
||||||
|
REGISTRY_USER=melabidi
|
||||||
|
```
|
||||||
|
|
||||||
|
From the VPS, use the VPN container network to copy the image into the transfer folder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VPN_CONTAINER="6c5dca44e1c2" ./fetch-image-through-vpn.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Equivalent manual command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /opt/docker-image-transfer
|
||||||
|
sudo chmod 700 /opt/docker-image-transfer
|
||||||
|
|
||||||
|
IMAGE_TAG="13d0512048d86e9fe93e9395459d04663c2b7eb0"
|
||||||
|
REGISTRY_USER="melabidi"
|
||||||
|
VPN_CONTAINER="6c5dca44e1c2"
|
||||||
|
|
||||||
|
read -rsp "Registry token: " REGISTRY_TOKEN
|
||||||
|
echo
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
--network "container:${VPN_CONTAINER}" \
|
||||||
|
-v /opt/docker-image-transfer:/transfer \
|
||||||
|
quay.io/skopeo/stable:latest \
|
||||||
|
copy \
|
||||||
|
--override-os linux \
|
||||||
|
--override-arch amd64 \
|
||||||
|
--src-tls-verify=false \
|
||||||
|
--src-creds "${REGISTRY_USER}:${REGISTRY_TOKEN}" \
|
||||||
|
"docker://10.0.0.4/melabidi/carmanagement:${IMAGE_TAG}" \
|
||||||
|
"docker-archive:/transfer/carmanagement-${IMAGE_TAG}.tar:10.0.0.4/melabidi/carmanagement:${IMAGE_TAG}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the same `IMAGE_TAG` in `.env.docker.production`, then run the deployment. The deploy script will run `docker load -i /opt/docker-image-transfer/carmanagement-${IMAGE_TAG}.tar` before starting services. Because `IMAGE_ARCHIVE_REQUIRED=true`, the VPS will not try to pull from `10.0.0.4` directly.
|
||||||
|
|
||||||
|
## Deploy A Prebuilt Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/rentaldrivego
|
||||||
|
chmod +x deploy-production.sh scripts/*.sh
|
||||||
|
./deploy-production.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The deploy script loads the shared-folder image archive, starts Traefik and shared services, runs migrations, then starts the app services.
|
||||||
|
|
||||||
|
## Manual Compose Flow
|
||||||
|
|
||||||
|
Use this only when you want to run compose directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/rentaldrivego
|
||||||
|
docker load -i /opt/docker-image-transfer/carmanagement-latest.tar
|
||||||
|
APP_ENV_FILE=.env.docker.production docker compose --env-file .env.docker.production -f docker-compose.production.yml up -d postgres redis
|
||||||
|
APP_ENV_FILE=.env.docker.production docker compose --env-file .env.docker.production -f docker-compose.production.yml run --rm migrate
|
||||||
|
APP_ENV_FILE=.env.docker.production docker compose --env-file .env.docker.production -f docker-compose.production.yml up -d api storefront dashboard admin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Source Builds
|
||||||
|
|
||||||
|
If you need to build on the VPS, copy the full repository instead of only this folder. `Dockerfile.production` requires:
|
||||||
|
|
||||||
|
- `package.json`
|
||||||
|
- `package-lock.json`
|
||||||
|
- `turbo.json`
|
||||||
|
- `tsconfig.base.json`
|
||||||
|
- `apps/`
|
||||||
|
- `packages/`
|
||||||
|
- `config/`
|
||||||
|
- `docker/entrypoint.production.sh`
|
||||||
Executable
+326
@@ -0,0 +1,326 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "${ROOT_DIR}"
|
||||||
|
|
||||||
|
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./deploy-production.sh [--with-pgmanage] [--skip-prune]
|
||||||
|
|
||||||
|
What it does:
|
||||||
|
1. Validates .env.docker.production
|
||||||
|
2. Loads IMAGE_TAG, APP_IMAGE, registry, and archive settings from the env file
|
||||||
|
3. Loads a matching Docker image archive from IMAGE_ARCHIVE_DIR when present
|
||||||
|
4. Otherwise logs in to the registry if credentials are configured, then pulls
|
||||||
|
5. Reuses healthy Traefik, Postgres, and Redis containers when present
|
||||||
|
6. Waits for health checks and prints container status
|
||||||
|
|
||||||
|
VPN/shared-folder image flow:
|
||||||
|
Put an archive matching IMAGE_TAG in ./image-archives before running this script.
|
||||||
|
Example: ./image-archives/carmanagement-latest.tar
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
include_pgmanage=0
|
||||||
|
skip_prune=0
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--with-pgmanage)
|
||||||
|
include_pgmanage=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--skip-prune)
|
||||||
|
skip_prune=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
require_command() {
|
||||||
|
local command_name="$1"
|
||||||
|
|
||||||
|
if ! command -v "${command_name}" >/dev/null 2>&1; then
|
||||||
|
echo "Missing required command: ${command_name}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_archive_dir() {
|
||||||
|
if [[ -z "${IMAGE_ARCHIVE_DIR:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${IMAGE_ARCHIVE_DIR}" in
|
||||||
|
/*)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
IMAGE_ARCHIVE_DIR="${ROOT_DIR}/${IMAGE_ARCHIVE_DIR#./}"
|
||||||
|
export IMAGE_ARCHIVE_DIR
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
print_release_summary() {
|
||||||
|
echo "Production deployment"
|
||||||
|
echo " Image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG}"
|
||||||
|
echo " Env: ${ENV_FILE}"
|
||||||
|
echo " Compose project: ${PROJECT_NAME}"
|
||||||
|
|
||||||
|
if [[ -n "${IMAGE_ARCHIVE_DIR:-}" ]]; then
|
||||||
|
echo " Image archive dir: ${IMAGE_ARCHIVE_DIR}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
container_status() {
|
||||||
|
local container_name="$1"
|
||||||
|
|
||||||
|
docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "${container_name}" 2>/dev/null || echo "missing"
|
||||||
|
}
|
||||||
|
|
||||||
|
compose_service_status() {
|
||||||
|
local compose_name="$1"
|
||||||
|
local service_name="$2"
|
||||||
|
local container_id
|
||||||
|
|
||||||
|
case "${compose_name}" in
|
||||||
|
prod)
|
||||||
|
container_id="$(prod_compose ps -q "${service_name}" 2>/dev/null || true)"
|
||||||
|
;;
|
||||||
|
traefik)
|
||||||
|
container_id="$(traefik_compose ps -q "${service_name}" 2>/dev/null || true)"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "missing"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ -z "${container_id}" ]]; then
|
||||||
|
echo "missing"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "${container_id}" 2>/dev/null || echo "missing"
|
||||||
|
}
|
||||||
|
|
||||||
|
first_container_id() {
|
||||||
|
docker ps -q "$@" 2>/dev/null | sed -n '1p'
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_container_on_traefik_network() {
|
||||||
|
local container_id="$1"
|
||||||
|
local networks
|
||||||
|
|
||||||
|
networks="$(docker inspect --format='{{range $name, $_ := .NetworkSettings.Networks}}{{printf "%s " $name}}{{end}}' "${container_id}" 2>/dev/null || true)"
|
||||||
|
case " ${networks} " in
|
||||||
|
*" ${TRAEFIK_NETWORK} "*)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Connecting existing Traefik container ${container_id} to ${TRAEFIK_NETWORK}."
|
||||||
|
docker network connect "${TRAEFIK_NETWORK}" "${container_id}" 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
find_running_traefik_container() {
|
||||||
|
local container_id
|
||||||
|
|
||||||
|
container_id="$(first_container_id --filter name=traefik)"
|
||||||
|
if [[ -n "${container_id}" ]]; then
|
||||||
|
printf '%s' "${container_id}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
container_id="$(first_container_id --filter ancestor=traefik --filter status=running)"
|
||||||
|
if [[ -n "${container_id}" ]]; then
|
||||||
|
printf '%s' "${container_id}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
container_id="$(first_container_id --filter ancestor=traefik:latest --filter status=running)"
|
||||||
|
if [[ -n "${container_id}" ]]; then
|
||||||
|
printf '%s' "${container_id}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_ports_available_for_traefik() {
|
||||||
|
local container_id
|
||||||
|
local port_listener
|
||||||
|
|
||||||
|
container_id="$(first_container_id --filter publish=80 --filter status=running)"
|
||||||
|
if [[ -n "${container_id}" ]]; then
|
||||||
|
echo "Port 80 is already used by container ${container_id}, and it does not look like Traefik." >&2
|
||||||
|
echo "Stop that container or set up Traefik outside this deployment before rerunning." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
port_listener="$(sudo ss -H -ltnp 'sport = :80' 2>/dev/null || ss -H -ltnp 'sport = :80' 2>/dev/null || true)"
|
||||||
|
if [[ -n "${port_listener}" ]]; then
|
||||||
|
echo "Port 80 is already used by a host process:" >&2
|
||||||
|
echo "${port_listener}" >&2
|
||||||
|
echo "Stop that process before starting Traefik, or manage Traefik outside this deployment." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
container_id="$(first_container_id --filter publish=443 --filter status=running)"
|
||||||
|
if [[ -n "${container_id}" ]]; then
|
||||||
|
echo "Port 443 is already used by container ${container_id}, and it does not look like Traefik." >&2
|
||||||
|
echo "Stop that container or set up Traefik outside this deployment before rerunning." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
port_listener="$(sudo ss -H -ltnp 'sport = :443' 2>/dev/null || ss -H -ltnp 'sport = :443' 2>/dev/null || true)"
|
||||||
|
if [[ -n "${port_listener}" ]]; then
|
||||||
|
echo "Port 443 is already used by a host process:" >&2
|
||||||
|
echo "${port_listener}" >&2
|
||||||
|
echo "Stop that process before starting Traefik, or manage Traefik outside this deployment." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_traefik_ready() {
|
||||||
|
local existing_container_id
|
||||||
|
local status
|
||||||
|
|
||||||
|
status="$(compose_service_status traefik traefik)"
|
||||||
|
case "${status}" in
|
||||||
|
running|healthy)
|
||||||
|
echo "Traefik is already ${status}; reusing existing container."
|
||||||
|
;;
|
||||||
|
missing|exited|dead|created|restarting|removing|paused|unhealthy)
|
||||||
|
existing_container_id="$(find_running_traefik_container)"
|
||||||
|
if [[ -n "${existing_container_id}" ]]; then
|
||||||
|
echo "Found existing Traefik container ${existing_container_id}; reusing it."
|
||||||
|
ensure_container_on_traefik_network "${existing_container_id}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
ensure_ports_available_for_traefik
|
||||||
|
echo "Starting Traefik (status: ${status})"
|
||||||
|
start_traefik
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
existing_container_id="$(find_running_traefik_container)"
|
||||||
|
if [[ -n "${existing_container_id}" ]]; then
|
||||||
|
echo "Found existing Traefik container ${existing_container_id}; reusing it."
|
||||||
|
ensure_container_on_traefik_network "${existing_container_id}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
ensure_ports_available_for_traefik
|
||||||
|
echo "Starting Traefik (status: ${status})"
|
||||||
|
start_traefik
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_postgres_ready() {
|
||||||
|
local status
|
||||||
|
|
||||||
|
status="$(compose_service_status prod postgres)"
|
||||||
|
case "${status}" in
|
||||||
|
healthy)
|
||||||
|
echo "Postgres is already healthy; reusing existing container."
|
||||||
|
;;
|
||||||
|
running)
|
||||||
|
echo "Postgres is running; waiting for health check."
|
||||||
|
wait_for_healthy postgres 120
|
||||||
|
;;
|
||||||
|
missing|exited|dead|created|restarting|removing|paused|unhealthy)
|
||||||
|
echo "Starting Postgres (status: ${status})"
|
||||||
|
prod_compose up -d postgres
|
||||||
|
wait_for_healthy postgres 120
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Starting Postgres (status: ${status})"
|
||||||
|
prod_compose up -d postgres
|
||||||
|
wait_for_healthy postgres 120
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_redis_ready() {
|
||||||
|
local status
|
||||||
|
|
||||||
|
status="$(compose_service_status prod redis)"
|
||||||
|
case "${status}" in
|
||||||
|
healthy)
|
||||||
|
echo "Redis is already healthy; reusing existing container."
|
||||||
|
;;
|
||||||
|
running)
|
||||||
|
echo "Redis is running; waiting for health check."
|
||||||
|
wait_for_healthy redis 120
|
||||||
|
;;
|
||||||
|
missing|exited|dead|created|restarting|removing|paused|unhealthy)
|
||||||
|
echo "Starting Redis (status: ${status})"
|
||||||
|
prod_compose up -d redis
|
||||||
|
wait_for_healthy redis 120
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Starting Redis (status: ${status})"
|
||||||
|
prod_compose up -d redis
|
||||||
|
wait_for_healthy redis 120
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
require_command docker
|
||||||
|
|
||||||
|
ensure_env_file
|
||||||
|
load_prod_env_exports
|
||||||
|
require_release_image
|
||||||
|
normalize_archive_dir
|
||||||
|
print_release_summary
|
||||||
|
|
||||||
|
ensure_traefik_network
|
||||||
|
ensure_api_uploads_volume
|
||||||
|
login_registry_if_configured
|
||||||
|
|
||||||
|
echo "Preparing release image"
|
||||||
|
pull_prod_release_images
|
||||||
|
|
||||||
|
echo "Checking shared infrastructure"
|
||||||
|
ensure_traefik_ready
|
||||||
|
ensure_postgres_ready
|
||||||
|
ensure_redis_ready
|
||||||
|
|
||||||
|
echo "Running database migrations"
|
||||||
|
run_prod_migrations
|
||||||
|
|
||||||
|
echo "Starting application services"
|
||||||
|
app_services=(api storefront dashboard admin)
|
||||||
|
|
||||||
|
if [[ "${include_pgmanage}" -eq 1 ]]; then
|
||||||
|
prod_compose --profile private-tools up -d "${app_services[@]}" pgmanage
|
||||||
|
else
|
||||||
|
prod_compose up -d "${app_services[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
wait_for_healthy api 180
|
||||||
|
wait_for_healthy storefront 180
|
||||||
|
wait_for_healthy dashboard 180
|
||||||
|
wait_for_healthy admin 180
|
||||||
|
|
||||||
|
if [[ "${skip_prune}" -eq 0 ]]; then
|
||||||
|
echo "Pruning dangling images"
|
||||||
|
docker image prune -f >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Final container status"
|
||||||
|
prod_compose ps
|
||||||
|
|
||||||
|
echo "Production release ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG} is up and healthy."
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
name: rentaldrivego-prod
|
||||||
|
|
||||||
|
x-node-runtime-hardening: &node-runtime-hardening
|
||||||
|
init: true
|
||||||
|
user: "10001:10001"
|
||||||
|
entrypoint: []
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp:size=64m,mode=1777
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-rentaldrivego}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
volumes:
|
||||||
|
- postgres_prod_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
environment:
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD must be set}
|
||||||
|
command: >
|
||||||
|
sh -ec 'exec redis-server --appendonly yes --requirepass "$${REDIS_PASSWORD}" --maxmemory "$${REDIS_MAXMEMORY:-256mb}" --maxmemory-policy allkeys-lru'
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", 'redis-cli -a "$${REDIS_PASSWORD}" ping | grep PONG']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
volumes:
|
||||||
|
- redis_prod_data:/data
|
||||||
|
|
||||||
|
migrate:
|
||||||
|
<<: *node-runtime-hardening
|
||||||
|
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.production
|
||||||
|
args:
|
||||||
|
API_INTERNAL_URL: ${API_INTERNAL_URL:-http://api:4000/api/v1}
|
||||||
|
DASHBOARD_INTERNAL_URL: ${DASHBOARD_INTERNAL_URL:-http://dashboard:3001}
|
||||||
|
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
||||||
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
||||||
|
NEXT_PUBLIC_MARKETPLACE_URL: ${NEXT_PUBLIC_MARKETPLACE_URL}
|
||||||
|
NEXT_PUBLIC_HOMEPAGE_URL: ${NEXT_PUBLIC_HOMEPAGE_URL}
|
||||||
|
NEXT_PUBLIC_STOREFRONT_URL: ${NEXT_PUBLIC_STOREFRONT_URL}
|
||||||
|
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
||||||
|
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
||||||
|
SITE_ORIGIN: ${SITE_ORIGIN}
|
||||||
|
command: ["npm", "run", "db:deploy"]
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
env_file:
|
||||||
|
- ${APP_ENV_FILE:-.env.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:
|
||||||
|
<<: *node-runtime-hardening
|
||||||
|
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.production
|
||||||
|
args:
|
||||||
|
API_INTERNAL_URL: ${API_INTERNAL_URL:-http://api:4000/api/v1}
|
||||||
|
DASHBOARD_INTERNAL_URL: ${DASHBOARD_INTERNAL_URL:-http://dashboard:3001}
|
||||||
|
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
||||||
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
||||||
|
NEXT_PUBLIC_MARKETPLACE_URL: ${NEXT_PUBLIC_MARKETPLACE_URL}
|
||||||
|
NEXT_PUBLIC_HOMEPAGE_URL: ${NEXT_PUBLIC_HOMEPAGE_URL}
|
||||||
|
NEXT_PUBLIC_STOREFRONT_URL: ${NEXT_PUBLIC_STOREFRONT_URL}
|
||||||
|
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
||||||
|
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
||||||
|
SITE_ORIGIN: ${SITE_ORIGIN}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:4000/health', (r) => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))\""]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 60s
|
||||||
|
command: ["npm", "run", "start", "--workspace", "@rentaldrivego/api"]
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
- traefik-proxy
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
env_file:
|
||||||
|
- ${APP_ENV_FILE:-.env.production}
|
||||||
|
environment:
|
||||||
|
DATABASE_URL_FROM_POSTGRES: ${DATABASE_URL_FROM_POSTGRES:-true}
|
||||||
|
FILE_STORAGE_ROOT: /var/lib/rentaldrivego/storage
|
||||||
|
POSTGRES_HOST: ${POSTGRES_HOST:-postgres}
|
||||||
|
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-rentaldrivego}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||||
|
volumes:
|
||||||
|
- api_uploads:/var/lib/rentaldrivego/storage
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.api.rule=Host(`${API_DOMAIN}`) && !HeadersRegexp(`x-middleware-subrequest`, `.+`)
|
||||||
|
- traefik.http.routers.api.entrypoints=websecure
|
||||||
|
- traefik.http.routers.api.tls=true
|
||||||
|
- traefik.http.routers.api.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.services.api.loadbalancer.server.port=4000
|
||||||
|
- traefik.http.routers.api.middlewares=rdg-security-headers@docker
|
||||||
|
- traefik.http.middlewares.rdg-security-headers.headers.stsSeconds=31536000
|
||||||
|
- traefik.http.middlewares.rdg-security-headers.headers.stsIncludeSubdomains=true
|
||||||
|
- traefik.http.middlewares.rdg-security-headers.headers.contentTypeNosniff=true
|
||||||
|
- traefik.http.middlewares.rdg-security-headers.headers.frameDeny=true
|
||||||
|
- traefik.http.middlewares.rdg-security-headers.headers.referrerPolicy=strict-origin-when-cross-origin
|
||||||
|
- traefik.http.middlewares.rdg-security-headers.headers.permissionsPolicy=camera=(), microphone=(), geolocation=()
|
||||||
|
|
||||||
|
homepage:
|
||||||
|
<<: *node-runtime-hardening
|
||||||
|
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.production
|
||||||
|
args:
|
||||||
|
API_INTERNAL_URL: ${API_INTERNAL_URL:-http://api:4000/api/v1}
|
||||||
|
DASHBOARD_INTERNAL_URL: ${DASHBOARD_INTERNAL_URL:-http://dashboard:3001}
|
||||||
|
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
||||||
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
||||||
|
NEXT_PUBLIC_HOMEPAGE_URL: ${NEXT_PUBLIC_HOMEPAGE_URL}
|
||||||
|
NEXT_PUBLIC_MARKETPLACE_URL: ${NEXT_PUBLIC_MARKETPLACE_URL}
|
||||||
|
NEXT_PUBLIC_STOREFRONT_URL: ${NEXT_PUBLIC_STOREFRONT_URL}
|
||||||
|
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
||||||
|
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
||||||
|
SITE_ORIGIN: ${SITE_ORIGIN}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3000/', (r) => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))\""]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 60s
|
||||||
|
command: ["npm", "run", "start", "--workspace", "@rentaldrivego/homepage"]
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
- traefik-proxy
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
env_file:
|
||||||
|
- ${APP_ENV_FILE:-.env.production}
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.homepage.rule=(Host(`${PUBLIC_SITE_DOMAIN}`) || Host(`www.${PUBLIC_SITE_DOMAIN}`)) && !HeadersRegexp(`x-middleware-subrequest`, `.+`)
|
||||||
|
- traefik.http.routers.homepage.entrypoints=websecure
|
||||||
|
- traefik.http.routers.homepage.tls=true
|
||||||
|
- traefik.http.routers.homepage.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.routers.homepage.service=homepage-svc
|
||||||
|
- traefik.http.routers.homepage.priority=10
|
||||||
|
- traefik.http.services.homepage-svc.loadbalancer.server.port=3000
|
||||||
|
- traefik.http.routers.homepage.middlewares=rdg-security-headers@docker
|
||||||
|
|
||||||
|
storefront:
|
||||||
|
<<: *node-runtime-hardening
|
||||||
|
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.production
|
||||||
|
args:
|
||||||
|
API_INTERNAL_URL: ${API_INTERNAL_URL:-http://api:4000/api/v1}
|
||||||
|
DASHBOARD_INTERNAL_URL: ${DASHBOARD_INTERNAL_URL:-http://dashboard:3001}
|
||||||
|
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
||||||
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
||||||
|
NEXT_PUBLIC_MARKETPLACE_URL: ${NEXT_PUBLIC_MARKETPLACE_URL}
|
||||||
|
NEXT_PUBLIC_HOMEPAGE_URL: ${NEXT_PUBLIC_HOMEPAGE_URL}
|
||||||
|
NEXT_PUBLIC_STOREFRONT_URL: ${NEXT_PUBLIC_STOREFRONT_URL}
|
||||||
|
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
||||||
|
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
||||||
|
SITE_ORIGIN: ${SITE_ORIGIN}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3004/', (r) => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))\""]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 60s
|
||||||
|
command: ["npm", "run", "start", "--workspace", "@rentaldrivego/storefront"]
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
- traefik-proxy
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
env_file:
|
||||||
|
- ${APP_ENV_FILE:-.env.production}
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.storefront.rule=(Host(`${PUBLIC_SITE_DOMAIN}`) || Host(`www.${PUBLIC_SITE_DOMAIN}`)) && PathPrefix(`/explore`, `/renter`, `/sign-in`)
|
||||||
|
- traefik.http.routers.storefront.entrypoints=websecure
|
||||||
|
- traefik.http.routers.storefront.tls=true
|
||||||
|
- traefik.http.routers.storefront.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.routers.storefront.service=storefront-svc
|
||||||
|
- traefik.http.routers.storefront.priority=20
|
||||||
|
- traefik.http.services.storefront-svc.loadbalancer.server.port=3004
|
||||||
|
- traefik.http.routers.storefront.middlewares=rdg-security-headers@docker
|
||||||
|
|
||||||
|
dashboard:
|
||||||
|
<<: *node-runtime-hardening
|
||||||
|
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.production
|
||||||
|
args:
|
||||||
|
API_INTERNAL_URL: ${API_INTERNAL_URL:-http://api:4000/api/v1}
|
||||||
|
DASHBOARD_INTERNAL_URL: ${DASHBOARD_INTERNAL_URL:-http://dashboard:3001}
|
||||||
|
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
||||||
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
||||||
|
NEXT_PUBLIC_MARKETPLACE_URL: ${NEXT_PUBLIC_MARKETPLACE_URL}
|
||||||
|
NEXT_PUBLIC_HOMEPAGE_URL: ${NEXT_PUBLIC_HOMEPAGE_URL}
|
||||||
|
NEXT_PUBLIC_STOREFRONT_URL: ${NEXT_PUBLIC_STOREFRONT_URL}
|
||||||
|
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
||||||
|
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
||||||
|
SITE_ORIGIN: ${SITE_ORIGIN}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3001/dashboard', (r) => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))\""]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 60s
|
||||||
|
command: ["npm", "run", "start", "--workspace", "@rentaldrivego/dashboard"]
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
- traefik-proxy
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
env_file:
|
||||||
|
- ${APP_ENV_FILE:-.env.production}
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.dashboard.rule=(Host(`${PUBLIC_SITE_DOMAIN}`) || Host(`www.${PUBLIC_SITE_DOMAIN}`)) && PathPrefix(`/dashboard`) && !HeadersRegexp(`x-middleware-subrequest`, `.+`)
|
||||||
|
- traefik.http.routers.dashboard.entrypoints=websecure
|
||||||
|
- traefik.http.routers.dashboard.tls=true
|
||||||
|
- traefik.http.routers.dashboard.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.routers.dashboard.service=dashboard-svc
|
||||||
|
- traefik.http.routers.dashboard.priority=20
|
||||||
|
- traefik.http.services.dashboard-svc.loadbalancer.server.port=3001
|
||||||
|
- traefik.http.routers.dashboard.middlewares=rdg-security-headers@docker
|
||||||
|
|
||||||
|
admin:
|
||||||
|
<<: *node-runtime-hardening
|
||||||
|
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.production
|
||||||
|
args:
|
||||||
|
API_INTERNAL_URL: ${API_INTERNAL_URL:-http://api:4000/api/v1}
|
||||||
|
DASHBOARD_INTERNAL_URL: ${DASHBOARD_INTERNAL_URL:-http://dashboard:3001}
|
||||||
|
ADMIN_INTERNAL_URL: ${ADMIN_INTERNAL_URL:-http://admin:3002}
|
||||||
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
||||||
|
NEXT_PUBLIC_MARKETPLACE_URL: ${NEXT_PUBLIC_MARKETPLACE_URL}
|
||||||
|
NEXT_PUBLIC_HOMEPAGE_URL: ${NEXT_PUBLIC_HOMEPAGE_URL}
|
||||||
|
NEXT_PUBLIC_STOREFRONT_URL: ${NEXT_PUBLIC_STOREFRONT_URL}
|
||||||
|
NEXT_PUBLIC_DASHBOARD_URL: ${NEXT_PUBLIC_DASHBOARD_URL}
|
||||||
|
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL}
|
||||||
|
SITE_ORIGIN: ${SITE_ORIGIN}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3002/admin', (r) => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))\""]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 60s
|
||||||
|
command: ["npm", "run", "start", "--workspace", "@rentaldrivego/admin"]
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
- traefik-proxy
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
env_file:
|
||||||
|
- ${APP_ENV_FILE:-.env.production}
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.admin.rule=(Host(`${PUBLIC_SITE_DOMAIN}`) || Host(`www.${PUBLIC_SITE_DOMAIN}`)) && PathPrefix(`/admin`) && !HeadersRegexp(`x-middleware-subrequest`, `.+`)
|
||||||
|
- traefik.http.routers.admin.entrypoints=websecure
|
||||||
|
- traefik.http.routers.admin.tls=true
|
||||||
|
- traefik.http.routers.admin.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.routers.admin.service=admin-svc
|
||||||
|
- traefik.http.routers.admin.priority=20
|
||||||
|
- traefik.http.services.admin-svc.loadbalancer.server.port=3002
|
||||||
|
- traefik.http.routers.admin.middlewares=rdg-security-headers@docker
|
||||||
|
|
||||||
|
pgmanage:
|
||||||
|
image: cmdpromptinc/pgmanage-enterprise:latest
|
||||||
|
profiles:
|
||||||
|
- private-tools
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
PGMANAGE_LISTEN_PORT: "8000"
|
||||||
|
PGMANAGE_DEFAULT_USERNAME: ${PGMANAGE_DEFAULT_USERNAME:-}
|
||||||
|
PGMANAGE_DEFAULT_PASSWORD: ${PGMANAGE_DEFAULT_PASSWORD:-}
|
||||||
|
PGMANAGE_LICENSE_KEY: ${PGMANAGE_LICENSE_KEY:-}
|
||||||
|
PGMANAGE_SECURE_COOKIES: ${PGMANAGE_SECURE_COOKIES:-True}
|
||||||
|
volumes:
|
||||||
|
- pgmanage_prod_data:/appdata
|
||||||
|
- ./docker/pgmanage/override.py:/appdata/override.py:ro
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
driver: bridge
|
||||||
|
traefik-proxy:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_prod_data:
|
||||||
|
redis_prod_data:
|
||||||
|
pgmanage_prod_data:
|
||||||
|
api_uploads:
|
||||||
|
external: true
|
||||||
|
name: ${DOCKER_PROD_API_UPLOADS_VOLUME:-rentaldrivego-prod_api_uploads}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
CSRF_TRUSTED_ORIGINS = ["https://pgmanage.rentaldrivego.ma"]
|
||||||
|
ALLOWED_HOSTS = ["pgmanage.rentaldrivego.ma"]
|
||||||
|
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||||
|
USE_X_FORWARDED_HOST = True
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
|
CSRF_COOKIE_SECURE = True
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
Executable
+69
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
IMAGE_TAG="${IMAGE_TAG:-${1:-13d0512048d86e9fe93e9395459d04663c2b7eb0}}"
|
||||||
|
APP_IMAGE="${APP_IMAGE:-10.0.0.4/melabidi/carmanagement}"
|
||||||
|
REGISTRY_USER="${REGISTRY_USER:-melabidi}"
|
||||||
|
REGISTRY_TOKEN="${REGISTRY_TOKEN:-}"
|
||||||
|
VPN_CONTAINER="${VPN_CONTAINER:-}"
|
||||||
|
TRANSFER_DIR="${TRANSFER_DIR:-/opt/docker-image-transfer}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
VPN_CONTAINER=<container-id-or-name> ./fetch-image-through-vpn.sh [image-tag]
|
||||||
|
|
||||||
|
Optional env vars:
|
||||||
|
APP_IMAGE Default: 10.0.0.4/melabidi/carmanagement
|
||||||
|
IMAGE_TAG Default: 13d0512048d86e9fe93e9395459d04663c2b7eb0
|
||||||
|
REGISTRY_USER Default: melabidi
|
||||||
|
REGISTRY_TOKEN Prompted when unset
|
||||||
|
TRANSFER_DIR Default: /opt/docker-image-transfer
|
||||||
|
|
||||||
|
The script uses skopeo inside the VPN container network and writes:
|
||||||
|
${TRANSFER_DIR}/carmanagement-${IMAGE_TAG}.tar
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -z "${IMAGE_TAG}" ]]; then
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${VPN_CONTAINER}" ]]; then
|
||||||
|
echo "VPN_CONTAINER is required." >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
echo "Missing required command: docker" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
archive_name="carmanagement-${IMAGE_TAG}.tar"
|
||||||
|
archive_path="${TRANSFER_DIR}/${archive_name}"
|
||||||
|
|
||||||
|
sudo mkdir -p "${TRANSFER_DIR}"
|
||||||
|
sudo chmod 700 "${TRANSFER_DIR}"
|
||||||
|
sudo rm -f "${archive_path}"
|
||||||
|
|
||||||
|
if [[ -z "${REGISTRY_TOKEN}" ]]; then
|
||||||
|
read -rsp "Registry token: " REGISTRY_TOKEN
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
--network "container:${VPN_CONTAINER}" \
|
||||||
|
-v "${TRANSFER_DIR}:/transfer" \
|
||||||
|
quay.io/skopeo/stable:latest \
|
||||||
|
copy \
|
||||||
|
--override-os linux \
|
||||||
|
--override-arch amd64 \
|
||||||
|
--src-tls-verify=false \
|
||||||
|
--src-creds "${REGISTRY_USER}:${REGISTRY_TOKEN}" \
|
||||||
|
"docker://${APP_IMAGE}:${IMAGE_TAG}" \
|
||||||
|
"docker-archive:/transfer/${archive_name}:${APP_IMAGE}:${IMAGE_TAG}"
|
||||||
|
|
||||||
|
echo "Image archive ready: ${TRANSFER_DIR}/${archive_name}"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# Backup & Restore Guide
|
||||||
|
|
||||||
|
## What gets backed up
|
||||||
|
|
||||||
|
| File | Contents |
|
||||||
|
|------|----------|
|
||||||
|
| `postgres.dump` | Full PostgreSQL database (pg_dump custom format) |
|
||||||
|
| `api-uploads.tar.gz` | User-uploaded files (vehicle images, documents, etc.) |
|
||||||
|
| `traefik-letsencrypt.tar.gz` | Let's Encrypt SSL certificates |
|
||||||
|
| `volumes/` | Raw Docker volume archives (Redis, pgmanage, etc.) |
|
||||||
|
| `manifest.txt` | Backup metadata (timestamp, project name) |
|
||||||
|
|
||||||
|
> **Note:** The `.env.docker.production` file is NOT included in backups. Store it separately in a secure location (password manager, encrypted storage). You will need it to restore on a new server.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running a Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/car_management_system
|
||||||
|
|
||||||
|
# Default — saves to ./backups/<timestamp>/
|
||||||
|
bash scripts/docker-prod-backup.sh
|
||||||
|
|
||||||
|
# Custom backup directory
|
||||||
|
bash scripts/docker-prod-backup.sh /mnt/external/backups
|
||||||
|
```
|
||||||
|
|
||||||
|
The script will create a timestamped directory, e.g.:
|
||||||
|
```
|
||||||
|
backups/rentaldrivego-prod-20260522T103000Z/
|
||||||
|
```
|
||||||
|
|
||||||
|
PostgreSQL does not need to be stopped. The script brings it up automatically if needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running a Restore
|
||||||
|
|
||||||
|
> **Warning:** Restore is destructive. It will overwrite the current database and uploaded files.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/car_management_system
|
||||||
|
|
||||||
|
bash scripts/docker-prod-restore.sh backups/rentaldrivego-prod-<timestamp> --yes
|
||||||
|
```
|
||||||
|
|
||||||
|
The `--yes` flag is required. Without it, the script exits with instructions.
|
||||||
|
|
||||||
|
### What the restore does (in order)
|
||||||
|
|
||||||
|
1. Stops all app services (api, dashboard, storefront, admin, pgmanage, redis, traefik)
|
||||||
|
2. Starts PostgreSQL
|
||||||
|
3. Restores the database with `pg_restore --clean --if-exists`
|
||||||
|
4. Restores uploaded files into the api_uploads volume
|
||||||
|
5. Restores Traefik SSL certificates
|
||||||
|
6. Restores remaining Docker volumes (Redis, pgmanage)
|
||||||
|
7. Starts all services back up
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Automated Daily Backups (cron)
|
||||||
|
|
||||||
|
To schedule a daily backup at 3:00 AM:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
(crontab -l 2>/dev/null; echo '0 3 * * * cd /root/car_management_system && bash scripts/docker-prod-backup.sh /root/car_management_system/backups >> /var/log/rentaldrivego-backup.log 2>&1') | crontab -
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the cron job was added:
|
||||||
|
```bash
|
||||||
|
crontab -l
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cleaning up old backups
|
||||||
|
|
||||||
|
To keep only the last 7 days of backups, add a cleanup job:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
(crontab -l 2>/dev/null; echo '30 3 * * * find /root/car_management_system/backups -maxdepth 1 -name "rentaldrivego-prod-*" -mtime +7 -exec rm -rf {} +') | crontab -
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Restoring on a Fresh Server
|
||||||
|
|
||||||
|
1. Install Docker and Docker Compose on the new server
|
||||||
|
2. Clone the repository
|
||||||
|
3. Recreate `.env.docker.production` (from your secure backup of that file)
|
||||||
|
4. Copy the backup directory to the new server
|
||||||
|
5. Run the restore script:
|
||||||
|
```bash
|
||||||
|
bash scripts/docker-prod-restore.sh /path/to/backup --yes
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verifying a Backup
|
||||||
|
|
||||||
|
Check the backup directory contents and sizes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -lh backups/rentaldrivego-prod-<timestamp>/
|
||||||
|
cat backups/rentaldrivego-prod-<timestamp>/manifest.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Test that the database dump is valid:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pg_restore --list backups/rentaldrivego-prod-<timestamp>/postgres.dump | head -20
|
||||||
|
```
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
root@rentaldrivego:~# command -v docker && (crontab -l 2>/dev/null | grep -v 'docker image prune -a -f'; echo '0 2 * * * /usr/bin/docker image prune -a -f >> /var/log/docker-image-prune.log 2>&1') | crontab - && crontab -l | tail -n 5
|
||||||
|
/usr/bin/docker
|
||||||
|
0 2 * * * /usr/bin/docker image prune -a -f >> /var/log/docker-image-prune.log 2>&1
|
||||||
|
|
||||||
|
#how to run scripts
|
||||||
|
bash scripts/docker-prod-up-frontends.sh
|
||||||
Executable
+100
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||||
|
|
||||||
|
BACKUP_ROOT="${1:-${ROOT_DIR}/backups}"
|
||||||
|
shift || true
|
||||||
|
EXTRA_VOLUMES=("$@")
|
||||||
|
TIMESTAMP="$(date -u +"%Y%m%dT%H%M%SZ")"
|
||||||
|
mkdir -p "${BACKUP_ROOT}"
|
||||||
|
BACKUP_ROOT="$(cd "${BACKUP_ROOT}" && pwd)"
|
||||||
|
BACKUP_DIR="${BACKUP_ROOT}/rentaldrivego-prod-${TIMESTAMP}"
|
||||||
|
VOLUME_BACKUP_DIR="${BACKUP_DIR}/volumes"
|
||||||
|
|
||||||
|
DEFAULT_VOLUMES=(
|
||||||
|
"${PROJECT_NAME}_api_uploads"
|
||||||
|
"${PROJECT_NAME}_pgmanage_prod_data"
|
||||||
|
"${PROJECT_NAME}_postgres_prod_data"
|
||||||
|
"${PROJECT_NAME}_redis_prod_data"
|
||||||
|
)
|
||||||
|
|
||||||
|
mkdir -p "${BACKUP_DIR}"
|
||||||
|
mkdir -p "${VOLUME_BACKUP_DIR}"
|
||||||
|
|
||||||
|
ensure_env_file
|
||||||
|
ensure_traefik_network
|
||||||
|
|
||||||
|
echo "Creating production backup in ${BACKUP_DIR}"
|
||||||
|
|
||||||
|
prod_compose up -d postgres >/dev/null
|
||||||
|
|
||||||
|
echo "Backing up PostgreSQL database..."
|
||||||
|
prod_compose exec -T postgres sh -lc 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' > "${BACKUP_DIR}/postgres.dump"
|
||||||
|
|
||||||
|
echo "Backing up uploaded files..."
|
||||||
|
prod_compose run --rm --no-deps api bash -lc '
|
||||||
|
mkdir -p /var/lib/rentaldrivego/storage
|
||||||
|
tar -czf - -C /var/lib/rentaldrivego/storage .
|
||||||
|
' > "${BACKUP_DIR}/api-uploads.tar.gz"
|
||||||
|
|
||||||
|
echo "Backing up Traefik certificates..."
|
||||||
|
if traefik_compose run --rm traefik sh -lc 'test -d /letsencrypt' > /dev/null 2>&1; then
|
||||||
|
traefik_compose run --rm traefik sh -lc '
|
||||||
|
mkdir -p /letsencrypt
|
||||||
|
tar -czf - -C /letsencrypt .
|
||||||
|
' > "${BACKUP_DIR}/traefik-letsencrypt.tar.gz"
|
||||||
|
else
|
||||||
|
echo "Skipping Traefik certificate backup; Traefik runtime is not available."
|
||||||
|
fi
|
||||||
|
|
||||||
|
declare -a ALL_VOLUMES=("${DEFAULT_VOLUMES[@]}")
|
||||||
|
|
||||||
|
if [[ -n "${DOCKER_EXTRA_BACKUP_VOLUMES:-}" ]]; then
|
||||||
|
# shellcheck disable=SC2206
|
||||||
|
ALL_VOLUMES+=(${DOCKER_EXTRA_BACKUP_VOLUMES})
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${#EXTRA_VOLUMES[@]}" -gt 0 ]]; then
|
||||||
|
ALL_VOLUMES+=("${EXTRA_VOLUMES[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Backing up named Docker volumes..."
|
||||||
|
declare -A seen_volumes=()
|
||||||
|
for volume_name in "${ALL_VOLUMES[@]}"; do
|
||||||
|
[[ -n "${volume_name}" ]] || continue
|
||||||
|
if [[ -n "${seen_volumes[${volume_name}]:-}" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
seen_volumes["${volume_name}"]=1
|
||||||
|
|
||||||
|
if ! docker_volume_exists "${volume_name}"; then
|
||||||
|
echo "Skipping missing volume: ${volume_name}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " - ${volume_name}"
|
||||||
|
backup_named_volume "${volume_name}" "${VOLUME_BACKUP_DIR}/${volume_name}.tar.gz"
|
||||||
|
done
|
||||||
|
|
||||||
|
cat > "${BACKUP_DIR}/manifest.txt" <<EOF
|
||||||
|
project=${PROJECT_NAME}
|
||||||
|
created_at_utc=${TIMESTAMP}
|
||||||
|
database_backup=postgres.dump
|
||||||
|
uploads_backup=api-uploads.tar.gz
|
||||||
|
traefik_backup=traefik-letsencrypt.tar.gz
|
||||||
|
volume_backup_dir=volumes
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Backup complete:"
|
||||||
|
echo " - ${BACKUP_DIR}/postgres.dump"
|
||||||
|
echo " - ${BACKUP_DIR}/api-uploads.tar.gz"
|
||||||
|
if [[ -f "${BACKUP_DIR}/traefik-letsencrypt.tar.gz" ]]; then
|
||||||
|
echo " - ${BACKUP_DIR}/traefik-letsencrypt.tar.gz"
|
||||||
|
fi
|
||||||
|
if compgen -G "${VOLUME_BACKUP_DIR}/*.tar.gz" > /dev/null; then
|
||||||
|
echo " - ${VOLUME_BACKUP_DIR}/*.tar.gz"
|
||||||
|
fi
|
||||||
|
echo " - ${BACKUP_DIR}/manifest.txt"
|
||||||
Executable
+340
@@ -0,0 +1,340 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
PROD_COMPOSE_FILE="${ROOT_DIR}/docker-compose.production.yml"
|
||||||
|
TRAEFIK_COMPOSE_FILE="${ROOT_DIR}/traefik.yaml"
|
||||||
|
ENV_FILE="${ROOT_DIR}/.env.docker.production"
|
||||||
|
PROJECT_NAME="${DOCKER_PROD_PROJECT_NAME:-rentaldrivego-prod}"
|
||||||
|
TRAEFIK_NETWORK="${DOCKER_PROD_TRAEFIK_NETWORK:-traefik-proxy}"
|
||||||
|
VOLUME_BACKUP_IMAGE="${DOCKER_VOLUME_BACKUP_IMAGE:-postgres:16-alpine}"
|
||||||
|
API_UPLOADS_VOLUME="${DOCKER_PROD_API_UPLOADS_VOLUME:-${PROJECT_NAME}_api_uploads}"
|
||||||
|
|
||||||
|
ensure_env_file() {
|
||||||
|
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||||
|
echo "Missing ${ENV_FILE}" >&2
|
||||||
|
echo "Create it from .env.docker.production.example before starting production containers." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
validate_prod_env_file
|
||||||
|
}
|
||||||
|
|
||||||
|
read_env_value() {
|
||||||
|
local key="$1"
|
||||||
|
local value
|
||||||
|
|
||||||
|
value="$(
|
||||||
|
awk -F= -v key="$key" '
|
||||||
|
$1 == key {
|
||||||
|
value = substr($0, index($0, "=") + 1)
|
||||||
|
}
|
||||||
|
END {
|
||||||
|
if (value != "") {
|
||||||
|
print value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' "${ENV_FILE}"
|
||||||
|
)"
|
||||||
|
|
||||||
|
value="${value%$'\r'}"
|
||||||
|
value="${value#\"}"
|
||||||
|
value="${value%\"}"
|
||||||
|
printf '%s' "${value}"
|
||||||
|
}
|
||||||
|
|
||||||
|
export_env_value_if_unset() {
|
||||||
|
local key="$1"
|
||||||
|
local value
|
||||||
|
|
||||||
|
if [[ -n "${!key:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
value="$(read_env_value "${key}")"
|
||||||
|
if [[ -n "${value}" ]]; then
|
||||||
|
export "${key}=${value}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
load_prod_env_exports() {
|
||||||
|
local key
|
||||||
|
|
||||||
|
for key in \
|
||||||
|
APP_IMAGE \
|
||||||
|
APP_VERSION \
|
||||||
|
IMAGE_TAG \
|
||||||
|
IMAGE_ARCHIVE_DIR \
|
||||||
|
IMAGE_ARCHIVE_REQUIRED \
|
||||||
|
REGISTRY_HOST \
|
||||||
|
REGISTRY_USER \
|
||||||
|
REGISTRY_PASSWORD
|
||||||
|
do
|
||||||
|
export_env_value_if_unset "${key}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_prod_env_file() {
|
||||||
|
local api_domain public_site_domain cors_origins postgres_password redis_password jwt_secret
|
||||||
|
|
||||||
|
api_domain="$(read_env_value API_DOMAIN)"
|
||||||
|
public_site_domain="$(read_env_value PUBLIC_SITE_DOMAIN)"
|
||||||
|
cors_origins="$(read_env_value CORS_ORIGINS)"
|
||||||
|
postgres_password="$(read_env_value POSTGRES_PASSWORD)"
|
||||||
|
redis_password="$(read_env_value REDIS_PASSWORD)"
|
||||||
|
jwt_secret="$(read_env_value JWT_SECRET)"
|
||||||
|
|
||||||
|
if [[ -z "${api_domain}" || "${api_domain}" == *example.com* ]]; then
|
||||||
|
echo "Invalid API_DOMAIN in ${ENV_FILE}. Set it to the real production API hostname, not example.com." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${public_site_domain}" || "${public_site_domain}" == *example.com* ]]; then
|
||||||
|
echo "Invalid PUBLIC_SITE_DOMAIN in ${ENV_FILE}. Set it to the real production site hostname, not example.com." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${cors_origins}" || "${cors_origins}" == *example.com* ]]; then
|
||||||
|
echo "Invalid CORS_ORIGINS in ${ENV_FILE}. Replace example.com origins with the real production domains." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for pair in "POSTGRES_PASSWORD:${postgres_password}" "REDIS_PASSWORD:${redis_password}" "JWT_SECRET:${jwt_secret}"; do
|
||||||
|
key="${pair%%:*}"
|
||||||
|
value="${pair#*:}"
|
||||||
|
if [[ -z "${value}" || "${value}" == replace-with-* || "${value}" == *changeme* || "${value}" == *change-me* ]]; then
|
||||||
|
echo "Invalid ${key} in ${ENV_FILE}. Set a real production secret." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_traefik_network() {
|
||||||
|
if ! docker network inspect "${TRAEFIK_NETWORK}" >/dev/null 2>&1; then
|
||||||
|
echo "Creating Docker network: ${TRAEFIK_NETWORK}"
|
||||||
|
docker network create "${TRAEFIK_NETWORK}" >/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_api_uploads_volume() {
|
||||||
|
if ! docker volume inspect "${API_UPLOADS_VOLUME}" >/dev/null 2>&1; then
|
||||||
|
echo "Creating Docker volume: ${API_UPLOADS_VOLUME}"
|
||||||
|
docker volume create "${API_UPLOADS_VOLUME}" >/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
prod_compose() {
|
||||||
|
APP_ENV_FILE="${ENV_FILE}" DOCKER_PROD_API_UPLOADS_VOLUME="${API_UPLOADS_VOLUME}" docker compose -p "${PROJECT_NAME}" --env-file "${ENV_FILE}" -f "${PROD_COMPOSE_FILE}" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
traefik_compose() {
|
||||||
|
docker compose --env-file "${ENV_FILE}" -f "${TRAEFIK_COMPOSE_FILE}" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
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|storefront|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() {
|
||||||
|
IMAGE_TAG="${IMAGE_TAG:-${APP_VERSION:-}}"
|
||||||
|
export IMAGE_TAG
|
||||||
|
|
||||||
|
if [[ -z "${IMAGE_TAG:-}" ]]; then
|
||||||
|
echo "IMAGE_TAG must be set for production deploys." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
login_registry_if_configured() {
|
||||||
|
if [[ "${IMAGE_ARCHIVE_REQUIRED:-false}" == "true" || "${IMAGE_ARCHIVE_REQUIRED:-0}" == "1" ]]; then
|
||||||
|
echo "IMAGE_ARCHIVE_REQUIRED is enabled; skipping docker login."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${IMAGE_ARCHIVE_DIR:-}" && -d "${IMAGE_ARCHIVE_DIR}" ]]; then
|
||||||
|
shopt -s nullglob
|
||||||
|
local archives=(
|
||||||
|
"${IMAGE_ARCHIVE_DIR}"/*"${IMAGE_TAG:-latest}"*.tar
|
||||||
|
"${IMAGE_ARCHIVE_DIR}"/*"${IMAGE_TAG:-latest}"*.tar.gz
|
||||||
|
"${IMAGE_ARCHIVE_DIR}"/*"${IMAGE_TAG:-latest}"*.tgz
|
||||||
|
)
|
||||||
|
shopt -u nullglob
|
||||||
|
|
||||||
|
if [[ ${#archives[@]} -gt 0 && -z "${REGISTRY_USER:-}" && -z "${REGISTRY_PASSWORD:-}" ]]; then
|
||||||
|
echo "Found image archive in IMAGE_ARCHIVE_DIR; skipping registry login."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${REGISTRY_USER:-}" && -z "${REGISTRY_PASSWORD:-}" ]]; then
|
||||||
|
if [[ -n "${REGISTRY_HOST:-}" ]]; then
|
||||||
|
echo "REGISTRY_HOST is set but no registry credentials are configured; skipping docker login."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${REGISTRY_HOST:-}" || -z "${REGISTRY_USER:-}" || -z "${REGISTRY_PASSWORD:-}" ]]; then
|
||||||
|
echo "REGISTRY_HOST, REGISTRY_USER, and REGISTRY_PASSWORD are all required when registry credentials are configured." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${REGISTRY_PASSWORD}" | docker login "${REGISTRY_HOST}" -u "${REGISTRY_USER}" --password-stdin
|
||||||
|
}
|
||||||
|
|
||||||
|
pull_prod_release_images() {
|
||||||
|
require_release_image
|
||||||
|
|
||||||
|
if load_release_image_archives; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${IMAGE_ARCHIVE_REQUIRED:-false}" == "true" || "${IMAGE_ARCHIVE_REQUIRED:-0}" == "1" ]]; then
|
||||||
|
echo "IMAGE_ARCHIVE_REQUIRED is enabled, but no archive for IMAGE_TAG=${IMAGE_TAG} was found in ${IMAGE_ARCHIVE_DIR:-<unset>}." >&2
|
||||||
|
echo "The VPS cannot reach the VPN-only registry. Create a shared archive first, for example:" >&2
|
||||||
|
echo " docker save ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG} -o ${IMAGE_ARCHIVE_DIR:-./image-archives}/carmanagement-${IMAGE_TAG}.tar" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
prod_compose pull migrate api storefront dashboard admin
|
||||||
|
}
|
||||||
|
|
||||||
|
load_release_image_archives() {
|
||||||
|
local archive_dir="${IMAGE_ARCHIVE_DIR:-}"
|
||||||
|
local archive
|
||||||
|
local loaded=0
|
||||||
|
|
||||||
|
if [[ -z "${archive_dir}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "${archive_dir}" ]]; then
|
||||||
|
echo "IMAGE_ARCHIVE_DIR is set but does not exist: ${archive_dir}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
for archive in \
|
||||||
|
"${archive_dir}"/*"${IMAGE_TAG}"*.tar \
|
||||||
|
"${archive_dir}"/*"${IMAGE_TAG}"*.tar.gz \
|
||||||
|
"${archive_dir}"/*"${IMAGE_TAG}"*.tgz
|
||||||
|
do
|
||||||
|
echo "Loading Docker image archive: ${archive}"
|
||||||
|
docker load -i "${archive}"
|
||||||
|
loaded=1
|
||||||
|
done
|
||||||
|
shopt -u nullglob
|
||||||
|
|
||||||
|
if [[ ${loaded} -eq 0 ]]; then
|
||||||
|
echo "No Docker image archive found for IMAGE_TAG=${IMAGE_TAG} in ${archive_dir}."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Loaded image archive(s) for ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG}; skipping docker pull."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
run_prod_migrations() {
|
||||||
|
prod_compose run --rm migrate
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_healthy() {
|
||||||
|
local service="$1"
|
||||||
|
local timeout="${2:-120}"
|
||||||
|
local container="${PROJECT_NAME}-${service}-1"
|
||||||
|
local elapsed=0
|
||||||
|
|
||||||
|
echo "Waiting for ${service} to be healthy (timeout: ${timeout}s)..."
|
||||||
|
while [[ $elapsed -lt $timeout ]]; do
|
||||||
|
local status
|
||||||
|
status=$(docker inspect --format='{{.State.Health.Status}}' "${container}" 2>/dev/null || echo "missing")
|
||||||
|
case "$status" in
|
||||||
|
healthy)
|
||||||
|
echo "${service} is healthy."
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
unhealthy)
|
||||||
|
echo "ERROR: ${service} is unhealthy. Check logs with: docker logs ${container}" >&2
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
sleep 5
|
||||||
|
elapsed=$((elapsed + 5))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "ERROR: Timed out waiting for ${service} to become healthy." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
start_traefik() {
|
||||||
|
ensure_env_file
|
||||||
|
ensure_traefik_network
|
||||||
|
traefik_compose up -d
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_traefik() {
|
||||||
|
ensure_env_file
|
||||||
|
traefik_compose down >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
docker_volume_exists() {
|
||||||
|
docker volume inspect "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_named_volume() {
|
||||||
|
local volume_name="$1"
|
||||||
|
local archive_path="$2"
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "${volume_name}:/volume:ro" \
|
||||||
|
-v "$(dirname "${archive_path}"):/backup" \
|
||||||
|
"${VOLUME_BACKUP_IMAGE}" \
|
||||||
|
sh -lc "tar -czf /backup/$(basename "${archive_path}") -C /volume ."
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_running_containers() {
|
||||||
|
docker ps --filter "volume=$1" --format '{{.Names}}'
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_named_volume() {
|
||||||
|
local volume_name="$1"
|
||||||
|
local archive_path="$2"
|
||||||
|
local running
|
||||||
|
|
||||||
|
running="$(volume_running_containers "${volume_name}")"
|
||||||
|
if [[ -n "${running}" ]]; then
|
||||||
|
echo "Volume ${volume_name} is currently used by running containers:" >&2
|
||||||
|
echo "${running}" >&2
|
||||||
|
echo "Stop those containers before restoring this volume." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "${volume_name}:/volume" \
|
||||||
|
-v "$(dirname "${archive_path}"):/backup" \
|
||||||
|
"${VOLUME_BACKUP_IMAGE}" \
|
||||||
|
sh -lc "find /volume -mindepth 1 -delete && tar -xzf /backup/$(basename "${archive_path}") -C /volume"
|
||||||
|
}
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/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:-rentaldrivego/carmanagement}:${IMAGE_TAG:-${APP_VERSION:-<missing>}}"
|
||||||
|
|
||||||
|
ensure_env_file
|
||||||
|
load_prod_env_exports
|
||||||
|
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 storefront dashboard admin
|
||||||
|
wait_for_healthy api 180
|
||||||
|
wait_for_healthy storefront 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:-rentaldrivego/carmanagement}:${IMAGE_TAG} is up and healthy."
|
||||||
Executable
+114
@@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||||
|
|
||||||
|
BACKUP_DIR="${1:-}"
|
||||||
|
CONFIRM_FLAG="${2:-}"
|
||||||
|
|
||||||
|
if [[ -z "${BACKUP_DIR}" ]]; then
|
||||||
|
echo "Usage: bash scripts/docker-prod-restore.sh <backup-dir> --yes" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${CONFIRM_FLAG}" != "--yes" ]]; then
|
||||||
|
echo "Restore is destructive and will overwrite the production database and uploaded files." >&2
|
||||||
|
echo "Re-run with: bash scripts/docker-prod-restore.sh ${BACKUP_DIR} --yes" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "${BACKUP_DIR}" ]]; then
|
||||||
|
echo "Backup directory not found: ${BACKUP_DIR}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "${BACKUP_DIR}/postgres.dump" ]]; then
|
||||||
|
echo "Missing database backup: ${BACKUP_DIR}/postgres.dump" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "${BACKUP_DIR}/api-uploads.tar.gz" ]]; then
|
||||||
|
echo "Missing uploads backup: ${BACKUP_DIR}/api-uploads.tar.gz" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ensure_env_file
|
||||||
|
ensure_traefik_network
|
||||||
|
|
||||||
|
RAW_VOLUME_DIR="${BACKUP_DIR}/volumes"
|
||||||
|
SKIP_RAW_RESTORE_VOLUMES=(
|
||||||
|
"${PROJECT_NAME}_api_uploads"
|
||||||
|
"${PROJECT_NAME}_postgres_prod_data"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Stopping application services before restore..."
|
||||||
|
prod_compose stop api storefront dashboard admin pgmanage >/dev/null || true
|
||||||
|
prod_compose stop redis >/dev/null || true
|
||||||
|
stop_traefik
|
||||||
|
|
||||||
|
echo "Ensuring PostgreSQL is running..."
|
||||||
|
prod_compose up -d postgres >/dev/null
|
||||||
|
|
||||||
|
echo "Waiting for PostgreSQL..."
|
||||||
|
until prod_compose exec -T postgres sh -lc 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"' >/dev/null 2>&1; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Restoring PostgreSQL database..."
|
||||||
|
cat "${BACKUP_DIR}/postgres.dump" | prod_compose exec -T postgres sh -lc '
|
||||||
|
pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists --no-owner --no-privileges
|
||||||
|
'
|
||||||
|
|
||||||
|
echo "Restoring uploaded files..."
|
||||||
|
cat "${BACKUP_DIR}/api-uploads.tar.gz" | prod_compose run --rm --no-deps api bash -lc '
|
||||||
|
mkdir -p /var/lib/rentaldrivego/storage
|
||||||
|
find /var/lib/rentaldrivego/storage -mindepth 1 -delete
|
||||||
|
tar -xzf - -C /var/lib/rentaldrivego/storage
|
||||||
|
'
|
||||||
|
|
||||||
|
if [[ -f "${BACKUP_DIR}/traefik-letsencrypt.tar.gz" ]]; then
|
||||||
|
echo "Restoring Traefik certificates..."
|
||||||
|
cat "${BACKUP_DIR}/traefik-letsencrypt.tar.gz" | traefik_compose run --rm traefik sh -lc '
|
||||||
|
mkdir -p /letsencrypt
|
||||||
|
find /letsencrypt -mindepth 1 -delete
|
||||||
|
tar -xzf - -C /letsencrypt
|
||||||
|
chmod 600 /letsencrypt/acme.json 2>/dev/null || true
|
||||||
|
'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "${RAW_VOLUME_DIR}" ]]; then
|
||||||
|
echo "Restoring raw Docker volume archives..."
|
||||||
|
for archive_path in "${RAW_VOLUME_DIR}"/*.tar.gz; do
|
||||||
|
[[ -e "${archive_path}" ]] || continue
|
||||||
|
volume_name="$(basename "${archive_path}" .tar.gz)"
|
||||||
|
|
||||||
|
skip_volume=false
|
||||||
|
for managed_volume in "${SKIP_RAW_RESTORE_VOLUMES[@]}"; do
|
||||||
|
if [[ "${volume_name}" == "${managed_volume}" ]]; then
|
||||||
|
skip_volume=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${skip_volume}" == true ]]; then
|
||||||
|
echo "Skipping raw restore for ${volume_name}; handled by service-aware restore."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! docker_volume_exists "${volume_name}"; then
|
||||||
|
echo "Creating missing volume ${volume_name}"
|
||||||
|
docker volume create "${volume_name}" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " - ${volume_name}"
|
||||||
|
restore_named_volume "${volume_name}" "${archive_path}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting production services..."
|
||||||
|
start_traefik
|
||||||
|
start_prod_services
|
||||||
|
|
||||||
|
echo "Restore complete from ${BACKUP_DIR}"
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||||
|
|
||||||
|
APP_IMAGE="${APP_IMAGE:-registry.rentaldrivego.ma/rentaldrivego/car_management_system}"
|
||||||
|
APP_VERSION="${APP_VERSION:-${1:-}}"
|
||||||
|
REGISTRY_USER="${REGISTRY_USER:-rentaldrivego_registry}"
|
||||||
|
INCLUDE_PGMANAGE="${INCLUDE_PGMANAGE:-0}"
|
||||||
|
POSTGRES_CONTAINER="${PROJECT_NAME}-postgres-1"
|
||||||
|
REDIS_CONTAINER="${PROJECT_NAME}-redis-1"
|
||||||
|
|
||||||
|
export APP_IMAGE APP_VERSION REGISTRY_USER
|
||||||
|
|
||||||
|
require_release_image
|
||||||
|
ensure_env_file
|
||||||
|
ensure_api_uploads_volume
|
||||||
|
|
||||||
|
container_health_status() {
|
||||||
|
local container_name="$1"
|
||||||
|
docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "${container_name}" 2>/dev/null || echo "missing"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_postgres_ready() {
|
||||||
|
local status
|
||||||
|
status="$(container_health_status "${POSTGRES_CONTAINER}")"
|
||||||
|
|
||||||
|
if [[ "${status}" == "healthy" ]]; then
|
||||||
|
echo "Postgres is already healthy; reusing existing database container."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting Postgres"
|
||||||
|
prod_compose up -d postgres
|
||||||
|
wait_for_healthy postgres 120
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_redis_ready() {
|
||||||
|
local status
|
||||||
|
status="$(container_health_status "${REDIS_CONTAINER}")"
|
||||||
|
|
||||||
|
if [[ "${status}" == "running" || "${status}" == "healthy" ]]; then
|
||||||
|
echo "Redis is already running; leaving it in place."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting Redis"
|
||||||
|
prod_compose up -d redis
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt_registry_password_if_needed() {
|
||||||
|
if [[ -z "${REGISTRY_HOST:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${REGISTRY_PASSWORD:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -t 0 ]]; then
|
||||||
|
echo "REGISTRY_PASSWORD is not set and no interactive terminal is available for a prompt." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'Registry password for %s@%s: ' "${REGISTRY_USER}" "${REGISTRY_HOST}" >&2
|
||||||
|
read -r -s REGISTRY_PASSWORD
|
||||||
|
printf '\n' >&2
|
||||||
|
export REGISTRY_PASSWORD
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Starting production services from pulled image ${APP_IMAGE}:${APP_VERSION}"
|
||||||
|
|
||||||
|
prompt_registry_password_if_needed
|
||||||
|
|
||||||
|
echo "Logging into registry if configured"
|
||||||
|
login_registry_if_configured
|
||||||
|
|
||||||
|
echo "Pulling release images"
|
||||||
|
pull_prod_release_images
|
||||||
|
|
||||||
|
ensure_postgres_ready
|
||||||
|
ensure_redis_ready
|
||||||
|
|
||||||
|
echo "Migration status"
|
||||||
|
prod_compose run --rm --entrypoint sh migrate -lc 'npx prisma migrate status --schema packages/database/prisma/schema.prisma'
|
||||||
|
|
||||||
|
echo "Applying migrations"
|
||||||
|
prod_compose run --rm migrate
|
||||||
|
|
||||||
|
echo "Starting application services"
|
||||||
|
app_services=(api storefront dashboard admin)
|
||||||
|
|
||||||
|
if [[ "${INCLUDE_PGMANAGE}" == "1" ]]; then
|
||||||
|
prod_compose --profile private-tools up -d "${app_services[@]}" pgmanage
|
||||||
|
else
|
||||||
|
prod_compose up -d "${app_services[@]}"
|
||||||
|
fi
|
||||||
|
wait_for_healthy api 180
|
||||||
|
wait_for_healthy storefront 180
|
||||||
|
wait_for_healthy dashboard 180
|
||||||
|
wait_for_healthy admin 180
|
||||||
|
|
||||||
|
echo "Production services are up from ${APP_IMAGE}:${APP_VERSION}"
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||||
|
|
||||||
|
start_prod_services admin
|
||||||
|
wait_for_healthy admin
|
||||||
|
|
||||||
|
echo "Admin is up and healthy."
|
||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||||
|
|
||||||
|
start_traefik
|
||||||
|
start_prod_services postgres redis api storefront dashboard admin
|
||||||
|
wait_for_healthy api
|
||||||
|
wait_for_healthy storefront
|
||||||
|
wait_for_healthy dashboard
|
||||||
|
wait_for_healthy admin
|
||||||
|
|
||||||
|
echo "Production stack is up and healthy: traefik, postgres, redis, api, storefront, dashboard, admin"
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||||
|
|
||||||
|
start_prod_services api
|
||||||
|
wait_for_healthy api
|
||||||
|
|
||||||
|
echo "API is up and healthy."
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||||
|
|
||||||
|
start_prod_services dashboard
|
||||||
|
wait_for_healthy dashboard
|
||||||
|
|
||||||
|
echo "Dashboard is up and healthy."
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||||
|
|
||||||
|
start_prod_services homepage storefront dashboard admin
|
||||||
|
wait_for_healthy homepage
|
||||||
|
wait_for_healthy storefront
|
||||||
|
wait_for_healthy dashboard
|
||||||
|
wait_for_healthy admin
|
||||||
|
|
||||||
|
echo "All frontend services are up and healthy."
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||||
|
|
||||||
|
start_prod_services homepage
|
||||||
|
wait_for_healthy homepage
|
||||||
|
|
||||||
|
echo "Homepage is up and healthy."
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||||
|
|
||||||
|
ensure_env_file
|
||||||
|
|
||||||
|
pgmanage_user="$(read_env_value PGMANAGE_DEFAULT_USERNAME)"
|
||||||
|
pgmanage_password="$(read_env_value PGMANAGE_DEFAULT_PASSWORD)"
|
||||||
|
if [[ -z "${pgmanage_user}" || -z "${pgmanage_password}" || "${pgmanage_user}" == "admin" || "${pgmanage_password}" == replace-with-* ]]; then
|
||||||
|
echo "Set strong PGMANAGE_DEFAULT_USERNAME and PGMANAGE_DEFAULT_PASSWORD before starting pgManage." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting pgManage on the private Docker network only."
|
||||||
|
echo "Access it with an SSH tunnel or VPN path to the Docker host, not through public Traefik."
|
||||||
|
prod_compose --profile private-tools up --build -d pgmanage
|
||||||
|
|
||||||
|
echo "pgManage is starting as a private tool."
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||||
|
|
||||||
|
start_prod_services postgres
|
||||||
|
|
||||||
|
echo "Postgres is starting."
|
||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||||
|
|
||||||
|
start_prod_services redis
|
||||||
|
|
||||||
|
echo "Redis is starting."
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||||
|
|
||||||
|
start_prod_services storefront
|
||||||
|
wait_for_healthy storefront
|
||||||
|
|
||||||
|
echo "Storefront is up and healthy."
|
||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||||
|
|
||||||
|
start_traefik
|
||||||
|
|
||||||
|
echo "Traefik is starting."
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:latest
|
||||||
|
command:
|
||||||
|
- --api.dashboard=false
|
||||||
|
- --api.insecure=false
|
||||||
|
- --providers.docker=true
|
||||||
|
- --providers.docker.exposedbydefault=false
|
||||||
|
- --providers.docker.network=traefik-proxy
|
||||||
|
- --providers.file.directory=/etc/traefik/dynamic
|
||||||
|
- --providers.file.watch=true
|
||||||
|
- --entrypoints.web.address=:80
|
||||||
|
- --entrypoints.websecure.address=:443
|
||||||
|
- --entrypoints.websecure.transport.respondingTimeouts.readTimeout=0
|
||||||
|
- --entrypoints.websecure.transport.respondingTimeouts.writeTimeout=0
|
||||||
|
- --entrypoints.websecure.transport.respondingTimeouts.idleTimeout=1800s
|
||||||
|
- --entrypoints.web.http.redirections.entrypoint.to=websecure
|
||||||
|
- --entrypoints.web.http.redirections.entrypoint.scheme=https
|
||||||
|
- --certificatesresolvers.letsencrypt.acme.tlschallenge=true
|
||||||
|
- --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}
|
||||||
|
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 443:443
|
||||||
|
volumes:
|
||||||
|
- traefik-letsencrypt:/letsencrypt
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./dynamic:/etc/traefik/dynamic:ro
|
||||||
|
networks:
|
||||||
|
- traefik-proxy
|
||||||
|
networks:
|
||||||
|
traefik-proxy:
|
||||||
|
external: true
|
||||||
|
volumes:
|
||||||
|
traefik-letsencrypt:
|
||||||
+110
-12
@@ -52,6 +52,37 @@ read_env_value() {
|
|||||||
printf '%s' "${value}"
|
printf '%s' "${value}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export_env_value_if_unset() {
|
||||||
|
local key="$1"
|
||||||
|
local value
|
||||||
|
|
||||||
|
if [[ -n "${!key:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
value="$(read_env_value "${key}")"
|
||||||
|
if [[ -n "${value}" ]]; then
|
||||||
|
export "${key}=${value}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
load_prod_env_exports() {
|
||||||
|
local key
|
||||||
|
|
||||||
|
for key in \
|
||||||
|
APP_IMAGE \
|
||||||
|
APP_VERSION \
|
||||||
|
IMAGE_TAG \
|
||||||
|
IMAGE_ARCHIVE_DIR \
|
||||||
|
IMAGE_ARCHIVE_REQUIRED \
|
||||||
|
REGISTRY_HOST \
|
||||||
|
REGISTRY_USER \
|
||||||
|
REGISTRY_PASSWORD
|
||||||
|
do
|
||||||
|
export_env_value_if_unset "${key}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
validate_prod_env_file() {
|
validate_prod_env_file() {
|
||||||
local api_domain public_site_domain cors_origins portainer_domain postgres_password redis_password jwt_secret
|
local api_domain public_site_domain cors_origins portainer_domain postgres_password redis_password jwt_secret
|
||||||
|
|
||||||
@@ -113,7 +144,7 @@ ensure_dynamic_dir() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prod_compose() {
|
prod_compose() {
|
||||||
docker compose -p "${PROJECT_NAME}" --env-file "${ENV_FILE}" -f "${PROD_COMPOSE_FILE}" "$@"
|
APP_ENV_FILE="${ENV_FILE}" DOCKER_PROD_API_UPLOADS_VOLUME="${API_UPLOADS_VOLUME}" docker compose -p "${PROJECT_NAME}" --env-file "${ENV_FILE}" -f "${PROD_COMPOSE_FILE}" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
portainer_compose() {
|
portainer_compose() {
|
||||||
@@ -154,25 +185,45 @@ start_prod_services() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
require_release_image() {
|
require_release_image() {
|
||||||
if [[ -z "${APP_IMAGE:-}" || -z "${APP_VERSION:-}" ]]; then
|
IMAGE_TAG="${IMAGE_TAG:-${APP_VERSION:-}}"
|
||||||
echo "APP_IMAGE and APP_VERSION must be set for pull-based production deploys." >&2
|
export IMAGE_TAG
|
||||||
|
|
||||||
|
if [[ -z "${IMAGE_TAG:-}" ]]; then
|
||||||
|
echo "IMAGE_TAG must be set for production deploys." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
login_registry_if_configured() {
|
login_registry_if_configured() {
|
||||||
local configured=0
|
if [[ "${IMAGE_ARCHIVE_REQUIRED:-false}" == "true" || "${IMAGE_ARCHIVE_REQUIRED:-0}" == "1" ]]; then
|
||||||
|
echo "IMAGE_ARCHIVE_REQUIRED is enabled; skipping docker login."
|
||||||
[[ -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
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ${configured} -ne 3 ]]; then
|
if [[ -n "${IMAGE_ARCHIVE_DIR:-}" && -d "${IMAGE_ARCHIVE_DIR}" ]]; then
|
||||||
echo "REGISTRY_HOST, REGISTRY_USER, and REGISTRY_PASSWORD must either all be set or all be unset." >&2
|
shopt -s nullglob
|
||||||
|
local archives=(
|
||||||
|
"${IMAGE_ARCHIVE_DIR}"/*"${IMAGE_TAG:-latest}"*.tar
|
||||||
|
"${IMAGE_ARCHIVE_DIR}"/*"${IMAGE_TAG:-latest}"*.tar.gz
|
||||||
|
"${IMAGE_ARCHIVE_DIR}"/*"${IMAGE_TAG:-latest}"*.tgz
|
||||||
|
)
|
||||||
|
shopt -u nullglob
|
||||||
|
|
||||||
|
if [[ ${#archives[@]} -gt 0 && -z "${REGISTRY_USER:-}" && -z "${REGISTRY_PASSWORD:-}" ]]; then
|
||||||
|
echo "Found image archive in IMAGE_ARCHIVE_DIR; skipping registry login."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${REGISTRY_USER:-}" && -z "${REGISTRY_PASSWORD:-}" ]]; then
|
||||||
|
if [[ -n "${REGISTRY_HOST:-}" ]]; then
|
||||||
|
echo "REGISTRY_HOST is set but no registry credentials are configured; skipping docker login."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${REGISTRY_HOST:-}" || -z "${REGISTRY_USER:-}" || -z "${REGISTRY_PASSWORD:-}" ]]; then
|
||||||
|
echo "REGISTRY_HOST, REGISTRY_USER, and REGISTRY_PASSWORD are all required when registry credentials are configured." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -181,9 +232,56 @@ login_registry_if_configured() {
|
|||||||
|
|
||||||
pull_prod_release_images() {
|
pull_prod_release_images() {
|
||||||
require_release_image
|
require_release_image
|
||||||
|
|
||||||
|
if load_release_image_archives; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${IMAGE_ARCHIVE_REQUIRED:-false}" == "true" || "${IMAGE_ARCHIVE_REQUIRED:-0}" == "1" ]]; then
|
||||||
|
echo "IMAGE_ARCHIVE_REQUIRED is enabled, but no archive for IMAGE_TAG=${IMAGE_TAG} was found in ${IMAGE_ARCHIVE_DIR:-<unset>}." >&2
|
||||||
|
echo "The VPS cannot reach the VPN-only registry. Create a shared archive first, for example:" >&2
|
||||||
|
echo " docker save ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG} -o ${IMAGE_ARCHIVE_DIR:-./image-archives}/carmanagement-${IMAGE_TAG}.tar" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
prod_compose pull migrate api storefront dashboard admin
|
prod_compose pull migrate api storefront dashboard admin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
load_release_image_archives() {
|
||||||
|
local archive_dir="${IMAGE_ARCHIVE_DIR:-}"
|
||||||
|
local archive
|
||||||
|
local loaded=0
|
||||||
|
|
||||||
|
if [[ -z "${archive_dir}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "${archive_dir}" ]]; then
|
||||||
|
echo "IMAGE_ARCHIVE_DIR is set but does not exist: ${archive_dir}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
for archive in \
|
||||||
|
"${archive_dir}"/*"${IMAGE_TAG}"*.tar \
|
||||||
|
"${archive_dir}"/*"${IMAGE_TAG}"*.tar.gz \
|
||||||
|
"${archive_dir}"/*"${IMAGE_TAG}"*.tgz
|
||||||
|
do
|
||||||
|
echo "Loading Docker image archive: ${archive}"
|
||||||
|
docker load -i "${archive}"
|
||||||
|
loaded=1
|
||||||
|
done
|
||||||
|
shopt -u nullglob
|
||||||
|
|
||||||
|
if [[ ${loaded} -eq 0 ]]; then
|
||||||
|
echo "No Docker image archive found for IMAGE_TAG=${IMAGE_TAG} in ${archive_dir}."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Loaded image archive(s) for ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG}; skipping docker pull."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
run_prod_migrations() {
|
run_prod_migrations() {
|
||||||
prod_compose run --rm migrate
|
prod_compose run --rm migrate
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ set -euo pipefail
|
|||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||||
|
|
||||||
echo "Deploying production release ${APP_IMAGE:-<missing>}:${APP_VERSION:-<missing>}"
|
echo "Deploying production release ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-${APP_VERSION:-<missing>}}"
|
||||||
|
|
||||||
ensure_env_file
|
ensure_env_file
|
||||||
|
load_prod_env_exports
|
||||||
ensure_traefik_network
|
ensure_traefik_network
|
||||||
ensure_api_uploads_volume
|
ensure_api_uploads_volume
|
||||||
require_release_image
|
require_release_image
|
||||||
@@ -34,4 +35,4 @@ wait_for_healthy admin 180
|
|||||||
echo "Pruning old images"
|
echo "Pruning old images"
|
||||||
docker image prune -f >/dev/null
|
docker image prune -f >/dev/null
|
||||||
|
|
||||||
echo "Production release ${APP_IMAGE}:${APP_VERSION} is up and healthy."
|
echo "Production release ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG} is up and healthy."
|
||||||
|
|||||||
Reference in New Issue
Block a user