Dynamically redirect www to non-www & vice-versa

1. Caddy version (caddy version): 2.0.0

2. How I run Caddy:

I use Docker (docker-compose) to run Caddyserver. Here is the command of my caddy service: command: ["caddy", "run", "--config", "/etc/caddy/Caddyfile"]

a. System environment:

Docker version 19.03.1
docker-compose version 1.22.0

b. Command:

docker-compose -f docker-compose.caddy.yml up -d --build

c. Service/unit/compose file:

version: "3.4"
services:
  # other services...
  caddy:
    image: caddy:2.0.0
    restart: always
    container_name: caddy
    deploy:
      replicas: 2
      update_config:
        delay: 10s
      restart_policy:
        condition: on-failure
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - www:/var/www/html
      - caddy_data:/data
      - caddy_config:/config
    ports:
      - "80:80"
      - "443:443"
    command: ["caddy", "run", "--config", "/etc/caddy/Caddyfile"]

volumes:
  db-data:
  conf:
  www:
  cert:
  caddy_data:
  caddy_config:

d. My complete Caddyfile or JSON config:

:443 {
  root * /var/www/html/{host}
  file_server
  tls {
    on_demand
  }
  reverse_proxy /sites/action/* api:4000
}

3. The problem I’m having:

I run a platform where users can host their websites, so I have plenty of domains hosted into the server, each domain has its own directory. Whenever visitors hit into a domain I look into the parent directory by the {host} parameter and serve its files. It works totally fine until I want to redirect non-www to www or www to non-www. This is not my problem as well, the problem arises when I don’t know whether the domain should redirect to www or non-www!

When the domains are created by the users some of them may choose to forward to www and some of them may choose to forward to non-www. I can create a system file depending on that choice (eg: redirect-rules.conf). I need a way to read the content of that file and execute it in Caddyfile.

Let’s say a domain directory example.com was created and chosen www to be forwarded into example.com. I am thinking of a pseudo redirect-rules.conf file that contains:
redir www.example.com example.com

4. Error messages and/or full log output:

I tried to read a conf file using import directive like this: import /var/www/html/{host}/redir_rules. But this configuration is wrong because I am not allowed to use variable into a file path!
The error I get: Error during parsing: File to import not found: /var/www/html/{host}/redir

What would be the ideal solution for this scenario?

5. What I already tried:

import a conf file from root, seems like it import directive reads file from absolute path.

6. Links to relevant resources:

G’day @Anwar_Hussain, welcome to the Caddy community.

One thing to note is that Caddyfile isn’t Caddy v2’s “native language”, so to speak. It’s an abstracted configuration layer that is translated to JSON when Caddy runs.

This is important to note because import is a Caddyfile-specific convention and imported files are included before parsing and translation begins. That means that the concept of a {host} placeholder doesn’t really exist at the stage of execution where we are importing Caddyfile config.

In fact, {host} only exists in the context of a request. Caddy definitely can’t import configuration, on the fly, per request, based on request information.

I briefly toyed with the idea of suggesting you allow for the customer to place an arbitrary file (lets say .redir-to-www for sake of discussion) in the web root you could test for, and if present, redirect to add www (and vice versa for hypothetical file .redir-no-www). The premise here being that you can generically redirect to append www… But as it turns out it’s quite difficult to strip www from the host dynamically and issue that redirect appropriately, so it wouldn’t work both ways.

You may need to manually aggregate all your rewrite rules in one file and import that, e.g.

# Add www
@example.com host example.com
redir @example.com www.example.com{uri}

# Strip www
@www.example.com host www.example.com
redir @www.example.com example.com{uri}

(Shorthand matchers require Caddy v2.1 or later).

3 Likes

Using snippets and import args (v2.1+), you could generalize this:

(strip-www) {
	@www.{args.0} host www.{args.0}
	redir @www.{args.0} https://{args.0}{uri}
}

(add-www) {
	@{args.0} host {args.0}
	redir @{args.0} https://www.{args.0}{uri}
}

:443 {
	import strip-www foo.com
	import add-www bar.com
}

This gives you rules that you can import on the fly to do the redirect for you in either direction. Let me know if you want a more thorough explanation of how this works.

3 Likes

Hey Francis & Matthew,

Thank you guys for your responses. The solution you gave seems to be almost perfect for my scenario. But I am thinking to take this one step further! I do not know whether it will work or not, but my idea is:

In Caddyfile

(strip-www) {
	@www.{args.0} host www.{args.0}
	redir @www.{args.0} https://{args.0}{uri}
}

(add-www) {
	@{args.0} host {args.0}
	redir @{args.0} https://www.{args.0}{uri}
}

:443 {
     import /var/site-rules/*
}

Each file in site-rules folder represents each domain (eg: a.com, www.b.com) and the content of these files will be either strip-www or add-www named import block. example:
a.com —> import add-www a.com
www.b.com —> import strip-www b.com

I think it should work since the documentation says this:

Its contents will replace this line as if that file’s contents appeared here to begin with

Yeah, could work. Give it a shot! You don’t actually need to run that config to test it, you can use the caddy adapt command to see if the JSON output has the things you expect.

1 Like

Hey Francis,

I have tried it and saw the result from adapt! It successfully generates the JSON structure I wanted. But now I am concerned about the performance of the server when it will serve 1000+ domains! If you assure me I will set up the configuration for my production server.

Regards

The best thing you could do is benchmark it yourself to see how much of an impact it’ll have. I can’t say it won’t be nothing, because essentially you’re doing 1000+ string comparisons on every request; but it might be barely noticeable depending on the kinds of loads your sites get.

You could consider wrapping your import in a handle which has its own matcher, to try and reduce the amount of requests on which those matchers will take effect. For example, maybe you want to avoid checking all the domains if the request ends in .css or .js to avoid slowing down those requests.

@wwwRedir not path *.css *.js
handle @wwwRedir {
	import /var/site-rules/*
}

It’ll be up to you to figure out what might work for you there, you could get pretty creative.

2 Likes

wondering how to create a rules for the following case:
add “www” if domain name contains a word “banana” with different TLD, and remove “www” if exists for the domain name that belongs to another specific domain, say “blabla.com” which has a subdomain instead

You can use the header_regexp matcher to match whatever pattern you need in the Host header and trigger the redirect based on that.

1 Like

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