Repository: discourse/discourse_docker
Pull request #959: Use Nginx config outlets in templates
Author: login: nbianca
Created: 2025-05-09T12:05:15Z | Merged: 2025-05-19T16:12:49Z

This reapplies commit 3c2234830a71e7590a0eaa9b73da46fd2c09ea71 with a fix to keep outlet files instead of removing them so `replace` continues to work.


Diff:
diff --git a/samples/standalone.yml b/samples/standalone.yml
index 3f43a6241..99f9060b7 100644
--- a/samples/standalone.yml
+++ b/samples/standalone.yml
@@ -11,8 +11,6 @@ templates:
   - "templates/postgres.template.yml"
   - "templates/redis.template.yml"
   - "templates/web.template.yml"
-  ## Uncomment the next line to enable the IPv6 listener
-  #- "templates/web.ipv6.template.yml"
   - "templates/web.ratelimited.template.yml"
   ## Uncomment these two lines if you wish to add Lets Encrypt (https)
   #- "templates/web.ssl.template.yml"
diff --git a/samples/web_only.yml b/samples/web_only.yml
index c4753dde0..d335c847e 100644
--- a/samples/web_only.yml
+++ b/samples/web_only.yml
@@ -3,8 +3,6 @@
 
 templates:
   - "templates/web.template.yml"
-  ## Uncomment the next line to enable the IPv6 listener
-  #- "templates/web.ipv6.template.yml"
   - "templates/web.ratelimited.template.yml"
   ## Uncomment these two lines if you wish to add Lets Encrypt (https)
   #- "templates/web.ssl.template.yml"
diff --git a/templates/offline-page.template.yml b/templates/offline-page.template.yml
index 9bcd7723f..7d035012d 100644
--- a/templates/offline-page.template.yml
+++ b/templates/offline-page.template.yml
@@ -7,17 +7,14 @@ params:
   offline_page_repository: https://github.com/discourse/discourse-offline-page.git
 
 run:
-  - replace:
-     filename: "/etc/nginx/conf.d/discourse.conf"
-     global: true
-     from: /server.+{/
-     to: |
-       server {
-         error_page 502 /error_page.html;
-         location /error_page.html {
-           root /var/www/discourse-offline-page/html;
-           internal;
-         }
+  - file:
+     path: "/etc/nginx/conf.d/outlets/server/30-offline-page.conf"
+     contents: |
+        error_page 502 /error_page.html;
+        location /error_page.html {
+          root /var/www/discourse-offline-page/html;
+          internal;
+        }
 
   - exec:
       cmd: git clone $offline_page_repository /var/www/discourse-offline-page
diff --git a/templates/sshd.template.yml b/templates/sshd.template.yml
index d9bf57c2a..b8f3f1587 100644
--- a/templates/sshd.template.yml
+++ b/templates/sshd.template.yml
@@ -1,2 +1,6 @@
 # This file is deprecated; you can remove it from your app.yml
+# TODO(2026-01-01): Remove this file
 run: 
+  - exec: |-
+      echo "Deprecation warning: sshd is no longer supported"
+      echo "Remove templates/sshd.template.yml from your containers/*.yml files"
diff --git a/templates/web.ipv6.template.yml b/templates/web.ipv6.template.yml
index bf589fe24..c429fc31c 100644
--- a/templates/web.ipv6.template.yml
+++ b/templates/web.ipv6.template.yml
@@ -1,8 +1,6 @@
+# This file is deprecated; you can remove it from your app.yml
+# TODO(2026-01-01): Remove this file
 run:
-  - exec: echo "Enabling IPv6 listener"
-  - replace:
-      filename: "/etc/nginx/conf.d/discourse.conf"
-      from: listen 80;
-      to: |
-        listen 80;
-        listen [::]:80;
+  - exec: |-
+      echo "Deprecation warning: IPv6 is enabled by default when possible"
+      echo "Remove templates/web.ipv6.template.yml from your containers/*.yml files"
diff --git a/templates/web.letsencrypt.ssl.template.yml b/templates/web.letsencrypt.ssl.template.yml
index ba5f55178..04412af37 100644
--- a/templates/web.letsencrypt.ssl.template.yml
+++ b/templates/web.letsencrypt.ssl.template.yml
@@ -106,13 +106,6 @@ hooks:
 
         /usr/sbin/nginx -c /etc/nginx/letsencrypt.conf -s stop
 
-    - replace:
-       filename: "/etc/nginx/conf.d/discourse.conf"
-       from: /ssl_certificate.+/
-       to: |
-         ssl_certificate /shared/ssl/$$ENV_DISCOURSE_HOSTNAME.cer;
-         ssl_certificate /shared/ssl/$$ENV_DISCOURSE_HOSTNAME_ecc.cer;
-
     - replace:
        filename: /shared/letsencrypt/account.conf
        from: /#?ACCOUNT_EMAIL=.+/
@@ -120,14 +113,15 @@ hooks:
          ACCOUNT_EMAIL=$$ENV_LETSENCRYPT_ACCOUNT_EMAIL
 
     - replace:
-       filename: "/etc/nginx/conf.d/discourse.conf"
-       from: /ssl_certificate_key.+/
+       filename: "/etc/nginx/conf.d/outlets/server/20-https.conf"
+       from: /ssl_certificate.+/
        to: |
-         ssl_certificate_key /shared/ssl/$$ENV_DISCOURSE_HOSTNAME.key;
-         ssl_certificate_key /shared/ssl/$$ENV_DISCOURSE_HOSTNAME_ecc.key;
+         ssl_certificate /shared/ssl/$$ENV_DISCOURSE_HOSTNAME.cer;
+         ssl_certificate /shared/ssl/$$ENV_DISCOURSE_HOSTNAME_ecc.cer;
 
     - replace:
-       filename: "/etc/nginx/conf.d/discourse.conf"
-       from: /add_header.+/
+       filename: "/etc/nginx/conf.d/outlets/server/20-https.conf"
+       from: /ssl_certificate_key.+/
        to: |
-         add_header Strict-Transport-Security 'max-age=63072000';
+         ssl_certificate_key /shared/ssl/$$ENV_DISCOURSE_HOSTNAME.key;
+         ssl_certificate_key /shared/ssl/$$ENV_DISCOURSE_HOSTNAME_ecc.key;
diff --git a/templates/web.ratelimited.template.yml b/templates/web.ratelimited.template.yml
index ae9cd636c..46bbd21e3 100644
--- a/templates/web.ratelimited.template.yml
+++ b/templates/web.ratelimited.template.yml
@@ -6,21 +6,18 @@ params:
   conn_per_ip: 20
 
 run:
-  - replace:
-     filename: "/etc/nginx/conf.d/discourse.conf"
-     from: /server.+{/
-     to: |
+  - file:
+     path: "/etc/nginx/conf.d/outlets/before-server/30-ratelimited.conf"
+     contents: |
        limit_req_zone $binary_remote_addr zone=flood:10m rate=$reqs_per_secondr/s;
        limit_req_zone $binary_remote_addr zone=bot:10m rate=$reqs_per_minuter/m;
        limit_req_status 429;
        limit_conn_zone $binary_remote_addr zone=connperip:10m;
        limit_conn_status 429;
-       server {
-  - replace:
-     filename: "/etc/nginx/conf.d/discourse.conf"
-     from: "/location @discourse {/"
-     to: |
-       location @discourse {
+
+  - file:
+     path: "/etc/nginx/conf.d/outlets/discourse/30-ratelimited.conf"
+     contents: |
          limit_conn connperip $conn_per_ip;
          limit_req zone=flood burst=$burst_per_second nodelay;
          limit_req zone=bot burst=$burst_per_minute nodelay;
diff --git a/templates/web.socketed.template.yml b/templates/web.socketed.template.yml
index e6748bae8..69458a1fe 100644
--- a/templates/web.socketed.template.yml
+++ b/templates/web.socketed.template.yml
@@ -12,14 +12,20 @@ run:
         #!/bin/bash
         rm -rf /shared/nginx.http*.sock
   - replace:
-     filename: "/etc/nginx/conf.d/discourse.conf"
-     from: /listen 80;/
+     filename: "/etc/nginx/conf.d/outlets/server/10-http.conf"
+     from: /listen 80;(\nlisten \[::\]:80;)?/
      to: |
        listen unix:/shared/nginx.http.sock;
        set_real_ip_from unix:;
   - replace:
-     filename: "/etc/nginx/conf.d/discourse.conf"
-     from: /listen 443 ssl;/
+     filename: "/etc/nginx/conf.d/outlets/server/20-https.conf"
+     from: /listen 443 ssl;(\nlisten \[::\]:443 ssl;)?/
      to: |
        listen unix:/shared/nginx.https.sock ssl;
        set_real_ip_from unix:;
+  - replace:
+     filename: "/etc/nginx/conf.d/outlets/before-server/20-redirect-http-to-https.conf"
+     from: /listen 80;(\nlisten \[::\]:80;)?/
+     to: |
+       listen unix:/shared/nginx.http.sock;
+       set_real_ip_from unix:;
diff --git a/templates/web.ssl.template.yml b/templates/web.ssl.template.yml
index 06c351468..71e6fe041 100644
--- a/templates/web.ssl.template.yml
+++ b/templates/web.ssl.template.yml
@@ -1,56 +1,48 @@
 run:
   - exec:
-     cmd:
-       - "mkdir -p /shared/ssl/"
-  - replace:
-     filename: "/etc/nginx/conf.d/discourse.conf"
-     from: /listen 80;\s+listen \[::\]:80;/m
-     to: |
-       listen 443 ssl;
-       listen [::]:443 ssl;
-       http2 on;
-       SSL_TEMPLATE_SSL_BLOCK
-  - replace:
-     filename: "/etc/nginx/conf.d/discourse.conf"
-     from: /listen 80;/
-     to: |
-       listen 443 ssl;
-       http2 on;
-       SSL_TEMPLATE_SSL_BLOCK
-  - replace:
-     hook: ssl
-     filename: "/etc/nginx/conf.d/discourse.conf"
-     from: /SSL_TEMPLATE_SSL_BLOCK/
-     to: |
+      cmd:
+        - "mkdir -p /shared/ssl/"
+  - file:
+      path: "/etc/nginx/conf.d/outlets/before-server/20-redirect-http-to-https.conf"
+      contents: |
+        server {
+          listen 80;
+          return 301 https://$$ENV_DISCOURSE_HOSTNAME$request_uri;
+        }
+  - file:
+      path: "/etc/nginx/conf.d/outlets/server/10-http.conf"
+      contents: ""
+  - file:
+      hook: ssl
+      path: "/etc/nginx/conf.d/outlets/server/20-https.conf"
+      contents: |
+        listen 443 ssl;
+        http2 on;
 
-       ssl_protocols TLSv1.2 TLSv1.3;
-       ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
-       ssl_prefer_server_ciphers off;
+        ssl_protocols TLSv1.2 TLSv1.3;
+        ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
+        ssl_prefer_server_ciphers off;
 
-       ssl_certificate /shared/ssl/ssl.crt;
-       ssl_certificate_key /shared/ssl/ssl.key;
+        ssl_certificate /shared/ssl/ssl.crt;
+        ssl_certificate_key /shared/ssl/ssl.key;
 
-       ssl_session_tickets off;
-       ssl_session_timeout 1d;
-       ssl_session_cache shared:SSL:1m;
+        ssl_session_tickets off;
+        ssl_session_timeout 1d;
+        ssl_session_cache shared:SSL:1m;
 
-       add_header Strict-Transport-Security 'max-age=31536000'; # remember the certificate for a year and automatically connect to HTTPS for this domain
+        add_header Strict-Transport-Security 'max-age=31536000';
 
-       if ($http_host != $$ENV_DISCOURSE_HOSTNAME) {
+        if ($http_host != $$ENV_DISCOURSE_HOSTNAME) {
           rewrite (.*) https://$$ENV_DISCOURSE_HOSTNAME$1 permanent;
-       }
-  - replace:
-     filename: "/etc/nginx/conf.d/discourse.conf"
-     from: "location @discourse {"
-     to: |
-       location @discourse {
-       add_header Strict-Transport-Security 'max-age=31536000'; # remember the certificate for a year and automatically connect to HTTPS for this domain
-  - replace:
-     filename: "/etc/nginx/conf.d/discourse.conf"
-     from: /server.+{/
-     to: |
-       server {
-         listen 80;
-         return 301 https://$$ENV_DISCOURSE_HOSTNAME$request_uri;
-       }
-       server {
+        }
+  - file:
+      path: "/etc/nginx/conf.d/outlets/discourse/20-https.conf"
+      contents: |
+        add_header Strict-Transport-Security 'max-age=31536000';
+  - exec:
+      cmd:
+        - |-
+          if [ -f "/proc/net/if_inet6" ] ; then
+            sed -i 's/listen 80;/listen 80;\nlisten [::]:80;/g' /etc/nginx/conf.d/outlets/before-server/20-redirect-http-to-https.conf
+            sed -i 's/listen 443 ssl;/listen 443 ssl;\nlisten [::]:443 ssl;/g' /etc/nginx/conf.d/outlets/server/20-https.conf
+          fi
diff --git a/templates/web.template.yml b/templates/web.template.yml
index 813a63f7f..7803055dc 100644
--- a/templates/web.template.yml
+++ b/templates/web.template.yml
@@ -136,32 +136,48 @@ run:
         - "rm /etc/nginx/sites-enabled/default"
         - "mkdir -p /var/nginx/cache"
 
+        # Stop building the container if the Nginx outlets are missing
+        - "grep -q 'outlets/before-server' /etc/nginx/conf.d/discourse.conf || ( >&2 echo 'The \"before-server\" Nginx outlet is missing. This version of discourse_docker is not compatible with the chosen Discourse version.' ; exit 1 )"
+        - "grep -q 'outlets/server' /etc/nginx/conf.d/discourse.conf || ( >&2 echo 'The \"server\" Nginx outlet is missing. This version of discourse_docker is not compatible with the chosen Discourse version.' ; exit 1 )"
+        - "grep -q 'outlets/discourse' /etc/nginx/conf.d/discourse.conf || ( >&2 echo 'The \"discourse\" Nginx outlet is missing. This version of discourse_docker is not compatible with the chosen Discourse version.' ; exit 1 )"
+
+        # Create placeholders for the Nginx outlets
+        - "mkdir -p /etc/nginx/conf.d/outlets/before-server"
+        - "touch /etc/nginx/conf.d/outlets/before-server/20-redirect-http-to-https.conf"
+        - "touch /etc/nginx/conf.d/outlets/before-server/30-ratelimited.conf"
+        - "mkdir -p /etc/nginx/conf.d/outlets/server"
+        - "touch /etc/nginx/conf.d/outlets/server/10-http.conf"
+        - "touch /etc/nginx/conf.d/outlets/server/20-https.conf"
+        - "touch /etc/nginx/conf.d/outlets/server/30-offline-page.conf"
+        - "mkdir -p /etc/nginx/conf.d/outlets/discourse"
+        - "touch /etc/nginx/conf.d/outlets/discourse/20-https.conf"
+        - "touch /etc/nginx/conf.d/outlets/discourse/30-ratelimited.conf"
+
   - replace:
       filename: /etc/nginx/nginx.conf
       from: pid /run/nginx.pid;
       to: daemon off;
 
   - replace:
-      filename: "/etc/nginx/conf.d/discourse.conf"
-      from: /upstream[^\}]+\}/m
-      to: "upstream discourse {
-        server 127.0.0.1:3000;
-      }"
-
-  - replace:
-      filename: "/etc/nginx/conf.d/discourse.conf"
-      from: /server_name.+$/
-      to: server_name _ ;
+      filename: "/etc/nginx/nginx.conf"
+      from: /worker_connections.+$/
+      to: worker_connections $nginx_worker_connections;
 
   - replace:
       filename: "/etc/nginx/conf.d/discourse.conf"
       from: /client_max_body_size.+$/
-      to: client_max_body_size $upload_size ;
+      to: client_max_body_size $upload_size;
 
-  - replace:
-      filename: "/etc/nginx/nginx.conf"
-      from: /worker_connections.+$/
-      to: worker_connections $nginx_worker_connections ;
+  - exec:
+      cmd:
+        # Move `listen 80` to an outlet
+        - sed -i 's#listen 80;##g' /etc/nginx/conf.d/discourse.conf
+        - |-
+          if [ -f "/proc/net/if_inet6" ]; then
+            echo "listen 80;\nlisten [::]:80;" > /etc/nginx/conf.d/outlets/server/10-http.conf
+          else
+            echo "listen 80;" > /etc/nginx/conf.d/outlets/server/10-http.conf
+          fi
 
   - exec:
       cmd: echo "done configuring web"
@@ -222,6 +238,7 @@ run:
       hook: assets_precompile
       cmd:
         - su discourse -c 'SKIP_EMBER_CLI_COMPILE=1 bundle exec rake themes:update assets:precompile'
+
   - replace:
       tag: precompile
       filename: /etc/service/unicorn/run


Reviews:
user: login: tgxworld
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T12:15:11Z
user: login: pfaffman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T12:40:15Z
user: login: nbianca
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T13:31:23Z
user: login: tgxworld
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T08:18:27Z

Review comments:
diff_hunk: @@ -23,3 +23,9 @@ run:
      to: |
        listen unix:/shared/nginx.https.sock ssl;
        set_real_ip_from unix:;
+  - replace:
+     filename: "/etc/nginx/conf.d/outlets/before-server/10-redirect-http-to-https.conf"
path: templates/web.socketed.template.yml
user: login: tgxworld
body: `20-https.conf` is only created in the `web.ssl.template.yml` so we suffer from a similar problem. If this template is used without the `web.ssl.template.yml`, this template will break. 


created_at: 2025-05-09T12:15:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,3 +23,9 @@ run:
      to: |
        listen unix:/shared/nginx.https.sock ssl;
        set_real_ip_from unix:;
+  - replace:
+     filename: "/etc/nginx/conf.d/outlets/before-server/10-redirect-http-to-https.conf"
path: templates/web.socketed.template.yml
user: login: pfaffman
body: Is there a reason not to just always create the socket? 
created_at: 2025-05-09T12:40:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,3 +23,9 @@ run:
      to: |
        listen unix:/shared/nginx.https.sock ssl;
        set_real_ip_from unix:;
+  - replace:
+     filename: "/etc/nginx/conf.d/outlets/before-server/10-redirect-http-to-https.conf"
path: templates/web.socketed.template.yml
user: login: nbianca
body: I have considered creating placeholders for all used templates. This made me reconsider and I defined all of them.
created_at: 2025-05-09T13:31:23Z
author_association: MEMBER

PR comments:
user: login: discoursebot
created_at: 2025-05-09T13:33:21Z
body: This pull request has been mentioned on **Discourse Meta**. There might be relevant details there:

https://meta.discourse.org/t/cannot-rebuild-app-after-recent-upgrade-05-07-2025/365213/16

author_association: NONE
user: login: discoursebot
created_at: 2025-05-10T13:02:06Z
body: This pull request has been mentioned on **Discourse Meta**. There might be relevant details there:

https://meta.discourse.org/t/daily-summary-1pm-utc/291852/471

author_association: NONE
user: login: MichaIng
created_at: 2025-05-11T21:47:44Z
body: Here is an alternative approach to apply related replacements via web and ssl hooks: #957
That way also the order in which templates are applied does not matter anymore.
author_association: CONTRIBUTOR
user: login: discoursebot
created_at: 2025-12-22T04:32:52Z
body: This pull request has been mentioned on **Discourse Meta**. There might be relevant details there:

https://meta.discourse.org/t/discourse-failure-to-renew-certificate/385148/15

author_association: NONE
user: login: discoursebot
created_at: 2025-12-22T21:02:10Z
body: This pull request has been mentioned on **Discourse Meta**. There might be relevant details there:

https://meta.discourse.org/t/daily-summary-9pm-utc/291850/723

author_association: NONE