add registry setup
This commit is contained in:
@@ -4,23 +4,29 @@ API_DOMAIN=api.example.com
|
||||
PUBLIC_SITE_DOMAIN=example.com
|
||||
PGMANAGE_DOMAIN=pgmanage.example.com
|
||||
PORTAINER_DOMAIN=portainer.example.com
|
||||
REGISTRY_DOMAIN=registry.rentaldrivego.ma
|
||||
|
||||
# ── 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.
|
||||
# APP_IMAGE=registry.example.com/rentaldrivego/car_management_system
|
||||
# APP_VERSION=latest
|
||||
REGISTRY_HOST=registry.rentaldrivego.ma
|
||||
REGISTRY_USER=rentaldrivego_registry
|
||||
REGISTRY_PASSWORD=PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK
|
||||
REGISTRY_IMAGE=registry.rentaldrivego.ma/rentaldrivego/car_management_system
|
||||
|
||||
# ── Database ──────────────────────────────────────────────────────────────────
|
||||
# Full connection URL (used when DATABASE_URL_FROM_POSTGRES=false)
|
||||
DATABASE_URL=postgresql://postgres:change-me@postgres:5432/rentaldrivego
|
||||
DATABASE_URL=postgresql://postgres:24DY&1u5FLCkNMeiO_p@postgres:5432/rentaldrivego
|
||||
# Build the URL from individual vars (set to true to use POSTGRES_* vars below)
|
||||
DATABASE_URL_FROM_POSTGRES=true
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=rentaldrivego
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=change-me
|
||||
POSTGRES_PASSWORD=24DY&1u5FLCkNMeiO_p
|
||||
|
||||
|
||||
# ── Cache ─────────────────────────────────────────────────────────────────────
|
||||
REDIS_URL=redis://redis:6379
|
||||
@@ -30,46 +36,50 @@ API_PORT=4000
|
||||
# SSR server-side calls go via the internal Docker network
|
||||
API_INTERNAL_URL=http://api:4000/api/v1
|
||||
# Public-facing API URL visible to browsers — MUST be your real domain, not localhost
|
||||
API_URL=https://api.example.com
|
||||
API_URL=https://api.rentaldrivego.ma
|
||||
# Baked into Next.js bundles at build time — MUST be set before running `npm run build`
|
||||
NEXT_PUBLIC_API_URL=https://api.example.com/api/v1
|
||||
NEXT_PUBLIC_API_URL=https://api.rentaldrivego.ma/api/v1
|
||||
|
||||
|
||||
# ── Frontend public URLs ──────────────────────────────────────────────────────
|
||||
# Marketplace sits at the domain root (Traefik routes /dashboard and /admin to their apps)
|
||||
NEXT_PUBLIC_MARKETING_URL=https://example.com
|
||||
NEXT_PUBLIC_MARKETPLACE_URL=https://example.com
|
||||
NEXT_PUBLIC_DASHBOARD_URL=https://example.com/dashboard
|
||||
NEXT_PUBLIC_ADMIN_URL=https://example.com/admin
|
||||
NEXT_PUBLIC_PUBLIC_SITE_DOMAIN=example.com
|
||||
DASHBOARD_URL=https://example.com/dashboard
|
||||
ADMIN_URL=https://example.com/admin
|
||||
NEXT_PUBLIC_MARKETING_URL=https://rentaldrivego.ma
|
||||
NEXT_PUBLIC_MARKETPLACE_URL=https://rentaldrivego.ma
|
||||
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 ──────────────────────────────────────────────────────────────────────
|
||||
# Comma-separated list of allowed browser origins. REQUIRED in production.
|
||||
CORS_ORIGINS=https://example.com,https://www.example.com
|
||||
|
||||
# ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
JWT_SECRET=replace-with-a-long-random-secret
|
||||
JWT_SECRET=PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK
|
||||
JWT_EXPIRY=8h
|
||||
RENTER_JWT_EXPIRY=7d
|
||||
NODE_ENV=production
|
||||
|
||||
|
||||
# ── Email ─────────────────────────────────────────────────────────────────────
|
||||
EMAIL_FROM=noreply@example.com
|
||||
EMAIL_FROM=rentaldrivego@gmail.com
|
||||
EMAIL_FROM_NAME=RentalDriveGo
|
||||
# Option A — Resend
|
||||
#RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
|
||||
#RESEND_API_KEY=C8qPDuFwsv5l@KsGhL/V
|
||||
# Option B — SMTP (Gmail)
|
||||
MAIL_HOST=smtp.gmail.com
|
||||
MAIL_PORT=587
|
||||
MAIL_SCHEME=smtp
|
||||
MAIL_USERNAME=your-smtp-user@example.com
|
||||
MAIL_PASSWORD=replace-with-an-app-password
|
||||
MAIL_FROM_ADDRESS=noreply@example.com
|
||||
MAIL_USERNAME=rentaldrivego@gmail.com
|
||||
MAIL_PASSWORD="xmhg ibqy muoc rntc"
|
||||
MAIL_FROM_ADDRESS=rentaldrivego@gmail.com
|
||||
MAIL_FROM_NAME=RentalDriveGo
|
||||
MAIL_REPLY_TO_ADDRESS=support@example.com
|
||||
MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com
|
||||
MAIL_REPLY_TO_NAME=RentalDriveGo
|
||||
|
||||
|
||||
# ── Firebase push notifications (optional) ────────────────────────────────────
|
||||
# FIREBASE_PROJECT_ID=your-firebase-project-id
|
||||
# FIREBASE_CLIENT_EMAIL=firebase-adminsdk@your-project.iam.gserviceaccount.com
|
||||
|
||||
@@ -4,6 +4,7 @@ API_DOMAIN=api.example.com
|
||||
PUBLIC_SITE_DOMAIN=example.com
|
||||
PGMANAGE_DOMAIN=pgmanage.example.com
|
||||
PORTAINER_DOMAIN=portainer.example.com
|
||||
REGISTRY_DOMAIN=registry.example.com
|
||||
|
||||
# Database
|
||||
DATABASE_URL_FROM_POSTGRES=true
|
||||
|
||||
+6
-6
@@ -108,9 +108,9 @@ build_image:
|
||||
- docker push $DOCKER_IMAGE
|
||||
- docker push $DOCKER_IMAGE_LATEST
|
||||
rules:
|
||||
# Skip the container publish step unless either the GitLab registry or an
|
||||
# explicit fallback registry configuration is available.
|
||||
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && (($CI_REGISTRY && $CI_REGISTRY_USER && $CI_REGISTRY_PASSWORD && $CI_REGISTRY_IMAGE) || ($REGISTRY_HOST && $REGISTRY_USER && $REGISTRY_PASSWORD && $REGISTRY_IMAGE))'
|
||||
# On the default branch, always attempt the image build so missing registry
|
||||
# configuration fails loudly in before_script instead of skipping the job.
|
||||
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
||||
- when: never
|
||||
|
||||
# ====================================
|
||||
@@ -147,10 +147,10 @@ deploy_to_vps:
|
||||
echo "-- Syncing deployment assets --"
|
||||
ssh $VPS_USER@$VPS_IP "
|
||||
set -e
|
||||
mkdir -p '$DEPLOY_ROOT/scripts' '$DEPLOY_ROOT/docker/pgmanage'
|
||||
mkdir -p '$DEPLOY_ROOT/scripts' '$DEPLOY_ROOT/docker/pgmanage' '$DEPLOY_ROOT/docker/registry/auth'
|
||||
"
|
||||
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-compose.production.yml docker-compose.portainer.production.yml docker-compose.registry.production.yml traefik.yaml $VPS_USER@$VPS_IP:$DEPLOY_ROOT/
|
||||
scp scripts/docker-prod-common.sh scripts/docker-prod-deploy.sh scripts/docker-prod-up-registry.sh $VPS_USER@$VPS_IP:$DEPLOY_ROOT/scripts/
|
||||
scp docker/pgmanage/override.py $VPS_USER@$VPS_IP:$DEPLOY_ROOT/docker/pgmanage/
|
||||
|
||||
echo "-- Running deploy script on VPS --"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: rentaldrivego-registry-prod
|
||||
|
||||
services:
|
||||
registry:
|
||||
image: registry:2
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- traefik-proxy
|
||||
environment:
|
||||
REGISTRY_HTTP_ADDR: 0.0.0.0:5000
|
||||
REGISTRY_HTTP_HOST: https://${REGISTRY_DOMAIN}
|
||||
REGISTRY_HTTP_RELATIVEURLS: "true"
|
||||
REGISTRY_AUTH: htpasswd
|
||||
REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
|
||||
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
|
||||
REGISTRY_STORAGE_DELETE_ENABLED: "true"
|
||||
volumes:
|
||||
- registry_prod_data:/var/lib/registry
|
||||
- ./docker/registry/auth:/auth:ro
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.registry.rule=Host(`${REGISTRY_DOMAIN}`)
|
||||
- traefik.http.routers.registry.entrypoints=websecure
|
||||
- traefik.http.routers.registry.tls=true
|
||||
- traefik.http.routers.registry.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.registry.service=registry-svc
|
||||
- traefik.http.services.registry-svc.loadbalancer.server.port=5000
|
||||
|
||||
networks:
|
||||
traefik-proxy:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
registry_prod_data:
|
||||
+43
-5
@@ -7,6 +7,7 @@ Three Docker environments are available:
|
||||
- `Dockerfile.production` with `docker-compose.production.yml`
|
||||
- `docker-compose.pgmanage.yml` for a standalone pgManage container
|
||||
- `docker-compose.portainer.production.yml` for a standalone production Portainer deployment behind Traefik
|
||||
- `docker-compose.registry.production.yml` for a standalone production Docker registry behind Traefik
|
||||
|
||||
### Development
|
||||
|
||||
@@ -123,6 +124,7 @@ Add an A record for every subdomain to your server's public IP before deploying
|
||||
| `api.rentaldrivego.ma` | API |
|
||||
| `pgmanage.rentaldrivego.ma` | pgManage (DB admin) |
|
||||
| `portainer.rentaldrivego.ma` | Portainer |
|
||||
| `registry.rentaldrivego.ma` | Docker registry |
|
||||
|
||||
#### 2. Install Docker and clone the repo
|
||||
|
||||
@@ -163,6 +165,18 @@ Production now derives `DATABASE_URL` inside the app container from `POSTGRES_HO
|
||||
The example file uses `rentaldrivego.ma` for the marketplace and public site. The dashboard and admin panel are routed under that same host at `/dashboard` and `/admin`.
|
||||
Set `PORTAINER_DOMAIN=portainer.rentaldrivego.ma` in `.env.docker.production` to expose Portainer through Traefik.
|
||||
Set `PGMANAGE_DOMAIN=pgmanage.rentaldrivego.ma` to expose pgManage through Traefik, and set `PGMANAGE_LICENSE_KEY` if you are running the enterprise image.
|
||||
Set `REGISTRY_DOMAIN=registry.rentaldrivego.ma` to expose the local Docker registry through Traefik.
|
||||
|
||||
If you want to use a self-hosted registry on the same VPS, also set these values in `.env.docker.production`:
|
||||
|
||||
```text
|
||||
REGISTRY_HOST=registry.rentaldrivego.ma
|
||||
REGISTRY_USER=<registry-user>
|
||||
REGISTRY_PASSWORD=<strong-registry-password>
|
||||
REGISTRY_IMAGE=registry.rentaldrivego.ma/rentaldrivego/car_management_system
|
||||
```
|
||||
|
||||
The registry bootstrap script uses `REGISTRY_USER` and `REGISTRY_PASSWORD` to generate the htpasswd file mounted into the registry container, so the same credentials can be used by GitLab CI for `docker login`.
|
||||
|
||||
#### 4a. Configure the registry for CI deploys
|
||||
|
||||
@@ -173,13 +187,13 @@ If you want GitLab CI to build once and deploy by pulling a prebuilt image on th
|
||||
- Explicit registry variables:
|
||||
Set `REGISTRY_HOST`, `REGISTRY_USER`, `REGISTRY_PASSWORD`, and `REGISTRY_IMAGE` yourself. This works for GitLab Registry, Docker Hub, GHCR, or another OCI registry.
|
||||
|
||||
Example explicit values for GitLab's hosted registry:
|
||||
Example explicit values for a local registry exposed through Traefik:
|
||||
|
||||
```text
|
||||
REGISTRY_HOST=gitlab.rentaldrivego.ma:5050
|
||||
REGISTRY_IMAGE=gitlab.rentaldrivego.ma:5050/rental_car/car_management_system
|
||||
REGISTRY_USER=<deploy-user-or-token-name>
|
||||
REGISTRY_PASSWORD=<personal-access-token-or-deploy-token>
|
||||
REGISTRY_HOST=registry.rentaldrivego.ma
|
||||
REGISTRY_IMAGE=registry.rentaldrivego.ma/rentaldrivego/car_management_system
|
||||
REGISTRY_USER=<registry-user>
|
||||
REGISTRY_PASSWORD=<registry-password>
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -215,6 +229,7 @@ npm run docker:prod:start:admin
|
||||
npm run docker:prod:start:frontends
|
||||
npm run docker:prod:start:pgmanage
|
||||
npm run docker:prod:start:portainer
|
||||
npm run docker:prod:start:registry
|
||||
npm run docker:prod:start:all
|
||||
npm run docker:prod:start:api && npm run docker:prod:start:frontends
|
||||
```
|
||||
@@ -250,6 +265,29 @@ Equivalent raw Docker Compose command:
|
||||
docker compose -p rentaldrivego-portainer-prod --env-file .env.docker.production -f docker-compose.portainer.production.yml up -d
|
||||
```
|
||||
|
||||
#### 6b. Deploy the local registry
|
||||
|
||||
The registry is deployed separately from the main app stack and reuses the shared `traefik-proxy` network managed by Traefik.
|
||||
|
||||
```bash
|
||||
npm run docker:prod:start:registry
|
||||
```
|
||||
|
||||
Equivalent raw Docker Compose command:
|
||||
|
||||
```bash
|
||||
docker compose -p rentaldrivego-registry-prod --env-file .env.docker.production -f docker-compose.registry.production.yml up -d
|
||||
```
|
||||
|
||||
On first start, the script generates `docker/registry/auth/htpasswd` from `REGISTRY_USER` and `REGISTRY_PASSWORD` in `.env.docker.production`. After the registry is up, verify it with:
|
||||
|
||||
```bash
|
||||
docker login registry.rentaldrivego.ma
|
||||
docker pull registry:2
|
||||
docker tag registry:2 registry.rentaldrivego.ma/smoke-test:latest
|
||||
docker push registry.rentaldrivego.ma/smoke-test:latest
|
||||
```
|
||||
|
||||
Uploaded assets handled by the API, including vehicle photos and company branding images, are persisted in the named Docker volume `api_uploads`, mounted inside the API container at `/var/lib/rentaldrivego/storage`. Rebuilding or redeploying the API no longer clears those files as long as that volume is kept.
|
||||
|
||||
The production helper scripts and the GitLab deploy job now explicitly create the Docker volume `${DOCKER_PROD_PROJECT_NAME:-rentaldrivego-prod}_api_uploads` before starting or redeploying the API stack, so a fresh server bootstrap does not accidentally start the API without its upload storage volume.
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"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:portainer": "bash scripts/docker-prod-up-portainer.sh",
|
||||
"docker:prod:start:registry": "bash scripts/docker-prod-up-registry.sh",
|
||||
"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",
|
||||
|
||||
@@ -5,13 +5,17 @@ set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PROD_COMPOSE_FILE="${ROOT_DIR}/docker-compose.production.yml"
|
||||
PORTAINER_COMPOSE_FILE="${ROOT_DIR}/docker-compose.portainer.production.yml"
|
||||
REGISTRY_COMPOSE_FILE="${ROOT_DIR}/docker-compose.registry.production.yml"
|
||||
TRAEFIK_COMPOSE_FILE="${ROOT_DIR}/traefik.yaml"
|
||||
ENV_FILE="${ROOT_DIR}/.env.docker.production"
|
||||
PROJECT_NAME="${DOCKER_PROD_PROJECT_NAME:-rentaldrivego-prod}"
|
||||
PORTAINER_PROJECT_NAME="${DOCKER_PORTAINER_PROJECT_NAME:-rentaldrivego-portainer-prod}"
|
||||
REGISTRY_PROJECT_NAME="${DOCKER_REGISTRY_PROJECT_NAME:-rentaldrivego-registry-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}"
|
||||
REGISTRY_AUTH_DIR="${ROOT_DIR}/docker/registry/auth"
|
||||
REGISTRY_AUTH_FILE="${REGISTRY_AUTH_DIR}/htpasswd"
|
||||
|
||||
ensure_env_file() {
|
||||
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||
@@ -43,6 +47,10 @@ portainer_compose() {
|
||||
docker compose -p "${PORTAINER_PROJECT_NAME}" --env-file "${ENV_FILE}" -f "${PORTAINER_COMPOSE_FILE}" "$@"
|
||||
}
|
||||
|
||||
registry_compose() {
|
||||
docker compose -p "${REGISTRY_PROJECT_NAME}" --env-file "${ENV_FILE}" -f "${REGISTRY_COMPOSE_FILE}" "$@"
|
||||
}
|
||||
|
||||
traefik_compose() {
|
||||
docker compose --env-file "${ENV_FILE}" -f "${TRAEFIK_COMPOSE_FILE}" "$@"
|
||||
}
|
||||
@@ -141,6 +149,30 @@ start_portainer() {
|
||||
portainer_compose up -d
|
||||
}
|
||||
|
||||
ensure_registry_auth_file() {
|
||||
mkdir -p "${REGISTRY_AUTH_DIR}"
|
||||
|
||||
if [[ -n "${REGISTRY_USER:-}" && -n "${REGISTRY_PASSWORD:-}" ]]; then
|
||||
docker run --rm --entrypoint htpasswd httpd:2-alpine -Bbn "${REGISTRY_USER}" "${REGISTRY_PASSWORD}" > "${REGISTRY_AUTH_FILE}"
|
||||
chmod 600 "${REGISTRY_AUTH_FILE}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -f "${REGISTRY_AUTH_FILE}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Set REGISTRY_USER and REGISTRY_PASSWORD in .env.docker.production or create ${REGISTRY_AUTH_FILE} manually." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
start_registry() {
|
||||
ensure_env_file
|
||||
ensure_traefik_network
|
||||
ensure_registry_auth_file
|
||||
registry_compose up -d
|
||||
}
|
||||
|
||||
start_traefik() {
|
||||
ensure_env_file
|
||||
ensure_traefik_network
|
||||
|
||||
@@ -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_registry
|
||||
|
||||
echo "Registry is starting."
|
||||
Reference in New Issue
Block a user