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
|
||||
PGMANAGE_DOMAIN=pgmanage.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 ────────────────────────────────────────────
|
||||
# Used for pull-based deployments. CI injects APP_IMAGE and APP_VERSION
|
||||
# automatically during registry-backed deploys, so these can stay commented out.
|
||||
# Used for production deployments. CI injects IMAGE_TAG automatically during
|
||||
# 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_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 ──────────────────────────────────────────────────────────────────
|
||||
# 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)
|
||||
DATABASE_URL=postgresql://dbadmin:PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK@localhost:5432/rentaldrivego
|
||||
DATABASE_URL_FROM_POSTGRES=true
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=rentaldrivego
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=placeholder
|
||||
POSTGRES_USER=dbadmin
|
||||
POSTGRES_PASSWORD=PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK
|
||||
|
||||
|
||||
# ── Cache ─────────────────────────────────────────────────────────────────────
|
||||
REDIS_URL=redis://redis:6379
|
||||
REDIS_PASSWORD=PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK
|
||||
REDIS_URL=redis://:PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK@redis:6379
|
||||
|
||||
# ── API ───────────────────────────────────────────────────────────────────────
|
||||
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)
|
||||
NEXT_PUBLIC_MARKETING_URL=https://rentaldrivego.ma
|
||||
NEXT_PUBLIC_MARKETPLACE_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
|
||||
@@ -67,7 +66,7 @@ ADMIN_URL=https://rentaldrivego.ma/admin
|
||||
CORS_ORIGINS=https://rentaldrivego.ma,https://www.rentaldrivego.ma
|
||||
|
||||
# ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
JWT_SECRET=placeholder
|
||||
JWT_SECRET=eb9ab3eb5d6648bdb82cbef29afde498c58f089829a037dfea6cb5e98ef2aae0
|
||||
JWT_EXPIRY=8h
|
||||
RENTER_JWT_EXPIRY=7d
|
||||
NODE_ENV=production
|
||||
@@ -79,11 +78,12 @@ EMAIL_FROM_NAME=RentalDriveGo
|
||||
# Option A — Resend
|
||||
#RESEND_API_KEY=C8qPDuFwsv5l@KsGhL/V
|
||||
# Option B — SMTP (Gmail)
|
||||
# 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=placeholder
|
||||
MAIL_PASSWORD=kfahihfzbcvkczew
|
||||
MAIL_FROM_ADDRESS=rentaldrivego@gmail.com
|
||||
MAIL_FROM_NAME=RentalDriveGo
|
||||
MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com
|
||||
|
||||
@@ -7,6 +7,9 @@ PORTAINER_DOMAIN=portainer.example.com
|
||||
REGISTRY_DOMAIN=registry.example.com
|
||||
REGISTRY_UPSTREAM_URL=http://10.0.0.10:5000
|
||||
|
||||
# Image tag
|
||||
IMAGE_TAG=latest
|
||||
|
||||
# Database
|
||||
DATABASE_URL_FROM_POSTGRES=true
|
||||
POSTGRES_HOST=postgres
|
||||
@@ -17,6 +20,7 @@ POSTGRES_PASSWORD=placeholder
|
||||
DATABASE_URL=postgresql://placeholder:placeholder@placeholder:5432/placeholder
|
||||
|
||||
# Cache
|
||||
REDIS_PASSWORD=placeholder
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
# API
|
||||
@@ -27,6 +31,7 @@ NEXT_PUBLIC_API_URL=https://api.example.com/api/v1
|
||||
|
||||
# Frontend public URLs
|
||||
NEXT_PUBLIC_MARKETING_URL=https://example.com
|
||||
NEXT_PUBLIC_MARKETPLACE_URL=https://example.com
|
||||
NEXT_PUBLIC_HOMEPAGE_URL=https://example.com
|
||||
NEXT_PUBLIC_STOREFRONT_URL=https://example.com/storefront
|
||||
NEXT_PUBLIC_DASHBOARD_URL=https://example.com/dashboard
|
||||
|
||||
+28
-4
@@ -4,7 +4,13 @@
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
# ─── 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_PORT=4000
|
||||
@@ -12,7 +18,7 @@ API_URL=http://localhost:4000
|
||||
NEXT_PUBLIC_API_URL=http://localhost:4000/api/v1
|
||||
|
||||
# ─── JWT (Renter auth + Admin auth) ───────────────────────────
|
||||
JWT_SECRET=placeholder
|
||||
JWT_SECRET=8bf96de20297c2c295a60e4040e937a7eb8ec7d7d2b83e79a1fc98463322a97c
|
||||
JWT_EXPIRY=8h
|
||||
RENTER_JWT_EXPIRY=7d
|
||||
|
||||
@@ -71,6 +77,7 @@ NEXT_PUBLIC_FIREBASE_APP_ID=1:123456789:web:abc123
|
||||
# MAIL_* SMTP variables are intentionally omitted here.
|
||||
|
||||
# ─── Redis (Real-time / Socket.io) ────────────────────────────
|
||||
REDIS_PASSWORD=replace-with-production-redis-password
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
# ─── App URLs ──────────────────────────────────────────────────
|
||||
@@ -80,19 +87,36 @@ REDIS_URL=redis://localhost:6379
|
||||
|
||||
|
||||
NEXT_PUBLIC_MARKETING_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_MARKETPLACE_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_DASHBOARD_URL=http://localhost:3000/dashboard
|
||||
NEXT_PUBLIC_ADMIN_URL=http://localhost:3000/admin
|
||||
NEXT_PUBLIC_STOREFRONT_URL=http://localhost:3000/storefront
|
||||
NEXT_PUBLIC_HOMEPAGE_URL=http://localhost:3000
|
||||
# Public site is subdomain-based; use this for local dev:
|
||||
NEXT_PUBLIC_PUBLIC_SITE_DOMAIN=localhost:3003
|
||||
API_DOMAIN=api.rentaldrivego.ma
|
||||
PUBLIC_SITE_DOMAIN=rentaldrivego.ma
|
||||
DASHBOARD_URL=http://localhost:3000/dashboard
|
||||
|
||||
# ─── Admin seed (first SUPER_ADMIN created on db:seed) ────────
|
||||
ADMIN_SEED_EMAIL=admin@rentaldrivego.ma
|
||||
ADMIN_SEED_PASSWORD=placeholder
|
||||
ADMIN_SEED_PASSWORD=PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK
|
||||
ADMIN_SEED_FIRST_NAME=Super
|
||||
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 ──────────────────────────────────────────────────────
|
||||
NODE_ENV=development
|
||||
NODE_ENV=production
|
||||
|
||||
@@ -13,7 +13,7 @@ env:
|
||||
NODE_VERSION: "20"
|
||||
# TODO: Remove after installing internal CA certificate on the runner
|
||||
GIT_SSL_NO_VERIFY: "true"
|
||||
REGISTRY_HOST: 192.168.3.80
|
||||
REGISTRY_HOST: 10.0.0.4
|
||||
DOCKERFILE_PATH: Dockerfile.production
|
||||
DOCKER_PLATFORM: linux/amd64
|
||||
DEPLOY_ROOT: /opt/rentaldrivego
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
buildkitd-config-inline: |
|
||||
[registry."192.168.3.80"]
|
||||
[registry."10.0.0.4"]
|
||||
insecure = true
|
||||
|
||||
- name: Docker image metadata
|
||||
@@ -190,6 +190,7 @@ jobs:
|
||||
cd '$DEPLOY_ROOT'
|
||||
APP_IMAGE='$IMAGE_REPOSITORY' \
|
||||
APP_VERSION='${{ gitea.sha }}' \
|
||||
IMAGE_TAG='${{ gitea.sha }}' \
|
||||
REGISTRY_HOST='$REGISTRY_HOST' \
|
||||
REGISTRY_USER='${{ secrets.REGISTRY_USERNAME }}' \
|
||||
REGISTRY_PASSWORD=\$(printf '%s' '$REGISTRY_PASSWORD_B64' | base64 -d) \
|
||||
|
||||
@@ -13,6 +13,8 @@ apps/api/storage/
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.docker.production
|
||||
production/.env.docker.production
|
||||
|
||||
# Turbo
|
||||
.turbo
|
||||
|
||||
@@ -44,7 +44,8 @@ FROM node:20-bookworm AS runner
|
||||
|
||||
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 \
|
||||
&& 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/apps ./apps
|
||||
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
|
||||
|
||||
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
|
||||
init: true
|
||||
user: "10001:10001"
|
||||
entrypoint: []
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
tmpfs:
|
||||
@@ -14,11 +16,11 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
environment:
|
||||
POSTGRES_DB: rentaldrivego
|
||||
POSTGRES_USER: postgres
|
||||
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 -d rentaldrivego"]
|
||||
test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
@@ -44,9 +46,7 @@ services:
|
||||
|
||||
migrate:
|
||||
<<: *node-runtime-hardening
|
||||
cap_drop:
|
||||
- ALL
|
||||
image: ${APP_IMAGE:-rentaldrivego-prod-app}:${APP_VERSION:-latest}
|
||||
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.production
|
||||
@@ -56,8 +56,11 @@ services:
|
||||
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
|
||||
@@ -65,7 +68,7 @@ services:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env.docker.production
|
||||
- ${APP_ENV_FILE:-.env.production}
|
||||
environment:
|
||||
DATABASE_URL_FROM_POSTGRES: ${DATABASE_URL_FROM_POSTGRES:-true}
|
||||
POSTGRES_HOST: ${POSTGRES_HOST:-postgres}
|
||||
@@ -76,7 +79,7 @@ services:
|
||||
|
||||
api:
|
||||
<<: *node-runtime-hardening
|
||||
image: ${APP_IMAGE:-rentaldrivego-prod-app}:${APP_VERSION:-latest}
|
||||
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.production
|
||||
@@ -86,8 +89,11 @@ services:
|
||||
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
|
||||
@@ -104,7 +110,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env.docker.production
|
||||
- ${APP_ENV_FILE:-.env.production}
|
||||
environment:
|
||||
DATABASE_URL_FROM_POSTGRES: ${DATABASE_URL_FROM_POSTGRES:-true}
|
||||
FILE_STORAGE_ROOT: /var/lib/rentaldrivego/storage
|
||||
@@ -132,9 +138,7 @@ services:
|
||||
|
||||
homepage:
|
||||
<<: *node-runtime-hardening
|
||||
cap_drop:
|
||||
- ALL
|
||||
image: ${APP_IMAGE:-rentaldrivego-prod-app}:${APP_VERSION:-latest}
|
||||
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.production
|
||||
@@ -144,8 +148,11 @@ services:
|
||||
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
|
||||
@@ -160,7 +167,7 @@ services:
|
||||
api:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env.docker.production
|
||||
- ${APP_ENV_FILE:-.env.production}
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
@@ -175,9 +182,7 @@ services:
|
||||
|
||||
storefront:
|
||||
<<: *node-runtime-hardening
|
||||
cap_drop:
|
||||
- ALL
|
||||
image: ${APP_IMAGE:-rentaldrivego-prod-app}:${APP_VERSION:-latest}
|
||||
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.production
|
||||
@@ -186,9 +191,12 @@ services:
|
||||
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
|
||||
@@ -203,7 +211,7 @@ services:
|
||||
api:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env.docker.production
|
||||
- ${APP_ENV_FILE:-.env.production}
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
@@ -218,9 +226,7 @@ services:
|
||||
|
||||
dashboard:
|
||||
<<: *node-runtime-hardening
|
||||
cap_drop:
|
||||
- ALL
|
||||
image: ${APP_IMAGE:-rentaldrivego-prod-app}:${APP_VERSION:-latest}
|
||||
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.production
|
||||
@@ -230,8 +236,11 @@ services:
|
||||
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
|
||||
@@ -246,7 +255,7 @@ services:
|
||||
api:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env.docker.production
|
||||
- ${APP_ENV_FILE:-.env.production}
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
@@ -261,9 +270,7 @@ services:
|
||||
|
||||
admin:
|
||||
<<: *node-runtime-hardening
|
||||
cap_drop:
|
||||
- ALL
|
||||
image: ${APP_IMAGE:-rentaldrivego-prod-app}:${APP_VERSION:-latest}
|
||||
image: ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.production
|
||||
@@ -273,8 +280,11 @@ services:
|
||||
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
|
||||
@@ -289,7 +299,7 @@ services:
|
||||
api:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env.docker.production
|
||||
- ${APP_ENV_FILE:-.env.production}
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
@@ -333,3 +343,5 @@ volumes:
|
||||
redis_prod_data:
|
||||
pgmanage_prod_data:
|
||||
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}"
|
||||
}
|
||||
|
||||
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 portainer_domain postgres_password redis_password jwt_secret
|
||||
|
||||
@@ -113,7 +144,7 @@ ensure_dynamic_dir() {
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -154,25 +185,45 @@ start_prod_services() {
|
||||
}
|
||||
|
||||
require_release_image() {
|
||||
if [[ -z "${APP_IMAGE:-}" || -z "${APP_VERSION:-}" ]]; then
|
||||
echo "APP_IMAGE and APP_VERSION must be set for pull-based production deploys." >&2
|
||||
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() {
|
||||
local configured=0
|
||||
|
||||
[[ -n "${REGISTRY_HOST:-}" ]] && configured=$((configured + 1))
|
||||
[[ -n "${REGISTRY_USER:-}" ]] && configured=$((configured + 1))
|
||||
[[ -n "${REGISTRY_PASSWORD:-}" ]] && configured=$((configured + 1))
|
||||
|
||||
if [[ ${configured} -eq 0 ]]; then
|
||||
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 [[ ${configured} -ne 3 ]]; then
|
||||
echo "REGISTRY_HOST, REGISTRY_USER, and REGISTRY_PASSWORD must either all be set or all be unset." >&2
|
||||
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
|
||||
|
||||
@@ -181,9 +232,56 @@ login_registry_if_configured() {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@ 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:-<missing>}:${APP_VERSION:-<missing>}"
|
||||
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
|
||||
@@ -34,4 +35,4 @@ wait_for_healthy admin 180
|
||||
echo "Pruning old images"
|
||||
docker image prune -f >/dev/null
|
||||
|
||||
echo "Production release ${APP_IMAGE}:${APP_VERSION} is up and healthy."
|
||||
echo "Production release ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG} is up and healthy."
|
||||
|
||||
Reference in New Issue
Block a user