Caddyfile Wordpress Multisite

.htaccess Wordpress example

For future reference it would be nice to include an example for the .htaccess files for wordpress multisites. As wordpress is a super easy, yet powerfull tool just like caddy.
I mean, I freaking love caddy, how it magically just works is simply amazing! And I hate it, that something to mundane like .htaccess is for a server/web noob like me with caddy such a great impediment :’(
Wordpress .htaccess files

Thank you guys for your time and interest in helping me! :slight_smile:

1. Caddy version (caddy version):

v2.2.1 h1:Q62GWHMtztnvyRU+KPOpw6fNfeCD3SkwH7SfT1Tgt2c=

2. How I run Caddy:

a. System environment:

OS: Debian 10, no container
php 7.4
updated wordpress multisite subdirectory.
Caddy as my webserver

b. Command:

curl -v https://datoo.wissensbisse.com/demo/wp-admin

c. My complete Caddyfile or JSON config:

datoo.wissensbisse.com {
	root * /var/www/datoo.cloud/wordpress
	php_fastcgi unix//run/php/php7.4-fpm.sock
	file_server
}

3. The problem I’m having:

Redirect loop, trying to access https://datoo.wissensbisse.com/wp-admin
Checking browser Network tool, one can find the 302 redirects and a lot of 404 errors.
Also, styles and js files are not being found.
Need to translate the standard .htaccess file to caddy rules, but am not able to do so myself.
Examples I found here in the forum seem to be for Caddy v1 or solving a different problem.

RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]

# add a trailing slash to /wp-admin
RewriteRule ^wp-admin$ wp-admin/ [R=301,L]

RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteRule ^(wp-(content|admin|includes).*) $1 [L]
RewriteRule ^(.*\.php)$ $1 [L]
RewriteRule . index.php [L]
curl -v https://datoo.wissensbisse.com/demo/wp-admin
* Expire in 0 ms for 6 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 1 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 2 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 2 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 2 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 2 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 2 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 2 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 2 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 2 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 2 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 2 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 2 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 2 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 2 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 2 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 2 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 2 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 2 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
* Expire in 0 ms for 1 (transfer 0x56044290e9c0)
*   Trying 46.38.237.15...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x56044290e9c0)
* Connected to datoo.wissensbisse.com (46.38.237.15) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: none
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=datoo.wissensbisse.com
*  start date: Jun  7 16:25:21 2021 GMT
*  expire date: Sep  5 16:25:21 2021 GMT
*  subjectAltName: host "datoo.wissensbisse.com" matched cert's "datoo.wissensbisse.com"
*  issuer: C=US; O=Let's Encrypt; CN=R3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x56044290e9c0)
> GET /demo/wp-admin HTTP/2
> Host: datoo.wissensbisse.com
> User-Agent: curl/7.64.0
> Accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 302
< cache-control: no-cache, must-revalidate, max-age=0
< content-type: text/html; charset=UTF-8
< expires: Wed, 11 Jan 1984 05:00:00 GMT
< link: <https://datoo.wissensbisse.com/demo/wp-json/>; rel="https://api.w.org/"
< location: https://datoo.wissensbisse.com/demo/wp-admin/
< server: Caddy
< status: 302 Found
< x-redirect-by: WordPress
< content-length: 0
< date: Sat, 26 Jun 2021 20:57:24 GMT
<
* Connection #0 to host datoo.wissensbisse.com left intact

5. What I already tried:

Checked my wordpress database for correct URLs, aswell as the wp-config.php
Checked this forum aswell but could not find a solution for a standard wordpress multisite installation with subdirectories.
Tried to find a tool to convert the .htaccess file to caddyfile but failed to do so (or convert it myself.)

6. Links to relevant resources:

One of the links is unfortunatelly pointing to the wrong domain.
Here is the correct URL from section #3 https://datoo.wissensbisse.com/demo/wp-admin/

I am unfortunately stuck :frowning:

Do you have an existing WordPress multisite installation? Is it already behind an Apache RP? If not and you’re building a new WP installation, do you have a good use case for multisite? Unless you do, multisite is putting all you eggs in one basket. This arises because a common database is used for all sites. While the risks are similar to single site, the consequences are greater as the likelihood is that all sites are affected if a problem arises. Troubleshooting is also considerably more complex with multisite. Multisite inherently has more limitations as well.

1 Like

The idea was to automatically generate user sub-websites, like i.e. demo-pages for wp-plugins.
For that functionality to work multisite is necessary.

I am pretty new to the whole web-development topic, so of course I am thankful for any tips about how to accomplish just a solution without using multisite (the inter-dependability is indeed an issue)

It is a clean wp-installation just using caddy no nginx or apache

You’re right. Multisite is required for auto-generated sub-websites, however, multisite isn’t required to demo pages for wp-plugins. Two points here:

  1. Not all wp-plugins are compatible with WP multisite; and
  2. You can demo wp-plugins using a single WP site.

You may be trying to bite off more than you can chew here. If WP and Caddy are new to you, you might find it more manageable starting with a WP single site - serving WP PHP files using a Caddy webserver and Caddy reverse proxy. There’s a lot of good information in this forum you can tap into e.g. Example: WordPress and the Caddy forum moderators are extremely helpful. Just be aware, they’re Caddy experts and will do what they can to help you make your applications web accessible and secure, but they are not necessarily versed in any application forum members are trying to integrate into Caddy.

Once you have WP single site working, you’ll be in a much more informed position to consider transitioning to multiple WP single sites (uncoupled from each other) or WP multisite (tightly coupled sites).

I started with multisite last year, but then switched to multiple single sites. FWIW, here’s a blog post with my reasons for switching WordPress: To Multisite or not?. Personally, there’s only one situation I would consider multisite and that’s for building a multilingual WordPress site.

2 Likes

Hey Basil,

thanks for your input. The key functionality is to auto-generate custom user-websites in subdirectories and wordpress multisite seemed great for the purpose of auto generating websites and the vast ocean of plugins to enable the users to use the features they need.

The wordpress single site is working just fine. The issue is really the routing to the subdirectories.
@francislavoie gave me the tip to look into checking out the nginx setting file and try to convert the settings from there.
see: NGINX Wordpress Subdirectories config

However, this is still a closed book to me, honestly.

Honestly, I’d love to take on @Whitestrake on his offer to help and translate the rewrite rules to the caddyfile :smiley:
Whitestrake offering to translate .htaccess to caddyfile

I’d be tremendously thankful ^_^"

@basil btw. really awesome animation when opening you website, bro :+1:

1 Like

The nginx method here appears to use the nginx map directive and a config file generated by a plugin to determine blog ID from the URI. As far as I can see, it does this to allow nginx to serve files without relying on PHP, for performance reasons.

I’m not sure this particular section of config can be translated due to its nginx-specific function.

At a glance, though, neither the missing nginx config equivalent (or .htaccess equivalent) should be causing a redirect loop on /wp-admin/. The salient parts of those config actually just tell the web server not to redirect from those areas. With your Caddyfile, Caddy itself isn’t generating any redirects, so not telling it not to redirect logically shouldn’t be an issue.

Is your WordPress multisite install fresh or ported over to Caddy? If it’s the latter, have you tried a completely fresh setup? My hunch is that these redirects are coming from PHP via site configuration, but multisite is a black box to me.

2 Likes

Thanks again for all the kind folk helping me out here! @basil @Whitestrake @francislavoie

Looking at the user facing website ( Test Site) I could see that all ressources (styles, images, etc) were not being found, because wordpress multisite uses for all subsites actually the same ressources.
So removing the “/demo/” part from the uri requesting ressources fixed that issue.

	uri replace /demo/wp-includes/ /wp-includes/
	uri replace /demo/wp-content/ /wp-content/
	uri replace /demo/wp-admin/ /wp-admin/

Here my question: Any idea how I can put a wildcard instead of “demo” there to have the config working for future subsites?

Ideas:

  • Replace the the whole first part of the uri including host á la host.com/site/wp-content/host.com/wp-content/
  • Is there a caddy placeholder suitable? To me it doesn’t seem like it, but I might be wrong
  • Maybe use a regex?

Which way is best, how would you do it?

The uri rewrite fixes the original issue. (at least for now)

Thanks guys! :+1:

p.s.

I created this regex, which in theory should work.

	uri replace (\/{1})(\w|\d|\s)+\/wp-includes /wp-includes
	uri replace (\/{1})(\w|\d|\s)+\/wp-content /wp-content
	uri replace (\/{1})(\w|\d|\s)+\/wp-admin /wp-admin

However interacting with the site prompt this error:

 ERROR   http.handlers.reverse_proxy     aborting with incomplete response       {"error": "http2: stream closed"}
2021/07/17 16:57:50.819 ERROR   http.handlers.reverse_proxy     aborting with incomplete response       {"error": "http2: stream closed"}

Any ideas?

Why won’t uri strip_prefix won’t do what you need? It looks like you just need to strip /demo from the beginning, yeah?

@matt
The issue is that this should work for a variety of subdirectories, not just /demo/ but /site/ etc

Any ideas?

Like so:

/demo/wp-.... => /wp-...
/site1/wp-... => /wp-...
/site2/wp-.. => /wp-...

But only for those requests that request ressources from one of the wp- directories, as mentioned in one of the earlier posts

Maybe you can do something like this (would strip any first path segment from the front of the URL):

uri path_regexp ^/[a-zA-Z0-9]+/wp- /wp-

Basically this would match the beginning of the path (starting with a /) followed by any number of alphanumeric characters (you can adjust this if you have any special characters to deal with like - or whatever) followed by /wp- and replace it with /wp-.

You can see how it works here, and play around with it:

1 Like

@francislavoie That is also what my own regex does, (see the post just before matt’s).

What I did differently though was using the replace command instead of path_regexp

Changing this resulted into caddy aborting:

run: adapting config using caddyfile: parsing caddyfile tokens for 'uri': Caddyfile:12 - Error during parsing: unrecognized URI manipulation 'path_regexp'

Using your regex in combination with replace resulted in the same error as mine :frowning:

ERROR   http.handlers.reverse_proxy     aborting with incomplete response       {"error": "http2: stream closed"}

Any ideas?

Make sure to use the latest version of Caddy.

That’s quite an old version. Use v2.4.3.

replace does substring replacement, not regexp replacement. That means replace only deals with exact matches of the string you specify, it doesn’t parse the <target> as a regexp pattern.

1 Like

At the end: This directive is the solution in combination with updating Caddy to the current verision:

	uri path_regexp (\/{1})(\w|\d|\s)+\/wp- /wp-

Thank you guys! :bowing_man: :bowing_man: :bowing_man:

The thread can now be closed

This topic was automatically closed after 30 days. New replies are no longer allowed.