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

This commit is contained in:
root
2026-07-01 01:37:37 -04:00
parent 13d0512048
commit 5dfd5b1814
37 changed files with 2156 additions and 61 deletions
+111
View File
@@ -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
```
+6
View File
@@ -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
+100
View File
@@ -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"
+340
View File
@@ -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"
}
+38
View File
@@ -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."
+114
View File
@@ -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
View File
@@ -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}"
+11
View File
@@ -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."
+15
View File
@@ -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"
+11
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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."
+10
View File
@@ -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
View File
@@ -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."
+10
View File
@@ -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."