From 1cd40ed74227889d0cb7e42037f3350a157b0811 Mon Sep 17 00:00:00 2001 From: Bastien Ogier Date: Wed, 10 Dec 2025 19:40:49 +0100 Subject: [PATCH 01/13] first try --- src/backend/Dockerfile | 45 +++++++++++-------- src/backend/entrypoint | 6 +++ src/frontend/Dockerfile | 10 ++++- src/frontend/nginx/nginx.conf.template | 61 ++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 20 deletions(-) create mode 100644 src/frontend/nginx/nginx.conf.template diff --git a/src/backend/Dockerfile b/src/backend/Dockerfile index b4e66dbc4..e70de530b 100644 --- a/src/backend/Dockerfile +++ b/src/backend/Dockerfile @@ -2,22 +2,29 @@ FROM python:3.13.7-slim-trixie AS base # Bump this to force an update of the apt repositories -ENV MIN_UPDATE_DATE="2025-09-02" +ENV MIN_UPDATE_DATE="2025-12-01" -RUN apt-get update && apt-get upgrade -y \ - && rm -rf /var/lib/apt/lists/* +RUN < /dev/null 2>&1; then fi fi +# This _HAS_ to be "true" on a single backend instance +if [ "${ENABLE_DB_MIGRATIONS:-false}" = "true" ]; then + echo "🐳(entrypoint) running django migrations..." + python manage.py migrate --noinput +fi + echo "🐳(entrypoint) running your command: ${*}" exec "$@" diff --git a/src/frontend/Dockerfile b/src/frontend/Dockerfile index e4c2b083d..00b174ad9 100644 --- a/src/frontend/Dockerfile +++ b/src/frontend/Dockerfile @@ -14,7 +14,6 @@ USER ${DOCKER_USER} COPY --chown=${DOCKER_USER} package*.json ./ RUN npm install - FROM frontend-deps AS frontend-build WORKDIR /home/frontend/ @@ -24,3 +23,12 @@ ARG API_ORIGIN ENV NEXT_PUBLIC_API_ORIGIN=${API_ORIGIN} RUN npm run build + +FROM docker.io/nginxinc/nginx-unprivileged:1-alpine AS runtime-prod + +COPY --from=frontend-build /home/frontend/out /app +COPY nginx/nginx.conf.template /etc/nginx/templates/default.conf.template + +ENV MESSAGES_FRONTEND_PORT=8080 +ENV MESSAGES_FRONTEND_BACKEND_SERVER=localhost:8000 +ENV DJANGO_ADMIN_URL=admin diff --git a/src/frontend/nginx/nginx.conf.template b/src/frontend/nginx/nginx.conf.template new file mode 100644 index 000000000..36eafb2a4 --- /dev/null +++ b/src/frontend/nginx/nginx.conf.template @@ -0,0 +1,61 @@ +upstream backend_server { + server ${MESSAGES_FRONTEND_BACKEND_SERVER} fail_timeout=0; +} + +server { + listen ${MESSAGES_FRONTEND_PORT}; + server_name _; + server_tokens off; + root /app; + error_page 404 /404.html; + + # Django rest framework + location ^~ /api/ { + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_redirect off; + proxy_pass http://backend_server; + } + + # Django admin + location ^~ ^/${DJANGO_ADMIN_URL}/ { + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_redirect off; + proxy_pass http://backend_server; + } + + # Next static export + location ~* ^/mailbox/[^/]+$ { + try_files /mailbox/[mailboxId].html =404; + } + location ~* ^/mailbox/[^/]+/thread/[^/]+$ { + try_files /mailbox/[mailboxId]/thread/[threadId].html =404; + } + location ~* ^/mailbox/[^/]+/new$ { + try_files /mailbox/[mailboxId]/new.html =404; + } + location ~* ^/domain$ { + try_files /domain.html =404; + } + location ~* ^/domain/[^/]+$ { + try_files /domain/[maildomainId].html =404; + } + location ~* ^/domain/[^/]+/dns$ { + try_files /domain/[maildomainId]/dns.html =404; + } + location ~* ^/domain/[^/]+/signatures$ { + try_files /domain/[maildomainId]/signatures.html =404; + } + + location = /404.html { + internal; + } + + # Frontend export + location / { + try_files $uri index.html $uri/ =404; + } +} From d05535811b96eecb2ca0932f9abcaffe7faa48a4 Mon Sep 17 00:00:00 2001 From: Bastien Ogier Date: Fri, 12 Dec 2025 18:53:12 +0100 Subject: [PATCH 02/13] fixed /admin redirection and /static proxy_pass --- src/frontend/nginx/nginx.conf.template | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/frontend/nginx/nginx.conf.template b/src/frontend/nginx/nginx.conf.template index 36eafb2a4..f55a048f5 100644 --- a/src/frontend/nginx/nginx.conf.template +++ b/src/frontend/nginx/nginx.conf.template @@ -19,7 +19,11 @@ server { } # Django admin - location ^~ ^/${DJANGO_ADMIN_URL}/ { + location /${DJANGO_ADMIN_URL} { + return 301 https://$host/${DJANGO_ADMIN_URL}/; + } + + location ^~ /${DJANGO_ADMIN_URL}/ { proxy_set_header X-Forwarded-Proto https; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $remote_addr; @@ -27,6 +31,15 @@ server { proxy_pass http://backend_server; } + # Django statics + location ^~ /static/ { + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_redirect off; + proxy_pass http://backend_server; + } + # Next static export location ~* ^/mailbox/[^/]+$ { try_files /mailbox/[mailboxId].html =404; From 28bb5ec5707824ac0695e47913d37bea68134882 Mon Sep 17 00:00:00 2001 From: Bastien Ogier Date: Mon, 15 Dec 2025 22:29:52 +0100 Subject: [PATCH 03/13] added github workflows, try something for scalingo nginx --- .github/workflows/messages.yml | 26 +++++++++++++ bin/scalingo_postfrontend | 10 ++++- src/nginx/servers.conf.erb | 70 ---------------------------------- 3 files changed, 35 insertions(+), 71 deletions(-) delete mode 100644 src/nginx/servers.conf.erb diff --git a/.github/workflows/messages.yml b/.github/workflows/messages.yml index d640a3b71..50e43790e 100644 --- a/.github/workflows/messages.yml +++ b/.github/workflows/messages.yml @@ -148,3 +148,29 @@ jobs: with: image_name: "socks-proxy" context: "src/socks-proxy" + + docker-publish-frontend: + uses: ./.github/workflows/docker-publish.yml + permissions: + contents: read + packages: write + attestations: write + id-token: write + secrets: inherit + with: + image_name: "messages-frontend" + context: "src/frontend" + target: runtime-prod + + docker-publish-backend: + uses: ./.github/workflows/docker-publish.yml + permissions: + contents: read + packages: write + attestations: write + id-token: write + secrets: inherit + with: + image_name: "messages-backend" + context: "src/backend" + target: runtime-prod diff --git a/bin/scalingo_postfrontend b/bin/scalingo_postfrontend index 5a9e1824e..a56e6efb2 100644 --- a/bin/scalingo_postfrontend +++ b/bin/scalingo_postfrontend @@ -10,6 +10,14 @@ mkdir -p build/ mv src/frontend/out build/frontend-out mv src/backend/* ./ -mv src/nginx/* ./ + +# Replace ${MESSAGES_FRONTEND_PORT} to Scalingo mandatory PORT env +# Replace ${DJANGO_ADMIN_URL} to the erb equivalent with the "admin" default value +# Replace every other shell variable to its erb equivalent +sed \ + -e 's/\${MESSAGES_FRONTEND_PORT}/<%= ENV["PORT"] %>/g' \ + -e 's/\${DJANGO_ADMIN_URL}/<%= ENV["DJANGO_ADMIN_URL"] || "admin" %>/g' \ + -e 's/\${\([A-Z_][A-Z0-9_]*\)}/<%= ENV["\1"] %>/g' \ + src/frontend/nginx/nginx.conf.template > ./servers.conf.erb echo "3.13" > .python-version diff --git a/src/nginx/servers.conf.erb b/src/nginx/servers.conf.erb deleted file mode 100644 index 3cd728bff..000000000 --- a/src/nginx/servers.conf.erb +++ /dev/null @@ -1,70 +0,0 @@ -# ERB templated nginx configuration -# see https://doc.scalingo.com/platform/deployment/buildpacks/nginx - -upstream backend_server { - server localhost:8000 fail_timeout=0; -} - -server { - - listen <%= ENV["PORT"] %>; - server_name _; - server_tokens off; - - root /app/build/frontend-out; - - error_page 404 /404.html; - - # Django rest framework - location ^~ /api/ { - proxy_set_header X-Forwarded-Proto https; - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-For $remote_addr; - - proxy_redirect off; - proxy_pass http://backend_server; - } - - # Django admin - location ^~ /<%= ENV["DJANGO_ADMIN_URL"] || "admin/" %> { - proxy_set_header X-Forwarded-Proto https; - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-For $remote_addr; - - proxy_redirect off; - proxy_pass http://backend_server; - } - - # Next static export - location ~* ^/mailbox/[^/]+$ { - try_files /mailbox/[mailboxId].html =404; - } - location ~* ^/mailbox/[^/]+/thread/[^/]+$ { - try_files /mailbox/[mailboxId]/thread/[threadId].html =404; - } - location ~* ^/mailbox/[^/]+/new$ { - try_files /mailbox/[mailboxId]/new.html =404; - } - location ~* ^/domain$ { - try_files /domain.html =404; - } - location ~* ^/domain/[^/]+$ { - try_files /domain/[maildomainId].html =404; - } - location ~* ^/domain/[^/]+/dns$ { - try_files /domain/[maildomainId]/dns.html =404; - } - location ~* ^/domain/[^/]+/signatures$ { - try_files /domain/[maildomainId]/signatures.html =404; - } - - location = /404.html { - internal; - } - - # Frontend export - location / { - try_files $uri index.html $uri/ =404; - } - -} \ No newline at end of file From 9bc2fc114041705d63112b9fadf471365bdb9088 Mon Sep 17 00:00:00 2001 From: Bastien Ogier Date: Mon, 15 Dec 2025 22:34:14 +0100 Subject: [PATCH 04/13] fix frontend build --- src/frontend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/Dockerfile b/src/frontend/Dockerfile index 00b174ad9..10f7e9815 100644 --- a/src/frontend/Dockerfile +++ b/src/frontend/Dockerfile @@ -3,7 +3,7 @@ FROM node:22-slim AS frontend-deps ENV npm_config_cache=/tmp/npm-cache RUN npm install -g npm@11.6.2 && npm cache clean -f -ARG DOCKER_USER +ARG DOCKER_USER=1000 WORKDIR /home/frontend/ RUN chown -R ${DOCKER_USER} /tmp/npm-cache From 10d6bbc81291e7a75eb090782b4cf33b8c2c6f16 Mon Sep 17 00:00:00 2001 From: Bastien Ogier Date: Mon, 15 Dec 2025 22:40:04 +0100 Subject: [PATCH 05/13] fix images names --- .github/workflows/messages.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/messages.yml b/.github/workflows/messages.yml index 50e43790e..f6a92bf26 100644 --- a/.github/workflows/messages.yml +++ b/.github/workflows/messages.yml @@ -158,7 +158,7 @@ jobs: id-token: write secrets: inherit with: - image_name: "messages-frontend" + image_name: "frontend" context: "src/frontend" target: runtime-prod @@ -171,6 +171,6 @@ jobs: id-token: write secrets: inherit with: - image_name: "messages-backend" + image_name: "backend" context: "src/backend" target: runtime-prod From a183c234eb17a469d7d91eedacdb2b8c19a4216f Mon Sep 17 00:00:00 2001 From: Bastien Ogier Date: Mon, 15 Dec 2025 23:26:46 +0100 Subject: [PATCH 06/13] fix exit if migrate fails --- src/backend/entrypoint | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/entrypoint b/src/backend/entrypoint index 3f4a7cb82..cf8359196 100755 --- a/src/backend/entrypoint +++ b/src/backend/entrypoint @@ -34,7 +34,7 @@ fi # This _HAS_ to be "true" on a single backend instance if [ "${ENABLE_DB_MIGRATIONS:-false}" = "true" ]; then echo "🐳(entrypoint) running django migrations..." - python manage.py migrate --noinput + python manage.py migrate --noinput || exit 1 fi echo "🐳(entrypoint) running your command: ${*}" From 9f5450d99cddf90256757064992bd84c79ce611f Mon Sep 17 00:00:00 2001 From: Bastien Ogier Date: Tue, 16 Dec 2025 19:29:27 +0100 Subject: [PATCH 07/13] added healthchecks to dockerfile --- src/backend/Dockerfile | 3 +++ src/frontend/Dockerfile | 3 +++ src/frontend/nginx/nginx.conf.template | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/src/backend/Dockerfile b/src/backend/Dockerfile index e70de530b..1e7089759 100644 --- a/src/backend/Dockerfile +++ b/src/backend/Dockerfile @@ -126,3 +126,6 @@ COPY . /app/ # The default command runs gunicorn WSGI server in messages's main module CMD ["gunicorn", "-c", "/app/gunicorn.conf.py", "messages.wsgi:application"] + +HEALTHCHECK --interval=30s --timeout=2s --start-period=30s \ + CMD ["python", "-c", "import os; from urllib.request import Request, urlopen; urlopen(Request('http://localhost:8000/healthz/', headers={'Host': f'{os.getenv('DJANGO_ALLOWED_HOSTS', 'localhost').split(',')[0].strip()}', 'X-Forwarded-Proto': 'https'}), timeout=2)"] diff --git a/src/frontend/Dockerfile b/src/frontend/Dockerfile index 10f7e9815..73a7af687 100644 --- a/src/frontend/Dockerfile +++ b/src/frontend/Dockerfile @@ -32,3 +32,6 @@ COPY nginx/nginx.conf.template /etc/nginx/templates/default.conf.template ENV MESSAGES_FRONTEND_PORT=8080 ENV MESSAGES_FRONTEND_BACKEND_SERVER=localhost:8000 ENV DJANGO_ADMIN_URL=admin + +HEALTHCHECK --interval=60s --timeout=3s --start-interval=10s \ + CMD curl -fsS http://localhost:${MESSAGES_FRONTEND_PORT}/health diff --git a/src/frontend/nginx/nginx.conf.template b/src/frontend/nginx/nginx.conf.template index f55a048f5..b23221956 100644 --- a/src/frontend/nginx/nginx.conf.template +++ b/src/frontend/nginx/nginx.conf.template @@ -63,6 +63,12 @@ server { try_files /domain/[maildomainId]/signatures.html =404; } + location /health { + access_log off; + error_log off; + return 200 'ok'; + } + location = /404.html { internal; } From 44fb3219a557666cbe83735b5cc5fb0e9bcc04b8 Mon Sep 17 00:00:00 2001 From: Bastien Ogier Date: Fri, 19 Dec 2025 16:24:17 +0100 Subject: [PATCH 08/13] (backend) delete ENABLE_DB_MIGRATIONS --- src/backend/entrypoint | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/backend/entrypoint b/src/backend/entrypoint index cf8359196..d3e8cf196 100755 --- a/src/backend/entrypoint +++ b/src/backend/entrypoint @@ -31,11 +31,5 @@ if ! whoami > /dev/null 2>&1; then fi fi -# This _HAS_ to be "true" on a single backend instance -if [ "${ENABLE_DB_MIGRATIONS:-false}" = "true" ]; then - echo "🐳(entrypoint) running django migrations..." - python manage.py migrate --noinput || exit 1 -fi - echo "🐳(entrypoint) running your command: ${*}" exec "$@" From f3f610853798e09f59de43a88c9a910def615a25 Mon Sep 17 00:00:00 2001 From: Bastien Ogier Date: Fri, 19 Dec 2025 18:03:07 +0100 Subject: [PATCH 09/13] (workflows) ensure we run docker builds on main only --- .github/workflows/docker-publish.yml | 9 ++-- .github/workflows/messages-ghcr.yml | 72 ++++++++++++++++++++++++++++ .github/workflows/messages.yml | 66 +------------------------ 3 files changed, 77 insertions(+), 70 deletions(-) create mode 100644 .github/workflows/messages-ghcr.yml diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index dc88096a3..d52f099c1 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -1,6 +1,6 @@ -name: Build and Push Docker Image +name: Build and Push Container Image -on: +"on": workflow_call: inputs: registry: @@ -47,7 +47,6 @@ jobs: images: ${{ inputs.registry }}/${{ github.repository }}-${{ inputs.image_name }} tags: | type=ref,event=branch - type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - name: Build and push Docker image @@ -71,11 +70,11 @@ jobs: package-name: messages-${{ inputs.image_name }} package-type: 'container' min-versions-to-keep: 0 - delete-only-untagged-versions: 'true' + delete-only-untagged-versions: true - name: Container images retention uses: actions/delete-package-versions@v5 with: package-name: messages-${{ inputs.image_name }} package-type: 'container' min-versions-to-keep: 30 - ignore-versions: '^latest|main|(\\d+\\.\\d+(\\.\\d+)?)$' + ignore-versions: 'latest|main|(\\d+\\.\\d+(\\.\\d+)?)' diff --git a/.github/workflows/messages-ghcr.yml b/.github/workflows/messages-ghcr.yml new file mode 100644 index 000000000..1022b7ba4 --- /dev/null +++ b/.github/workflows/messages-ghcr.yml @@ -0,0 +1,72 @@ +name: Build and publish OCI images + +"on": + push: + branches: + - main + +jobs: + + docker-publish-mta-in: + uses: ./.github/workflows/docker-publish.yml + permissions: + contents: read + packages: write + attestations: write + id-token: write + secrets: inherit + with: + image_name: "mta-in" + context: "src/mta-in" + target: runtime-prod + + docker-publish-mta-out: + uses: ./.github/workflows/docker-publish.yml + permissions: + contents: read + packages: write + attestations: write + id-token: write + secrets: inherit + with: + image_name: "mta-out" + context: "src/mta-out" + target: runtime-prod + + docker-publish-socks-proxy: + uses: ./.github/workflows/docker-publish.yml + permissions: + contents: read + packages: write + attestations: write + id-token: write + secrets: inherit + with: + image_name: "socks-proxy" + context: "src/socks-proxy" + + docker-publish-frontend: + uses: ./.github/workflows/docker-publish.yml + permissions: + contents: read + packages: write + attestations: write + id-token: write + secrets: inherit + with: + image_name: "frontend" + context: "src/frontend" + target: runtime-prod + + docker-publish-backend: + uses: ./.github/workflows/docker-publish.yml + permissions: + contents: read + packages: write + attestations: write + id-token: write + secrets: inherit + with: + image_name: "backend" + context: "src/backend" + target: runtime-prod diff --git a/.github/workflows/messages.yml b/.github/workflows/messages.yml index f6a92bf26..d32eac46b 100644 --- a/.github/workflows/messages.yml +++ b/.github/workflows/messages.yml @@ -1,6 +1,6 @@ name: Lint and tests -on: +"on": push: branches: - main @@ -110,67 +110,3 @@ jobs: run: | git diff --quiet || \ (echo "API changes detected. Please run 'make api-update' then commit the changes." && exit 1) - - docker-publish-mta-in: - uses: ./.github/workflows/docker-publish.yml - permissions: - contents: read - packages: write - attestations: write - id-token: write - secrets: inherit - with: - image_name: "mta-in" - context: "src/mta-in" - target: runtime-prod - - docker-publish-mta-out: - uses: ./.github/workflows/docker-publish.yml - permissions: - contents: read - packages: write - attestations: write - id-token: write - secrets: inherit - with: - image_name: "mta-out" - context: "src/mta-out" - target: runtime-prod - - docker-publish-socks-proxy: - uses: ./.github/workflows/docker-publish.yml - permissions: - contents: read - packages: write - attestations: write - id-token: write - secrets: inherit - with: - image_name: "socks-proxy" - context: "src/socks-proxy" - - docker-publish-frontend: - uses: ./.github/workflows/docker-publish.yml - permissions: - contents: read - packages: write - attestations: write - id-token: write - secrets: inherit - with: - image_name: "frontend" - context: "src/frontend" - target: runtime-prod - - docker-publish-backend: - uses: ./.github/workflows/docker-publish.yml - permissions: - contents: read - packages: write - attestations: write - id-token: write - secrets: inherit - with: - image_name: "backend" - context: "src/backend" - target: runtime-prod From d927f0bd51fd996bada808c172076ebddfbed933 Mon Sep 17 00:00:00 2001 From: Bastien Ogier Date: Sat, 20 Dec 2025 01:00:23 +0100 Subject: [PATCH 10/13] replace MESSAGES_FRONTEND_PORT by PORT, remove scalingo pgdump --- bin/scalingo_pgdump.sh | 37 -------------------------- bin/scalingo_postfrontend | 2 -- cron.json | 7 ----- src/frontend/Dockerfile | 4 +-- src/frontend/nginx/nginx.conf.template | 2 +- 5 files changed, 3 insertions(+), 49 deletions(-) delete mode 100755 bin/scalingo_pgdump.sh delete mode 100644 cron.json diff --git a/bin/scalingo_pgdump.sh b/bin/scalingo_pgdump.sh deleted file mode 100755 index 5f7b7167a..000000000 --- a/bin/scalingo_pgdump.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -mkdir -p /tmp/pgdump -cd /tmp/pgdump - -RESTIC_VERSION=0.18.0 -curl -fsSL --remote-name-all "https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/restic_${RESTIC_VERSION}_linux_amd64.bz2" \ - "https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/SHA256SUMS" \ - "https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/SHA256SUMS.asc" - -curl -fsSLo - https://restic.net/gpg-key-alex.asc | gpg --import -gpg --verify SHA256SUMS.asc SHA256SUMS -grep _linux_amd64.bz2 SHA256SUMS | sha256sum -c -bzip2 -d restic_${RESTIC_VERSION}_linux_amd64.bz2 -mv restic_${RESTIC_VERSION}_linux_amd64 restic -chmod +x ./restic - -# Download postgresql client binaries -dbclient-fetcher pgsql - -# Actually dump and upload to scaleway s3 -FILENAME="${APP}_pgdump.sql" - -pg_dump --clean --if-exists --format c --dbname "${SCALINGO_POSTGRESQL_URL}" --no-owner --no-privileges --no-comments --exclude-schema 'information_schema' --exclude-schema '^pg_*' --file "${FILENAME}" - -export AWS_ACCESS_KEY_ID=${BACKUP_PGSQL_S3_KEY} -export AWS_SECRET_ACCESS_KEY=${BACKUP_PGSQL_S3_SECRET} -export RESTIC_PASSWORD=${BACKUP_PGSQL_ENCRYPTION_PASS} -export RESTIC_REPOSITORY=s3:${BACKUP_PGSQL_S3_REPOSITORY}/${APP} - -./restic snapshots -q || ./restic init - -./restic backup ${FILENAME} -# Scalingo one-offs use a different hostname every time group only by paths and not by hosts,paths. -./restic forget --keep-daily 30 --group-by 'paths' --prune diff --git a/bin/scalingo_postfrontend b/bin/scalingo_postfrontend index a56e6efb2..c2eb4e22a 100644 --- a/bin/scalingo_postfrontend +++ b/bin/scalingo_postfrontend @@ -11,11 +11,9 @@ mv src/frontend/out build/frontend-out mv src/backend/* ./ -# Replace ${MESSAGES_FRONTEND_PORT} to Scalingo mandatory PORT env # Replace ${DJANGO_ADMIN_URL} to the erb equivalent with the "admin" default value # Replace every other shell variable to its erb equivalent sed \ - -e 's/\${MESSAGES_FRONTEND_PORT}/<%= ENV["PORT"] %>/g' \ -e 's/\${DJANGO_ADMIN_URL}/<%= ENV["DJANGO_ADMIN_URL"] || "admin" %>/g' \ -e 's/\${\([A-Z_][A-Z0-9_]*\)}/<%= ENV["\1"] %>/g' \ src/frontend/nginx/nginx.conf.template > ./servers.conf.erb diff --git a/cron.json b/cron.json deleted file mode 100644 index 61675d390..000000000 --- a/cron.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "jobs": [ - { - "command": "0 0 * * * bin/scalingo_pgdump.sh" - } - ] -} diff --git a/src/frontend/Dockerfile b/src/frontend/Dockerfile index 73a7af687..7d9a9df93 100644 --- a/src/frontend/Dockerfile +++ b/src/frontend/Dockerfile @@ -29,9 +29,9 @@ FROM docker.io/nginxinc/nginx-unprivileged:1-alpine AS runtime-prod COPY --from=frontend-build /home/frontend/out /app COPY nginx/nginx.conf.template /etc/nginx/templates/default.conf.template -ENV MESSAGES_FRONTEND_PORT=8080 +ENV PORT=8080 ENV MESSAGES_FRONTEND_BACKEND_SERVER=localhost:8000 ENV DJANGO_ADMIN_URL=admin HEALTHCHECK --interval=60s --timeout=3s --start-interval=10s \ - CMD curl -fsS http://localhost:${MESSAGES_FRONTEND_PORT}/health + CMD curl -fsS http://localhost:${PORT}/health diff --git a/src/frontend/nginx/nginx.conf.template b/src/frontend/nginx/nginx.conf.template index b23221956..6b3a4701a 100644 --- a/src/frontend/nginx/nginx.conf.template +++ b/src/frontend/nginx/nginx.conf.template @@ -3,7 +3,7 @@ upstream backend_server { } server { - listen ${MESSAGES_FRONTEND_PORT}; + listen ${PORT}; server_name _; server_tokens off; root /app; From 4f296c7df5917492a862b9005da41cd9515340ba Mon Sep 17 00:00:00 2001 From: Bastien Ogier Date: Sat, 20 Dec 2025 01:12:05 +0100 Subject: [PATCH 11/13] fix nginx root different for container and scalingo --- src/frontend/Dockerfile | 1 + src/frontend/nginx/nginx.conf.template | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/frontend/Dockerfile b/src/frontend/Dockerfile index 7d9a9df93..cde94ff69 100644 --- a/src/frontend/Dockerfile +++ b/src/frontend/Dockerfile @@ -30,6 +30,7 @@ COPY --from=frontend-build /home/frontend/out /app COPY nginx/nginx.conf.template /etc/nginx/templates/default.conf.template ENV PORT=8080 +ENV MESSAGES_FRONTEND_ROOT=/app ENV MESSAGES_FRONTEND_BACKEND_SERVER=localhost:8000 ENV DJANGO_ADMIN_URL=admin diff --git a/src/frontend/nginx/nginx.conf.template b/src/frontend/nginx/nginx.conf.template index 6b3a4701a..35e3ae96c 100644 --- a/src/frontend/nginx/nginx.conf.template +++ b/src/frontend/nginx/nginx.conf.template @@ -6,7 +6,7 @@ server { listen ${PORT}; server_name _; server_tokens off; - root /app; + root ${MESSAGES_FRONTEND_ROOT}; error_page 404 /404.html; # Django rest framework From 169534110a68121e20d234e72be7174856029509 Mon Sep 17 00:00:00 2001 From: Stanislas Bruhiere Date: Sun, 21 Dec 2025 21:14:50 +0100 Subject: [PATCH 12/13] (backend) move healthz to __heartbeat__ and check for db readiness --- compose.yaml | 2 +- src/backend/Dockerfile | 2 +- src/backend/messages/urls.py | 20 ++++++++++++++++++-- src/backend/messages/wsgi.py | 2 +- src/e2e/nginx/e2e.conf.template | 4 ++-- src/frontend/nginx/nginx.conf.template | 10 +++++++++- 6 files changed, 32 insertions(+), 8 deletions(-) diff --git a/compose.yaml b/compose.yaml index d984e3895..53abde962 100644 --- a/compose.yaml +++ b/compose.yaml @@ -95,7 +95,7 @@ services: - ./src/backend:/app - ./data/static:/data/static healthcheck: - test: ["CMD", "python", "-c", "import urllib.request as u; u.urlopen('http://localhost:8000/healthz/', timeout=1)"] + test: ["CMD", "python", "-c", "import urllib.request as u; u.urlopen('http://localhost:8000/__heartbeat__/', timeout=1)"] interval: 3s retries: 3 start_period: 10s diff --git a/src/backend/Dockerfile b/src/backend/Dockerfile index 1e7089759..541925fcd 100644 --- a/src/backend/Dockerfile +++ b/src/backend/Dockerfile @@ -128,4 +128,4 @@ COPY . /app/ CMD ["gunicorn", "-c", "/app/gunicorn.conf.py", "messages.wsgi:application"] HEALTHCHECK --interval=30s --timeout=2s --start-period=30s \ - CMD ["python", "-c", "import os; from urllib.request import Request, urlopen; urlopen(Request('http://localhost:8000/healthz/', headers={'Host': f'{os.getenv('DJANGO_ALLOWED_HOSTS', 'localhost').split(',')[0].strip()}', 'X-Forwarded-Proto': 'https'}), timeout=2)"] + CMD ["python", "-c", "import os; from urllib.request import Request, urlopen; urlopen(Request('http://localhost:8000/__heartbeat__/', headers={'Host': f'{os.getenv('DJANGO_ALLOWED_HOSTS', 'localhost').split(',')[0].strip()}', 'X-Forwarded-Proto': 'https'}), timeout=2)"] diff --git a/src/backend/messages/urls.py b/src/backend/messages/urls.py index 4b6dc5581..d4d6bac74 100644 --- a/src/backend/messages/urls.py +++ b/src/backend/messages/urls.py @@ -1,9 +1,13 @@ """URL configuration for the messages project""" +from logging import getLogger + from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns +from django.db import connection +from django.db.utils import OperationalError from django.http import HttpResponse from django.urls import include, path, re_path @@ -13,12 +17,24 @@ SpectacularSwaggerView, ) +logger = getLogger(__name__) + + +def heartbeat(request): + try: + connection.ensure_connection() + return HttpResponse("OK") + except OperationalError as e: + logger.error("Database error: %s", e) + return HttpResponse("DB Unavailable", status=500) + + urlpatterns = [ path(settings.ADMIN_URL, admin.site.urls), path("", include("core.urls")), path( - "healthz/", - lambda _: HttpResponse("OK"), + "__heartbeat__/", + heartbeat, name="healthcheck", ), ] diff --git a/src/backend/messages/wsgi.py b/src/backend/messages/wsgi.py index 29e43f8c9..4d06f08bf 100644 --- a/src/backend/messages/wsgi.py +++ b/src/backend/messages/wsgi.py @@ -19,7 +19,7 @@ class QuietWSGIRequestHandler(django.core.servers.basehttp.WSGIRequestHandler): def log_message(self, format, *args): path = getattr(self, "path", "") - if path.strip("/") == "healthz": + if path.strip("/") == "__heartbeat__": return super().log_message(format, *args) diff --git a/src/e2e/nginx/e2e.conf.template b/src/e2e/nginx/e2e.conf.template index 189871505..33120dfb6 100644 --- a/src/e2e/nginx/e2e.conf.template +++ b/src/e2e/nginx/e2e.conf.template @@ -48,8 +48,8 @@ server { } # Backend healthcheck - location /healthz/ { - proxy_pass http://backend:8000/healthz/; + location /__heartbeat__/ { + proxy_pass http://backend:8000/__heartbeat__/; proxy_http_version 1.1; proxy_set_header Host $host; } diff --git a/src/frontend/nginx/nginx.conf.template b/src/frontend/nginx/nginx.conf.template index 35e3ae96c..1924e3181 100644 --- a/src/frontend/nginx/nginx.conf.template +++ b/src/frontend/nginx/nginx.conf.template @@ -16,7 +16,15 @@ server { proxy_set_header X-Forwarded-For $remote_addr; proxy_redirect off; proxy_pass http://backend_server; - } + } + # Backend heartbeat + location /__heartbeat__/ { + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_redirect off; + proxy_pass http://backend_server; + } # Django admin location /${DJANGO_ADMIN_URL} { From 67bf8c6ed821b9afa9db6afbd07cd00846fabd05 Mon Sep 17 00:00:00 2001 From: Stanislas Bruhiere Date: Mon, 22 Dec 2025 01:41:45 +0100 Subject: [PATCH 13/13] (backend) fix linting + allow trailing slash --- src/backend/messages/urls.py | 1 + src/frontend/nginx/nginx.conf.template | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/messages/urls.py b/src/backend/messages/urls.py index d4d6bac74..4493ed1dd 100644 --- a/src/backend/messages/urls.py +++ b/src/backend/messages/urls.py @@ -21,6 +21,7 @@ def heartbeat(request): + """Healthcheck endpoint to verify that the application is running and DB is reachable.""" try: connection.ensure_connection() return HttpResponse("OK") diff --git a/src/frontend/nginx/nginx.conf.template b/src/frontend/nginx/nginx.conf.template index 1924e3181..ab756b3de 100644 --- a/src/frontend/nginx/nginx.conf.template +++ b/src/frontend/nginx/nginx.conf.template @@ -18,7 +18,7 @@ server { proxy_pass http://backend_server; } # Backend heartbeat - location /__heartbeat__/ { + location ~ /__heartbeat__/? { proxy_set_header X-Forwarded-Proto https; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $remote_addr;