diff --git a/.env.example b/.env.example index 3f004c9..8add7f7 100644 --- a/.env.example +++ b/.env.example @@ -37,13 +37,26 @@ EXPOSE_PORT=8785 # Baza danych # ================================ -# Adres SQLAlchemy -# +# Wybór silnika bazy +# Dozwolone: +# - sqlite +# - pgsql +# - mysql +DB_ENGINE=sqlite + +# Baza danych - dane +DB_HOST=postgres +DB_PORT=5432 +DB_NAME=ksef +DB_USER=ksef +DB_PASSWORD=ksef + +# Jeśli ustawisz DATABASE_URL ręcznie, ma priorytet nad DB_ENGINE/DB_* # Przykłady: # sqlite:///instance/app.db -# postgresql://user:pass@localhost/dbname -DATABASE_URL=sqlite:///instance/app.db - +# postgresql+psycopg://ksef:ksef@postgres:5432/ksef +# mysql+pymysql://ksef:ksef@mysql:3306/ksef +DATABASE_URL=sqlite:///db/sqlite/app.db # ================================ # Redis / Cache / Rate limit diff --git a/.gitignore b/.gitignore index 31d1df0..a7fdf92 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ storage/* backups/* certs/* pdf/* +db/* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2068d49..e316ee9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,6 @@ COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . -RUN mkdir -p instance storage/archive storage/pdf storage/backups +RUN mkdir -p db storage/archive storage/pdf storage/backups CMD ["gunicorn", "-w", "1", "-k", "gthread", "--threads", "8", "-b", "0.0.0.0:5000", "run:app"] diff --git a/config.py b/config.py index 2cdc23e..ad2a9b1 100644 --- a/config.py +++ b/config.py @@ -7,12 +7,34 @@ load_dotenv(BASE_DIR / '.env') def _normalize_sqlalchemy_db_url(raw: str | None) -> str: - if not raw: - return f"sqlite:///{(BASE_DIR / 'instance' / 'app.db').resolve()}" - if raw.startswith('sqlite:///') and not raw.startswith('sqlite:////'): - rel = raw.replace('sqlite:///', '', 1) - return f"sqlite:///{(BASE_DIR / rel).resolve()}" - return raw + if raw: + raw = raw.strip() + if raw.startswith('postgres://'): + return raw.replace('postgres://', 'postgresql+psycopg://', 1) + if raw.startswith('sqlite:///') and not raw.startswith('sqlite:////'): + rel = raw.replace('sqlite:///', '', 1) + return f"sqlite:///{(BASE_DIR / rel).resolve()}" + return raw + + db_engine = os.getenv('DB_ENGINE', 'sqlite').strip().lower() + db_host = os.getenv('DB_HOST', 'localhost').strip() + db_port = os.getenv('DB_PORT', '').strip() + db_name = os.getenv('DB_NAME', 'ksef').strip() + db_user = os.getenv('DB_USER', '').strip() + db_password = os.getenv('DB_PASSWORD', '').strip() + + if db_engine == 'sqlite': + return f"sqlite:///{(BASE_DIR / 'db' / 'sqlite' / 'app.db').resolve()}" + + if db_engine in ('pgsql', 'postgres', 'postgresql'): + port = db_port or '5432' + return f"postgresql+psycopg://{db_user}:{db_password}@{db_host}:{port}/{db_name}" + + if db_engine == 'mysql': + port = db_port or '3306' + return f"mysql+pymysql://{db_user}:{db_password}@{db_host}:{port}/{db_name}" + + raise ValueError(f"Nieobsługiwany DB_ENGINE: {db_engine}") def _path_from_env(name: str, default: Path) -> Path: diff --git a/deploy_docker.sh b/deploy_docker.sh index f1e6534..079d0c4 100755 --- a/deploy_docker.sh +++ b/deploy_docker.sh @@ -1,9 +1,18 @@ #!/usr/bin/env sh set -eu +DB_TARGET="${1:-sqlite}" + STACK_NAME="${STACK_NAME:-ksef_app}" COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" SSL_DIR="${SSL_DIR:-./deploy/caddy/ssl}" + +if [ -f .env ]; then + set -a + . ./.env + set +a +fi + APP_DOMAIN="${APP_DOMAIN:-localhost}" CERT_FILE="${CERT_FILE:-${SSL_DIR}/server.crt}" KEY_FILE="${KEY_FILE:-${SSL_DIR}/server.key}" @@ -24,6 +33,38 @@ need_cmd openssl mkdir -p "$SSL_DIR" +case "$DB_TARGET" in + sqlite) + export DB_ENGINE=sqlite + export DB_HOST="" + export DB_PORT="" + COMPOSE_PROFILES="" + mkdir -p ./db/sqlite + DB_PATH_INFO="./db/sqlite/app.db" + ;; + pgsql|postgres|postgresql) + export DB_ENGINE=pgsql + export DB_HOST="${DB_HOST:-postgres}" + export DB_PORT="${DB_PORT:-5432}" + COMPOSE_PROFILES="pgsql" + mkdir -p ./db/pgsql + DB_PATH_INFO="./db/pgsql" + ;; + mysql) + export DB_ENGINE=mysql + export DB_HOST="${DB_HOST:-mysql}" + export DB_PORT="${DB_PORT:-3306}" + COMPOSE_PROFILES="mysql" + mkdir -p ./db/mysql + DB_PATH_INFO="./db/mysql" + ;; + *) + printf 'Nieznany typ bazy: %s\n' "$DB_TARGET" >&2 + printf 'Użycie: ./deploy_docker.sh [sqlite|pgsql|mysql]\n' >&2 + exit 1 + ;; +esac + if [ ! -f "$CERT_FILE" ] || [ ! -f "$KEY_FILE" ]; then log "Nie znaleziono certyfikatu SSL w katalogu ${SSL_DIR}, tworzę self-signed cert..." rm -f "$CERT_FILE" "$KEY_FILE" @@ -38,22 +79,31 @@ else log "Znaleziono istniejący certyfikat SSL w katalogu ${SSL_DIR}." fi -log "Pobieram najnowsze obrazy bazowe..." -docker compose -f "$COMPOSE_FILE" pull +log "Wybrany silnik bazy: ${DB_ENGINE}" -log "Buduję obraz bez cache..." -docker compose -f "$COMPOSE_FILE" build --no-cache +if [ -n "$COMPOSE_PROFILES" ]; then + export COMPOSE_PROFILES + log "Aktywne profile Compose: ${COMPOSE_PROFILES}" +else + unset COMPOSE_PROFILES || true + log "Brak aktywnych profili Compose" +fi -log "Zatrzymuję aktualny stack..." +log "Pobieram najnowsze obrazy dla bieżącego stacka..." +docker compose -p "$STACK_NAME" -f "$COMPOSE_FILE" pull + +log "Zatrzymuję aktualny stack projektu ${STACK_NAME}..." docker compose -p "$STACK_NAME" -f "$COMPOSE_FILE" stop || true -log "Usuwam osierocone kontenery i stare nieużywane obrazy..." +log "Usuwam kontenery i osierocone zasoby tylko dla projektu ${STACK_NAME}..." docker compose -p "$STACK_NAME" -f "$COMPOSE_FILE" down --remove-orphans || true -docker image prune -af || true -docker builder prune -af || true -authoritative_stack="${STACK_NAME}" -log "Uruchamiam stack ${authoritative_stack}..." +log "Buduję obrazy projektu ${STACK_NAME} bez cache..." +docker compose -p "$STACK_NAME" -f "$COMPOSE_FILE" build --no-cache + +log "Uruchamiam stack ${STACK_NAME}..." docker compose -p "$STACK_NAME" -f "$COMPOSE_FILE" up -d log "Deployment zakończony. Aplikacja powinna być dostępna pod https://${APP_DOMAIN}" +log "Silnik bazy: ${DB_ENGINE}" +log "Ścieżka danych bazy: ${DB_PATH_INFO}" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 6554a79..de7202d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,14 +3,16 @@ services: build: . env_file: [.env] environment: - APP_PORT: 5000 - APP_EXTERNAL_SCHEME: https + APP_PORT: ${APP_PORT:-5000} + APP_EXTERNAL_SCHEME: ${APP_EXTERNAL_SCHEME:-https} APP_EXTERNAL_HOST: ${APP_DOMAIN:-localhost} APP_EXTERNAL_PORT: ${EXPOSE_PORT:-8785} TZ: ${APP_TIMEZONE:-Europe/Warsaw} volumes: - ./:/app - depends_on: [redis] + - ./db/sqlite:/app/db/sqlite + depends_on: + - redis restart: unless-stopped redis: @@ -19,6 +21,36 @@ services: - "6379:6379" restart: unless-stopped + postgres: + image: postgres:16-alpine + profiles: ["pgsql"] + environment: + POSTGRES_DB: ${DB_NAME:-ksef} + POSTGRES_USER: ${DB_USER:-ksef} + POSTGRES_PASSWORD: ${DB_PASSWORD:-ksef} + TZ: ${APP_TIMEZONE:-Europe/Warsaw} + volumes: + - ./db/pgsql:/var/lib/postgresql/data + ports: + - "${DB_PORT:-5432}:5432" + restart: unless-stopped + + mysql: + image: mysql:8.4 + profiles: ["mysql"] + command: --default-authentication-plugin=mysql_native_password + environment: + MYSQL_DATABASE: ${DB_NAME:-ksef} + MYSQL_USER: ${DB_USER:-ksef} + MYSQL_PASSWORD: ${DB_PASSWORD:-ksef} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root} + TZ: ${APP_TIMEZONE:-Europe/Warsaw} + volumes: + - ./db/mysql:/var/lib/mysql + ports: + - "${DB_PORT:-3306}:3306" + restart: unless-stopped + caddy: image: caddy:2-alpine env_file: [.env] @@ -33,9 +65,10 @@ services: - ./deploy/caddy/ssl:/certs:ro - caddy_data:/data - caddy_config:/config - depends_on: [web] + depends_on: + - web restart: unless-stopped volumes: caddy_data: - caddy_config: + caddy_config: \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6b06c51..c8a6c95 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,3 +22,5 @@ cryptography==45.0.0 xhtml2pdf==0.2.17 psutil gunicorn==23.0.0 +PyMySQL==1.1.1 +psycopg==3.2.6