A simple, powerful, and featureful Docker-aware reverse proxy powered by Caddy
251
MXProxy (formerly Malasaur Proxy) 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"
extra_hosts:
- "dockerhost:host-gateway"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/data
- ./config:/config
- ./custom:/custom
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
A port on the Docker host:
reverse_proxy: example.com -> :8080
Using :8080 tells MXProxy to proxy to port 8080 on the Docker host.
This is useful for services running directly on the host instead of inside Docker. It is similar to using localhost:8080 when configuring a proxy directly on the host, but it works through the Docker host gateway instead.
You can also explicitly use dockerhost:
labels:
reverse_proxy: example.com -> dockerhost:8080
For dockerhost to work, the MXProxy container must have the following in its Compose configuration:
extra_hosts:
- "dockerhost:host-gateway"
The default MXProxy Compose configuration since v0.2 already includes this.
Warning
`:8080` means port `8080` on the **Docker host**. It does not mean that a service listening only on `127.0.0.1:8080` on the host will necessarily be reachable.The service must be listening on an address that is reachable through the Docker host gateway.
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
MXProxy can also proxy services that are not running in Docker.
Docker labels must belong to a Docker container, so if the service you want to proxy is not running in Docker, you can use the malasaur/dummy image as a lightweight container whose only purpose is to carry MXProxy labels.
For example:
services:
copyparty-proxy:
image: malasaur/dummy:latest
networks: [mxproxy]
labels:
reverse_proxy: files.example.com -> 192.168.1.100:3923
networks:
mxproxy:
external: true
The dummy container does not run an actual application. It exists solely to provide a Docker service on which you can define MXProxy configuration.
You can use the same approach for services running on another machine, a host system, or any other address reachable from the MXProxy container.
For services running directly on the Docker host, you can use the special :port syntax:
services:
copyparty-proxy:
image: malasaur/dummy:latest
networks: [mxproxy]
labels:
reverse_proxy: files.example.com -> :3923
networks:
mxproxy:
external: true
This is equivalent to:
reverse_proxy: files.example.com -> dockerhost:3923
The dockerhost hostname points to the Docker host through Docker's host gateway.
Warning
`dockerhost` is not the same thing as `localhost`.
localhostinside the MXProxy container refers to the MXProxy container itself. It does not refer to the Docker host.Similarly,
127.0.0.1refers to the MXProxy container and should not be used to reach services running on the host.
The host service must also listen on an address that is reachable through the Docker host gateway. A service bound exclusively to the host's loopback interface may not be reachable from the MXProxy container.
For services running on another machine, use their IP address or hostname directly:
labels:
reverse_proxy: example.com -> 10.0.0.50:8080
For example:
labels:
reverse_proxy: example.com -> 192.168.1.100:8080
The address only needs to be reachable from the MXProxy container.
MXProxy also supports custom Caddy configuration through the /custom directory.
Mount a directory from the host into /custom:
services:
mxproxy:
volumes:
- ./custom:/custom
You can also mount individual files:
services:
mxproxy:
volumes:
- /srv/Caddyfile:/custom/srv.caddy
MXProxy automatically imports files from this directory.
Files ending in .caddy are imported as regular Caddy configuration. Files ending in .global are imported inside Caddy's global options block.
Custom Caddyfiles can also use dockerhost:
example.com {
reverse_proxy dockerhost:8080
}
This uses the same Docker host gateway described in Services on the Docker host.
Remember that localhost still refers to the MXProxy/Caddy container:
example.com {
reverse_proxy localhost:8080
}
The above connects to port 8080 inside the MXProxy container, not port 8080 on the Docker host.
MXProxy watches the contents of /custom and automatically reloads Caddy when a .caddy or .global file changes.
This means you can edit a mounted custom Caddyfile without restarting MXProxy:
$EDITOR custom/example.caddy
Caddy will be reloaded automatically once the file stops changing.
Note
MXProxy watches files inside the existing `/custom` mount. If you change the Docker Compose configuration itself, such as adding the `/custom` mount for the first time, you still need to recreate the MXProxy container.
MXProxy releases are published using semantic versioning.
For production deployments, do not use the :latest tag. Pin MXProxy to a minor or patch release instead:
image: malasaur/mxproxy:0.2
or:
image: malasaur/mxproxy:0.2.0
Using a minor version such as :0.2 allows patch releases while keeping the deployment on the same minor version.
Using a patch version such as :0.2.0 completely pins the deployment to that release.
Always check the release notes before updating to a new minor version. Minor releases may contain breaking changes or require changes to your Docker Compose configuration.
For example, when updating to v0.2.0, the MXProxy Compose configuration must include:
extra_hosts:
- "dockerhost:host-gateway"
This is required for the :port and dockerhost:port external-service features.
After changing the Compose configuration, recreate the MXProxy container:
docker compose up -d --force-recreate
Content type
Image
Digest
sha256:1c3f6cf19…
Size
92.1 MB
Last updated
19 days ago
docker pull malasaur/mxproxy