fix prod ddeployment

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