diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..27eb973 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.env +.venv +__pycache__ +*.py[cod] +*.sqlite3 +db.sqlite3 +*.log +.git +.gitignore +README.md diff --git a/.env.example b/.env.example index e63f5cd..5be53fb 100644 --- a/.env.example +++ b/.env.example @@ -10,4 +10,7 @@ ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0,testserver # CORS (the accounting frontend dev server runs on port 5176) CORS_ALLOW_ALL_ORIGINS=True -CORS_ALLOWED_ORIGINS=http://localhost:5176,http://127.0.0.1:5176 +CORS_ALLOWED_ORIGINS=http://localhost:5176,http://127.0.0.1:5176,http://localhost:3006,http://127.0.0.1:3006 + +# Docker: persist SQLite in a named volume +# SQLITE_PATH=/data/db.sqlite3 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5dd0d44 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# FunZone Accounting Backend Dockerfile +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV DEBIAN_FRONTEND=noninteractive + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + postgresql-client \ + build-essential \ + libpq-dev \ + curl \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt /app/ +RUN pip install --no-cache-dir -r requirements.txt + +COPY . /app/ + +RUN mkdir -p /data \ + && adduser --disabled-password --gecos '' appuser \ + && chown -R appuser:appuser /app /data \ + && chmod +x /app/docker-entrypoint.sh + +USER appuser + +EXPOSE 8010 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD curl -f http://localhost:8010/health || exit 1 + +ENTRYPOINT ["/app/docker-entrypoint.sh"] +CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8010", "--workers", "2", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-"] diff --git a/README.md b/README.md index 765d400..72fc372 100644 --- a/README.md +++ b/README.md @@ -52,3 +52,27 @@ default company settings/account mapping. No demo business data is created. The frontend logs in via the FunZone admin endpoint (`/api/auth/token/admin/`) and reuses the returned access token for this service. Make sure `SECRET_KEY` here is identical to the FunZone backend's. + +## Docker + +SQLite (default, zero extra services): + +```bash +docker compose up -d --build +``` + +The API listens on **http://localhost:8010** (`/health`, `/api/accounting/...`). + +Set `SECRET_KEY` to match the FunZone backend before starting: + +```bash +SECRET_KEY=your-shared-secret docker compose up -d --build +``` + +Optional PostgreSQL instead of SQLite: + +```bash +docker compose --profile postgres up -d --build +``` + +Then set `DATABASE_URL=postgresql://accounting_user:accounting_password@accounting-db:5432/accounting_db` in your environment or `.env` file. diff --git a/config/settings.py b/config/settings.py index 714e48c..0a550cd 100644 --- a/config/settings.py +++ b/config/settings.py @@ -71,12 +71,17 @@ if DATABASE_URL: } } else: - DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3'}} + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': config('SQLITE_PATH', default=str(BASE_DIR / 'db.sqlite3')), + } + } else: DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + 'NAME': config('SQLITE_PATH', default=str(BASE_DIR / 'db.sqlite3')), } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..32eb950 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,53 @@ +services: + accounting-backend: + build: + context: . + dockerfile: Dockerfile + container_name: funzone_accounting_backend + ports: + - "127.0.0.1:8010:8010" + environment: + SECRET_KEY: ${SECRET_KEY:-your-super-secret-key-change-in-production} + DEBUG: ${DEBUG:-False} + ALLOWED_HOSTS: ${ALLOWED_HOSTS:-localhost,127.0.0.1,accounting-backend} + CORS_ALLOW_ALL_ORIGINS: ${CORS_ALLOW_ALL_ORIGINS:-True} + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-http://localhost:3006,http://localhost:5176,http://127.0.0.1:3006,http://127.0.0.1:5176} + SQLITE_PATH: /data/db.sqlite3 + DATABASE_URL: ${DATABASE_URL:-} + DB_HOST: accounting-db + POSTGRES_USER: ${POSTGRES_USER:-accounting_user} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-accounting_password} + POSTGRES_DB: ${POSTGRES_DB:-accounting_db} + volumes: + - accounting_sqlite:/data + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8010/health"] + interval: 30s + timeout: 10s + start_period: 40s + retries: 3 + + accounting-db: + image: postgres:16-alpine + container_name: funzone_accounting_postgres + profiles: + - postgres + environment: + POSTGRES_DB: ${POSTGRES_DB:-accounting_db} + POSTGRES_USER: ${POSTGRES_USER:-accounting_user} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-accounting_password} + volumes: + - accounting_postgres:/var/lib/postgresql/data + ports: + - "127.0.0.1:5434:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-accounting_user} -d ${POSTGRES_DB:-accounting_db}"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + +volumes: + accounting_sqlite: + accounting_postgres: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..32e13d3 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,14 @@ +#!/bin/sh +set -e + +if [ -n "$DATABASE_URL" ]; then + until PGPASSWORD="${POSTGRES_PASSWORD:-accounting_password}" psql -h "${DB_HOST:-accounting-db}" -U "${POSTGRES_USER:-accounting_user}" -d "${POSTGRES_DB:-accounting_db}" -c '\q' 2>/dev/null; do + >&2 echo "PostgreSQL is unavailable - sleeping" + sleep 1 + done + >&2 echo "PostgreSQL is up" +fi + +python manage.py migrate --noinput + +exec "$@" diff --git a/requirements.txt b/requirements.txt index 9ebf734..a6712e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,6 @@ python-decouple==3.8 # Optional PostgreSQL driver (SQLite is used by default) psycopg2-binary==2.9.9 + +# Production server +gunicorn==21.2.0