diff --git a/.env.example b/.env.example index d5edc82..9d29791 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,8 @@ NODE_ENV="development" VITE_API_BASE_URL="http://localhost:4000/api" VITE_GOOGLE_MAPS_PLATFORM_KEY="YOUR_BROWSER_MAPS_KEY" -# Local backend env vars +# Backend env vars +# For Docker Compose deployments, point DATABASE_URL at the internal "db" host. # If your password contains special characters, URL-encode it in DATABASE_URL. DATABASE_URL="postgres://postgres:postgres@localhost:5432/leads4less" COOKIE_SECRET="CHANGE_ME_IN_LOCAL_ENV" @@ -20,3 +21,6 @@ GOOGLE_MAPS_SERVER_KEY="YOUR_SERVER_MAPS_KEY" POSTGRES_DB="leads4less" POSTGRES_USER="postgres" POSTGRES_PASSWORD="CHANGE_ME_IN_LOCAL_ENV" + +# Example Compose DATABASE_URL +# DATABASE_URL="postgres://postgres:CHANGE_ME_IN_LOCAL_ENV@db:5432/leads4less" diff --git a/CHANGELOG.md b/CHANGELOG.md index 273c2e0..18abe56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +## [2026-05-01] + ### Added - Added a multi-stage `Dockerfile` and `docker-compose.yml` for running the web app, API, worker, and PostGIS database as a local container stack. ### Changed - Updated the Docker Compose deployment config to read app secrets and Vite build settings from environment variables, and added a `.dockerignore` to keep container builds leaner. - Aligned the Docker Compose and example environment settings so local and deployment configs use the same variable names and document URL-encoded database passwords when needed. +- Simplified container deployment by adding a dedicated migration image and Compose startup ordering so the database becomes healthy, migrations run automatically, and the API, worker, and web services start afterward. ## [2026-04-19] diff --git a/Dockerfile b/Dockerfile index 016d018..d2e0863 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ RUN npm run build && npm run build:api FROM nginx:alpine AS web COPY --from=build /app/dist /usr/share/nginx/html -FROM node:22-alpine AS api +FROM node:22-alpine AS runtime-base WORKDIR /app COPY package*.json ./ @@ -24,14 +24,11 @@ RUN npm ci --omit=dev COPY --from=build /app/dist-server ./dist-server -CMD ["node", "dist-server/server/src/index.js"] +FROM runtime-base AS api +CMD ["npm", "run", "start:api"] -FROM node:22-alpine AS worker -WORKDIR /app +FROM runtime-base AS worker +CMD ["npm", "run", "start:worker"] -COPY package*.json ./ -RUN npm ci --omit=dev - -COPY --from=build /app/dist-server ./dist-server - -CMD ["node", "dist-server/server/src/worker.js"] +FROM runtime-base AS migrate +CMD ["node", "dist-server/db/scripts/migrate.js"] diff --git a/README.md b/README.md index 49cb576..24dadbf 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,22 @@ If you open the app from another machine on your LAN, set `VITE_API_BASE_URL` an 4. Start the worker: `npm run dev:worker` +## Docker Deployment + +1. Copy `.env.example` to `.env` and set at least: + - `DATABASE_URL` to the Compose database host, for example `postgres://postgres:YOUR_PASSWORD@db:5432/leads4less` + - `POSTGRES_DB` + - `POSTGRES_USER` + - `POSTGRES_PASSWORD` + - `COOKIE_SECRET` + - `GOOGLE_MAPS_SERVER_KEY` + - `VITE_API_BASE_URL` + - `VITE_GOOGLE_MAPS_PLATFORM_KEY` +2. Build and start the full stack: + `docker compose up --build` + +The Compose stack starts PostGIS, waits for the database to become healthy, runs migrations automatically, and then starts the API, worker, and web containers. + ## Database Layout - `db/migrations/0001_local_core.sql` creates the local-first schema. diff --git a/docker-compose.yml b/docker-compose.yml index 094165e..aaa757c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,19 +1,42 @@ services: db: image: postgis/postgis:16-3.4 + restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 10s volumes: - leads4less-db:/var/lib/postgresql/data + migrate: + build: + context: . + target: migrate + depends_on: + db: + condition: service_healthy + environment: + NODE_ENV: ${NODE_ENV} + DATABASE_URL: ${DATABASE_URL} + PG_BOSS_SCHEMA: ${PG_BOSS_SCHEMA} + restart: "no" + api: build: context: . target: api depends_on: - - db + db: + condition: service_healthy + migrate: + condition: service_completed_successfully environment: NODE_ENV: ${NODE_ENV} APP_HOST: ${APP_HOST} @@ -24,6 +47,7 @@ services: GOOGLE_MAPS_SERVER_KEY: ${GOOGLE_MAPS_SERVER_KEY} PG_BOSS_SCHEMA: ${PG_BOSS_SCHEMA} SESSION_TTL_DAYS: ${SESSION_TTL_DAYS} + restart: unless-stopped expose: - ${APP_PORT} @@ -32,7 +56,10 @@ services: context: . target: worker depends_on: - - db + db: + condition: service_healthy + migrate: + condition: service_completed_successfully environment: NODE_ENV: ${NODE_ENV} DATABASE_URL: ${DATABASE_URL} @@ -40,6 +67,7 @@ services: GOOGLE_MAPS_SERVER_KEY: ${GOOGLE_MAPS_SERVER_KEY} PG_BOSS_SCHEMA: ${PG_BOSS_SCHEMA} SESSION_TTL_DAYS: ${SESSION_TTL_DAYS} + restart: unless-stopped web: build: @@ -49,7 +77,9 @@ services: VITE_API_BASE_URL: ${VITE_API_BASE_URL} VITE_GOOGLE_MAPS_PLATFORM_KEY: ${VITE_GOOGLE_MAPS_PLATFORM_KEY} depends_on: - - api + api: + condition: service_started + restart: unless-stopped expose: - "80"