From 058caf9807b3cd57b05419a90618d191ae2ee4ad Mon Sep 17 00:00:00 2001
From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com>
Date: Mon, 2 Oct 2023 13:50:11 +1100
Subject: [PATCH 1/7] Add nginx proxy for frontend

---
 docker-compose.yml           | 17 ++++++++++
 frontend/Dockerfile          | 22 ++++++++++--
 frontend/nginx.conf.template | 65 ++++++++++++++++++++++++++++++++++++
 justfile                     |  4 +++
 4 files changed, 105 insertions(+), 3 deletions(-)
 create mode 100644 frontend/nginx.conf.template

diff --git a/docker-compose.yml b/docker-compose.yml
index 7ff70da8bf7..992d9c2d5ae 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -318,6 +318,23 @@ services:
       - ./docker/nginx/templates:/etc/nginx/templates
       - ./docker/nginx/certs:/etc/nginx/certs
 
+  frontend_nginx:
+    profiles:
+      - frontend
+    build:
+      context: ./frontend/
+      additional_contexts:
+        repo_root: .
+      target: nginx
+      args: # Automatically inferred from env vars, unless specified
+        - SEMANTIC_VERSION=${SEMANTIC_VERSION:-v1.0.0}
+        - FRONTEND_NODE_VERSION
+        - FRONTEND_PNPM_VERSION
+    ports:
+      - "50290:8080"
+    environment:
+      OPENVERSE_NGINX_UPSTREAM_URL: ${HOST_NETWORK_ADDRESS}:8443
+
 volumes:
   api-postgres:
   catalog-postgres:
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index 692af03504c..186696ea1bf 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -48,9 +48,6 @@ ENV NODE_ENV=production
 ENV SENTRY_DSN=https://b6466b74788a4a2f8a7912eea912beb7@o787041.ingest.sentry.io/5799642
 
 ARG API_URL
-ARG SEMANTIC_VERSION
-
-RUN echo "{\"release\":\"${SEMANTIC_VERSION}\"}" > /home/node/frontend/src/static/version.json
 
 RUN pnpm build:only
 
@@ -84,3 +81,22 @@ ENV PORT=8443
 EXPOSE 8443
 
 ENTRYPOINT [ "npm", "start", "--" ]
+
+#########
+# NGINX #
+#########
+
+FROM docker.io/nginx:1.25.2-alpine as nginx
+
+LABEL org.opencontainers.image.source = "https://github.com/WordPress/openverse"
+
+WORKDIR /app
+
+COPY nginx.conf.template /etc/nginx/templates/openverse-frontend.conf.template
+
+# Only environment variables with this prefix will be available in the template
+ENV NGINX_ENVSUBST_FILTER="OPENVERSE_NGINX_"
+ENV OPENVERSE_NGINX_ENVIRONMENT="local"
+# Add the release version to the docker container
+ARG SEMANTIC_VERSION
+ENV OPENVERSE_NGINX_GIT_REVISION=$SEMANTIC_VERSION
diff --git a/frontend/nginx.conf.template b/frontend/nginx.conf.template
new file mode 100644
index 00000000000..861220082de
--- /dev/null
+++ b/frontend/nginx.conf.template
@@ -0,0 +1,65 @@
+# The following variables are substituted into the environment using envsubst.
+# This occurs automatically as part of the nginx image startup.
+# See: https://github.com/nginxinc/docker-nginx/blob/456bf337ceb922a207651aa7c6077a316c3e368c/entrypoint/20-envsubst-on-templates.sh#L17
+# - OPENVERSE_NGINX_UPSTREAM_URL
+# - OPENVERSE_NGINX_GIT_REVISION
+# - OPENVERSE_NGINX_ENVIRONMENT
+
+error_log /var/log/nginx/error.log;
+
+log_format json_combined escape=json
+    '{'
+    '"time_local":"$time_local",'
+    '"remote_addr":"$remote_addr",'
+    '"remote_user":"$remote_user",'
+    '"request":"$request",'
+    '"status": "$status",'
+    '"host_header": "$host",'
+    '"body_bytes_sent":$body_bytes_sent,'
+    '"request_time":"$request_time",'
+    '"http_referrer":"$http_referer",'
+    '"http_user_agent":"$http_user_agent",'
+    '"upstream_response_time":$upstream_response_time,'
+    '"http_x_forwarded_for":"$http_x_forwarded_for"'
+    '}';
+
+access_log  /var/log/nginx/access.log  json_combined;
+
+tcp_nopush          on;
+tcp_nodelay         on;
+types_hash_max_size 2048;
+
+# Compress large responses to save bandwidth and improve latency
+gzip on;
+gzip_min_length 860;
+gzip_vary on;
+gzip_proxied expired private auth;
+gzip_types application/json text/plain application/javascript;
+gzip_disable "MSIE [1-6]\.";
+
+upstream ov_service {
+    server          $OPENVERSE_NGINX_UPSTREAM_URL;
+}
+
+server {
+    listen          8080;
+    server_name     _;
+    charset        utf-8;
+    client_max_body_size 75M;
+    error_page 500 /500.json;
+    absolute_redirect off;
+
+    location / {
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header Host $http_host;
+        proxy_redirect off;
+        proxy_pass http://ov_service$request_uri;
+        error_page 500 /500.json;
+    }
+
+    location /version {
+        default_type "application/json";
+        return 200 '{"release": "$OPENVERSE_NGINX_GIT_REVISION", "environment": "$OPENVERSE_NGINX_ENVIRONMENT"}';
+    }
+}
diff --git a/justfile b/justfile
index 7d74a8f4cb4..cf1da5c2a2e 100644
--- a/justfile
+++ b/justfile
@@ -10,6 +10,8 @@ PROD_ENV := env_var_or_default("PROD_ENV", "")
 IS_CI := env_var_or_default("CI", "")
 DC_USER := env_var_or_default("DC_USER", "opener")
 
+export HOST_NETWORK_ADDRESS := if os() == "macos" { "host.docker.internal" } else { "172.17.0.1" }
+
 # Show all available recipes, also recurses inside nested justfiles
 @_default:
     just --list --unsorted
@@ -123,6 +125,8 @@ export CATALOG_PY_VERSION := `just catalog/py-version`
 export CATALOG_AIRFLOW_VERSION := `just catalog/airflow-version`
 export API_PY_VERSION := `just api/py-version`
 export INGESTION_PY_VERSION := `just ingestion_server/py-version`
+export FRONTEND_NODE_VERSION := `just frontend/node-version`
+export FRONTEND_PNPM_VERSION := `just frontend/pnpm-version`
 
 versions:
     #!/usr/bin/env bash

From a43f76590b77bf08573dd431b8bac0b87d4d09b9 Mon Sep 17 00:00:00 2001
From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com>
Date: Mon, 2 Oct 2023 14:24:28 +1100
Subject: [PATCH 2/7] Forward plausible requests with Nginx, removing this
 responsibility from Nuxt

---
 docker-compose.yml                        |  4 +-
 documentation/frontend/reference/nginx.md | 24 ++++++++++
 frontend/Dockerfile                       | 19 --------
 frontend/Dockerfile.nginx                 | 29 +++++++++++++
 frontend/nginx.conf.template              | 19 +++++++-
 frontend/nuxt.config.ts                   | 53 +++++------------------
 6 files changed, 85 insertions(+), 63 deletions(-)
 create mode 100644 documentation/frontend/reference/nginx.md
 create mode 100644 frontend/Dockerfile.nginx

diff --git a/docker-compose.yml b/docker-compose.yml
index 992d9c2d5ae..2634a59472b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -323,8 +323,7 @@ services:
       - frontend
     build:
       context: ./frontend/
-      additional_contexts:
-        repo_root: .
+      dockerfile: Dockerfile.nginx
       target: nginx
       args: # Automatically inferred from env vars, unless specified
         - SEMANTIC_VERSION=${SEMANTIC_VERSION:-v1.0.0}
@@ -334,6 +333,7 @@ services:
       - "50290:8080"
     environment:
       OPENVERSE_NGINX_UPSTREAM_URL: ${HOST_NETWORK_ADDRESS}:8443
+      OPENVERSE_NGINX_PLAUSIBLE_EVENT_URL: http://plausible:8000/api/event
 
 volumes:
   api-postgres:
diff --git a/documentation/frontend/reference/nginx.md b/documentation/frontend/reference/nginx.md
new file mode 100644
index 00000000000..bbba4f62856
--- /dev/null
+++ b/documentation/frontend/reference/nginx.md
@@ -0,0 +1,24 @@
+# Frontend reverse proxy
+
+The frontend service relies on an Nginx reverse proxy in live environments to
+facilitate per-request logging in a format that matches other services we run
+(namely, Django). It also acts as a [proxy for Plausible](plausible_proxy),
+removing the need for Nuxt to handle these requests, freeing it up to handle
+rendering SSR requests.
+
+[plausible_proxy]: https://plausible.io/docs/proxy/guides/nginx
+
+Additionally, the reverse proxy may be used in the future for the following:
+
+- Serving static Nuxt content, freeing Nuxt from serving these requests. This is
+  considered relatively low value due to Cloudflare caching already handling 99%
+  of the benefit this would bring for the vast majority of cases.
+
+## Testing
+
+To test the frontend reverse proxy locally, run `just frontend/up`. To test the
+integration with your local Plausible container, follow the existing
+instructions in [the frontend analytics documentation](analytics_docs).
+Everything should "just work".
+
+[analytics_docs]: /frontend/guides/analytics.md#plausible-set-up-and-first-run
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index 186696ea1bf..5c575e939f6 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -81,22 +81,3 @@ ENV PORT=8443
 EXPOSE 8443
 
 ENTRYPOINT [ "npm", "start", "--" ]
-
-#########
-# NGINX #
-#########
-
-FROM docker.io/nginx:1.25.2-alpine as nginx
-
-LABEL org.opencontainers.image.source = "https://github.com/WordPress/openverse"
-
-WORKDIR /app
-
-COPY nginx.conf.template /etc/nginx/templates/openverse-frontend.conf.template
-
-# Only environment variables with this prefix will be available in the template
-ENV NGINX_ENVSUBST_FILTER="OPENVERSE_NGINX_"
-ENV OPENVERSE_NGINX_ENVIRONMENT="local"
-# Add the release version to the docker container
-ARG SEMANTIC_VERSION
-ENV OPENVERSE_NGINX_GIT_REVISION=$SEMANTIC_VERSION
diff --git a/frontend/Dockerfile.nginx b/frontend/Dockerfile.nginx
new file mode 100644
index 00000000000..d8854202f0a
--- /dev/null
+++ b/frontend/Dockerfile.nginx
@@ -0,0 +1,29 @@
+# Keeping this as a separate Dockerfile prevents the need to rely on additional
+# contexts required for the frontend build (namely, `repo_root`). These are
+# possible with compose as of https://github.com/docker/compose/pull/10369
+# and do appear to work, but it increases the complexity and these features
+# are not yet all available in every containerd implementation or distribution
+# (Debian, for example, still distributes an older version of compose without
+# this new feature). For the sake of keeping the local setup simpler and not
+# requiring contributors to have bleeding-edge Docker and compose plugin versions,
+# we can simply use a separate file for the time being. Once/if we start to use
+# nginx to serve static files for the frontend, we will need to integrate this
+# target into the shared Dockerfile.
+#########
+# NGINX #
+#########
+
+FROM docker.io/nginx:1.25.2-alpine as nginx
+
+LABEL org.opencontainers.image.source = "https://github.com/WordPress/openverse"
+
+WORKDIR /app
+
+COPY nginx.conf.template /etc/nginx/templates/openverse-frontend.conf.template
+
+# Only environment variables with this prefix will be available in the template
+ENV NGINX_ENVSUBST_FILTER="OPENVERSE_NGINX_"
+ENV OPENVERSE_NGINX_ENVIRONMENT="local"
+# Add the release version to the docker container
+ARG SEMANTIC_VERSION
+ENV OPENVERSE_NGINX_GIT_REVISION=$SEMANTIC_VERSION
diff --git a/frontend/nginx.conf.template b/frontend/nginx.conf.template
index 861220082de..341ee7f1c5c 100644
--- a/frontend/nginx.conf.template
+++ b/frontend/nginx.conf.template
@@ -4,6 +4,7 @@
 # - OPENVERSE_NGINX_UPSTREAM_URL
 # - OPENVERSE_NGINX_GIT_REVISION
 # - OPENVERSE_NGINX_ENVIRONMENT
+# - OPENVERSE_NGINX_PLAUSIBLE_EVENT_URL
 
 error_log /var/log/nginx/error.log;
 
@@ -54,7 +55,7 @@ server {
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_set_header Host $http_host;
         proxy_redirect off;
-        proxy_pass http://ov_service$request_uri;
+        proxy_pass http://ov_service;
         error_page 500 /500.json;
     }
 
@@ -62,4 +63,20 @@ server {
         default_type "application/json";
         return 200 '{"release": "$OPENVERSE_NGINX_GIT_REVISION", "environment": "$OPENVERSE_NGINX_ENVIRONMENT"}';
     }
+
+    # This is Docker's internal DNS resolver
+    # In localhost it will resolve the local Plausible and forward anything else to the host
+    # In production, AWS replaces
+    resolver 127.0.0.11;
+    set $plausible_event_url $OPENVERSE_NGINX_PLAUSIBLE_EVENT_URL;
+    location = /api/event {
+        proxy_pass $plausible_event_url;
+        proxy_set_header Host plausible.io;
+        proxy_buffering on;
+        proxy_http_version 1.1;
+
+        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header X-Forwarded-Host  $host;
+    }
 }
diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts
index a48a008de9e..734a9a60adf 100644
--- a/frontend/nuxt.config.ts
+++ b/frontend/nuxt.config.ts
@@ -15,11 +15,8 @@ import type http from "http"
 
 import type { NuxtConfig } from "@nuxt/types"
 import type { LocaleObject } from "@nuxtjs/i18n"
-import type { Options as ProxyOptions } from "http-proxy-middleware"
 import type { IncomingMessage, NextFunction } from "connect"
 
-let plausibleLogged = false
-
 if (process.env.NODE_ENV === "production") {
   meta.push({
     // @ts-expect-error: 'http-equiv' isn't allowed here by Nuxt
@@ -306,43 +303,6 @@ const config: NuxtConfig = {
       },
     },
   },
-  proxy: {
-    // The key is appended to the address in the value.
-    "/api/event": {
-      target:
-        process.env.PLAUSIBLE_ORIGIN ??
-        (isProdNotPlaywright
-          ? "https://plausible.io"
-          : "http://localhost:50288"),
-      // Prevent ECONNREFUSED errors in the server console.
-      logProvider: () => {
-        return {
-          ...console,
-          error: isProdNotPlaywright
-            ? console.error
-            : (...data: unknown[]) => {
-                if (
-                  !plausibleLogged &&
-                  data.some(
-                    (item) =>
-                      typeof item === "string" && item.includes("ECONNREFUSED")
-                  )
-                ) {
-                  console.warn("Plausible is not running.")
-                  plausibleLogged = true
-                }
-              },
-        }
-      },
-      // Prevent 504 errors in the browser console.
-      onError: (err, _req, res) => {
-        if (!isProdNotPlaywright && err.message.includes("ECONNREFUSED")) {
-          res.writeHead(200, { "Content-Type": "text/plain" })
-          res.end("plausible not running")
-        }
-      },
-    } satisfies ProxyOptions,
-  },
   plausible: {
     trackLocalhost: !isProdNotPlaywright,
   },
@@ -356,7 +316,18 @@ const config: NuxtConfig = {
         process.env.SITE_DOMAIN ??
         (isProdNotPlaywright
           ? "https://openverse.org"
-          : `http://localhost:${port}`),
+          : /**
+             * We rely on the Nginx container running as `frontend_nginx`
+             * in the local compose stack to proxy requests. Therefore, the
+             * URL here is not for the Plausible container in the local stack,
+             * but the Nginx service, which then itself forwards the requests
+             * to the local Plausible instance.
+             *
+             * In production, the Nginx container is handling all requests
+             * made to the root URL (openverse.org), and is configured to
+             * forward Plausible requests to upstream Plausible.
+             */
+            "http://localhost:50290"),
     },
     sentry: {
       config: {

From a108fdcbe182edc1a769e0462ff86438609685e1 Mon Sep 17 00:00:00 2001
From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com>
Date: Mon, 2 Oct 2023 16:44:38 +1100
Subject: [PATCH 3/7] Build and publish frontend_nginx from CI

---
 .github/workflows/ci_cd.yml                     |  3 ++-
 .../python/workflows/set_matrix_images.py       | 17 +++++++++++++++--
 2 files changed, 17 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml
index b0ff59ec849..f62a57c8974 100644
--- a/.github/workflows/ci_cd.yml
+++ b/.github/workflows/ci_cd.yml
@@ -199,10 +199,11 @@ jobs:
       - name: Build image `${{ matrix.image }}`
         uses: docker/build-push-action@v5
         with:
-          context: ${{ matrix.context || matrix.image }}
+          context: ${{ matrix.context }}
           target: ${{ matrix.target }}
           push: false
           tags: openverse-${{ matrix.image }}
+          file: ${{ matrix.file }}
           cache-from: type=gha,scope=${{ matrix.image }}
           cache-to: type=gha,scope=${{ matrix.image }}
           outputs: type=docker,dest=/tmp/${{ matrix.image }}.tar
diff --git a/automations/python/workflows/set_matrix_images.py b/automations/python/workflows/set_matrix_images.py
index ac51e79b797..e5d181910a3 100644
--- a/automations/python/workflows/set_matrix_images.py
+++ b/automations/python/workflows/set_matrix_images.py
@@ -32,6 +32,12 @@ def ser_set(x):
     "api": {"image": "api", "target": "api"},
     "api_nginx": {"image": "api_nginx", "context": "api", "target": "nginx"},
     "frontend": {"image": "frontend", "target": "app", "build-contexts": "repo_root=."},
+    "frontend_nginx": {
+        "image": "frontend_nginx",
+        "context": "frontend",
+        "file": "frontend/Dockerfile.nginx",
+        "target": "nginx",
+    },
 }
 
 if "ci_cd" in changes:
@@ -46,11 +52,18 @@ def ser_set(x):
     build_matrix["image"] |= {"upstream_db", "ingestion_server", "api", "api_nginx"}
     publish_matrix["image"] |= {"api", "api_nginx"}
 if "frontend" in changes:
-    build_matrix["image"].add("frontend")
-    publish_matrix["image"].add("frontend")
+    build_matrix["image"] |= {"frontend", "frontend_nginx"}
+    publish_matrix["image"] |= {"frontend", "frontend_nginx"}
 
 build_matrix["include"] = [includes[item] for item in build_matrix["image"]]
 
+for item in build_matrix:
+    if "context" not in item:
+        item["context"] = item["image"]
+
+    if "file" not in item:
+        item["file"] = f"{item['context']}/Dockerfile"
+
 do_build = "true" if len(build_matrix["image"]) else "false"
 do_publish = "true" if len(publish_matrix["image"]) else "false"
 build_matrix = json.dumps(build_matrix, default=ser_set)

From e3f26b66350c461b1b7011e8eb8947917c6999bd Mon Sep 17 00:00:00 2001
From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com>
Date: Mon, 2 Oct 2023 18:10:20 +1100
Subject: [PATCH 4/7] Fix analytics playwright tracking

---
 frontend/test/playwright/utils/analytics.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/frontend/test/playwright/utils/analytics.ts b/frontend/test/playwright/utils/analytics.ts
index 32580990a73..f9cf6cfe94a 100644
--- a/frontend/test/playwright/utils/analytics.ts
+++ b/frontend/test/playwright/utils/analytics.ts
@@ -27,7 +27,7 @@ export function expectEventPayloadToMatch<T extends EventName>(
 
 export const collectAnalyticsEvents = (context: BrowserContext) => {
   const sentAnalyticsEvents: AnalyticEventResponses = []
-  context.route("/api/event", (route, request) => {
+  context.route(/\/api\/event$/, (route, request) => {
     const postData = request.postData()
     if (postData) {
       const parsedData = JSON.parse(postData)

From 7ee3e6a52b07bf892b53f813872af6647b69b85e Mon Sep 17 00:00:00 2001
From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com>
Date: Tue, 3 Oct 2023 06:43:07 +1100
Subject: [PATCH 5/7] Fix iteration

---
 automations/python/workflows/set_matrix_images.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/automations/python/workflows/set_matrix_images.py b/automations/python/workflows/set_matrix_images.py
index e5d181910a3..cbaddce1511 100644
--- a/automations/python/workflows/set_matrix_images.py
+++ b/automations/python/workflows/set_matrix_images.py
@@ -57,7 +57,7 @@ def ser_set(x):
 
 build_matrix["include"] = [includes[item] for item in build_matrix["image"]]
 
-for item in build_matrix:
+for item in build_matrix["include"]:
     if "context" not in item:
         item["context"] = item["image"]
 

From a647836c30945b4d2d0017450d10bba03f923a8b Mon Sep 17 00:00:00 2001
From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com>
Date: Tue, 3 Oct 2023 06:59:42 +1100
Subject: [PATCH 6/7] Fix docs build

---
 documentation/frontend/reference/index.md | 1 +
 documentation/frontend/reference/nginx.md | 4 ++--
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/documentation/frontend/reference/index.md b/documentation/frontend/reference/index.md
index ced85c51370..5eafbc596ad 100644
--- a/documentation/frontend/reference/index.md
+++ b/documentation/frontend/reference/index.md
@@ -10,4 +10,5 @@ playwright_tests
 storybook_tests
 miscellaneous
 healthcheck
+nginx
 ```
diff --git a/documentation/frontend/reference/nginx.md b/documentation/frontend/reference/nginx.md
index bbba4f62856..d2cad7fc968 100644
--- a/documentation/frontend/reference/nginx.md
+++ b/documentation/frontend/reference/nginx.md
@@ -2,7 +2,7 @@
 
 The frontend service relies on an Nginx reverse proxy in live environments to
 facilitate per-request logging in a format that matches other services we run
-(namely, Django). It also acts as a [proxy for Plausible](plausible_proxy),
+(namely, Django). It also acts as a [proxy for Plausible][plausible_proxy],
 removing the need for Nuxt to handle these requests, freeing it up to handle
 rendering SSR requests.
 
@@ -18,7 +18,7 @@ Additionally, the reverse proxy may be used in the future for the following:
 
 To test the frontend reverse proxy locally, run `just frontend/up`. To test the
 integration with your local Plausible container, follow the existing
-instructions in [the frontend analytics documentation](analytics_docs).
+instructions in [the frontend analytics documentation][analytics_docs].
 Everything should "just work".
 
 [analytics_docs]: /frontend/guides/analytics.md#plausible-set-up-and-first-run

From bd22c48bc9a726725bee4c01d4899d311e1b4efe Mon Sep 17 00:00:00 2001
From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com>
Date: Tue, 10 Oct 2023 12:59:14 +1100
Subject: [PATCH 7/7] Update frontend/nginx.conf.template

---
 frontend/nginx.conf.template | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/frontend/nginx.conf.template b/frontend/nginx.conf.template
index 341ee7f1c5c..268104d0056 100644
--- a/frontend/nginx.conf.template
+++ b/frontend/nginx.conf.template
@@ -43,6 +43,8 @@ upstream ov_service {
 }
 
 server {
+    access_log  /var/log/nginx/access.log  json_combined;
+
     listen          8080;
     server_name     _;
     charset        utf-8;