15 KiB
Docker Environments
Three Docker environments are available:
Dockerfile.devwithdocker-compose.dev.ymlDockerfile.testwithdocker-compose.test.ymlDockerfile.productionwithdocker-compose.production.ymldocker-compose.pgmanage.ymlfor a standalone pgManage containerdocker-compose.portainer.production.ymlfor a standalone production Portainer deployment behind Traefik
Development
Use the full dev stack for local work with hot reload and bundled Postgres and Redis:
docker compose -f docker-compose.dev.yml --profile full up --build
Services:
- marketplace:
http://localhost:3000 - dashboard:
http://localhost:3000/dashboard - admin:
http://localhost:3000/admin - public-site:
http://localhost:3003 - api:
http://localhost:4000 - pgAdmin:
http://localhost:5050
Each dev app now runs in its own container and can be started independently with a profile tag:
docker compose -f docker-compose.dev.yml --profile api up --build
docker compose -f docker-compose.dev.yml --profile marketplace up --build
docker compose -f docker-compose.dev.yml --profile dashboard up --build
docker compose -f docker-compose.dev.yml --profile admin up --build
docker compose -f docker-compose.dev.yml --profile public-site up --build
docker compose -f docker-compose.dev.yml --profile tools up --build
docker compose -f docker-compose.dev.yml --profile api --profile marketplace --profile dashboard --profile admin up --build
Notes:
apistartspostgres,redis, andmigrateautomatically through dependencies.- frontend profiles also start
apiand its dependencies automatically. toolsstarts onlypgadminplus its requiredpostgresdependency.
On startup, Docker now waits for Postgres to become healthy, runs a one-shot migrate service, and only then starts the selected app container. For development, that bootstrap runs db:generate every time, but db:deploy and db:seed only the first time for a persisted dev database, so your local data survives rebuilds and normal restarts.
Uploaded assets handled by the API, including vehicle photos and company branding images, are stored in the named Docker volume api_uploads_dev at /var/lib/rentaldrivego/storage inside the container. They stay available across normal container rebuilds and restarts.
Default dev platform administrator:
- email:
admin@rentaldrivego.com - password:
changeme123
If you intentionally want a fresh dev bootstrap:
docker compose -f docker-compose.dev.yml down -v
If you want to keep the database and only apply new schema changes manually:
docker compose -f docker-compose.dev.yml run --rm migrate sh -c "npm run db:deploy"
pgAdmin dev login:
- email:
admin@rentaldrivego.local - email:
admin@rentaldrivego.dev - password:
admin
pgAdmin opens with the dev Postgres server pre-registered as RentalDriveGo Dev DB.
pgAdmin Postgres connection:
- host:
postgres - port:
5432 - database:
rentaldrivego - username:
postgres - password:
password
Standalone pgManage
If you want a standalone Postgres management UI without starting the full development stack:
docker compose -f docker-compose.pgmanage.yml up -d
It publishes http://localhost:8000 with a standard Docker port mapping and persists its data in the named Docker volume pgmanage_data.
From inside the container, connect to the local Postgres service through host.docker.internal:5432.
Test
Use the test stack to run repeatable containerized verification:
docker compose -f docker-compose.test.yml up --build --abort-on-container-exit
The test container runs:
npm run db:deploynpm run db:generatenpm run type-checknpm run buildnpm run test:api:integration
Production
The production stack runs behind Traefik (reverse proxy + automatic HTTPS via Let's Encrypt). All services communicate over a private Docker network (internal). Traefik reaches public-facing services via a separate traefik-proxy network.
1. Point DNS to your server
Add an A record for every subdomain to your server's public IP before deploying so Let's Encrypt can issue certificates:
| Subdomain | Service |
|---|---|
rentaldrivego.ma |
marketplace and public site |
api.rentaldrivego.ma |
API |
pgmanage.rentaldrivego.ma |
pgManage (DB admin) |
portainer.rentaldrivego.ma |
Portainer |
2. Install Docker and clone the repo
# Install Docker (if not already installed)
curl -fsSL https://get.docker.com | sh
git clone <repo-url> rentaldrivego
cd rentaldrivego
3. Create the shared Traefik network
Only needs to be done once per server. If it already exists this is a no-op.
docker network create traefik-proxy
4. Configure environment variables
cp .env.docker.production.example .env.docker.production
Open .env.docker.production and fill in every value. The minimum required secrets are:
| Variable | What to set |
|---|---|
POSTGRES_PASSWORD |
Strong random password |
JWT_SECRET |
Long random string (e.g. openssl rand -hex 64) |
ACME_EMAIL |
Your email for Let's Encrypt notifications |
RESEND_API_KEY |
Resend API key (or configure SMTP vars instead) |
Production now derives DATABASE_URL inside the app container from POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, and POSTGRES_PASSWORD when DATABASE_URL_FROM_POSTGRES=true. That avoids Prisma auth failures when the database password contains reserved URL characters such as @, :, or /.
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.
4a. Configure the registry for CI deploys
If you want GitLab CI to build once and deploy by pulling a prebuilt image on the VPS, configure one of these two modes in GitLab Settings > CI/CD > Variables:
- Built-in GitLab Container Registry:
Enable the project registry and use GitLab's default
CI_REGISTRY,CI_REGISTRY_USER,CI_REGISTRY_PASSWORD, andCI_REGISTRY_IMAGEvariables. - Explicit registry variables:
Set
REGISTRY_HOST,REGISTRY_USER,REGISTRY_PASSWORD, andREGISTRY_IMAGEyourself. This works for GitLab Registry, Docker Hub, GHCR, or another OCI registry.
Example explicit values for GitLab's hosted registry:
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>
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 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.
5. Start Traefik
Traefik must be running before the app stack so it can wire up routes at startup.
docker compose -f traefik.yaml up -d
6. Build and start the app stack
npm run docker:prod:up
Or use the new helper scripts if you want to start one production container at a time:
npm run docker:prod:start:traefik
npm run docker:prod:start:postgres
npm run docker:prod:start:redis
npm run docker:prod:start:api
npm run docker:prod:start:marketplace
npm run docker:prod:start:dashboard
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:all
npm run docker:prod:start:api && npm run docker:prod:start:frontends
Docker will:
- Build the monorepo image
- 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.
6a. Deploy Portainer
Portainer is deployed separately from the main app stack and reuses the shared traefik-proxy network managed by Traefik.
npm run docker:prod:start:portainer
Equivalent raw Docker Compose command:
docker compose -p rentaldrivego-portainer-prod --env-file .env.docker.production -f docker-compose.portainer.production.yml up -d
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.
Updating after a code change
Pull the latest code and rebuild only the changed service:
git pull
docker compose -p rentaldrivego-prod --env-file .env.docker.production -f docker-compose.production.yml up --build -d --no-deps <service>
# e.g. to redeploy only the API:
docker compose -p rentaldrivego-prod --env-file .env.docker.production -f docker-compose.production.yml up --build -d --no-deps api
To rebuild everything:
npm run docker:prod:up
Apply database migrations without downtime
docker compose -p rentaldrivego-prod --env-file .env.docker.production -f docker-compose.production.yml run --rm api npm run db:deploy
Create the first production admin
The repo includes a seed that creates the first SUPER_ADMIN if that email does not already exist.
docker compose -p rentaldrivego-prod --env-file .env.docker.production -f docker-compose.production.yml run --rm \
-e ADMIN_SEED_EMAIL=rentaldrivego@gmail.com \
-e ADMIN_SEED_PASSWORD='Qwerty00!@#$%' \
-e ADMIN_SEED_FIRST_NAME=Super \
-e ADMIN_SEED_LAST_NAME=Admin \
api npm run db:seed:admin
Then sign in at https://rentaldrivego.ma/admin/login and create any additional admin users from the admin panel.
View logs
# All services
npm run docker:prod:logs
# Single service
npm run docker:prod:logs:api
Backup production data
Create a timestamped backup directory under ./backups:
npm run docker:prod:backup
Or choose a different parent directory:
bash scripts/docker-prod-backup.sh /srv/rentaldrivego-backups
Each backup contains:
postgres.dump— logical PostgreSQL backup in custom formatapi-uploads.tar.gz— uploaded files from/var/lib/rentaldrivego/storagetraefik-letsencrypt.tar.gz— Traefik ACME certificate state, when availablevolumes/*.tar.gz— raw Docker volume archives for the default production volumesmanifest.txt— basic metadata
By default, the backup also archives these named production volumes when they exist:
${DOCKER_PROD_PROJECT_NAME:-rentaldrivego-prod}_api_uploads${DOCKER_PROD_PROJECT_NAME:-rentaldrivego-prod}_pgmanage_prod_data${DOCKER_PROD_PROJECT_NAME:-rentaldrivego-prod}_postgres_prod_data${DOCKER_PROD_PROJECT_NAME:-rentaldrivego-prod}_redis_prod_data
To include extra named volumes from the same server, pass them as additional arguments:
bash scripts/docker-prod-backup.sh /srv/rentaldrivego-backups \
gitlab-l3gq_gitlab-config \
gitlab-l3gq_gitlab-data \
gitlab-l3gq_gitlab-logs \
pgmanage_data \
traefik-gcjk_portainer_data \
traefik-gcjk_traefik-letsencrypt
You can also provide extra volume names through DOCKER_EXTRA_BACKUP_VOLUMES.
Restore production data
Restore is destructive: it overwrites the production database, uploaded files, and, when present in the backup, Traefik ACME state.
bash scripts/docker-prod-restore.sh ./backups/rentaldrivego-prod-YYYYMMDDTHHMMSSZ --yes
The restore script:
- Stops public app services
- Restores the PostgreSQL dump
- Replaces uploaded files
- Restores Traefik ACME state if the archive exists
- Restores raw volume archives from
volumes/except the ones already covered by the database and upload restore steps - Starts Traefik and the production stack again
If the backup contains raw archives for volumes used by other stacks such as GitLab or Portainer, stop those containers before running restore. The script will refuse to overwrite a volume that is currently attached to a running container.
Before running restore on a live server, take a fresh backup first.
Stop the stack
# Stop containers but keep volumes (data is preserved)
npm run docker:prod:down
# Stop and delete all data (destructive — irreversible)
docker compose -p rentaldrivego-prod --env-file .env.docker.production -f docker-compose.production.yml down -v
pgManage (DB admin UI)
pgManage is available at https://pgmanage.rentaldrivego.ma. To connect to the production database, add a connection inside pgManage with:
- Host:
localhost - Port:
5432 - Database:
rentaldrivego - Username:
postgres - Password: value of
POSTGRES_PASSWORDfrom.env.docker.production
Portainer
Portainer is available at https://portainer.rentaldrivego.ma.
Use this command to deploy or update it:
npm run docker:prod:start:portainer
Notes
- The production image builds the whole monorepo once, then each service overrides its runtime command.
- The dev compose file bind-mounts the repo and keeps
node_modulesin a named volume. API_INTERNAL_URLis used for server-side container-to-container calls, whileNEXT_PUBLIC_API_URLis used by the browser.- The Dockerfiles activate the repo's pinned
npm@10.5.0withcorepackbefore install so container builds do not depend on the npm version bundled with the base image. - The dev compose stack stores Postgres data in
postgres_dev_dataand the bootstrap marker inpostgres_bootstrap_state, soup --builddoes not reseed an existing local database. - If you need database schema updates inside Docker, run:
docker compose -f docker-compose.dev.yml run --rm migrate
If a cached base image still fails during npm ci, refresh it and rebuild without cache:
docker pull node:20-bookworm
docker compose -f docker-compose.dev.yml build --no-cache dashboard