fix prod ddeployment
This commit is contained in:
@@ -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) ────────────────────────────────────
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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."
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user