How to configure Browsersync in Docker with an reverse proxy Apache network in WSL2

341 Views Asked by At

I'm trying to get Browsersync, WordPress and Apache working in Docker in WSL2 (Ubuntu). I had Browsersync working a couple of years ago in XAMPP on Windows but I realise this is an order of magnitude more complex.

Everything works apart from Browsersync, I can access WordPress in my browser on Windows via port 80 and 443 at mysite.localhost but if I access it with Browsersync at http://mysite.localhost:3000 the Apache logs don't respond and the browser returns the following:

This site can’t be reached
mysite.localhost unexpectedly closed the connection
ERR_CONNECTION_CLOSED

If I change a file, Browsersync sees the change in the logs but I can't get the browser to see anything. I've spent a week on it, the closest I've found is this question which the author answered themselves although they are using Nginx as a proxy and Laravel, not sure how much difference that makes.

Here are the relevant sections of my Docker Compose:

services:

  node:
    build:
      context: .
      dockerfile: ./docker/node/Dockerfile
    image: node:18.16.1-build
    container_name: node
    restart: always
    depends_on:
      - wordpress
    # user: "node"
    expose:
      - 3000
      - 3001
    volumes:
      - ./wordpress/source/themes/omikuji/src:/home/node/omikuji/src
      - ./wordpress/source/themes/omikuji/dist:/home/node/omikuji/dist
      - ./wordpress/source/themes/omikuji/bs-config.js:/home/node/omikuji/bs-config.js
    command: "npm run dev"
    networks:
      - apache

  database:
    #...database stuff
    networks:
      - apache

  wordpress:
    build:
      context: .
      dockerfile: ./docker/wordpress/Dockerfile
    image: wordpress:6.2.2-php8.1-build
    container_name: wordpress
    restart: always
    hostname: mysite.localhost
    depends_on:
      database:
        condition: service_healthy
    expose:
      - 80
    volumes:
      - ./wordpress/source/themes/omikuji/dist:/var/www/html/wp-content/themes/omikuji
    environment:
      TZ: ${TZ} # Set local time
      WORDPRESS_DB_HOST: database
      WORDPRESS_DB_USER: ${MARIADB_USER}
      WORDPRESS_DB_PASSWORD: ${MARIADB_PASSWORD}
      WORDPRESS_DB_NAME: ${MARIADB_DATABASE}
      WORDPRESS_DEBUG: 1
    networks:
      - apache

  phpmyadmin:
    # ...phpmyadmin stuff
    networks:
      - apache

  apache:
    build:
      context: .
      dockerfile: ./docker/apache/Dockerfile
    image: httpd:2.4-build
    container_name: apache
    ports:
      - 80:80
      - 443:443
      - 3000:3000
      - 3001:3001
    environment:
      TZ: ${TZ} # Set local time
    networks:
      - apache

volumes:
  db_data:
    external: true

networks:
  apache:
    name: apache

The browsersync config:

module.exports = {
  "logLevel": "debug",
  "logConnections": false,
  "logFileChanges": true,
  "logSnippet": true,
  "open": false,
  "files": ["./dist/style.css", "./dist/js/*.js", "./dist/*.php"],
  "proxy": "http://node:3000",
  "host": "node",
  "port": 3000,
  "reloadDebounce": 2000,
  "watchOptions": {
    "awaitWriteFinish": true
  }
};

Apache virtual hosts in httpd-vhosts.conf:

<VirtualHost *:80>
  ServerName localhost
  RewriteEngine On
  RewriteCond %{HTTP:X-Forwarded-Proto} !https
  RewriteCond %{HTTPS} !=on
  RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>

<VirtualHost *:80>
  ServerName mysite.localhost

  # SSL redirection is in .htaccess

  # Reverse proxy
  ProxyPreserveHost On
  ProxyPass / http://wordpress:80/
  ProxyPassReverse / http://wordpress:80/
  RequestHeader set X-Forwarded-Proto http
</VirtualHost>

<VirtualHost *:443>
  ServerName mysite.localhost

  # Reverse proxy
  ProxyPreserveHost On
  ProxyPass / http://wordpress:80/
  ProxyPassReverse / http://wordpress:80/
  RequestHeader set X-Forwarded-Proto https

  # Enable SSL
  SSLEngine on
  SSLCertificateFile /etc/ssl/certs/mysite.localhost.pem
  SSLCertificateKeyFile /etc/ssl/private/mysite.localhost-key.pem
</VirtualHost>

<VirtualHost *:3000>
  ServerName mysite.localhost

  # Reverse proxy
  ProxyPreserveHost On
  ProxyPass / http://node:3000/
  ProxyPassReverse / http://node:3000/
  RequestHeader set X-Forwarded-Proto http

  # Enable SSL
  SSLEngine on
  SSLCertificateFile /etc/ssl/certs/mysite.localhost.pem
  SSLCertificateKeyFile /etc/ssl/private/mysite.localhost-key.pem
</VirtualHost>

Rewrite rules in .htaccess at root of WordPress directory:

RewriteEngine On

# If the request is not https on the development server (reverse proxy)
RewriteCond %{HTTP:X-Forwarded-Proto} !https

# If the request is not https on the production server
RewriteCond %{HTTPS} !=on

# Redirect to SSL
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

Apache Dockerfile:

FROM httpd:2.4

# Copy the configuration files
WORKDIR /usr/local/apache2/conf
COPY ./localhost/apache/httpd.conf httpd.conf

# Copy the configuration extra files
WORKDIR /usr/local/apache2/conf/extra
COPY ./localhost/apache/httpd-vhosts.conf httpd-vhosts.conf
COPY ./localhost/apache/httpd-ssl.conf httpd-ssl.conf
COPY ./localhost/apache/proxy-html.conf proxy-html.conf

# Copy the SSL certificates
WORKDIR /etc/ssl/certs
COPY ./localhost/certs/localhost.pem localhost.pem
COPY ./localhost/certs/mysite.localhost.pem mysite.localhost.pem
COPY ./localhost/certs/phpmyadmin.localhost.pem phpmyadmin.localhost.pem

# Copy the SSL certificate private keys
WORKDIR /etc/ssl/private
COPY ./localhost/private/localhost-key.pem localhost-key.pem
COPY ./localhost/private/mysite.localhost-key.pem mysite.localhost-key.pem
COPY ./localhost/private/phpmyadmin.localhost-key.pem phpmyadmin.localhost-key.pem

# Change to default directory
WORKDIR /usr/local/apache2

The Node Dockerfile:

FROM node:18.16.1

# Copy SSL certificates
WORKDIR /etc/ssl/certs
COPY ./localhost/certs/mysite.localhost.pem mysite.localhost.pem
WORKDIR /etc/ssl/private
COPY ./localhost/private/mysite.localhost-key.pem mysite.localhost-key.pem

# Copy configuration files to root
RUN mkdir /home/node/omikuji
WORKDIR /home/node/omikuji
COPY ./wordpress/source/themes/omikuji/.stylelintrc.json .stylelintrc.json
COPY ./wordpress/source/themes/omikuji/package.json package.json
COPY ./wordpress/source/themes/omikuji/package-lock.json package-lock.json

# Install npm packages
RUN npm install

# Add environment variables after installing npm
ENV NPM_CONFIG_LOGLEVEL info
ENV NODE_ENV development

The Node logs respond to file changes:

 *  Executing task in folder Development: docker logs --tail 1000 -f 78ed8100eca903cd446d057b7da9f05fefac45f31d97d865e9b32a9264c15665 

npm info using [email protected]
npm info using [email protected]

> [email protected] dev
> npm-run-all --parallel localhost dev:scss

npm info using [email protected]
npm info using [email protected]
npm info using [email protected]
npm info using [email protected]

> [email protected] dev:scss
> sass --watch --verbose --style=expanded ./src/scss/style.scss:./dist/style.css


> [email protected] localhost
> browser-sync start --config 'bs-config.js'

[Browsersync] [debug] -> Starting Step: Finding an empty port
[Browsersync] [debug] Found a free port: 3000
[Browsersync] [debug] Setting Option: {cyan:port} - {magenta:3000
[Browsersync] [debug] +  Step Complete: Finding an empty port
[Browsersync] [debug] -> Starting Step: Getting an extra port for Proxy
[Browsersync] [debug] +  Step Complete: Getting an extra port for Proxy
[Browsersync] [debug] -> Starting Step: Checking online status
[Browsersync] [debug] Resolved www.google.com, setting online: true
[Browsersync] [debug] Setting Option: {cyan:online} - {magenta:true
[Browsersync] [debug] +  Step Complete: Checking online status
[Browsersync] [debug] -> Starting Step: Resolve user plugins from options
[Browsersync] [debug] +  Step Complete: Resolve user plugins from options
[Browsersync] [debug] -> Starting Step: Set Urls and other options that rely on port/online status
[Browsersync] [debug] Setting multiple Options
[Browsersync] [debug] +  Step Complete: Set Urls and other options that rely on port/online status
[Browsersync] [debug] -> Starting Step: Setting Internal Events
[Browsersync] [debug] +  Step Complete: Setting Internal Events
[Browsersync] [debug] -> Starting Step: Setting file watchers
[Browsersync] [debug] +  Step Complete: Setting file watchers
[Browsersync] [debug] -> Starting Step: Merging middlewares from core + plugins
[Browsersync] [debug] Setting Option: {cyan:middleware} - {magenta:List []
[Browsersync] [debug] +  Step Complete: Merging middlewares from core + plugins
[Browsersync] [debug] -> Starting Step: Starting the Server
[Browsersync] [debug] Proxy running, proxing: {magenta:http://node:3000}
[Browsersync] [debug] Running mode: PROXY
[Browsersync] [debug] +  Step Complete: Starting the Server
[Browsersync] [debug] -> Starting Step: Starting the HTTPS Tunnel
[Browsersync] [debug] +  Step Complete: Starting the HTTPS Tunnel
[Browsersync] [debug] -> Starting Step: Starting the web-socket server
[Browsersync] [debug] Setting Option: {cyan:clientEvents} - {magenta:List [ "scroll", "scroll:element", "input:text", "input:toggles", "form:submit", "form:reset", "click" ]
[Browsersync] [debug] +  Step Complete: Starting the web-socket server
[Browsersync] [debug] -> Starting Step: Starting the UI
[Browsersync] [debug] Setting Option: {cyan:session} - {magenta:1690129836067
[Browsersync] [UI] Starting Step: Setting default plugins
[Browsersync] [UI] Step Complete: %s Setting default plugins
[Browsersync] [UI] Starting Step: Finding a free port
[Browsersync] [UI] Step Complete: %s Finding a free port
[Browsersync] [UI] Starting Step: Setting options also relevant to UI from BS
[Browsersync] [UI] Step Complete: %s Setting options also relevant to UI from BS
[Browsersync] [UI] Starting Step: Setting available URLS for UI
[Browsersync] [debug] Getting option via path: {magenta:[ 'urls' ]
[Browsersync] [UI] Step Complete: %s Setting available URLS for UI
[Browsersync] [UI] Starting Step: Starting the Control Panel Server
[Browsersync] [UI] Using port 3001
[Browsersync] [UI] Step Complete: %s Starting the Control Panel Server
[Browsersync] [UI] Starting Step: Add element events
[Browsersync] [UI] Step Complete: %s Add element events
[Browsersync] [UI] Starting Step: Registering default plugins
[Browsersync] [UI] Step Complete: %s Registering default plugins
[Browsersync] [UI] Starting Step: Add options setting event
[Browsersync] [UI] Step Complete: %s Add options setting event
[Browsersync] [debug] +  Step Complete: Starting the UI
[Browsersync] [debug] -> Starting Step: Merge UI settings
[Browsersync] [debug] Setting Option: {cyan:urls} - {magenta:Map { "local": "http://localhost:3000", "external": "http://node:3000", "ui": "http://localhost:3001", "ui-external": "http://localhost:3001" }
[Browsersync] [debug] +  Step Complete: Merge UI settings
[Browsersync] [debug] -> Starting Step: Init user plugins
[Browsersync] [debug] Setting Option: {cyan:userPlugins} - {magenta:
[Browsersync] [debug] +  Step Complete: Init user plugins
[Browsersync] Proxying: http://node:3000
[Browsersync] Access URLs:
 ----------------------------------
       Local: http://localhost:3000
    External: http://node:3000
 ----------------------------------
          UI: http://localhost:3001
 UI External: http://localhost:3001
 ----------------------------------
[Browsersync] Watching files...
Sass is watching for changes. Press Ctrl-C to stop.

[2023-07-23 16:30] Compiled src/scss/style.scss to dist/style.css.
[Browsersync] File event [change] : dist/style.css
1

There are 1 best solutions below

0
edwinbradford On

Something in Browsersync appears to be incompatible with Node as a separate service in Docker behind an Apache reverse proxy because I wrote my own Node server which connected to the same docker WordPress service at the same target address and port on the first attempt.

My server is fully working with live reload and SSL (hat tip chatGPT) and I have no further need for Browsersync. In case anyone else is interested in learning how to do this, these are the Node packages I used.

  "devDependencies": {
    "chokidar": "^3.5.3",
    "express": "^4.18.2",
    "http": "^0.0.1-security",
    "http-proxy-middleware": "^2.0.6",
    "https": "^1.0.0",
    "socket.io": "^4.7.1",
    "socket.io-client": "^4.7.2"
  },

The chokidar package is used to watch files for changes, the socket.io packages are used for live reload and the other packages are required for the core server, SSL, proxying requests to the Docker WordPress container and injecting HTML which is required for Socket.io.

In the Apache virtual hosts, the Proxy addresses point to the name and port of your Node container in Docker where my Node container name is node and the port is 3000.

Also note the upgrade=websocket directive. This is the updated method of supporting WebSocket and is all you need, significantly easier to get working than the deprecated method fiddling around with WebSocket rewrites and path redirects.

<VirtualHost *:80>
  ServerName mysite.localhost

  # SSL redirection is in .htaccess

  # Reverse proxy with websocket
  ProxyPreserveHost On
  ProxyPass / http://node:3000/ upgrade=websocket
  ProxyPassReverse / http://node:3000/ upgrade=websocket
  RequestHeader set X-Forwarded-Proto http
  ProxyTimeout 60
</VirtualHost>

<VirtualHost *:443>
  ServerName mysite.localhost

  # Reverse proxy with websocket
  ProxyPreserveHost On
  ProxyPass / http://node:3000/ upgrade=websocket
  ProxyPassReverse / http://node:3000/ upgrade=websocket
  RequestHeader set X-Forwarded-Proto https
  ProxyTimeout 60

  # Enable SSL
  SSLEngine on
  SSLCertificateFile /etc/ssl/certs/myproject.localhost.pem
  SSLCertificateKeyFile /etc/ssl/private/myproject.localhost-key.pem
</VirtualHost>