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
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.
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
nodeand the port is3000.Also note the
upgrade=websocketdirective. 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.