From 69631848cf5cdb28305141846b8a99f9bce61174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Z=C3=A4ske?= Date: Wed, 18 Jun 2025 00:00:51 +0200 Subject: [PATCH 1/5] Fixed frontend returning corrupt binary data --- frontend/src/routes/api/[...path]/+server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/routes/api/[...path]/+server.ts b/frontend/src/routes/api/[...path]/+server.ts index 815d4a7..b7fcdbf 100644 --- a/frontend/src/routes/api/[...path]/+server.ts +++ b/frontend/src/routes/api/[...path]/+server.ts @@ -85,7 +85,7 @@ async function handleRequest( }); } - const responseData = await response.text(); + const responseData = await response.bytes(); // Create a new Headers object without the 'set-cookie' header const cleanHeaders = new Headers(response.headers); cleanHeaders.delete('set-cookie'); From 3c3288eab9f2b166a35de961fa39547c7132ab88 Mon Sep 17 00:00:00 2001 From: Corbin Whitton Date: Sun, 22 Jun 2025 20:52:37 -0600 Subject: [PATCH 2/5] feat(devsecops): optimized runtime containers organized docker-compose files updated pipelines to fix created date using nonroot user in runtime environment synced .env.example and docker compose file normalized usage of PG variables --- .env.example | 59 +++++---- .github/.docker-compose-database.yml | 4 +- .github/workflows/backend-beta.yml | 43 +++++- .github/workflows/backend-latest.yml | 43 +++++- .github/workflows/backend-release.yml | 44 ++++++- .github/workflows/backend-test.yml | 8 +- .github/workflows/cdn-beta.yml | 39 +++++- .github/workflows/cdn-latest.yml | 39 +++++- .github/workflows/cdn-release.yml | 40 +++++- .github/workflows/frontend-beta.yml | 43 +++++- .github/workflows/frontend-latest.yml | 43 +++++- .github/workflows/frontend-release.yml | 43 +++++- backend/.dockerignore | 7 + backend/Dockerfile | 128 ++++++++++++------ backend/entrypoint.sh | 19 +-- backend/server/.env.example | 36 ----- backend/server/main/settings.py | 7 + backend/supervisord.conf | 2 + backup.sh | 2 +- cdn/Dockerfile | 48 ++++--- deploy.sh | 2 +- docker-compose-traefik.yaml | 78 ----------- docker-compose.yml | 142 ++++++++++++++++---- documentation/docs/install/docker.md | 4 +- frontend/.dockerignore | 6 + frontend/.env.example | 7 - frontend/Dockerfile | 82 +++++++----- frontend/startup.sh | 5 - kustomization.yml | 173 +++++++++++++++---------- 29 files changed, 784 insertions(+), 412 deletions(-) create mode 100644 backend/.dockerignore delete mode 100644 backend/server/.env.example delete mode 100644 docker-compose-traefik.yaml create mode 100644 frontend/.dockerignore delete mode 100644 frontend/.env.example delete mode 100644 frontend/startup.sh diff --git a/.env.example b/.env.example index 8a923ae..f8302ac 100644 --- a/.env.example +++ b/.env.example @@ -1,47 +1,46 @@ # 🌐 Frontend +FRONTEND_PORT=8015 # Don't forget to update CSRF_TRUSTED_ORIGINS in backend and FRONTEND_URL in backend and ORIGIN in frontend if you make changes to this. PUBLIC_SERVER_URL=http://server:8000 # PLEASE DON'T CHANGE :) - Should be the service name of the backend with port 8000, even if you change the port in the backend service. Only change if you are using a custom more complex setup. ORIGIN=http://localhost:8015 BODY_SIZE_LIMIT=Infinity -FRONTEND_PORT=8015 # 🐘 PostgreSQL Database -PGHOST=db -POSTGRES_DB=database -POSTGRES_USER=adventure +PGHOST=db # Supposed to match the host of the postgis database +POSTGRES_DB=adventureloga +POSTGRES_USER=adventurelog POSTGRES_PASSWORD=changeme123 # πŸ”’ Django Backend -SECRET_KEY=changeme123 -DJANGO_ADMIN_USERNAME=admin -DJANGO_ADMIN_PASSWORD=admin -DJANGO_ADMIN_EMAIL=admin@example.com -PUBLIC_URL=http://localhost:8016 # Match the outward port, used for the creation of image urls -CSRF_TRUSTED_ORIGINS=http://localhost:8016,http://localhost:8015 -DEBUG=False -FRONTEND_URL=http://localhost:8015 # Used for email generation. This should be the url of the frontend -BACKEND_PORT=8016 +SECRET_KEY=changeme123 # Replace with the actual secret key +DJANGO_ADMIN_USERNAME=admin # Replace with the actual admin username +DJANGO_ADMIN_PASSWORD=admin # Replace with the actual admin password +DJANGO_ADMIN_EMAIL=admin@example.com # Replace with the actual admin email +BACKEND_PORT=8016 # Don't forget to update CSRF_TRUSTED_ORIGINS and PUBLIC_URL in backend, and PUBLIC_SERVER_URL in frontend if you make changes to this. +PUBLIC_URL=http://localhost:8016 # Replace with your domain where the backend is accessible publicly, used for the creation of image urls +CSRF_TRUSTED_ORIGINS=http://localhost:8016,http://localhost:8015 # Replace with your domain, if applicable +FRONTEND_URL=http://localhost:8015 # Replace with your domain where the frontend is accessible, used for email generation. +# Disabling Registration: https://adventurelog.app/docs/configuration/disable_registration.html +# DISABLE_REGISTRATION=False +# DISABLE_REGISTRATION_MESSAGE='Registration is disabled for this instance of AdventureLog.' -# Optional: use Google Maps integration -# https://adventurelog.app/docs/configuration/google_maps_integration.html -# GOOGLE_MAPS_API_KEY=your_google_maps_api_key -# Optional: disable registration -# https://adventurelog.app/docs/configuration/disable_registration.html -DISABLE_REGISTRATION=False -# DISABLE_REGISTRATION_MESSAGE=Registration is disabled for this instance of AdventureLog. - -# Optional: Use email -# https://adventurelog.app/docs/configuration/email.html +# Integrations +## Email: https://adventurelog.app/docs/configuration/email.html # EMAIL_BACKEND=email -# EMAIL_HOST=smtp.gmail.com -# EMAIL_USE_TLS=True +# EMAIL_HOST=smtp.mail.com +# EMAIL_USE_TLS=False # EMAIL_PORT=587 -# EMAIL_USE_SSL=False +# EMAIL_USE_SSL=True # EMAIL_HOST_USER=user # EMAIL_HOST_PASSWORD=password -# DEFAULT_FROM_EMAIL=user@example.com +# DEFAULT_FROM_EMAIL=adventurelog@example.com -# Optional: Use Umami for analytics -# https://adventurelog.app/docs/configuration/analytics.html +## Google Maps: https://adventurelog.app/docs/configuration/google_maps_integration.html +# GOOGLE_MAPS_API_KEY=your_google_maps_api_key + +## Umami for analytics: https://adventurelog.app/docs/configuration/analytics.html # PUBLIC_UMAMI_SRC=https://cloud.umami.is/script.js # If you are using the hosted version of Umami -# PUBLIC_UMAMI_WEBSITE_ID= \ No newline at end of file +# PUBLIC_UMAMI_WEBSITE_ID= + +# Extra configuration +DEBUG=false # Use to enable debugging \ No newline at end of file diff --git a/.github/.docker-compose-database.yml b/.github/.docker-compose-database.yml index aa187bd..b14078e 100644 --- a/.github/.docker-compose-database.yml +++ b/.github/.docker-compose-database.yml @@ -6,8 +6,8 @@ services: ports: - "127.0.0.1:5432:5432" environment: - POSTGRES_DB: database - POSTGRES_USER: adventure + POSTGRES_DB: adventurelog + POSTGRES_USER: adventurelog POSTGRES_PASSWORD: changeme123 volumes: - postgres_data:/var/lib/postgresql/data/ diff --git a/.github/workflows/backend-beta.yml b/.github/workflows/backend-beta.yml index a02bbaf..6b3984e 100644 --- a/.github/workflows/backend-beta.yml +++ b/.github/workflows/backend-beta.yml @@ -36,11 +36,42 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: set lower case owner name - run: | - echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} + - name: Set build metadata + id: vars env: - OWNER: "${{ github.repository_owner }}" + OWNER: '${{ github.repository_owner }}' + run: | + echo "GIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "GIT_TAG=$(git describe --tags --abbrev=0 --exact-match 2>/dev/null || echo unknown)" >> $GITHUB_OUTPUT + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + echo "REPO_OWNER=${OWNER,,}" >> $GITHUB_OUTPUT - - name: Build Docker images - run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:beta -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:beta ./backend + - name: Build and push Docker images + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --push \ + --build-arg GIT_SHA=${{ steps.vars.outputs.GIT_SHA }} \ + --build-arg GIT_TAG=${{ steps.vars.outputs.GIT_TAG }} \ + --build-arg BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} \ + -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:beta \ + -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:beta \ + ./backend + + # Notify on success + # - name: Notify Discord on success + # if: success() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"βœ… Build and push succeeded for backend beta\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} + + # # Notify on failure + # - name: Notify Discord on failure + # if: failure() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"❌ Build or push failed for backend beta\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} diff --git a/.github/workflows/backend-latest.yml b/.github/workflows/backend-latest.yml index 5351d9d..27e58df 100644 --- a/.github/workflows/backend-latest.yml +++ b/.github/workflows/backend-latest.yml @@ -35,12 +35,43 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - - name: set lower case owner name - run: | - echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} + + - name: Set build metadata + id: vars env: OWNER: '${{ github.repository_owner }}' + run: | + echo "GIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "GIT_TAG=$(git describe --tags --abbrev=0 --exact-match 2>/dev/null || echo unknown)" >> $GITHUB_OUTPUT + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + echo "REPO_OWNER=${OWNER,,}" >> $GITHUB_OUTPUT - - name: Build Docker images - run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:latest -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:latest ./backend + - name: Build and push Docker images + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --push \ + --build-arg GIT_SHA=${{ steps.vars.outputs.GIT_SHA }} \ + --build-arg GIT_TAG=${{ steps.vars.outputs.GIT_TAG }} \ + --build-arg BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} \ + -t ghcr.io/${{ steps.vars.outputs.REPO_OWNER }}/$IMAGE_NAME:latest \ + -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:latest \ + ./backend + + # Notify on success + # - name: Notify Discord on success + # if: success() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"βœ… Build and push succeeded for backend latest\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} + + # # Notify on failure + # - name: Notify Discord on failure + # if: failure() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"❌ Build or push failed for backend latest\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} diff --git a/.github/workflows/backend-release.yml b/.github/workflows/backend-release.yml index 696f8b5..53cbfc6 100644 --- a/.github/workflows/backend-release.yml +++ b/.github/workflows/backend-release.yml @@ -33,11 +33,43 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: set lower case owner name - run: | - echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} + - name: Set build metadata + id: vars env: - OWNER: "${{ github.repository_owner }}" + OWNER: '${{ github.repository_owner }}' + run: | + echo "GIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "GIT_TAG=$(git describe --tags --abbrev=0 --exact-match 2>/dev/null || echo unknown)" >> $GITHUB_OUTPUT + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + echo "REPO_OWNER=${OWNER,,}" >> $GITHUB_OUTPUT - - name: Build Docker images - run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:${{ github.event.release.tag_name }} -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:${{ github.event.release.tag_name }} ./backend + - name: Build and push Docker images + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --push \ + --build-arg GIT_SHA=${{ steps.vars.outputs.GIT_SHA }} \ + --build-arg GIT_TAG=${{ steps.vars.outputs.GIT_TAG }} \ + --build-arg BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} \ + -t ghcr.io/${{ steps.vars.outputs.REPO_OWNER }}/$IMAGE_NAME:${{ github.event.release.tag_name }} \ + -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:${{ github.event.release.tag_name }} \ + ./backend + + + # # Notify on success + # - name: Notify Discord on success + # if: success() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"βœ… Build and push succeeded for backend release ${{ github.event.release.tag_name }}\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} + + # # Notify on failure + # - name: Notify Discord on failure + # if: failure() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"❌ Build or push failed for backend release ${{ github.event.release.tag_name }}\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} diff --git a/.github/workflows/backend-test.yml b/.github/workflows/backend-test.yml index 0ce9d7c..7d1ad17 100644 --- a/.github/workflows/backend-test.yml +++ b/.github/workflows/backend-test.yml @@ -43,10 +43,10 @@ jobs: working-directory: backend/server env: PGHOST: "127.0.0.1" - PGDATABASE: "database" - PGUSER: "adventure" - PGPASSWORD: "changeme123" - SECRET_KEY: "changeme123" + POSTGRES_DB: "adventurelog" + POSTGRES_USER: "adventurelog" + POSTGRES_PASSWORD: "changeme123" + SECRET_KEY: "secretkey" DJANGO_ADMIN_USERNAME: "admin" DJANGO_ADMIN_PASSWORD: "admin" DJANGO_ADMIN_EMAIL: "admin@example.com" diff --git a/.github/workflows/cdn-beta.yml b/.github/workflows/cdn-beta.yml index d5c2c85..bb882a8 100644 --- a/.github/workflows/cdn-beta.yml +++ b/.github/workflows/cdn-beta.yml @@ -36,11 +36,40 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: set lower case owner name - run: | - echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} + - name: Set build metadata + id: vars env: - OWNER: "${{ github.repository_owner }}" + OWNER: '${{ github.repository_owner }}' + run: | + echo "GIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + echo "REPO_OWNER=${OWNER,,}" >> $GITHUB_OUTPUT - name: Build Docker images - run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:beta -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:beta ./cdn + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --push \ + --build-arg GIT_SHA=${{ steps.vars.outputs.GIT_SHA }} \ + --build-arg BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} \ + -t ghcr.io/${{ steps.vars.outputs.REPO_OWNER }}/$IMAGE_NAME:beta \ + -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:beta \ + ./cdn + + # Notify on success + # - name: Notify Discord on success + # if: success() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"βœ… Build and push succeeded for CDN beta\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} + + # # Notify on failure + # - name: Notify Discord on failure + # if: failure() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"❌ Build or push failed for CDN beta\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} diff --git a/.github/workflows/cdn-latest.yml b/.github/workflows/cdn-latest.yml index 376ede9..f1f5a8c 100644 --- a/.github/workflows/cdn-latest.yml +++ b/.github/workflows/cdn-latest.yml @@ -36,11 +36,40 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: set lower case owner name - run: | - echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} + - name: Set build metadata + id: vars env: - OWNER: "${{ github.repository_owner }}" + OWNER: '${{ github.repository_owner }}' + run: | + echo "GIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + echo "REPO_OWNER=${OWNER,,}" >> $GITHUB_OUTPUT - name: Build Docker images - run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:latest -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:latest ./cdn + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --push \ + --build-arg GIT_SHA=${{ steps.vars.outputs.GIT_SHA }} \ + --build-arg BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} \ + -t ghcr.io/${{ steps.vars.outputs.REPO_OWNER }}/$IMAGE_NAME:latest \ + -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:latest \ + ./cdn + + # Notify on success + # - name: Notify Discord on success + # if: success() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"βœ… Build and push succeeded for CDN latest\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} + + # # Notify on failure + # - name: Notify Discord on failure + # if: failure() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"❌ Build or push failed for CDN latest\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} diff --git a/.github/workflows/cdn-release.yml b/.github/workflows/cdn-release.yml index 2bba9af..4cad516 100644 --- a/.github/workflows/cdn-release.yml +++ b/.github/workflows/cdn-release.yml @@ -33,11 +33,41 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: set lower case owner name - run: | - echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} + - name: Set build metadata + id: vars env: - OWNER: "${{ github.repository_owner }}" + OWNER: '${{ github.repository_owner }}' + run: | + echo "GIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + echo "REPO_OWNER=${OWNER,,}" >> $GITHUB_OUTPUT - name: Build Docker images - run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:${{ github.event.release.tag_name }} -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:${{ github.event.release.tag_name }} ./cdn + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --push \ + --build-arg GIT_SHA=${{ steps.vars.outputs.GIT_SHA }} \ + --build-arg BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} \ + -t ghcr.io/${{ steps.vars.outputs.REPO_OWNER }}/$IMAGE_NAME:${{ github.event.release.tag_name }} \ + -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:${{ github.event.release.tag_name }} \ + ./cdn + + # Notify on success + # - name: Notify Discord on success + # if: success() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"βœ… Build and push succeeded for CDN release ${{ github.event.release.tag_name }}\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} + + # # Notify on failure + # - name: Notify Discord on failure + # if: failure() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"❌ Build or push failed for CDN release ${{ github.event.release.tag_name }}\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} + diff --git a/.github/workflows/frontend-beta.yml b/.github/workflows/frontend-beta.yml index 29597ae..41f8813 100644 --- a/.github/workflows/frontend-beta.yml +++ b/.github/workflows/frontend-beta.yml @@ -36,11 +36,42 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: set lower case owner name - run: | - echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} + - name: Set build metadata + id: vars env: - OWNER: "${{ github.repository_owner }}" + OWNER: '${{ github.repository_owner }}' + run: | + echo "GIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "GIT_TAG=$(git describe --tags --abbrev=0 --exact-match 2>/dev/null || echo unknown)" >> $GITHUB_OUTPUT + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + echo "REPO_OWNER=${OWNER,,}" >> $GITHUB_OUTPUT - - name: Build Docker images - run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:beta -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:beta ./frontend + - name: Build and push Docker images + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --push \ + --build-arg GIT_SHA=${{ steps.vars.outputs.GIT_SHA }} \ + --build-arg GIT_TAG=${{ steps.vars.outputs.GIT_TAG }} \ + --build-arg BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} \ + -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:beta \ + -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:beta \ + ./frontend + + # Notify on success + # - name: Notify Discord on success + # if: success() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"βœ… Build and push succeeded for frontend beta\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} + + # # Notify on failure + # - name: Notify Discord on failure + # if: failure() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"❌ Build or push failed for frontend beta\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} diff --git a/.github/workflows/frontend-latest.yml b/.github/workflows/frontend-latest.yml index a74ae2f..c3ff303 100644 --- a/.github/workflows/frontend-latest.yml +++ b/.github/workflows/frontend-latest.yml @@ -35,12 +35,43 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - - name: set lower case owner name - run: | - echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} + + - name: Set build metadata + id: vars env: OWNER: '${{ github.repository_owner }}' + run: | + echo "GIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "GIT_TAG=$(git describe --tags --abbrev=0 --exact-match 2>/dev/null || echo unknown)" >> $GITHUB_OUTPUT + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + echo "REPO_OWNER=${OWNER,,}" >> $GITHUB_OUTPUT - - name: Build Docker images - run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:latest -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:latest ./frontend + - name: Build and push Docker images + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --push \ + --build-arg GIT_SHA=${{ steps.vars.outputs.GIT_SHA }} \ + --build-arg GIT_TAG=${{ steps.vars.outputs.GIT_TAG }} \ + --build-arg BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} \ + -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:latest \ + -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:latest \ + ./frontend + + # Notify on success + # - name: Notify Discord on success + # if: success() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"βœ… Build and push succeeded for frontend beta\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} + + # # Notify on failure + # - name: Notify Discord on failure + # if: failure() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"❌ Build or push failed for frontend beta\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} diff --git a/.github/workflows/frontend-release.yml b/.github/workflows/frontend-release.yml index bb7fc6b..9414736 100644 --- a/.github/workflows/frontend-release.yml +++ b/.github/workflows/frontend-release.yml @@ -33,11 +33,42 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: set lower case owner name - run: | - echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} + - name: Set build metadata + id: vars env: - OWNER: "${{ github.repository_owner }}" + OWNER: '${{ github.repository_owner }}' + run: | + echo "GIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "GIT_TAG=$(git describe --tags --abbrev=0 --exact-match 2>/dev/null || echo unknown)" >> $GITHUB_OUTPUT + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + echo "REPO_OWNER=${OWNER,,}" >> $GITHUB_OUTPUT - - name: Build Docker images - run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:${{ github.event.release.tag_name }} -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:${{ github.event.release.tag_name }} ./frontend + - name: Build and push Docker images + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --push \ + --build-arg GIT_SHA=${{ steps.vars.outputs.GIT_SHA }} \ + --build-arg GIT_TAG=${{ steps.vars.outputs.GIT_TAG }} \ + --build-arg BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }} \ + -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:${{ github.event.release.tag_name }} \ + -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:${{ github.event.release.tag_name }} \ + ./frontend + + # Notify on success + # - name: Notify Discord on success + # if: success() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"βœ… Build and push succeeded for frontend release ${{ github.event.release.tag_name }}\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} + + # # Notify on failure + # - name: Notify Discord on failure + # if: failure() + # run: | + # curl -H "Content-Type: application/json" \ + # -X POST \ + # -d "{\"username\": \"CI Bot\", \"content\": \"❌ Build or push failed for frontend release ${{ github.event.release.tag_name }}\"}" \ + # ${{ secrets.DISCORD_WEBHOOK_BUILD }} diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..55eb372 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,7 @@ +.env.example +.devcontainer +.github +server/build_files.sh +.gitignore +Dockerfile +.dockerignore \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index b3f41b7..f52de28 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,7 +1,49 @@ -# Use the official Python slim image as the base image -FROM python:3.13-slim +# ------------------- +# Stage 1: Build Stage +# ------------------- +FROM docker.io/python:3.13-alpine AS builder + +# Build-time metadata +ARG GIT_SHA=unknown +ARG GIT_TAG=latest +ARG BUILD_DATE=unknown + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/install + +# Install build dependencies for Alpine +RUN apk add --no-cache \ + build-base \ + libpq \ + libpq-dev \ + gdal \ + gdal-dev \ + geos \ + geos-dev \ + postgresql-dev \ + musl-dev \ + python3-dev \ + linux-headers \ + git + +# Set working directory +WORKDIR /build + +# Copy entire project into /tmp +COPY . /tmp/ + +# Install Python dependencies into /install +RUN pip install --upgrade pip && \ + pip install --target=/install -r /tmp/server/requirements.txt && \ + python3 /tmp/server/manage.py collectstatic --noinput --verbosity 2 + + +# ------------------------- +# Stage 2: Final Production +# ------------------------- +FROM docker.io/python:3.13-alpine AS final -# Metadata labels for the AdventureLog image LABEL maintainer="Sean Morley" \ version="v0.10.0" \ description="AdventureLog β€” the ultimate self-hosted travel companion." \ @@ -12,49 +54,51 @@ LABEL maintainer="Sean Morley" \ org.opencontainers.image.url="https://raw.githubusercontent.com/seanmorley15/AdventureLog/refs/heads/main/brand/banner.png" \ org.opencontainers.image.source="https://github.com/seanmorley15/AdventureLog" \ org.opencontainers.image.vendor="Sean Morley" \ - org.opencontainers.image.created="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ + org.opencontainers.image.version="${GIT_TAG}" \ + org.opencontainers.image.revision="${GIT_SHA}" \ + org.opencontainers.image.created="${BUILD_DATE}" \ org.opencontainers.image.licenses="GPL-3.0" -# Set environment variables -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH="/install:/code" \ + PATH="/install/bin:${PATH}" \ + GDAL_LIBRARY_PATH="/usr/lib/libgdal.so.36.3.10.3" \ + GEOS_LIBRARY_PATH="/usr/lib/libgeos_c.so" -# Set the working directory + + # Copy all from builder, organize in one RUN step +COPY --from=builder /install /install +COPY --from=builder /tmp /tmp + +RUN apk add --no-cache \ + nginx\ + supervisor\ + bash\ + curl\ + libpq\ + gdal\ + gdal-dev\ + geos\ + geos-dev\ + postgresql-client && \ + mv /tmp/server /code && \ + mkdir -p /code/static /code/media && \ + mv /tmp/nginx.conf /etc/nginx/nginx.conf && \ + mkdir -p /etc/supervisor/conf.d/ && \ + mv /tmp/supervisord.conf /etc/supervisor/conf.d/supervisord.conf && \ + mv /tmp/entrypoint.sh /code/entrypoint.sh && \ + chmod +x /code/entrypoint.sh && \ + rm -rf /tmp && \ + addgroup -g 349 adventurelog && \ + adduser -D -u 349 -G adventurelog adventurelog && \ + chown -R 349:349 /code + WORKDIR /code -# Install system dependencies (Nginx included) -RUN apt-get update \ - && apt-get install -y git postgresql-client gdal-bin libgdal-dev nginx supervisor \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Install Python dependencies -COPY ./server/requirements.txt /code/ -RUN pip install --upgrade pip \ - && pip install -r requirements.txt - -# Create necessary directories -RUN mkdir -p /code/static /code/media -# RUN mkdir -p /code/staticfiles /code/media - -# Copy the Django project code into the Docker image -COPY ./server /code/ - -# Copy Nginx configuration -COPY ./nginx.conf /etc/nginx/nginx.conf - -# Copy Supervisor configuration -COPY ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf - -# Collect static files -RUN python3 manage.py collectstatic --noinput --verbosity 2 - -# Set the entrypoint script -COPY ./entrypoint.sh /code/entrypoint.sh -RUN chmod +x /code/entrypoint.sh - -# Expose ports for NGINX and Gunicorn EXPOSE 80 8000 -# Command to start Supervisor (which starts Nginx and Gunicorn) -CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] + +HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=5 \ + CMD curl --fail --silent --show-error http://localhost:80/ || exit 1 \ No newline at end of file diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index 1031cb1..74663e9 100644 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Function to check PostgreSQL availability # Helper to get the first non-empty environment variable @@ -13,22 +13,27 @@ get_env() { } check_postgres() { + if ! command -v psql > /dev/null 2>&1; then + >&2 echo "psql command not found β€” PostgreSQL client is not installed" + return 1 + fi + local db_host local db_user local db_name local db_pass - db_host=$(get_env PGHOST) - db_user=$(get_env PGUSER POSTGRES_USER) - db_name=$(get_env PGDATABASE POSTGRES_DB) - db_pass=$(get_env PGPASSWORD POSTGRES_PASSWORD) + db_host="$(get_env PGHOST)" + db_user="$(get_env PGUSER POSTGRES_USER)" + db_name="$(get_env PGDATABASE POSTGRES_DB)" + db_pass="$(get_env PGPASSWORD POSTGRES_PASSWORD)" PGPASSWORD="$db_pass" psql -h "$db_host" -U "$db_user" -d "$db_name" -c '\q' >/dev/null 2>&1 } # Wait for PostgreSQL to become available -until check_postgres; do +until check_postgres; do >&2 echo "PostgreSQL is unavailable - sleeping" sleep 1 done @@ -73,8 +78,6 @@ else: EOF fi - -# Sync the countries and world travel regions # Sync the countries and world travel regions python manage.py download-countries if [ $? -eq 137 ]; then diff --git a/backend/server/.env.example b/backend/server/.env.example deleted file mode 100644 index 2c93208..0000000 --- a/backend/server/.env.example +++ /dev/null @@ -1,36 +0,0 @@ -PGHOST='' -PGDATABASE='' -PGUSER='' -PGPASSWORD='' - -SECRET_KEY='pleasechangethisbecauseifyoudontitwillbeverybadandyouwillgethackedinlessthanaminuteguaranteed' - -PUBLIC_URL='http://127.0.0.1:8000' - -DEBUG=True - -FRONTEND_URL='http://localhost:3000' - -EMAIL_BACKEND='console' - -# EMAIL_BACKEND='email' -# EMAIL_HOST='smtp.gmail.com' -# EMAIL_USE_TLS=False -# EMAIL_PORT=587 -# EMAIL_USE_SSL=True -# EMAIL_HOST_USER='user' -# EMAIL_HOST_PASSWORD='password' -# DEFAULT_FROM_EMAIL='user@example.com' - -# GOOGLE_MAPS_API_KEY='key' - - -# ------------------- # -# For Developers to start a Demo Database -# docker run --name adventurelog-development -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=admin -e POSTGRES_DB=adventurelog -p 5432:5432 -d postgis/postgis:15-3.3 - -# PGHOST='localhost' -# PGDATABASE='adventurelog' -# PGUSER='admin' -# PGPASSWORD='admin' -# ------------------- # \ No newline at end of file diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index 073fd77..49685b1 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -20,12 +20,19 @@ load_dotenv() BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +# GDAL library version path +GDAL_LIBRARY_PATH = getenv('GDAL_LIBRARY_PATH', None) + +# GEOS library version path +GEOS_LIBRARY_PATH = getenv('GEOS_LIBRARY_PATH', None) + # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = getenv('SECRET_KEY') + # SECURITY WARNING: don't run with debug turned on in production! DEBUG = getenv('DEBUG', 'true').lower() == 'true' diff --git a/backend/supervisord.conf b/backend/supervisord.conf index e7adec7..b991f93 100644 --- a/backend/supervisord.conf +++ b/backend/supervisord.conf @@ -3,12 +3,14 @@ nodaemon=true [program:nginx] command=/usr/sbin/nginx -g "daemon off;" +user=nginx autorestart=true stdout_logfile=/dev/stdout stderr_logfile=/dev/stderr [program:gunicorn] command=/code/entrypoint.sh +user=adventurelog autorestart=true stdout_logfile=/dev/stdout stderr_logfile=/dev/stderr diff --git a/backup.sh b/backup.sh index 69d5b12..fcb63b0 100644 --- a/backup.sh +++ b/backup.sh @@ -1,7 +1,7 @@ # This script will create a backup of the adventurelog_media volume and store it in the current directory as adventurelog-backup.tar.gz docker run --rm \ - -v adventurelog_adventurelog_media:/backup-volume \ + -v adventurelog_media:/backup-volume \ -v "$(pwd)":/backup \ busybox \ tar -zcvf /backup/adventurelog-backup.tar.gz /backup-volume \ No newline at end of file diff --git a/cdn/Dockerfile b/cdn/Dockerfile index f47e79a..3904322 100644 --- a/cdn/Dockerfile +++ b/cdn/Dockerfile @@ -1,36 +1,46 @@ -# Use an official Python image as a base -FROM python:3.11-slim +# --------------------- +# Stage 1: Data builder +# --------------------- +FROM docker.io/python:3.11-slim AS builder -# Set the working directory +ARG GIT_SHA=unknown +ARG BUILD_DATE=unknown + +# Set workdir and environment WORKDIR /app +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 # Install required Python packages RUN pip install --no-cache-dir requests osm2geojson -# Copy the script into the container +# Copy and run data generation script COPY main.py /app/main.py - -# Run the script to generate the data folder and GeoJSON files (this runs inside the container) RUN python -u /app/main.py -# Install Nginx -RUN apt update && apt install -y nginx && rm -rf /var/lib/apt/lists/* +# ------------------------ +# Stage 2: Final Nginx-only +# ------------------------ +FROM docker.io/nginx:alpine -# Copy the entire generated data folder to the Nginx serving directory -RUN mkdir -p /var/www/html/data && cp -r /app/data/* /var/www/html/data/ +ARG GIT_SHA=unknown +ARG BUILD_DATE=unknown -# Copy Nginx configuration +# Optional metadata +LABEL org.opencontainers.image.revision="${GIT_SHA}" \ + org.opencontainers.image.created="${BUILD_DATE}" + +# Create destination directory +RUN mkdir -p /var/www/html/data + +# Copy generated data and static files +COPY --from=builder /app/data /var/www/html/data +COPY index.html /usr/share/nginx/html/index.html COPY nginx.conf /etc/nginx/nginx.conf -# Copy the index.html file to the Nginx serving directory -COPY index.html /usr/share/nginx/html/index.html - -# Expose port 80 for Nginx -EXPOSE 80 - -# Copy the entrypoint script into the container +# Add entrypoint COPY entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh -# Set the entrypoint script as the default command +EXPOSE 80 ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/deploy.sh b/deploy.sh index e2372e7..114e32d 100644 --- a/deploy.sh +++ b/deploy.sh @@ -5,4 +5,4 @@ docker compose pull echo "Stating containers" docker compose up -d echo "All set!" -docker logs adventurelog-backend --follow \ No newline at end of file +docker compose logs --follow \ No newline at end of file diff --git a/docker-compose-traefik.yaml b/docker-compose-traefik.yaml deleted file mode 100644 index 16a1805..0000000 --- a/docker-compose-traefik.yaml +++ /dev/null @@ -1,78 +0,0 @@ -version: "3.9" - -services: - traefik: - image: traefik:v2.11 - command: - - "--api.insecure=true" # Enable Traefik dashboard (remove in production) - - "--providers.docker=true" - - "--entrypoints.web.address=:80" - - "--entrypoints.websecure.address=:443" - - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web" - - "--certificatesresolvers.letsencrypt.acme.email=your-email@example.com" # Replace with your email - - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" - ports: - - "80:80" - - "443:443" - volumes: - - "/var/run/docker.sock:/var/run/docker.sock:ro" - - "traefik-letsencrypt:/letsencrypt" - labels: - - "traefik.enable=true" - - db: - image: postgis/postgis:15-3.3 - restart: unless-stopped - environment: - POSTGRES_DB: database - POSTGRES_USER: adventure - POSTGRES_PASSWORD: your_postgres_password # Replace with the actual password - volumes: - - postgres-data:/var/lib/postgresql/data/ - - web: - image: ghcr.io/seanmorley15/adventurelog-frontend:latest - restart: unless-stopped - environment: - PUBLIC_SERVER_URL: "http://server:8000" - BODY_SIZE_LIMIT: "100000" - labels: - - "traefik.enable=true" - - "traefik.http.routers.adventurelogweb.entrypoints=websecure" - - "traefik.http.routers.adventurelogweb.rule=Host(`yourdomain.com`) && !(PathPrefix(`/media`) || PathPrefix(`/admin`) || PathPrefix(`/static`) || PathPrefix(`/accounts`))" # Replace with your domain - - "traefik.http.routers.adventurelogweb.tls=true" - - "traefik.http.routers.adventurelogweb.tls.certresolver=letsencrypt" - depends_on: - - server - - server: - image: ghcr.io/seanmorley15/adventurelog-backend:latest - restart: unless-stopped - environment: - PGHOST: "db" - PGDATABASE: "database" - PGUSER: "adventure" - PGPASSWORD: your_postgres_password # Replace with the actual password - SECRET_KEY: your_secret_key # Replace with the actual secret key - DJANGO_ADMIN_USERNAME: "admin" - DJANGO_ADMIN_PASSWORD: your_admin_password # Replace with the actual admin password - DJANGO_ADMIN_EMAIL: "adventurelog-admin@yourdomain.com" # Replace with your email - PUBLIC_URL: "https://yourdomain.com" # Replace with your domain - CSRF_TRUSTED_ORIGINS: "https://yourdomain.com" # Replace with your domain - DEBUG: "false" - FRONTEND_URL: "https://yourdomain.com" # Replace with your domain - volumes: - - adventurelog-media:/code/media - labels: - - "traefik.enable=true" - - "traefik.http.routers.adventurelogserver.entrypoints=websecure" - - "traefik.http.routers.adventurelogserver.rule=Host(`yourdomain.com`) && (PathPrefix(`/media`) || PathPrefix(`/admin`) || PathPrefix(`/static`) || PathPrefix(`/accounts`))" # Replace with your domain - - "traefik.http.routers.adventurelogserver.tls=true" - - "traefik.http.routers.adventurelogserver.tls.certresolver=letsencrypt" - depends_on: - - db - -volumes: - postgres-data: - adventurelog-media: - traefik-letsencrypt: diff --git a/docker-compose.yml b/docker-compose.yml index 034ec06..15dc46b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,36 +1,128 @@ +name: adventurelog + +# Common DB variables used by both backend and db +x-env-common: &env-common + POSTGRES_DB: adventurelog # If you change this, it needs to also be changed in the backend + POSTGRES_USER: adventurelog # If you change this, it needs to also be changed in the backend + POSTGRES_PASSWORD: changeme123 # If you change this, it needs to also be changed in the backend + +# Environment variables for the backend service +x-env-backend: &env-backend + # Database Configuration + PGHOST: db # Supposed to match the host of the postgis database + <<: *env-common + # AdventureLog base configuration + SECRET_KEY: changeme123 # Replace with the actual secret key + DJANGO_ADMIN_USERNAME: admin # Replace with the actual admin username + DJANGO_ADMIN_PASSWORD: admin # Replace with the actual admin password + DJANGO_ADMIN_EMAIL: admin@example.com # Replace with the actual admin email + BACKEND_PORT: 8016 # Don't forget to update CSRF_TRUSTED_ORIGINS and PUBLIC_URL in backend, and PUBLIC_SERVER_URL in frontend if you make changes to this. + PUBLIC_URL: http://localhost:8016 # Replace with your domain where the backend is accessible publicly, used for the creation of image URLs + CSRF_TRUSTED_ORIGINS: http://localhost:8016,http://localhost:8015 # Replace with your domain, if applicable + FRONTEND_URL: http://localhost:8015 # Replace with your domain where the frontend is accessible, used for email generation. + + # Optional integrations (commented out by default) + # DISABLE_REGISTRATION: False + # DISABLE_REGISTRATION_MESSAGE: Registration is disabled for this instance of AdventureLog. + # EMAIL_BACKEND: email + # EMAIL_HOST: smtp.mail.com + # EMAIL_USE_TLS: False + # EMAIL_PORT: 587 + # EMAIL_USE_SSL: True + # EMAIL_HOST_USER: user + # EMAIL_HOST_PASSWORD: password + # DEFAULT_FROM_EMAIL: ${COMPOSE_PROJECT_NAME@example.com + # GOOGLE_MAPS_API_KEY: your_google_maps_api_key + # PUBLIC_UMAMI_SRC: https://cloud.umami.is/script.js # If you are using the hosted version of Umami + # PUBLIC_UMAMI_WEBSITE_ID: + + # Extra configuration + DEBUG: false # Use to enable debugging + +# Environment variables for the frontend service +x-env-frontend: &env-frontend + FRONTEND_PORT: 8015 # Don't forget to update CSRF_TRUSTED_ORIGINS in backend and FRONTEND_URL in backend and ORIGIN in frontend if you make changes to this. + PUBLIC_SERVER_URL: http://backend:8000 # PLEASE DON'T CHANGE :) - Should be the service name of the backend with port 8000, even if you change the port in the backend service. Only change if you are using a custom more complex setup. + ORIGIN: http://localhost:8015 + BODY_SIZE_LIMIT: Infinity + services: - web: - #build: ./frontend/ - image: ghcr.io/seanmorley15/adventurelog-frontend:latest - container_name: adventurelog-frontend - restart: unless-stopped - env_file: .env - ports: - - "${FRONTEND_PORT:-8015}:3000" - depends_on: - - server - db: - image: postgis/postgis:16-3.5 - container_name: adventurelog-db + image: docker.io/postgis/postgis:16-3.5 restart: unless-stopped - env_file: .env + #env_file: .env # Disabled for security reasons, as all containers have access to these env vars. + environment: # Anything defined here (including the env-backend) overrides what's in the env_file. + <<: *env-common volumes: - - postgres_data:/var/lib/postgresql/data/ + - data:/var/lib/postgresql/data/ + networks: + - default + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"] + interval: 20s + timeout: 10s + retries: 5 + start_period: 10s - server: - #build: ./backend/ - image: ghcr.io/seanmorley15/adventurelog-backend:latest - container_name: adventurelog-backend + frontend: + #build: ./frontend/ + image: localhost/adventurelog-frontend:testing restart: unless-stopped - env_file: .env + # env_file: .env # Disabled for security reasons, as all containers have access to these env vars. + environment: # Anything defined here (including the env-backend) overrides what's in the env_file. + <<: *env-frontend ports: - - "${BACKEND_PORT:-8016}:80" + - "8015:3000" + networks: + - default depends_on: - - db + backend: + condition: service_healthy + healthcheck: + test: ["CMD", "/nodejs/bin/node", "-e", "require('net').connect(3000).on('connect', () => process.exit(0)).on('error', () => process.exit(1))"] + interval: 10s + timeout: 3s + retries: 3 + start_period: 10s + # labels: + # Traefik example: Refer to https://doc.traefik.io/traefik/user-guides/docker-compose/basic-example/ for help on deploying Traefik. + # - "traefik.enable=true" + # - "traefik.http.routers.adventurelogweb.entrypoints=websecure" + # - "traefik.http.routers.adventurelogweb.rule=Host(`yourdomain.com`) && !(PathPrefix(`/media`) || PathPrefix(`/admin`) || PathPrefix(`/static`) || PathPrefix(`/accounts`))" # Replace with your domain + # - "traefik.http.routers.adventurelogweb.tls=true" + # - "traefik.http.routers.adventurelogweb.tls.certresolver=letsencrypt" + + + backend: + #build: ./backend/ + image: localhost/adventurelog-backend:testing + restart: unless-stopped + # env_file: .env # Disabled for security reasons, as all containers have access to these env vars. + environment: # Anything defined here (including the env-backend) overrides what's in the env_file. + <<: *env-backend + networks: + - default + ports: + - "8016:80" + depends_on: + db: + condition: service_healthy volumes: - - adventurelog_media:/code/media/ + - media:/code/media/ + healthcheck: + test: ["CMD", "curl", "--fail", "--silent", "--show-error", "http://localhost:80/"] + interval: 30s + timeout: 10s + start_period: 20s + retries: 5 + # labels: + # Traefik example: Refer to https://doc.traefik.io/traefik/user-guides/docker-compose/basic-example/ for help on deploying Traefik. + # - "traefik.enable=true" + # - "traefik.http.routers.adventurelogserver.entrypoints=websecure" + # - "traefik.http.routers.adventurelogserver.rule=Host(`yourdomain.com`) && (PathPrefix(`/media`) || PathPrefix(`/admin`) || PathPrefix(`/static`) || PathPrefix(`/accounts`))" # Replace with your domain + # - "traefik.http.routers.adventurelogserver.tls=true" + # - "traefik.http.routers.adventurelogserver.tls.certresolver=letsencrypt" volumes: - postgres_data: - adventurelog_media: + data: + media: diff --git a/documentation/docs/install/docker.md b/documentation/docs/install/docker.md index b698502..45ab745 100644 --- a/documentation/docs/install/docker.md +++ b/documentation/docs/install/docker.md @@ -45,8 +45,8 @@ The `.env` file contains all the configuration settings for your AdventureLog in | Name | Required | Description | Default Value | | ------------------- | -------- | --------------------- | ------------- | | `PGHOST` | Yes | Internal DB hostname. | `db` | -| `POSTGRES_DB` | Yes | DB name. | `database` | -| `POSTGRES_USER` | Yes | DB user. | `adventure` | +| `POSTGRES_DB` | Yes | DB name. | `adventurelog` | +| `POSTGRES_USER` | Yes | DB user. | `adventurelog` | | `POSTGRES_PASSWORD` | Yes | DB password. | `changeme123` | ### πŸ”’ Backend (server) diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..2ebee79 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,6 @@ +.env.example +.prettierignore +.prettierrc +.gitignore +Dockerfile +.dockerignore \ No newline at end of file diff --git a/frontend/.env.example b/frontend/.env.example deleted file mode 100644 index 88f6c69..0000000 --- a/frontend/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -PUBLIC_SERVER_URL=http://127.0.0.1:8000 -BODY_SIZE_LIMIT=Infinity - - -# OPTIONAL VARIABLES FOR UMAMI ANALYTICS -PUBLIC_UMAMI_SRC= -PUBLIC_UMAMI_WEBSITE_ID= \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 6a8ceb3..5402575 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,49 +1,65 @@ -# Use this image as the platform to build the app -FROM node:22-alpine AS external-website +# ---------------------- +# Stage 1: Build the app +# ---------------------- +FROM docker.io/node:22-alpine AS builder + +ARG GIT_SHA=unknown +ARG GIT_TAG=latest +ARG BUILD_DATE=unknown + +# Set working directory +WORKDIR /app + +COPY . ./ +# Install pnpm globally +RUN npm install -g pnpm && \ + pnpm install --frozen-lockfile && \ + rm -f .env && \ + pnpm run build && \ + mv /app/package.json /app/build/package.json;\ + ls -lah .; \ + ls -lah ./build + +# ---------------------------- +# Stage 2: Install Production Dependencies +# ---------------------------- +FROM docker.io/node:22-alpine AS deps + +WORKDIR /app +COPY --from=builder /app/build/package.json ./ +RUN npm install --omit=dev && \ + addgroup -g 349 adventurelog && \ + adduser -D -u 349 -G adventurelog adventurelog && \ + chown -R 349:349 /app + +# ---------------------------- +# Stage 3: Distroless Runtime +# ---------------------------- +FROM gcr.io/distroless/nodejs20 -# Metadata labels for the AdventureLog image LABEL maintainer="Sean Morley" \ - version="v0.10.0" \ - description="AdventureLog β€” the ultimate self-hosted travel companion." \ org.opencontainers.image.title="AdventureLog" \ org.opencontainers.image.description="AdventureLog is a self-hosted travel companion that helps you plan, track, and share your adventures." \ - org.opencontainers.image.version="v0.10.0" \ org.opencontainers.image.authors="Sean Morley" \ org.opencontainers.image.url="https://raw.githubusercontent.com/seanmorley15/AdventureLog/refs/heads/main/brand/banner.png" \ org.opencontainers.image.source="https://github.com/seanmorley15/AdventureLog" \ org.opencontainers.image.vendor="Sean Morley" \ - org.opencontainers.image.created="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ + org.opencontainers.image.version="${GIT_TAG}" \ + org.opencontainers.image.revision="${GIT_SHA}" \ + org.opencontainers.image.created="${BUILD_DATE}" \ org.opencontainers.image.licenses="GPL-3.0" -# The WORKDIR instruction sets the working directory for everything that will happen next +USER 349:349 WORKDIR /app -# Install pnpm globally first -RUN npm install -g pnpm +COPY --from=builder /etc/passwd /etc/passwd +COPY --from=builder /etc/group /etc/group +COPY --from=builder /app/build ./ +COPY --from=deps /app/node_modules ./node_modules -# Copy package files first for better Docker layer caching -COPY package.json pnpm-lock.yaml* ./ +CMD ["index.js"] -# Clean install all node modules using pnpm with frozen lockfile -RUN pnpm install --frozen-lockfile - -# Copy the rest of the application files -COPY . . - -# Remove the development .env file if present -RUN rm -f .env - -# Build SvelteKit app -RUN pnpm run build - -# Make startup script executable -RUN chmod +x ./startup.sh - -# Change to non-root user for security -USER node:node - -# Expose the port that the app is listening on EXPOSE 3000 -# Run startup.sh instead of the default command -CMD ["./startup.sh"] \ No newline at end of file +HEALTHCHECK --interval=10s --timeout=3s --retries=3 \ + CMD /nodejs/bin/node -e "require('net').connect(3000).on('connect', () => process.exit(0)).on('error', () => process.exit(1))" \ No newline at end of file diff --git a/frontend/startup.sh b/frontend/startup.sh deleted file mode 100644 index 67dbb1e..0000000 --- a/frontend/startup.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -echo "The origin to be set is: $ORIGIN" -# Start the application -ORIGIN=$ORIGIN exec node build diff --git a/kustomization.yml b/kustomization.yml index da7c6e7..068f7d5 100644 --- a/kustomization.yml +++ b/kustomization.yml @@ -1,78 +1,63 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: example-name + name: adventurelog labels: - app: adventure + app: adventurelog spec: replicas: 1 selector: matchLabels: - app: adventure + app: adventurelog template: metadata: - name: adventure labels: - app: adventure + app: adventurelog spec: volumes: - - name: adventure-journal + - name: media persistentVolumeClaim: - claimName: adventure-journal-pvc - - name: adventure-journal-db + claimName: media-pvc + - name: data persistentVolumeClaim: - claimName: adventure-journal-db-pvc + claimName: data-pvc containers: - - name: adventure-frontend + - name: frontend image: ghcr.io/seanmorley15/adventurelog-frontend:latest - imagePullPolicy: IfNotPresent ports: - containerPort: 3000 env: + - name: FRONTEND_PORT + value: "8015" - name: PUBLIC_SERVER_URL - value: "http://internally-and-externally.reachable.io:80" + value: "http://backend:8000" - name: ORIGIN - value: "http://url-typed-into-browser.io:80" + value: "http://localhost:8015" - name: BODY_SIZE_LIMIT value: "Infinity" + readinessProbe: + exec: + command: ["/nodejs/bin/node", "-e", "require('net').connect(3000).on('connect', () => process.exit(0)).on('error', () => process.exit(1))"] + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 - - name: adventure-db - image: postgis/postgis:15-3.3 - imagePullPolicy: IfNotPresent - ports: - - containerPort: 5432 - volumeMounts: - - name: adventure-journal-db - mountPath: /var/lib/postgresql/data - env: - - name: POSTGRES_DB - value: database - - name: PGDATA - value: /var/lib/postgresql/data/pgdata/subdir - - name: POSTGRES_USER - value: adventure - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: adventurelog-secret - key: adventure-postgres-password - - - name: adventure-backend + - name: backend image: ghcr.io/seanmorley15/adventurelog-backend:latest - imagePullPolicy: IfNotPresent ports: - containerPort: 80 - containerPort: 8000 volumeMounts: - - name: adventure-journal + - name: media mountPath: /code/media env: - name: PGHOST - value: "adventure-db-svc" - - name: PGDATABASE - value: "database" - - name: PGUSER - value: "adventure" + value: "db" + - name: POSTGRES_DB + value: "adventurelog" + - name: POSTGRES_USER + value: "adventurelog" - name: PGPASSWORD valueFrom: secretKeyRef: @@ -82,58 +67,101 @@ spec: valueFrom: secretKeyRef: name: adventurelog-secret - key: adventure-postgres-password - - name: PUBLIC_URL - value: "http://internally-and-externally.reachable.io:80" # Match the outward port, used for the creation of image urls - - name: FRONTEND_URL - value: "http://url-typed-into-browser.io:80" - - name: CSRF_TRUSTED_ORIGINS - value: "http://url-typed-into-browser.io:80, http://internally-and-externally.reachable.io:80" + key: secret-key - name: DJANGO_ADMIN_USERNAME value: "admin" - name: DJANGO_ADMIN_PASSWORD value: "admin" - name: DJANGO_ADMIN_EMAIL value: "admin@example.com" + - name: BACKEND_PORT + value: "8016" + - name: PUBLIC_URL + value: "http://localhost:8016" + - name: FRONTEND_URL + value: "http://localhost:8015" + - name: CSRF_TRUSTED_ORIGINS + value: "http://localhost:8016,http://localhost:8015" - name: DEBUG - value: "True" + value: "false" + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 20 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 5 + + - name: db + image: postgis/postgis:16-3.5 + ports: + - containerPort: 5432 + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + env: + - name: POSTGRES_DB + value: "adventurelog" + - name: POSTGRES_USER + value: "adventurelog" + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: adventurelog-secret + key: adventure-postgres-password + readinessProbe: + exec: + command: ["pg_isready", "-U", "adventurelog"] + initialDelaySeconds: 10 + periodSeconds: 20 + timeoutSeconds: 10 + failureThreshold: 5 restartPolicy: Always --- apiVersion: v1 kind: Service metadata: - name: adventure-db-svc + name: backend spec: selector: - app: adventure + app: adventurelog ports: - - name: db - protocol: TCP - port: 5432 - targetPort: 5432 + - name: http + port: 80 + targetPort: 80 + - name: backend + port: 8000 + targetPort: 8000 --- apiVersion: v1 kind: Service metadata: - name: server + name: frontend spec: selector: - app: adventure + app: adventurelog ports: - name: http - protocol: TCP - port: 80 - targetPort: 80 - - name: base - protocol: TCP - port: 8000 - targetPort: 8000 + port: 3000 + targetPort: 3000 +--- +apiVersion: v1 +kind: Service +metadata: + name: db +spec: + selector: + app: adventurelog + ports: + - name: postgres + port: 5432 + targetPort: 5432 --- -# If you aren't automatically provisioning PVCs (i.e. with Longhorn, you'll need to also create the PV's) apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: adventure-journal-pvc + name: media-pvc spec: accessModes: - ReadWriteOnce @@ -144,10 +172,19 @@ spec: apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: adventure-journal-db-pvc + name: data-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi +--- +apiVersion: v1 +kind: Secret +metadata: + name: adventurelog-secret +type: Opaque +stringData: + adventure-postgres-password: changeme123 + secret-key: changeme123 From 4e96e529f4f764bcba80c46024509a83ed45427b Mon Sep 17 00:00:00 2001 From: Sean Morley <98704938+seanmorley15@users.noreply.github.com> Date: Wed, 9 Jul 2025 23:03:48 -0400 Subject: [PATCH 3/5] fix(adventure): enhance collection ownership validation in AdventureSerializer (#723) --- backend/server/adventures/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index 3023e04..622e2eb 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -128,7 +128,7 @@ class AdventureSerializer(CustomModelSerializer): user = self.context['request'].user for collection in collections: - if collection.user_id != user: + if collection.user_id != user and not collection.shared_with.filter(id=user.id).exists(): raise serializers.ValidationError( f"Collection '{collection.name}' does not belong to the current user." ) From bc9a9a631dc8fd32f2b4ce1f18edca041f642cb8 Mon Sep 17 00:00:00 2001 From: Corbin Whitton Date: Sun, 20 Jul 2025 21:20:08 -0600 Subject: [PATCH 4/5] feat(devsecops): updated installer to utilize new docker-compose format --- .env.example | 8 +- .gitignore | 1 + backend/entrypoint.sh | 13 + docker-compose.yml | 29 +- install_adventurelog.sh | 652 ++++++++++++++++++++++++++-------------- 5 files changed, 467 insertions(+), 236 deletions(-) diff --git a/.env.example b/.env.example index f8302ac..402a19d 100644 --- a/.env.example +++ b/.env.example @@ -6,14 +6,17 @@ BODY_SIZE_LIMIT=Infinity # 🐘 PostgreSQL Database PGHOST=db # Supposed to match the host of the postgis database -POSTGRES_DB=adventureloga +POSTGRES_DB=adventurelog POSTGRES_USER=adventurelog POSTGRES_PASSWORD=changeme123 +# POSTGRES_PASSWORD_FILE=/run/secrets/postgres-password # Uncomment this block if you'd rather use docker secrets (more-secure) # πŸ”’ Django Backend SECRET_KEY=changeme123 # Replace with the actual secret key +# SECRET_KEY_FILE=/run/secrets/secret-key # Uncomment this block if you'd rather use docker secrets (more-secure) DJANGO_ADMIN_USERNAME=admin # Replace with the actual admin username -DJANGO_ADMIN_PASSWORD=admin # Replace with the actual admin password +DJANGO_ADMIN_PASSWORD=admin # Replace with the actual admin password +# DJANGO_ADMIN_PASSWORD_FILE=/run/secrets/django-admin-password # Uncomment this block if you'd rather use docker secrets (more-secure) DJANGO_ADMIN_EMAIL=admin@example.com # Replace with the actual admin email BACKEND_PORT=8016 # Don't forget to update CSRF_TRUSTED_ORIGINS and PUBLIC_URL in backend, and PUBLIC_SERVER_URL in frontend if you make changes to this. PUBLIC_URL=http://localhost:8016 # Replace with your domain where the backend is accessible publicly, used for the creation of image urls @@ -33,6 +36,7 @@ FRONTEND_URL=http://localhost:8015 # Replace with your domain where the frontend # EMAIL_USE_SSL=True # EMAIL_HOST_USER=user # EMAIL_HOST_PASSWORD=password +# # EMAIL_HOST_PASSWORD_FILE=/run/secrets/email-host-password # Uncomment this block if you'd rather use docker secrets (more-secure) # DEFAULT_FROM_EMAIL=adventurelog@example.com ## Google Maps: https://adventurelog.app/docs/configuration/google_maps_integration.html diff --git a/.gitignore b/.gitignore index 090b681..648a104 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .vscode/settings.json .pnpm-store/ .env +.secrets diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index 74663e9..233c8a8 100644 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -1,5 +1,18 @@ #!/usr/bin/env bash +# Load environment variables from *_FILE if set +for file_var in $(env | grep -E '^[A-Z0-9_]+_FILE=' | sed -E 's/=.*//'); do + var_name="${file_var%_FILE}" + file_path="${!file_var}" + + if [ -r "$file_path" ]; then + export "$var_name"="$(< "$file_path")" + unset "$file_var" + else + >&2 echo "Warning: Cannot read file for $file_var: $file_path" + fi +done + # Function to check PostgreSQL availability # Helper to get the first non-empty environment variable get_env() { diff --git a/docker-compose.yml b/docker-compose.yml index 15dc46b..3c99b63 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,9 +2,10 @@ name: adventurelog # Common DB variables used by both backend and db x-env-common: &env-common - POSTGRES_DB: adventurelog # If you change this, it needs to also be changed in the backend - POSTGRES_USER: adventurelog # If you change this, it needs to also be changed in the backend - POSTGRES_PASSWORD: changeme123 # If you change this, it needs to also be changed in the backend + POSTGRES_DB: adventurelog + POSTGRES_USER: adventurelog + POSTGRES_PASSWORD: changeme123 + # POSTGRES_PASSWORD_FILE: /run/secrets/postgres-password # Uncomment this block if you'd rather use docker secrets (more-secure) # Environment variables for the backend service x-env-backend: &env-backend @@ -13,8 +14,10 @@ x-env-backend: &env-backend <<: *env-common # AdventureLog base configuration SECRET_KEY: changeme123 # Replace with the actual secret key + # SECRET_KEY_FILE: /run/secrets/secret-key # Uncomment this block if you'd rather use docker secrets (more-secure) DJANGO_ADMIN_USERNAME: admin # Replace with the actual admin username DJANGO_ADMIN_PASSWORD: admin # Replace with the actual admin password + # DJANGO_ADMIN_PASSWORD_FILE: /run/secrets/django-admin-password # Uncomment this block if you'd rather use docker secrets (more-secure) DJANGO_ADMIN_EMAIL: admin@example.com # Replace with the actual admin email BACKEND_PORT: 8016 # Don't forget to update CSRF_TRUSTED_ORIGINS and PUBLIC_URL in backend, and PUBLIC_SERVER_URL in frontend if you make changes to this. PUBLIC_URL: http://localhost:8016 # Replace with your domain where the backend is accessible publicly, used for the creation of image URLs @@ -31,7 +34,8 @@ x-env-backend: &env-backend # EMAIL_USE_SSL: True # EMAIL_HOST_USER: user # EMAIL_HOST_PASSWORD: password - # DEFAULT_FROM_EMAIL: ${COMPOSE_PROJECT_NAME@example.com + # # EMAIL_HOST_PASSWORD_FILE: /run/secrets/email-host-password # Uncomment this block if you'd rather use docker secrets (more-secure) + # DEFAULT_FROM_EMAIL: adventurelog@example.com # GOOGLE_MAPS_API_KEY: your_google_maps_api_key # PUBLIC_UMAMI_SRC: https://cloud.umami.is/script.js # If you are using the hosted version of Umami # PUBLIC_UMAMI_WEBSITE_ID: @@ -55,6 +59,8 @@ services: <<: *env-common volumes: - data:/var/lib/postgresql/data/ + # secrets: # Uncomment this block if you'd rather use docker secrets (more-secure) + # - postgres-password networks: - default healthcheck: @@ -109,6 +115,11 @@ services: condition: service_healthy volumes: - media:/code/media/ + # secrets: # Uncomment this block if you'd rather use docker secrets (more-secure) + # - postgres-password + # - secret-key + # # - email-host-password + # - django-admin-password healthcheck: test: ["CMD", "curl", "--fail", "--silent", "--show-error", "http://localhost:80/"] interval: 30s @@ -126,3 +137,13 @@ services: volumes: data: media: + +# secrets: # Uncomment this block if you'd rather use docker secrets (more-secure) +# postgres-password: +# file: .secrets/postgres-password.secret +# secret-key: +# file: .secrets/secret-key.secret +# # email-host-password: # Uncomment this if you want to use secrets with the email host password +# # file: .secrets/email-host-password.secret +# django-admin-password: +# file: .secrets/django-admin-password.secret diff --git a/install_adventurelog.sh b/install_adventurelog.sh index c5caf23..dcef475 100755 --- a/install_adventurelog.sh +++ b/install_adventurelog.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -euo pipefail # ============================================================================= @@ -20,6 +20,16 @@ declare -g ADMIN_PASSWORD="" declare -g DB_PASSWORD="" declare -g FRONTEND_PORT="" declare -g BACKEND_PORT="" +declare -g TRAEFIK_ENABLED="" +declare -g USE_EMAIL="" +declare -g USE_DOCKER_SECRETS="" +declare -g EMAIL_HOST="" +declare -g EMAIL_USE_TLS="" +declare -g EMAIL_PORT="" +declare -g EMAIL_USE_SSL="" +declare -g EMAIL_HOST_USER="" +declare -g EMAIL_HOST_PASSWORD="" +declare -g FINISHED_CORRECTLY="true" # Color codes for beautiful output readonly RED='\033[0;31m' @@ -56,6 +66,11 @@ log_header() { echo -e "${PURPLE}$1${NC}" } +# pause(){ +# read -s -n 1 -p "Press any key to continue . . ." +# echo "" +# } + print_banner() { cat << 'EOF' ╔═════════════════════════════════════════════════════════════════════════╗ @@ -183,18 +198,11 @@ check_dependencies() { log_info "Checking system dependencies..." local missing_deps=() - - if ! command -v curl &>/dev/null; then - missing_deps+=("curl") - fi - - if ! command -v docker &>/dev/null; then - missing_deps+=("docker") - fi - - if ! command -v docker-compose &>/dev/null && ! docker compose version &>/dev/null; then - missing_deps+=("docker-compose") - fi + for bin in curl docker docker-compose tr sed; do + if ! command -v "$bin" >/dev/null 2>&1; then + missing_deps+=("$bin") + fi + done if [ ${#missing_deps[@]} -ne 0 ]; then log_error "Missing dependencies: ${missing_deps[*]}" @@ -205,6 +213,12 @@ check_dependencies() { "curl") echo " β€’ curl: apt-get install curl (Ubuntu/Debian) or brew install curl (macOS)" ;; + "tr") + echo " β€’ curl: apt-get install coreutils (Ubuntu/Debian) and should be installed by default on macOS" + ;; + "sed") + echo " β€’ curl: apt-get install sed (Ubuntu/Debian) or brew install gnu-sed (macOS)" + ;; "docker") echo " β€’ Docker: https://docs.docker.com/get-docker/" ;; @@ -282,12 +296,93 @@ download_files() { exit 1 fi log_success "docker-compose.yml downloaded" - - if ! curl -fsSL --connect-timeout 10 --max-time 30 "$ENV_FILE_URL" -o .env; then - log_error "Failed to download .env template" - exit 1 - fi - log_success ".env template downloaded" +} + +prompt_email_configuration() { + echo "" + log_header "πŸ“§ SMTP Email Configuration" + echo "" + echo "Please enter the required SMTP settings for sending emails from AdventureLog." + echo "" + + # SMTP Host + while true; do + read -r -p "πŸ“¨ SMTP Host: " EMAIL_HOST + if [[ -n "$EMAIL_HOST" ]]; then + break + else + log_error "SMTP Host cannot be empty." + fi + done + + # Use TLS + while true; do + read -r -p "πŸ” Use TLS? (y/n): " input_tls + case "${input_tls,,}" in + y | yes) + EMAIL_USE_TLS=true + break + ;; + n | no) + EMAIL_USE_TLS=false + break + ;; + *) + log_error "Invalid input. Please enter y or n." + ;; + esac + done + + # Use SSL + while true; do + read -r -p "πŸ” Use SSL? (y/n): " input_ssl + case "${input_ssl,,}" in + y | yes) + EMAIL_USE_SSL=true + break + ;; + n | no) + EMAIL_USE_SSL=false + break + ;; + *) + log_error "Invalid input. Please enter y or n." + ;; + esac + done + + # SMTP Port + while true; do + read -r -p "πŸ”Œ SMTP Port: " EMAIL_PORT + if [[ "$EMAIL_PORT" =~ ^[0-9]+$ ]]; then + break + else + log_error "Please enter a valid numeric port." + fi + done + + # SMTP Username + while true; do + read -r -p "πŸ‘€ SMTP Username: " EMAIL_HOST_USER + if [[ -n "$EMAIL_HOST_USER" ]]; then + break + else + log_error "SMTP Username cannot be empty." + fi + done + + # SMTP Password + while true; do + read -r -s -p "πŸ”‘ SMTP Password: " EMAIL_HOST_PASSWORD + echo + if [[ -n "$EMAIL_HOST_PASSWORD" ]]; then + break + else + log_error "Passwords do not match or are empty. Please try again." + fi + done + + log_success "SMTP configuration completed." } prompt_configuration() { @@ -333,233 +428,330 @@ prompt_configuration() { # Check port availability check_port_availability "$FRONTEND_PORT" "frontend" check_port_availability "$BACKEND_PORT" "backend" + + while true; do + read -r -p "🚦 Use SMTP email with AdventureLog? (Y/n): " input_email + case "${input_email,,}" in + y | yes | "") + USE_EMAIL=true + prompt_email_configuration + break + ;; + n | no) + USE_EMAIL=false + break + ;; + *) + log_error "Invalid input. Please enter Y or N." + ;; + esac + done + log_success "Using SMTP email with AdventureLog?: $USE_EMAIL" + + while true; do + read -r -p "🚦 Enable Traefik labels? (Y/n): " input_traefik + case "${input_traefik,,}" in + y | yes | "") + TRAEFIK_ENABLED=true + break + ;; + n | no) + TRAEFIK_ENABLED=false + break + ;; + *) + log_error "Invalid input. Please enter Y or N." + ;; + esac + done + log_success "Traefik enabled?: $TRAEFIK_ENABLED" + + while true; do + read -r -p "🚦 Use docker secrets, which is more secure? (Y/n): " input_secrets + case "${input_secrets,,}" in + y | yes | "") + USE_DOCKER_SECRETS=true + break + ;; + n | no) + USE_DOCKER_SECRETS=false + break + ;; + *) + log_error "Invalid input. Please enter Y or N." + ;; + esac + done + log_success "Using Docker Secrets enabled?: $USE_DOCKER_SECRETS" echo "" } -configure_environment_fallback() { - log_info "Using simple configuration approach..." - - # Generate simple passwords using a basic method - DB_PASSWORD="$(date +%s | sha256sum | base64 | head -c 32)" - ADMIN_PASSWORD="$(date +%s | sha256sum | base64 | head -c 24)" - - log_info "Generated passwords using fallback method" - - # Create backup - cp .env .env.backup - - # Use simple string replacement with perl if available - if command -v perl &>/dev/null; then - log_info "Using perl for configuration..." - # Fix: Update BOTH password variables for database consistency - perl -pi -e "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$DB_PASSWORD/" .env - perl -pi -e "s/^DATABASE_PASSWORD=.*/DATABASE_PASSWORD=$DB_PASSWORD/" .env - perl -pi -e "s/^DJANGO_ADMIN_PASSWORD=.*/DJANGO_ADMIN_PASSWORD=$ADMIN_PASSWORD/" .env - perl -pi -e "s|^ORIGIN=.*|ORIGIN=$FRONTEND_ORIGIN|" .env - perl -pi -e "s|^PUBLIC_URL=.*|PUBLIC_URL=$BACKEND_URL|" .env - perl -pi -e "s|^CSRF_TRUSTED_ORIGINS=.*|CSRF_TRUSTED_ORIGINS=$FRONTEND_ORIGIN,$BACKEND_URL|" .env - perl -pi -e "s|^FRONTEND_URL=.*|FRONTEND_URL=$FRONTEND_ORIGIN|" .env - # Add port configuration - perl -pi -e "s/^FRONTEND_PORT=.*/FRONTEND_PORT=$FRONTEND_PORT/" .env - perl -pi -e "s/^BACKEND_PORT=.*/BACKEND_PORT=$BACKEND_PORT/" .env - - # Add port variables if they don't exist - if ! grep -q "^FRONTEND_PORT=" .env; then - echo "FRONTEND_PORT=$FRONTEND_PORT" >> .env - fi - if ! grep -q "^BACKEND_PORT=" .env; then - echo "BACKEND_PORT=$BACKEND_PORT" >> .env - fi - - if grep -q "POSTGRES_PASSWORD=$DB_PASSWORD" .env; then - log_success "Configuration completed successfully" - return 0 - fi - fi - - # Manual approach - create .env from scratch with key variables - log_info "Creating minimal .env configuration..." - cat > .env << EOF -# Database Configuration -POSTGRES_DB=adventurelog -POSTGRES_USER=adventurelog -POSTGRES_PASSWORD=$DB_PASSWORD -DATABASE_PASSWORD=$DB_PASSWORD - -# Django Configuration -DJANGO_ADMIN_USERNAME=admin -DJANGO_ADMIN_PASSWORD=$ADMIN_PASSWORD -SECRET_KEY=$(openssl rand -base64 32 2>/dev/null || echo "change-this-secret-key-$(date +%s)") - -# URL Configuration -ORIGIN=$FRONTEND_ORIGIN -PUBLIC_URL=$BACKEND_URL -FRONTEND_URL=$FRONTEND_ORIGIN -CSRF_TRUSTED_ORIGINS=$FRONTEND_ORIGIN,$BACKEND_URL - -# Port Configuration -FRONTEND_PORT=$FRONTEND_PORT -BACKEND_PORT=$BACKEND_PORT - -# Additional Settings -DEBUG=False -EOF - - log_success "Created minimal .env configuration" - return 0 -} - configure_environment() { - log_info "Generating secure configuration..." - - # Debug: Test password generation first - log_info "Testing password generation..." - if ! command -v tr &>/dev/null; then - log_error "tr command not found - required for password generation" - exit 1 - fi - - # Generate secure passwords with error checking - log_info "Generating database password..." + log_info "Verifying required tools..." + + for cmd in tr sed cp grep wc mkdir; do + if ! command -v "$cmd" &>/dev/null; then + log_error "$cmd is required but not found." + exit 1 + fi + done + + log_info "Generating secure secrets..." DB_PASSWORD=$(generate_secure_password 32) - if [[ -z "$DB_PASSWORD" ]]; then - log_error "Failed to generate database password" - exit 1 - fi - log_success "Database password generated (${#DB_PASSWORD} characters)" - - log_info "Generating admin password..." ADMIN_PASSWORD=$(generate_secure_password 24) - if [[ -z "$ADMIN_PASSWORD" ]]; then - log_error "Failed to generate admin password" + SECRET_KEY=$(generate_secure_password 50) + + if [[ -z "$DB_PASSWORD" || -z "$ADMIN_PASSWORD" || -z "$SECRET_KEY" ]]; then + log_error "❌ Failed to generate secure credentials" exit 1 fi - log_success "Admin password generated (${#ADMIN_PASSWORD} characters)" - - # Debug: Check if .env file exists and is readable - log_info "Checking .env file..." - if [[ ! -f ".env" ]]; then - log_error ".env file not found" + + log_success "βœ… Secrets generated: DB(${#DB_PASSWORD}), Admin(${#ADMIN_PASSWORD}), Key(${#SECRET_KEY})" + + FRONTEND_PORT="${FRONTEND_PORT:-8015}" + BACKEND_PORT="${BACKEND_PORT:-8016}" + FRONTEND_ORIGIN="${FRONTEND_ORIGIN:-"http://localhost:$FRONTEND_PORT"}" + BACKEND_URL="${BACKEND_URL:-"http://localhost:$BACKEND_PORT"}" + CSRF_TRUSTED="$BACKEND_URL,$FRONTEND_ORIGIN" + + if [[ ! -f "docker-compose.yml" ]]; then + log_error "docker-compose.yml not found" exit 1 fi - - if [[ ! -r ".env" ]]; then - log_error ".env file is not readable" + + cp docker-compose.yml docker-compose.yml.bak || { + log_error "Failed to back up docker-compose.yml" exit 1 - fi - - log_info "File check passed - .env exists and is readable ($(wc -l < .env) lines)" - - # Try fallback method first (simpler and more reliable) - log_info "Attempting configuration..." - if configure_environment_fallback; then - return 0 - fi - - log_warning "Fallback method failed, trying advanced processing..." - - # Fallback to bash processing - # Create backup of original .env - cp .env .env.backup - - # Create a new .env file by processing the original line by line - local temp_file=".env.temp" - local processed_lines=0 - local updated_lines=0 - - while IFS= read -r line || [[ -n "$line" ]]; do - ((processed_lines++)) - case "$line" in - POSTGRES_PASSWORD=*) - echo "POSTGRES_PASSWORD=$DB_PASSWORD" - ((updated_lines++)) - ;; - DATABASE_PASSWORD=*) - echo "DATABASE_PASSWORD=$DB_PASSWORD" - ((updated_lines++)) - ;; - DJANGO_ADMIN_PASSWORD=*) - echo "DJANGO_ADMIN_PASSWORD=$ADMIN_PASSWORD" - ((updated_lines++)) - ;; - ORIGIN=*) - echo "ORIGIN=$FRONTEND_ORIGIN" - ((updated_lines++)) - ;; - PUBLIC_URL=*) - echo "PUBLIC_URL=$BACKEND_URL" - ((updated_lines++)) - ;; - CSRF_TRUSTED_ORIGINS=*) - echo "CSRF_TRUSTED_ORIGINS=$FRONTEND_ORIGIN,$BACKEND_URL" - ((updated_lines++)) - ;; - FRONTEND_URL=*) - echo "FRONTEND_URL=$FRONTEND_ORIGIN" - ((updated_lines++)) - ;; - FRONTEND_PORT=*) - echo "FRONTEND_PORT=$FRONTEND_PORT" - ((updated_lines++)) - ;; - BACKEND_PORT=*) - echo "BACKEND_PORT=$BACKEND_PORT" - ((updated_lines++)) - ;; - *) - echo "$line" - ;; - esac - done < .env > "$temp_file" - - # Add port variables if they weren't found in the original file - if ! grep -q "^FRONTEND_PORT=" "$temp_file"; then - echo "FRONTEND_PORT=$FRONTEND_PORT" >> "$temp_file" - ((updated_lines++)) - fi - if ! grep -q "^BACKEND_PORT=" "$temp_file"; then - echo "BACKEND_PORT=$BACKEND_PORT" >> "$temp_file" - ((updated_lines++)) - fi - - log_info "Processed $processed_lines lines, updated $updated_lines configuration values" - - # Check if temp file was created successfully - if [[ ! -f "$temp_file" ]]; then - log_error "Failed to create temporary configuration file" - exit 1 - fi - - # Replace the original .env with the configured one - if mv "$temp_file" .env; then - log_success "Environment configured with secure passwords and port settings" + } + + log_info "Applying configuration to docker-compose.yml..." + + # Handle Docker secrets if enabled + if [[ "$USE_DOCKER_SECRETS" == "true" ]]; then + log_info "Enabling Docker secrets..." + + mkdir -p .secrets + + # Save secrets to files + echo "$DB_PASSWORD" > .secrets/postgres-password.secret || FINISHED_CORRECTLY=false + echo "$SECRET_KEY" > .secrets/secret-key.secret || FINISHED_CORRECTLY=false + echo "$ADMIN_PASSWORD" > .secrets/django-admin-password.secret || FINISHED_CORRECTLY=false + + # Uncomment secret references in docker-compose.yml + sed -i '' -E \ + -e 's/^([[:space:]]*)#[[:space:]]*(secrets:)/\1\2/' \ + -e 's/^([[:space:]]*)#[[:space:]]*( - postgres-password)/\1\2/' \ + -e 's/^([[:space:]]*)#[[:space:]]*( - secret-key)/\1\2/' \ + -e 's/^([[:space:]]*)#[[:space:]]*( - django-admin-password)/\1\2/' \ + -e 's/^([[:space:]]*)#[[:space:]]*#( - email-host-password)/\1# \2/' \ + -e 's/^([[:space:]]*)#[[:space:]](secrets:[[:space:]]*#.*)/\1\2/' \ + -e 's/^([[:space:]]*)#[[:space:]]*(postgres-password:)/\1 \2/' \ + -e 's/^([[:space:]]*)#[[:space:]]*(file: .secrets\/postgres-password\.secret)/\1 \2/' \ + -e 's/^([[:space:]]*)#[[:space:]]*(secret-key:)/\1 \2/' \ + -e 's/^([[:space:]]*)#[[:space:]]*(file: .secrets\/secret-key\.secret)/\1 \2/' \ + -e 's/^([[:space:]]*)#[[:space:]]*#[[:space:]]*(email-host-password:)/\1 # \2/' \ + -e 's/^([[:space:]]*)#[[:space:]]*#[[:space:]]*(file: .secrets\/email-host-password\.secret)/\1 # \2/' \ + -e 's/^([[:space:]]*)#[[:space:]]*(django-admin-password:)/\1 \2/' \ + -e 's/^([[:space:]]*)#[[:space:]]*(file: .secrets\/django-admin-password\.secret)/\1 \2/' \ + docker-compose.yml || FINISHED_CORRECTLY=false + # for swapping normal variables to secrets + awk ' + { + lines[NR] = $0 + + # Match lines like: [whitespace]#[whitespace]VARIABLE_FILE: + if ($0 ~ /^[[:space:]]*#[[:space:]]*[A-Z0-9_]+_FILE:/) { + line = $0 + indent = line + sub(/[^[:space:]]+.*/, "", indent) # Get leading whitespace + + # Remove leading `#` and any spaces after + sub(/^[[:space:]]*#[[:space:]]*/, "", line) + split(line, parts, ":") + split(parts[1], name_parts, "_FILE") + varname = name_parts[1] + + uncomment_line[NR] = indent line + comment_var[varname] = 1 + } + } + END { + for (i = 1; i <= NR; i++) { + line = lines[i] + + # Replace commented *_FILE line with uncommented version (with indent) + if (i in uncomment_line) { + print uncomment_line[i] + continue + } + + # Comment out original VARIABLE: line if a *_FILE was found + if (line ~ /^[[:space:]]*[A-Z0-9_]+:/) { + indent = line + sub(/[^[:space:]]+.*/, "", indent) + split(line, parts, ":") + var = parts[1] + gsub(/^[[:space:]]*/, "", var) + + if (var in comment_var) { + print indent "# " substr(line, length(indent)+1) + continue + } + } + + # Default: print line as-is + print line + } + } + ' docker-compose.yml > docker-compose.yml.tmp && { + mv docker-compose.yml.tmp docker-compose.yml + } || FINISHED_CORRECTLY=false + + # Handle email password secret comments + awk ' + { + lines[NR] = $0 + } + + END { + for (i = 1; i <= NR; i++) { + line1 = lines[i] + line2 = lines[i + 1] + + # Match: line1 = single-commented VAR, line2 = double-commented VAR_FILE + if (line1 ~ /^[[:space:]]*#[[:space:]]*[A-Z0-9_]+:[[:space:]]*/ && + line2 ~ /^[[:space:]]*#[[:space:]]*#[[:space:]]*[A-Z0-9_]+_FILE:/) { + + # Extract indent from line1 + indent1 = line1 + sub(/[^[:space:]]+.*/, "", indent1) + + # Clean VAR line for recommenting + clean1 = line1 + gsub(/^[[:space:]]*#[[:space:]]*/, "", clean1) + + # Print double-commented VAR line + print indent1 "# # " clean1 + + # Extract indent from line2 + indent2 = line2 + sub(/[^[:space:]]+.*/, "", indent2) + + # Clean VAR_FILE line to single-comment + clean2 = line2 + gsub(/^[[:space:]]*#[[:space:]]*/, "", clean2) # Remove first # + gsub(/^[[:space:]]*#[[:space:]]*/, "", clean2) # Remove second # + + # Print single-commented VAR_FILE line + print indent2 "# " clean2 + + i++ # Skip next line since we just processed it + continue + } + + # Default: print original line + print lines[i] + } + } + ' docker-compose.yml > docker-compose.yml.tmp && { + mv docker-compose.yml.tmp docker-compose.yml + } || FINISHED_CORRECTLY=false + + log_success "Docker secrets configured" else - log_error "Failed to replace .env file" - log_info "Restoring backup and exiting" - mv .env.backup .env - rm -f "$temp_file" + # Default env var replacements (no secrets) + sed -i '' -E \ + -e "s|^([[:space:]]*POSTGRES_PASSWORD:).*|\1 $DB_PASSWORD|" \ + -e "s|^([[:space:]]*DJANGO_ADMIN_PASSWORD:).*|\1 $ADMIN_PASSWORD|" \ + -e "s|^([[:space:]]*SECRET_KEY:).*|\1 $SECRET_KEY|" \ + -e "s|^([[:space:]]*EMAIL_HOST_PASSWORD:).*|\1 $EMAIL_HOST_PASSWORD|" \ + docker-compose.yml || FINISHED_CORRECTLY=false + + # if [[ "$USE_EMAIL" == "true" ]]; then + # fi + fi + # Handle email config if enabled + if [[ "$USE_EMAIL" == "true" ]]; then + log_info "Enabling email configuration..." + sed -i '' -E \ + -e "s/^([[:space:]]*)#[[:space:]]*(EMAIL_BACKEND:).*/\1\2 email/" \ + -e "s/^([[:space:]]*)#[[:space:]]*(EMAIL_HOST:).*/\1\2 $EMAIL_HOST/" \ + -e "s/^([[:space:]]*)#[[:space:]]*(EMAIL_USE_TLS:).*/\1\2 $EMAIL_USE_TLS/" \ + -e "s/^([[:space:]]*)#[[:space:]]*(EMAIL_PORT:).*/\1\2 $EMAIL_PORT/" \ + -e "s/^([[:space:]]*)#[[:space:]]*(EMAIL_USE_SSL:).*/\1\2 $EMAIL_USE_SSL/" \ + -e "s/^([[:space:]]*)#[[:space:]]*(EMAIL_HOST_USER:).*/\1\2 $EMAIL_HOST_USER/" \ + docker-compose.yml || FINISHED_CORRECTLY=false + + if [[ "$USE_DOCKER_SECRETS" == "true" ]]; then + sed -i '' -E \ + -e "s/^([[:space:]]*)#[[:space:]]*(# EMAIL_HOST_PASSWORD:).*/\1\2/" \ + -e "s/^([[:space:]]*)#[[:space:]]*(EMAIL_HOST_PASSWORD_FILE:).*/\1\2 \/run\/secrets\/email-host-password/" \ + -e "s/^([[:space:]]*)#[[:space:]]*(- email-host-password)/\1 \2/" \ + -e "s/^([[:space:]]*)#[[:space:]]*(email-host-password:)/\1\2/" \ + -e "s/^([[:space:]]*)#[[:space:]]*(file: .secrets\/email-host-password\.secret)/\1 \2/" \ + docker-compose.yml || FINISHED_CORRECTLY=false + else + sed -i '' -E \ + -e "s/^([[:space:]]*)#[[:space:]]*(EMAIL_HOST_PASSWORD:).*/\1\2 $EMAIL_HOST_PASSWORD/" \ + docker-compose.yml || FINISHED_CORRECTLY=false + fi + + log_success "Email configuration applied" + fi + + # Replace other standard values + sed -i '' -E \ + -e "s|^( *BACKEND_PORT:).*|\1 $BACKEND_PORT|" \ + -e "s|^( *FRONTEND_PORT:).*|\1 $FRONTEND_PORT|" \ + -e "s|^( *PUBLIC_URL:).*|\1 $BACKEND_URL|" \ + -e "s|^( *FRONTEND_URL:).*|\1 $FRONTEND_ORIGIN|" \ + -e "s|^( *ORIGIN:).*|\1 $FRONTEND_ORIGIN|" \ + -e "s|^( *CSRF_TRUSTED_ORIGINS:).*|\1 $CSRF_TRUSTED|" \ + -e "s|([[:space:]]+-[[:space:]]*\")([0-9]+)(:3000\")|\1$FRONTEND_PORT\3|" \ + -e "s|([[:space:]]+-[[:space:]]*\")([0-9]+)(:80\")|\1$BACKEND_PORT\3|" \ + docker-compose.yml || FINISHED_CORRECTLY=false + + if [[ "$TRAEFIK_ENABLED" == "true" ]]; then + sed -i '' -E \ + -e 's/^([[:space:]]*)#([[:space:]]*labels:)/\1 \2/' \ + -e 's/^([[:space:]]*)#([[:space:]]*-?[[:space:]]*"traefik\..*")/\1 \2/' \ + docker-compose.yml || FINISHED_CORRECTLY=false + fi + + if [[ "$FINISHED_CORRECTLY" == "false" ]]; then + log_error "configuration failed" + log_info "Restoring backup..." + mv docker-compose.yml.bak docker-compose.yml exit 1 fi - - # Verify critical configuration was applied - if grep -q "POSTGRES_PASSWORD=$DB_PASSWORD" .env && (grep -q "DATABASE_PASSWORD=$DB_PASSWORD" .env || grep -q "POSTGRES_PASSWORD=$DB_PASSWORD" .env); then - log_success "Configuration verification passed - database password variables set" + + log_success "docker-compose.yml updated" + + log_info "πŸ” Verifying updated values in docker-compose.yml:" + diff -y docker-compose.yml.bak docker-compose.yml || : + + # Basic verification of values present + if [[ "$USE_DOCKER_SECRETS" == "false" ]]; then + if grep -q "$DB_PASSWORD" docker-compose.yml && grep -q "$FRONTEND_PORT" docker-compose.yml && grep -q "$BACKEND_PORT" docker-compose.yml; then + log_success "βœ… Configuration verification passed" + else + log_warning "⚠️ Some configuration values may not have applied properly" + log_info "Restoring backup for safety... (renaming auto-configured docker-compose.yml to docker-compose.yml.install)" + mv docker-compose.yml docker-compose.yml.install + mv docker-compose.yml.bak docker-compose.yml + exit 1 + fi else - log_error "Configuration verification failed - database passwords not properly configured" - log_info "Showing database-related lines in .env for debugging:" - grep -E "(POSTGRES_PASSWORD|DATABASE_PASSWORD)" .env | while read -r line; do - echo " $line" - done - mv .env.backup .env - exit 1 - fi - - # Verify port configuration - if grep -q "FRONTEND_PORT=$FRONTEND_PORT" .env && grep -q "BACKEND_PORT=$BACKEND_PORT" .env; then - log_success "Port configuration verified - frontend: $FRONTEND_PORT, backend: $BACKEND_PORT" - else - log_warning "Port configuration may not be complete - check .env file manually" + if grep -q "postgres-password" docker-compose.yml && grep -q "$FRONTEND_PORT" docker-compose.yml && grep -q "$BACKEND_PORT" docker-compose.yml; then + log_success "βœ… Configuration verification passed" + else + log_warning "⚠️ Some configuration values may not have applied properly" + log_info "Restoring backup for safety... (renaming auto-configured docker-compose.yml to docker-compose.yml.install)" + mv docker-compose.yml docker-compose.yml.install + mv docker-compose.yml.bak docker-compose.yml + exit 1 + fi fi } From 324506ea10dbdf7e06ed0f9fff92da750b37e5cb Mon Sep 17 00:00:00 2001 From: Corbin Whitton Date: Sun, 20 Jul 2025 21:47:28 -0600 Subject: [PATCH 5/5] feat(devsecops): updated documentation to reference new configuration --- documentation/docs/install/docker.md | 66 ++++++++++++++++++----- documentation/docs/install/quick_start.md | 4 +- documentation/docs/install/traefik.md | 2 +- 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/documentation/docs/install/docker.md b/documentation/docs/install/docker.md index 45ab745..15e36a3 100644 --- a/documentation/docs/install/docker.md +++ b/documentation/docs/install/docker.md @@ -2,7 +2,7 @@ Docker is the preferred way to run AdventureLog on your local machine. It is a lightweight containerization technology that allows you to run applications in isolated environments called containers. -> **Note**: This guide mainly focuses on installation with a Linux-based host machine, but the steps are similar for other operating systems. +> **Note**: This guide mainly focuses on installation with a Linux-based host machine, but the steps are similar for other operating systems or when using Podman. ## Prerequisites @@ -29,16 +29,7 @@ If running on an ARM based machine, you will need to use a different PostGIS Ima ## Configuration -The `.env` file contains all the configuration settings for your AdventureLog instance. Here’s a breakdown of each section: - -### 🌐 Frontend (web) - -| Name | Required | Description | Default Value | -| ------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -| `PUBLIC_SERVER_URL` | Yes | Used by the frontend SSR server to connect to the backend. Almost every user user will **never have to change this from default**! | `http://server:8000` | -| `ORIGIN` | Sometimes | Needed only if not using HTTPS. Set it to the domain or IP you'll use to access the frontend. | `http://localhost:8015` | -| `BODY_SIZE_LIMIT` | Yes | Maximum upload size in bytes. | `Infinity` | -| `FRONTEND_PORT` | Yes | Port that the frontend will run on inside Docker. | `8015` | +The `docker-compose.yml` file contains all the configuration settings for your AdventureLog instance. Here’s a breakdown of each environment variable (at the top of the file): ### 🐘 PostgreSQL Database @@ -53,6 +44,7 @@ The `.env` file contains all the configuration settings for your AdventureLog in | Name | Required | Description | Default Value | | ----------------------- | -------- | ---------------------------------------------------------------------------------- | --------------------------------------------- | +| `PHHOST` | Yes | The hostname of the postgres container. | `pg` | | `SECRET_KEY` | Yes | Django secret key. Change this in production! | `changeme123` | | `DJANGO_ADMIN_USERNAME` | Yes | Default Django admin username. | `admin` | | `DJANGO_ADMIN_PASSWORD` | Yes | Default Django admin password. | `admin` | @@ -63,6 +55,15 @@ The `.env` file contains all the configuration settings for your AdventureLog in | `BACKEND_PORT` | Yes | Port that the backend will run on inside Docker. | `8016` | | `DEBUG` | No | Should be `False` in production. | `False` | +### 🌐 Frontend (web) + +| Name | Required | Description | Default Value | +| ------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | +| `PUBLIC_SERVER_URL` | Yes | Used by the frontend SSR server to connect to the backend. Almost every user user will **never have to change this from default**! | `http://backend:8000` | +| `ORIGIN` | Sometimes | Needed only if not using HTTPS. Set it to the domain or IP you'll use to access the frontend. | `http://localhost:8015` | +| `BODY_SIZE_LIMIT` | Yes | Maximum upload size in bytes. | `Infinity` | +| `FRONTEND_PORT` | Yes | Port that the frontend will run on inside Docker. | `8015` | + ## Optional Configuration - [Disable Registration](../configuration/disable_registration.md) @@ -71,9 +72,50 @@ The `.env` file contains all the configuration settings for your AdventureLog in - [Immich Integration](../configuration/immich_integration.md) - [Umami Analytics](../configuration/analytics.md) +## Additional Configuration Methods + +### Secret Files +If you're not comfortable storing sensitive values like `SECRET_KEY` or any `PASSWORD` directly in the environment file, you can either use Docker secrets or bind-mount a text file containing the secret value. In this case, append `_FILE` to the environment variable name and set its value to the file path. + +For example, instead of: + +```yml +SECRET_KEY: your_secret_value +``` + +You can use: + +```yml +SECRET_KEY_FILE: /run/secrets/secret_key # For a docker secret +SECRET_KEY_FILE: /some/directory/secret_key # For a volume bind mount +``` + +Make sure the any secret files are **not** committed to version control to keep your secrets safe. + +### .env File + +If you prefer not to hardcode sensitive values like `SECRET_KEY` or any `PASSWORD` directly in your `docker-compose.yml` file, you can reference them from a `.env` file. Docker Compose will automatically read this file and substitute the values into your configuration. An example .env file is found [on GitHub](https://github.com/seanmorley15/AdventureLog/blob/main/.env.example). + +For example, in your `.env` file: + +``` + +SECRET\_KEY=your\_secret\_value + +```` + +And in your `docker-compose.yml`: + +```yaml +environment: + - SECRET_KEY=${SECRET_KEY} +```` + +Make sure the `.env` file is **not** committed to version control to keep your secrets safe. + ## Running the Containers -Once you've configured `.env`, you can start AdventureLog with: +Once you've configured `docker-compose.yml`, you can start AdventureLog with: ```bash docker compose up -d diff --git a/documentation/docs/install/quick_start.md b/documentation/docs/install/quick_start.md index 64dbdce..f21cd3b 100644 --- a/documentation/docs/install/quick_start.md +++ b/documentation/docs/install/quick_start.md @@ -5,7 +5,7 @@ Install **AdventureLog** in seconds using our automated script. ## πŸ§ͺ One-Liner Install ```bash -curl -sSL https://get.adventurelog.app | bash +bash -c "$(curl -sSL https://get.adventurelog.app)" ``` This will: @@ -27,7 +27,7 @@ This will: The script automatically: 1. Verifies Docker is installed and running -2. Downloads `docker-compose.yml` and `.env` +2. Downloads `docker-compose.yml` 3. Prompts you for domain and port settings 4. Waits for services to start 5. Prints success info with next steps diff --git a/documentation/docs/install/traefik.md b/documentation/docs/install/traefik.md index 2923136..ad7bca3 100644 --- a/documentation/docs/install/traefik.md +++ b/documentation/docs/install/traefik.md @@ -4,4 +4,4 @@ Traefik is a modern HTTP reverse proxy and load balancer that makes deploying mi AdventureLog has a built-in Traefik configuration that makes it easy to deploy and manage your AdventureLog instance. -The most recent version of the Traefik `docker-compose.yml` file can be found in the [AdventureLog GitHub repository](https://github.com/seanmorley15/AdventureLog/blob/main/docker-compose-traefik.yaml). +The most recent version of [docker-compose.yml](https://github.com/seanmorley15/AdventureLog/blob/main/docker-compose.yml) file includes example Traefik container labels. To deploy Traefik itself, consult the [Traefik v3.x documentation](https://doc.traefik.io/traefik/).