diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..1af56d15 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,42 @@ +.git +.gitignore +.github +.vscode +.idea + +# Python +__pycache__ +*.pyc +*.pyo +venv/ +env/ +.venv/ +*.egg-info +.pytest_cache/ +.coverage +htmlcov/ + +# Node / Frontend (built separately) +frontend/node_modules/ +frontend/.svelte-kit/ +frontend/build/ + +# Mobile app +mobile/ + +# Docker +docker-compose.yml +.dockerignore + +# Env files (injected at runtime) +.env +.env.* +backend/.env +backend/.env.* + +# Misc +*.md +*.log +.DS_Store +Thumbs.db +.playwright-mcp diff --git a/.env.docker b/.env.docker new file mode 100644 index 00000000..b6fd3705 --- /dev/null +++ b/.env.docker @@ -0,0 +1,41 @@ +# Docker development environment variables +# Copy to .env.docker.local and customize if needed + +# Django +SECRET_KEY=django-insecure-docker-dev-key-change-in-production +DEBUG=True +ENV_TYPE=dev +ALLOWED_HOSTS=localhost,127.0.0.1,backend +DOMAIN_NAME=http://localhost:8000 +SWAGGER_ROOT_URL=http://localhost:8000 + +# Database (must match docker-compose db service) +DBNAME=crm_db +DBUSER=crm_user +DBPASSWORD=crm_password +DBHOST=db +DBPORT=5432 + +# Postgres superuser (used by db service to create the database) +POSTGRES_DB=crm_db +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres_password + +# Celery / Redis +CELERY_BROKER_URL=redis://redis:6379/0 +CELERY_RESULT_BACKEND=redis://redis:6379/0 + +# Email (console backend in dev — emails print to stdout) +DEFAULT_FROM_EMAIL=noreply@localhost +ADMIN_EMAIL=admin@localhost + +# CORS +CORS_ALLOW_ALL=True + +# Google OAuth (leave blank to disable) +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_REDIRECT_URI= + +# Frontend +PUBLIC_DJANGO_API_URL=http://localhost:8000 diff --git a/.gitignore b/.gitignore index 70844ff7..ab014a0d 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,9 @@ frontend/pnpm-lock.yaml logs/ *.log +# Docker local overrides +.env.docker.local + # OS .DS_Store Thumbs.db diff --git a/Dockerfile b/Dockerfile index 41983bac..91e5bf94 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,41 +1,27 @@ -FROM ubuntu:24.04 +FROM python:3.12-slim-bookworm -# invalidate cache -ARG APP_NAME +# Prevent Python from buffering stdout/stderr (useful for Docker logs) +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 -# test arg -RUN test -n "$APP_NAME" +# Install system dependencies for WeasyPrint (cairo, pango) and PostgreSQL +RUN apt-get update && apt-get install -y --no-install-recommends \ + libpq-dev \ + libcairo2 \ + libpango-1.0-0 \ + libpangocairo-1.0-0 \ + libgdk-pixbuf2.0-0 \ + libffi-dev \ + shared-mime-info \ + && rm -rf /var/lib/apt/lists/* -# install system packages -RUN apt-get update -y -RUN apt-get install -y \ - python3-pip \ - python3-venv \ - build-essential \ - libpq-dev \ - libmariadbclient-dev \ - libjpeg62-dev \ - zlib1g-dev \ - libwebp-dev \ - curl \ - vim \ - net-tools +WORKDIR /app -# setup user -RUN useradd -ms /bin/bash ubuntu -USER ubuntu +# Install Python dependencies (layer cached) +COPY backend/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt -# install app -RUN mkdir -p /home/ubuntu/"$APP_NAME"/"$APP_NAME" -WORKDIR /home/ubuntu/"$APP_NAME"/"$APP_NAME" -COPY . . -RUN python3 -m venv ../venv -RUN . ../venv/bin/activate -RUN /home/ubuntu/"$APP_NAME"/venv/bin/pip install -U pip -RUN /home/ubuntu/"$APP_NAME"/venv/bin/pip install -r requirements.txt -RUN /home/ubuntu/"$APP_NAME"/venv/bin/pip install gunicorn +# Copy backend source +COPY backend/ . -# setup path -ENV PATH="${PATH}:/home/ubuntu/$APP_NAME/$APP_NAME/scripts" - -USER ubuntu +EXPOSE 8000 diff --git a/README.md b/README.md index 1b0a99cd..cd501b65 100644 --- a/README.md +++ b/README.md @@ -120,12 +120,49 @@ celery -A crm worker --loglevel=INFO ## Docker Setup +Run the full stack (backend, frontend, PostgreSQL, Redis, Celery) with a single command: + ```bash -# Build and run with Docker Compose -docker build -t bottlecrm:latest -f docker/dockerfile . -docker-compose -f docker/docker-compose.yml up +# Start all services (first run will build images) +docker compose up --build + +# Create an admin user +docker compose exec backend python manage.py createsuperuser + +# (Optional) Load sample data +docker compose exec backend python manage.py seed_data --email admin@example.com ``` +Once running: +- **Frontend**: http://localhost:5173 +- **API / Swagger**: http://localhost:8000/swagger-ui/ +- **PostgreSQL**: localhost:5432 +- **Redis**: localhost:6379 + +### Daily workflow + +```bash +docker compose up # start all services (code changes auto-reload) +docker compose down # stop all services +docker compose down -v # stop and delete all data (full reset) +``` + +### Running commands inside containers + +```bash +docker compose exec backend python manage.py migrate +docker compose exec backend python -m pytest +docker compose exec backend python manage.py manage_rls --status +``` + +### Custom environment overrides + +The default env vars live in `.env.docker` (committed). To override locally without touching git: + +1. Copy `.env.docker` to `.env.docker.local` +2. Edit values as needed +3. Update `env_file` in `docker-compose.yml` to point to `.env.docker.local` + ## Project Structure ``` @@ -146,8 +183,16 @@ Django-CRM/ │ │ └── routes/ # SvelteKit routes │ │ ├── (app)/ # Authenticated app routes │ │ └── (no-layout)/ # Auth pages (login, etc.) -│ └── static/ # Static assets -└── docker/ # Docker configuration +│ ├── static/ # Static assets +│ └── Dockerfile # Frontend dev container +├── docker/ # Docker support files +│ ├── backend/ +│ │ └── entrypoint.sh # DB wait + migrate + runserver +│ └── postgres/ +│ └── init-rls-user.sql # Creates non-superuser for RLS +├── Dockerfile # Backend / Celery image +├── docker-compose.yml # Full-stack dev environment +└── .env.docker # Docker env vars (dev defaults) ``` ## Multi-Tenancy & Security diff --git a/docker-compose.yml b/docker-compose.yml index cc51de77..cc6853a1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,19 +1,84 @@ -version: '2.0' - services: db: - image: postgres - # volumes: - # - /home/db:/var/lib/postgresql/data - env_file: - - ./db.env - - web: - build: . - image: micropyramid/django-crm:1 - env_file: - - ./db.env + image: postgres:16-alpine + env_file: .env.docker + volumes: + - postgres_data:/var/lib/postgresql/data + - ./docker/postgres/init-rls-user.sql:/docker-entrypoint-initdb.d/init-rls-user.sql:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 3s + retries: 5 ports: - - "8001:8000" + - "5432:5432" + + redis: + image: redis:7-alpine + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + ports: + - "6379:6379" + + backend: + build: + context: . + dockerfile: Dockerfile + env_file: .env.docker + volumes: + - ./backend:/app + - ./docker/backend/entrypoint.sh:/entrypoint.sh:ro + ports: + - "8000:8000" depends_on: - - db \ No newline at end of file + db: + condition: service_healthy + redis: + condition: service_healthy + command: /bin/bash /entrypoint.sh + + celery-worker: + build: + context: . + dockerfile: Dockerfile + env_file: .env.docker + volumes: + - ./backend:/app + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + command: celery -A crm worker --loglevel=info + + celery-beat: + build: + context: . + dockerfile: Dockerfile + env_file: .env.docker + volumes: + - ./backend:/app + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + command: celery -A crm beat --loglevel=info + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + env_file: .env.docker + volumes: + - ./frontend:/app + - frontend_node_modules:/app/node_modules + ports: + - "5173:5173" + +volumes: + postgres_data: + frontend_node_modules: diff --git a/docker/backend/entrypoint.sh b/docker/backend/entrypoint.sh new file mode 100755 index 00000000..b5a61914 --- /dev/null +++ b/docker/backend/entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +echo "Waiting for PostgreSQL..." +while ! python -c " +import socket, os +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.connect((os.environ['DBHOST'], int(os.environ['DBPORT']))) +s.close() +" 2>/dev/null; do + sleep 1 +done +echo "PostgreSQL is ready." + +echo "Running migrations..." +python manage.py migrate --noinput + +echo "Collecting static files..." +python manage.py collectstatic --noinput + +echo "Starting development server..." +exec python manage.py runserver 0.0.0.0:8000 diff --git a/docker/postgres/init-rls-user.sql b/docker/postgres/init-rls-user.sql new file mode 100644 index 00000000..dea60338 --- /dev/null +++ b/docker/postgres/init-rls-user.sql @@ -0,0 +1,24 @@ +-- Create the application database user (non-superuser) for RLS enforcement. +-- This script runs automatically on first `docker compose up` via +-- PostgreSQL's /docker-entrypoint-initdb.d/ mechanism. + +-- The database itself (crm_db) is created by POSTGRES_DB env var. +-- We just need to create the app user and grant privileges. + +DO $$ +BEGIN + IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'crm_user') THEN + CREATE ROLE crm_user WITH LOGIN PASSWORD 'crm_password'; + END IF; +END +$$; + +-- Grant privileges on the database +GRANT ALL PRIVILEGES ON DATABASE crm_db TO crm_user; + +-- Allow crm_user to create schemas and objects in the public schema +\connect crm_db; +GRANT ALL ON SCHEMA public TO crm_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO crm_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO crm_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO crm_user; diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..0aecb717 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,14 @@ +FROM node:22-slim + +# Enable pnpm via corepack +RUN corepack enable && corepack prepare pnpm@latest --activate + +WORKDIR /app + +# Install dependencies (layer cached) +COPY package.json pnpm-lock.yaml* ./ +RUN pnpm install --frozen-lockfile || pnpm install + +EXPOSE 5173 + +CMD ["pnpm", "dev", "--host", "0.0.0.0"]