A simple, powerful, and featureful Docker-aware reverse proxy powered by Caddy
129
MXProxy is a Docker-aware proxy that turns Docker labels into Caddy configuration.
Instead of writing Caddyfiles by hand, you define your proxy configuration directly in Docker Compose files using a small, human-friendly DSL.
MXProxy watches Docker events, generates Caddy configuration automatically, and reloads Caddy when needed. No need to manually restart Caddy.
It also integrates with a few other services, such as Anubis.
MXProxy runs as a Docker container and communicates with the Docker daemon through its socket.
Create a compose.yaml:
services:
mxproxy:
image: malasaur/mxproxy:latest
container_name: mxproxy
restart: unless-stopped
ports:
- "80:80"
- "443:443/tcp"
- "443:443/udp"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/data
- ./config:/config
networks: [mxproxy]
networks:
mxproxy:
external: true
Currently, MXProxy requires an external network in order to work. By default, it uses mxproxy. All services that MXProxy should proxy must be connected to this network.
Create the network using:
docker network create mxproxy --ipv6
Then start MXProxy:
docker compose up -d
Once MXProxy is running, you can define your configuration in the labels section of any service in Docker Compose files.
For example:
services:
app:
image: traefik/whoami
networks: [mxproxy]
labels:
# MXProxy configuration goes here
networks:
mxproxy:
external: true
The current most powerful MXProxy directive is:
labels:
reverse_proxy: example.com -> 8080
This is equivalent to the following Caddyfile:
example.com {
reverse_proxy <container-ip>:8080
}
MXProxy automatically detects and adds the container's IP.
You can also specify multiple proxies in one label:
labels:
reverse_proxy: |
example.com -> 8080
api.example.com -> 8081
This is equivalent to the following Caddyfile:
example.com {
reverse_proxy <container-ip>:8080
}
api.example.com {
reverse_proxy <container-ip>:8081
}
The destination does not have to be a Docker port. It can be:
A Docker container:
reverse_proxy: example.com -> other-app:8000
A custom IP/port:
reverse_proxy: example.com -> 192.168.1.100:8080
A remote URL:
reverse_proxy: example.com -> https://example.org
MXProxy supports paths on both sides of a proxy.
A few examples:
labels:
reverse_proxy: example.com/api -> 8080
Requests are translated like this:
/api -> /
/api/foo -> /foo
labels:
reverse_proxy: example.com -> 8080/backend
Results in:
/ -> /backend
/foo -> /backend/foo
labels:
reverse_proxy: example.com/api -> 8080/backend
Results in:
/api -> /backend
/api/foo -> /backend/foo
labels:
reverse_proxy: |
example.com -> 8080
example.com/api -> 8081
/ -> 8080/
/home -> 8080/home
/api -> 8081/
/api/foo -> 8081/foo
This setup allows, for example, to proxy different services running on different ports under the same domain.
The reverse_proxy directive supports middleware. They are custom services that MXProxy automatically sets up and integrates with to provide additional functionality.
The syntax is:
labels:
reverse_proxy: source -> middleware -> destination
Currently, the only supported middleware is Anubis. More integrations will be added in the future.
You can write a reverse_proxy like this:
labels:
reverse_proxy: example.com -> anubis -> 8080
MXProxy automatically pulls and deploys a new Anubis container, then configures it to proxy requests to 8080.
When the container with the above configuration is removed, MXProxy automatically stops and removes the Anubis container too.
This way, you can protect your services with Anubis without having to configure it or deploy it manually.
You can control the headers sent and received by the upstream service through the headers subdirective.
This:
labels:
reverse_proxy: example.com -> 8080
reverse_proxy.headers: "> X-MXProxy-Test hello"
sets the X-MXProxy-Test: hello header to the upstream request.
This means the service running on port 8080 will see this header in each request.
labels:
reverse_proxy: example.com -> 8080
reverse_proxy.headers: "< X-MXProxy-Test hello"
sets the downstream header, meaning it will appear in the response and be visible by the client.
labels:
reverse_proxy: example.com -> 8080
reverse_proxy.headers: "<> X-MXProxy-Test hello"
sets the header in both directions.
Prefix the header name with +:
labels:
reverse_proxy: example.com -> 8080
reverse_proxy.headers: "> +X-MXProxy-Test"
This passes the incoming request header through the upstream.
For example:
curl -H "X-MXProxy-Test: hello" https://example.com
causes the upstream to receive X-MXProxy-Test: hello.
Prefix the header name with -:
labels:
reverse_proxy: example.com -> 8080
reverse_proxy.headers: "> -X-MXProxy-Test"
or from response headers:
labels:
reverse_proxy: example.com -> 8080
reverse_proxy.headers: "< -X-MXProxy-Test"
or both directions:
labels:
reverse_proxy: example.com -> 8080
reverse_proxy.headers: "<> -X-MXProxy-Test"
You can specify multiple rules as follows:
labels:
reverse_proxy: example.com -> 8080
reverse_proxy.headers: |
> X-Request-Source mxproxy
> +X-Forwarder-For
< X-Served-By mxproxy
<> X-Test enabled
< -Server
MXProxy supports grouping multiple directives and applying subdirectives to them.
For example:
labels:
reverse_proxy/site: |
example.com -> 8080
api.example.com -> 8081
reverse_proxy/site.headers: |
> X-MXProxy-Group site
The header rule is applied to both reverse proxies in the site group.
MXProxy supports HTTP Basic Authentication.
The auth directive can contain one or more username/password hashes:
labels:
reverse_proxy: example.com -> 8080
auth: admin:$$2b$$...
Note that you may need to replace $ with $$ in compose files, as that would cause Docker to treat it as an environment variable.
Basic auth can be combined with other directives:
labels:
reverse_proxy: example.com -> 8080
auth: admin:$$2b$$...
Use respond to return a response without contacting an upstream.
labels:
respond: example.com -> 204
labels:
respond: example.com -> "Hello from MXProxy!"
labels:
respond: example.com -> 418 "I'm a teapot"
Compression can be enabled with:
labels:
compress: gzip
MXProxy integrates with Cloudflare to automatically configure ACME challenges and set up DDNS.
To enable this integration, use the cloudflare directive with your Cloudflare API token:
labels:
cloudflare: $${API_KEY}
Warning! The above snippet uses
$${API_KEY}rather than${API_KEY}.If you used
${API_KEY}directly, Docker would immediately replace it with the value of theAPI_KEYenvironment variable. This means MXProxy would read the variable directly, and paste is as-is in the generated Caddy configuration.That can be a security risk, as it would result in having another file on your system that has the value of your Cloudflare API token written in plaintext, potentially without your knowledge of it.
By using
$${API_KEY}, Docker will not replace the variable and literally pass "${API_KEY}" as the value. MXProxy will detect this and replace it with{env.API_KEY}, which will cause Caddy to actually load the value from the environment variable, assuming that the .env file whereAPI_KEYis set is included in the container withenv_file.services: my-app: image: ... env_file: - .env labels: cloudflare: $${API_KEY} # gets replaced with `{env.API_KEY}`You may also use
{env.API_KEY}directly. The result is the same.
By simply setting the API key, MXProxy will automatically use Cloudflare as a provider for ACME challenges.
After setting an API key, you can also set up a DDNS for Cloudflare by using the cloudflare.ddns subdirective:
labels:
cloudflare: $${API_KEY}
cloudflare.ddns: example.com
This will automatically set up and update an A record for example.com pointing to the IP address of your server.
You can specify multiple domains and subdomains using the same syntax described by the caddy-dynamicdns module:
labels:
cloudflare: $${API_KEY}
# example.com, www.example.com, subdomain.example.net
cloudflare.ddns: |
example.com @ www
example.net subdomain
Content type
Image
Digest
sha256:044059d56…
Size
89.9 MB
Last updated
about 1 month ago
docker pull malasaur/mxproxy