docker compose -f docker-compose.dev.yml up -d postgres docker compose -f docker-compose.dev.yml up -d redis docker compose -f docker-compose.production.yml up --build -d --no-deps marketplace docker compose -f docker-compose.production.yml up --build -d --no-deps admin docker compose -f docker-compose.dev.yml --profile full up --build docker compose -f docker-compose.dev.yml --profile dashboard up --build docker compose -f docker-compose.dev.yml --profile marketplace up --build docker compose -f docker-compose.dev.yml --profile api up --build Default dev admin: Email: platform.ops@rentaldrivego.com Password: PlatformOps123! What this means: docker compose -f docker-compose.dev.yml --profile full up --build will no longer reseed or redeploy the DB on every fix. Your data stays as long as you do not run docker compose ... down -v or manually delete the Docker volumes. If you later change the schema and want to apply it without wiping data, run: docker compose -f docker-compose.dev.yml run --rm migrate sh -c "npm run db:deploy" Production was already using a persistent named Postgres volume, so the main issue was the dev bootstrap behavior. Restart the dev stack once for this change to take effect. Done. The DOCKER.md production section now covers the full deployment flow from a fresh server. The short version: # Once per server docker network create traefik-proxy # Clone & configure git clone rentaldrivego && cd rentaldrivego cp .env.docker.production.example .env.docker.production # → fill in POSTGRES_PASSWORD, JWT_SECRET, ACME_EMAIL, RESEND_API_KEY # Deploy docker compose -f traefik.yaml up -d docker compose -f docker-compose.production.yml up --build -d services: gitlab: image: gitlab/gitlab-ce:latest restart: unless-stopped hostname: ${DOMAIN_NAME} # Traefik will handle 80/443 on the host. # Keep SSH exposed separately (pick a fixed port on the host). ports: - "2222:22" environment: GITLAB_OMNIBUS_CONFIG: | # Public URL should be HTTPS (Traefik terminates TLS) external_url 'https://${DOMAIN_NAME}' # GitLab listens internally on HTTP; Traefik provides HTTPS nginx['listen_port'] = 80 nginx['listen_https'] = false # Help GitLab understand it's behind a TLS-terminating proxy nginx['proxy_set_headers'] = { "X-Forwarded-Proto" => "https", "X-Forwarded-Ssl" => "on" } gitlab_rails['initial_root_password'] = '${ROOT_PASSWORD}' # ------------------------- # SMTP (required for invites) # ------------------------- gitlab_rails['smtp_enable'] = true gitlab_rails['smtp_address'] = "${SMTP_HOST}" gitlab_rails['smtp_port'] = ${SMTP_PORT} gitlab_rails['smtp_user_name'] = "${SMTP_USER}" gitlab_rails['smtp_password'] = "${SMTP_PASS}" gitlab_rails['smtp_domain'] = "${SMTP_DOMAIN}" gitlab_rails['smtp_authentication'] = "login" gitlab_rails['smtp_enable_starttls_auto'] = true # Optional: if you use port 465 (implicit TLS), set: # gitlab_rails['smtp_tls'] = true gitlab_rails['gitlab_email_from'] = "${SMTP_FROM}" gitlab_rails['gitlab_email_reply_to'] = "${SMTP_REPLY_TO}" GITLAB_ROOT_EMAIL: ${ROOT_EMAIL} volumes: - gitlab-config:/etc/gitlab - gitlab-logs:/var/log/gitlab - gitlab-data:/var/opt/gitlab shm_size: '256m' sysctls: - net.ipv6.conf.all.disable_ipv6=1 - net.ipv6.conf.default.disable_ipv6=1 labels: - traefik.enable=true - traefik.http.routers.gitlab.rule=Host(`${DOMAIN_NAME}`) - traefik.http.routers.gitlab.tls=true - traefik.http.routers.gitlab.tls.certresolver=letsencrypt - traefik.http.services.gitlab.loadbalancer.server.port=80 networks: - traefik-proxy # ✅ Fixed logging configuration (moved inside the service) logging: driver: "json-file" options: max-size: "50m" max-file: "3" volumes: gitlab-config: gitlab-logs: gitlab-data: networks: traefik-proxy: external: true