From 270d85057216392f5a47f6f2b89b7ba430f4cc9d Mon Sep 17 00:00:00 2001 From: Brunobrno Date: Wed, 10 Jun 2026 01:29:30 +0200 Subject: [PATCH] Add S3/MinIO (RustFS) support and compose updates Introduce S3-compatible storage support and update local/dev configuration. - .claude: add "WebSearch" to local settings - backend/.env.example: add USE_S3 flag and detailed S3/MinIO (RustFS) example env vars + comments to explain dev vs production usage - backend/vontor_cz/settings.py: refactor to use USE_S3 boolean; set STATIC_ROOT; configure STORAGES for local filesystem vs S3 backends; add handling for S3 endpoint (MinIO/RustFS) vs real AWS S3 including path-style addressing, URL construction, and sensible AWS defaults - docker-compose.yml: add rustfs to service dependencies, include rustfs in networks, change frontend port mapping to 8001:80, and add rustfs-data volume binding These changes enable using a local S3-compatible backend (RustFS/MinIO) in development while keeping the option to swap to real AWS S3 for production. --- .claude/settings.local.json | 3 +- backend/.env.example | 31 ++++++++++--- backend/vontor_cz/settings.py | 86 +++++++++++++++++------------------ docker-compose.yml | 12 ++++- 4 files changed, 78 insertions(+), 54 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e538a39..643568f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -18,7 +18,8 @@ "Bash(node -e \"const r = require\\('react-icons/si'\\); const celery = Object.keys\\(r\\).filter\\(k => k.toLowerCase\\(\\).includes\\('celery'\\) || k.toLowerCase\\(\\).includes\\('worker'\\) || k.toLowerCase\\(\\).includes\\('task'\\)\\).slice\\(0,10\\); console.log\\(celery\\);\")", "Bash(Get-ChildItem -Path \"c:\\\\Users\\\\bruno\\\\Documents\\\\GitHub\\\\vontor-cz\\\\backend\\\\\" -Directory | Select-Object -ExpandProperty Name)", "PowerShell(Get-Command python)", - "PowerShell(python -c \"import django; print\\(django.__version__\\)\")" + "PowerShell(python -c \"import django; print\\(django.__version__\\)\")", + "WebSearch" ] } } diff --git a/backend/.env.example b/backend/.env.example index 0cc91a3..192d645 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -20,6 +20,8 @@ POSTGRES_USER=dockerDBuser POSTGRES_PASSWORD=AWSJeMocDrahaZalezitost # ------------------ MEDIA / STATIC ------------------ +# USE_S3=True enables S3-compatible storage (MinIO or AWS). +# Leave USE_S3=False (default) to use local filesystem in development. MEDIA_URL=/media/ # ------------------ REDIS / CACHING / CHANNELS ------------------ @@ -44,12 +46,29 @@ EMAIL_USER= EMAIL_USER_PASSWORD= DEFAULT_FROM_EMAIL= -# ------------------ AWS (disabled unless USE_AWS=True) ------------------ -USE_AWS=False -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_STORAGE_BUCKET_NAME= -AWS_S3_REGION_NAME=eu-central-1 +# ------------------ S3 STORAGE ------------------ +# Set USE_S3=True to enable. AWS_S3_ENDPOINT_URL selects the backend: +# set → MinIO / S3-compatible +# empty → real AWS S3 + +USE_S3=False + +# RustFS (docker-compose default — S3-compatible MinIO successor) +AWS_S3_ENDPOINT_URL=http://rustfs:9000 +AWS_S3_CUSTOM_DOMAIN=localhost:9000/vontor +AWS_STORAGE_BUCKET_NAME=vontor +AWS_ACCESS_KEY_ID=rustfsadmin +AWS_SECRET_ACCESS_KEY=rustfsadmin +RUSTFS_ACCESS_KEY=rustfsadmin +RUSTFS_SECRET_KEY=rustfsadmin +RUSTFS_BUCKET_NAME=vontor + +# AWS S3 (swap in for production — clear AWS_S3_ENDPOINT_URL) +# AWS_STORAGE_BUCKET_NAME=my-bucket +# AWS_ACCESS_KEY_ID= +# AWS_SECRET_ACCESS_KEY= +# AWS_S3_REGION_NAME=eu-central-1 +# AWS_S3_ENDPOINT_URL= # ------------------ JWT / TOKENS (lifetimes defined in code) ------------------ # (No env vars needed; kept placeholder section) diff --git a/backend/vontor_cz/settings.py b/backend/vontor_cz/settings.py index 4494613..27ad317 100644 --- a/backend/vontor_cz/settings.py +++ b/backend/vontor_cz/settings.py @@ -626,63 +626,59 @@ STATICFILES_DIRS = [ -if os.getenv("USE_AWS", "") == "True": - USE_AWS = True -else: - USE_AWS = False +USE_S3 = _env_bool("USE_S3") +print(f"USE_S3: {USE_S3}") -print(f"\n-------------- USE_AWS: {USE_AWS} --------------") +STATIC_ROOT = BASE_DIR / 'collectedstaticfiles' -if USE_AWS is False: - # Development: Use local file system storage for static files +if not USE_S3: + # Local filesystem — development only STORAGES = { - "default": { - "BACKEND": "django.core.files.storage.FileSystemStorage", - }, - "staticfiles": { - "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", - }, + "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"}, + "staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"}, } - - # Media and Static URL for local dev - MEDIA_URL = os.getenv("MEDIA_URL", "/media/") + MEDIA_URL = os.getenv("MEDIA_URL", "/media/") MEDIA_ROOT = os.path.join(BASE_DIR, 'media') - STATIC_URL = '/static/' - STATIC_ROOT = BASE_DIR / 'collectedstaticfiles' -elif USE_AWS: - # PRODUCTION - AWS_LOCATION = "static" +else: + # S3-compatible storage — MinIO (AWS_S3_ENDPOINT_URL set) or real AWS S3 (endpoint empty) + S3_BUCKET = os.getenv('AWS_STORAGE_BUCKET_NAME', 'vontor') + S3_ENDPOINT = os.getenv('AWS_S3_ENDPOINT_URL', '') + S3_CUSTOM_DOMAIN = os.getenv('AWS_S3_CUSTOM_DOMAIN', '') + S3_SSL = not S3_ENDPOINT or S3_ENDPOINT.startswith('https') + S3_PROTO = 'https' if S3_SSL else 'http' - # Production: Use S3 storage STORAGES = { - "default": { - "BACKEND" : "storages.backends.s3boto3.S3StaticStorage", - }, - - "staticfiles": { - "BACKEND" : "storages.backends.s3boto3.S3StaticStorage", - }, + "default": {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}, + "staticfiles": {"BACKEND": "storages.backends.s3boto3.S3StaticStorage"}, } - # Media and Static URL for AWS S3 - MEDIA_URL = f'https://{os.getenv("AWS_STORAGE_BUCKET_NAME")}.s3.amazonaws.com/media/' - STATIC_URL = f'https://{os.getenv("AWS_STORAGE_BUCKET_NAME")}.s3.amazonaws.com/static/' + AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID') + AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY') + AWS_STORAGE_BUCKET_NAME = S3_BUCKET + AWS_S3_SIGNATURE_VERSION = 's3v4' + AWS_S3_FILE_OVERWRITE = False + AWS_DEFAULT_ACL = None + AWS_QUERYSTRING_AUTH = False + AWS_S3_USE_SSL = S3_SSL - CSRF_TRUSTED_ORIGINS.append(STATIC_URL) - - # Static files should be collected to a local directory and then uploaded to S3 - STATIC_ROOT = BASE_DIR / 'collectedstaticfiles' - - AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID') - AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY') - AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME') - AWS_S3_REGION_NAME = os.getenv('AWS_S3_REGION_NAME', 'us-east-1') # Default to 'us-east-1' if not set - AWS_S3_SIGNATURE_VERSION = 's3v4' # Use AWS Signature Version 4 - AWS_S3_USE_SSL = True - AWS_S3_FILE_OVERWRITE = True - AWS_DEFAULT_ACL = None # Set to None to avoid setting a default ACL + if S3_ENDPOINT: + # MinIO / S3-compatible: needs path-style addressing + AWS_S3_ENDPOINT_URL = S3_ENDPOINT + AWS_S3_ADDRESSING_STYLE = 'path' + if S3_CUSTOM_DOMAIN: + AWS_S3_CUSTOM_DOMAIN = S3_CUSTOM_DOMAIN + AWS_S3_URL_PROTOCOL = f'{S3_PROTO}:' + base_host = S3_CUSTOM_DOMAIN or f'{S3_ENDPOINT.split("://")[1]}/{S3_BUCKET}' + MEDIA_URL = f'{S3_PROTO}://{base_host}/' + STATIC_URL = f'{S3_PROTO}://{base_host}/static/' + else: + # AWS S3 + AWS_S3_REGION_NAME = os.getenv('AWS_S3_REGION_NAME', 'us-east-1') + MEDIA_URL = f'https://{S3_BUCKET}.s3.amazonaws.com/media/' + STATIC_URL = f'https://{S3_BUCKET}.s3.amazonaws.com/static/' + CSRF_TRUSTED_ORIGINS.append(STATIC_URL) diff --git a/docker-compose.yml b/docker-compose.yml index 27aeaf3..a04d2c1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: depends_on: - db - redis + - rustfs volumes: - ./backend:/app - static-data:/app/collectedstaticfiles @@ -66,6 +67,7 @@ services: - redis - db - backend + - rustfs networks: - app_network @@ -84,6 +86,7 @@ services: - redis - db - backend + - rustfs networks: - app_network @@ -114,8 +117,7 @@ services: env_file: - ./frontend/.env ports: - - 80:80 - # - 9000:80 + - 8001:80 depends_on: - backend networks: @@ -157,4 +159,10 @@ volumes: type: none o: bind device: ./volumes/media + rustfs-data: + driver: local + driver_opts: + type: none + o: bind + device: ./volumes/rustfs