From d0c06e3096d3274591c4cbd962320efa5f845082 Mon Sep 17 00:00:00 2001 From: Taylor Osler Date: Thu, 6 Nov 2025 14:15:33 -0800 Subject: [PATCH 1/8] create nginx docker container --- docker/backend.Dockerfile | 34 +++++++++++++++ Dockerfile => docker/static.Dockerfile | 20 +++------ nginx.conf | 58 ++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 docker/backend.Dockerfile rename Dockerfile => docker/static.Dockerfile (62%) create mode 100644 nginx.conf diff --git a/docker/backend.Dockerfile b/docker/backend.Dockerfile new file mode 100644 index 00000000..61ea8415 --- /dev/null +++ b/docker/backend.Dockerfile @@ -0,0 +1,34 @@ +FROM python:3.12.11-alpine3.22 AS build_backend +COPY --from=ghcr.io/astral-sh/uv:0.8.18 /uv /uvx /bin/ + +WORKDIR /app + +# Install dependencies +COPY uv.lock . +COPY pyproject.toml . +RUN uv sync --no-dev --frozen --compile-bytecode --no-install-project + +COPY src/firetower . + +# Install the backend itself +COPY pyproject.toml . +COPY manage.py . +COPY README.md . +COPY src/ src/ +RUN uv sync --group prod --no-dev --frozen --compile-bytecode --no-editable + +FROM python:3.12.11-alpine3.22 + +RUN adduser app -h /app -u 1100 -D && chown -R 1100 /app + +# Copy the environment, but not the source code +COPY --from=build_backend --chown=1100 /app/.venv /app/.venv + +WORKDIR /app +USER 1100 + +ENV PORT=8080 +EXPOSE $PORT + +ENV DJANGO_ENV="prod" +ENTRYPOINT [ "sh", "-c", "/app/.venv/bin/granian --interface wsgi --host 0.0.0.0 --port $PORT firetower.wsgi:application"] diff --git a/Dockerfile b/docker/static.Dockerfile similarity index 62% rename from Dockerfile rename to docker/static.Dockerfile index a4a65194..0236cb1d 100644 --- a/Dockerfile +++ b/docker/static.Dockerfile @@ -39,21 +39,11 @@ ENV VITE_API_URL="/api" RUN bun run build -FROM python:3.12.11-alpine3.22 +FROM nginx:1.29.3-alpine3.22 -RUN adduser app -h /app -u 1100 -D && chown -R 1100 /app +USER 101 -# Copy the environment, but not the source code -COPY --from=build_backend --chown=1100 /app/.venv /app/.venv -COPY --from=build_frontend --chown=1100 /app/dist /app/static -COPY --from=build_backend --chown=1100 /app/static/backend /app/static/backend +COPY --from=build_frontend --chown=101 /app/dist /app/static +COPY --from=build_backend --chown=101 /app/static/backend /app/static/backend -WORKDIR /app -USER 1100 - -ENV PORT=8080 -EXPOSE $PORT - -ENV GRANIAN_STATIC_PATH_MOUNT="static/" -ENV DJANGO_ENV="prod" -ENTRYPOINT [ "sh", "-c", "/app/.venv/bin/granian --interface wsgi --host 0.0.0.0 --port $PORT firetower.wsgi:application"] +COPY ./nginx.conf /etc/nginx/nginx.conf diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 00000000..2f9deb64 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,58 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + + keepalive_timeout 65; + + #gzip on; + + #include /etc/nginx/conf.d/*.conf; +} + +server { + listen 80; + server_name localhost .firetower.getsentry.net; + + access_log /var/log/nginx/host.access.log main; + + location / { + root /app/static/ + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + + # proxy to granian backend + location @backend { + proxy_pass http://127.0.0.1:8080; + } +} From dec5016de4d312d08065ba30dcb51a196d7baa3b Mon Sep 17 00:00:00 2001 From: Taylor Osler Date: Thu, 6 Nov 2025 15:52:37 -0800 Subject: [PATCH 2/8] add django readiness probe --- pyproject.toml | 1 + src/firetower/settings.py | 7 +++++++ uv.lock | 16 +++++++++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ba5eda8d..6d87a23e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ dependencies = [ "cryptography>=46.0.3", "django>=5.2.5", "django-cors-headers>=4.9.0", + "django-kubernetes>=1.1.0", "djangorestframework>=3.15.2", "google-auth>=2.37.0", "google-cloud-logging>=3.11.4", diff --git a/src/firetower/settings.py b/src/firetower/settings.py index 8539e946..5665d4db 100644 --- a/src/firetower/settings.py +++ b/src/firetower/settings.py @@ -87,12 +87,14 @@ def dev_default(key: str, default: str = "") -> str: "django.contrib.staticfiles", "rest_framework", "corsheaders", + "djk8s", "firetower.auth", "firetower.incidents", "firetower.integrations", ] MIDDLEWARE = [ + "djk8s.middleware.ProbeMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "corsheaders.middleware.CorsMiddleware", @@ -223,3 +225,8 @@ def dev_default(key: str, default: str = "") -> str: client = google.cloud.logging.Client() client.setup_logging() + +# django-k8s proreadiness probes +DJK8S_READINESS_PROBES = [ + "djk8s.probes.DatabaseProbe", +] diff --git a/uv.lock b/uv.lock index c5e0cc8d..999292d4 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.14'", @@ -279,6 +279,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/d8/19ed1e47badf477d17fb177c1c19b5a21da0fd2d9f093f23be3fb86c5fab/django_cors_headers-4.9.0-py3-none-any.whl", hash = "sha256:15c7f20727f90044dcee2216a9fd7303741a864865f0c3657e28b7056f61b449", size = 12809, upload-time = "2025-09-18T10:40:50.843Z" }, ] +[[package]] +name = "django-kubernetes" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/6c/3a5bf9a360e14612dfa581d055cc27e78e35954b0edf17cb02c22f3ca154/django_kubernetes-1.1.0.tar.gz", hash = "sha256:6ca3e3b85c8cd7c11057156398d74398c585a7d782ad585a65463e749d2d86ed", size = 24804, upload-time = "2025-06-18T16:47:01.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d0/14e763857e44fc8d846f786527c777d6b85d649e5d990e29cf2b468a91d9/django_kubernetes-1.1.0-py3-none-any.whl", hash = "sha256:068cca992a6b3f8030774618ce23ee22cf68b565a35faa46bb6ccd97f034c029", size = 13731, upload-time = "2025-06-18T16:46:59.95Z" }, +] + [[package]] name = "django-stubs" version = "5.2.7" @@ -336,6 +348,7 @@ dependencies = [ { name = "cryptography" }, { name = "django" }, { name = "django-cors-headers" }, + { name = "django-kubernetes" }, { name = "djangorestframework" }, { name = "google-auth" }, { name = "google-cloud-logging" }, @@ -366,6 +379,7 @@ requires-dist = [ { name = "cryptography", specifier = ">=46.0.3" }, { name = "django", specifier = ">=5.2.5" }, { name = "django-cors-headers", specifier = ">=4.9.0" }, + { name = "django-kubernetes", specifier = ">=1.1.0" }, { name = "djangorestframework", specifier = ">=3.15.2" }, { name = "google-auth", specifier = ">=2.37.0" }, { name = "google-cloud-logging", specifier = ">=3.11.4" }, From bd769ea3643af82d8b5a38c104dc1a27d8145d9e Mon Sep 17 00:00:00 2001 From: Taylor Osler Date: Thu, 6 Nov 2025 16:10:59 -0800 Subject: [PATCH 3/8] fix nginx config structure --- nginx.conf | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/nginx.conf b/nginx.conf index 2f9deb64..d5759995 100644 --- a/nginx.conf +++ b/nginx.conf @@ -29,30 +29,30 @@ http { #gzip on; #include /etc/nginx/conf.d/*.conf; -} -server { - listen 80; - server_name localhost .firetower.getsentry.net; + server { + listen 80; + server_name localhost .firetower.getsentry.net; - access_log /var/log/nginx/host.access.log main; + access_log /var/log/nginx/host.access.log main; - location / { - root /app/static/ - index index.html index.htm; - try_files $uri $uri/ /index.html; - } + location / { + root /app/static/ + index index.html index.htm; + try_files $uri $uri/ /index.html; + } - #error_page 404 /404.html; + #error_page 404 /404.html; - # redirect server error pages to the static page /50x.html - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } + # redirect server error pages to the static page /50x.html + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } - # proxy to granian backend - location @backend { - proxy_pass http://127.0.0.1:8080; + # proxy to granian backend + location @backend { + proxy_pass http://127.0.0.1:8080; + } } } From 03cb41ac2c0905cf95605091474ad0d20648d04f Mon Sep 17 00:00:00 2001 From: Taylor Osler Date: Thu, 6 Nov 2025 16:38:12 -0800 Subject: [PATCH 4/8] nginx permission fixes --- docker/static.Dockerfile | 2 ++ nginx.conf | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docker/static.Dockerfile b/docker/static.Dockerfile index 0236cb1d..76f0c725 100644 --- a/docker/static.Dockerfile +++ b/docker/static.Dockerfile @@ -41,6 +41,8 @@ RUN bun run build FROM nginx:1.29.3-alpine3.22 +RUN chown 101:101 -R /var/cache/nginx/ /var/log/nginx/ +RUN touch /run/nginx.pid && chown 101:101 /run/nginx.pid USER 101 COPY --from=build_frontend --chown=101 /app/dist /app/static diff --git a/nginx.conf b/nginx.conf index d5759995..0a12082e 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,4 +1,4 @@ -user nginx; +# user nginx; worker_processes auto; error_log /var/log/nginx/error.log notice; @@ -37,7 +37,7 @@ http { access_log /var/log/nginx/host.access.log main; location / { - root /app/static/ + root /app/static/; index index.html index.htm; try_files $uri $uri/ /index.html; } From 74e9dd954fc213491c5f0fc7917dbce522d2d7fd Mon Sep 17 00:00:00 2001 From: Taylor Osler Date: Thu, 6 Nov 2025 17:16:58 -0800 Subject: [PATCH 5/8] nginx: more explicit proxy paths --- nginx.conf | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/nginx.conf b/nginx.conf index 0a12082e..a9565df6 100644 --- a/nginx.conf +++ b/nginx.conf @@ -39,7 +39,17 @@ http { location / { root /app/static/; index index.html index.htm; - try_files $uri $uri/ /index.html; + try_files $uri $uri/ /index.html @backend; + } + + location /api { + proxy_pass http://127.0.0.1:8080; + } + location /admin { + proxy_pass http://127.0.0.1:8080; + } + location @backend { + proxy_pass http://127.0.0.1:8080; } #error_page 404 /404.html; @@ -49,10 +59,5 @@ http { location = /50x.html { root /usr/share/nginx/html; } - - # proxy to granian backend - location @backend { - proxy_pass http://127.0.0.1:8080; - } } } From 9549e2f872a565e81aa7cec0b9ed04497a2a262c Mon Sep 17 00:00:00 2001 From: Taylor Osler Date: Thu, 6 Nov 2025 17:48:26 -0800 Subject: [PATCH 6/8] django: adjust static asset path --- src/firetower/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/firetower/settings.py b/src/firetower/settings.py index 5665d4db..94dfa003 100644 --- a/src/firetower/settings.py +++ b/src/firetower/settings.py @@ -174,7 +174,7 @@ def dev_default(key: str, default: str = "") -> str: # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.2/howto/static-files/ -STATIC_URL = "static/backend/" +STATIC_URL = "backend/" STATIC_ROOT = "static/backend/" # Default primary key field type From efbac091464cd426494b8d8be200a6fd6ede843b Mon Sep 17 00:00:00 2001 From: Taylor Osler Date: Fri, 7 Nov 2025 15:38:51 -0800 Subject: [PATCH 7/8] simplify nginx config --- nginx.conf | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/nginx.conf b/nginx.conf index a9565df6..75fcd060 100644 --- a/nginx.conf +++ b/nginx.conf @@ -39,7 +39,7 @@ http { location / { root /app/static/; index index.html index.htm; - try_files $uri $uri/ /index.html @backend; + try_files $uri $uri/ /index.html; } location /api { @@ -48,9 +48,6 @@ http { location /admin { proxy_pass http://127.0.0.1:8080; } - location @backend { - proxy_pass http://127.0.0.1:8080; - } #error_page 404 /404.html; From c8f49a4c6643f7a98b78825dac1029ced548f80b Mon Sep 17 00:00:00 2001 From: Taylor Osler Date: Thu, 6 Nov 2025 14:46:37 -0800 Subject: [PATCH 8/8] [ci] build and deploy nginx --- .github/workflows/deploy.yml | 62 +++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 57b779a4..d05e93a1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,13 +15,11 @@ on: required: true env: - IMAGE_REF: us-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT_SLUG }}/firetower-docker/firetower:${{ github.sha }} + BACKEND_IMAGE_REF: us-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT_SLUG }}/firetower-docker/firetower:${{ github.sha }} + STATIC_IMAGE_REF: us-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT_SLUG }}/firetower-docker/nginx:${{ github.sha }} jobs: - ci: - uses: ./.github/workflows/ci.yml - deploy: - needs: [ci] + build: runs-on: ubuntu-latest steps: - name: Checkout code @@ -35,13 +33,34 @@ jobs: run: gcloud auth configure-docker us-west1-docker.pkg.dev - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Build and push + - name: Build and push (backend) + uses: docker/build-push-action@v6 + with: + push: true + tags: ${{ env.BACKEND_IMAGE_REF }} + file: docker/backend.Dockerfile + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Build and push (static assets) uses: docker/build-push-action@v6 with: push: true - tags: ${{ env.IMAGE_REF }} + tags: ${{ env.STATIC_IMAGE_REF }} + file: docker/static.Dockerfile cache-from: type=gha cache-to: type=gha,mode=max + + deploy: + needs: [build] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + - name: Authenticate with GCP + uses: 'google-github-actions/auth@v3' + with: + project_id: ${{ secrets.GCP_PROJECT_SLUG }} + workload_identity_provider: projects/${{ secrets.GCP_PROJECT_NUM }}/locations/global/workloadIdentityPools/github/providers/github-prvdr - name: deploy-test-db-migration if: ${{ inputs.environment == 'test' }} uses: "google-github-actions/deploy-cloudrun@v3" @@ -49,25 +68,29 @@ jobs: job: firetower-test-db-migration project_id: ${{ secrets.GCP_PROJECT_SLUG }} region: us-west1 - image: ${{ env.IMAGE_REF }} + image: ${{ env.BACKEND_IMAGE_REF }} wait: true - - name: deploy-prod-db-migration - if: ${{ (github.ref == 'refs/heads/main' && !inputs.environment ) || inputs.environment == 'prod' }} + - name: deploy-test + if: ${{ inputs.environment == 'test' }} uses: "google-github-actions/deploy-cloudrun@v3" with: - job: firetower-prod-db-migration + service: firetower-test project_id: ${{ secrets.GCP_PROJECT_SLUG }} region: us-west1 - image: ${{ env.IMAGE_REF }} - wait: true - - name: deploy-test - if: ${{ inputs.environment == 'test' }} + # NOTE: we use flags here because the action does not natively support updating multiple containers. + flags: > + --container nginx --image "${{ env.STATIC_IMAGE_REF }}" --port 80 + --container firetower-backend --image "${{ env.BACKEND_IMAGE_REF }}" + + - name: deploy-prod-db-migration + if: ${{ (github.ref == 'refs/heads/main' && !inputs.environment ) || inputs.environment == 'prod' }} uses: "google-github-actions/deploy-cloudrun@v3" with: - service: firetower-test + job: firetower-prod-db-migration project_id: ${{ secrets.GCP_PROJECT_SLUG }} region: us-west1 - image: ${{ env.IMAGE_REF }} + image: ${{ env.BACKEND_IMAGE_REF }} + wait: true - name: deploy-prod if: ${{ (github.ref == 'refs/heads/main' && !inputs.environment ) || inputs.environment == 'prod' }} uses: "google-github-actions/deploy-cloudrun@v3" @@ -75,4 +98,7 @@ jobs: service: firetower project_id: ${{ secrets.GCP_PROJECT_SLUG }} region: us-west1 - image: ${{ env.IMAGE_REF }} + # NOTE: we use flags here because the action does not natively support updating multiple containers. + flags: > + --container nginx --image "${{ env.STATIC_IMAGE_REF }}" --port 80 + --container firetower-backend --image "${{ env.BACKEND_IMAGE_REF }}"