Sign inSign up

malasaur/mxproxy

By malasaur

•Updated 19 days ago

A simple, powerful, and featureful Docker-aware reverse proxy powered by Caddy

Image
Networking
Developer tools
Web servers
0

251

malasaur/mxproxy repository overview

⁠MXProxy

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⁠.

⁠Installation

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

⁠Usage

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
⁠Reverse Proxy

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
}
⁠More Upstreams

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.

⁠Paths

MXProxy supports paths on both sides of a proxy.

A few examples:

⁠Strip a path
labels:
  reverse_proxy: example.com/api -> 8080

Requests are translated like this:

/api     -> /
/api/foo -> /foo
⁠Append a path
labels:
  reverse_proxy: example.com -> 8080/backend

Results in:

/    -> /backend
/foo -> /backend/foo
⁠Rewrite both sides
labels:
  reverse_proxy: example.com/api -> 8080/backend

Results in:

/api     -> /backend
/api/foo -> /backend/foo
⁠Multiple proxies
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.

⁠Middleware

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.

⁠Anubis

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.

⁠Headers

You can control the headers sent and received by the upstream service through the headers subdirective.

⁠Set a header

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.

⁠Add a header

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.

⁠Delete a header

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"
⁠Multiple header rules

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
⁠Groups

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.

⁠Basic auth

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$$...
⁠Custom responses

Use respond to return a response without contacting an upstream.

⁠Status only
labels:
  respond: example.com -> 204
⁠Body only
labels:
  respond: example.com -> "Hello from MXProxy!"
⁠Status and body
labels:
  respond: example.com -> 418 "I'm a teapot"
⁠Compression

Compression can be enabled with:

labels:
  compress: gzip
⁠Cloudflare

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 the API_KEY environment 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 where API_KEY is set is included in the container with env_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

⁠External Services

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.

⁠Services on the Docker host

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`.

localhost inside the MXProxy container refers to the MXProxy container itself. It does not refer to the Docker host.

Similarly, 127.0.0.1 refers 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.

⁠Other external services

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.

⁠Custom Caddyfiles

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 and Docker host services

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.

⁠Automatic reloads

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.

⁠Updating

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

Tag summary

Content type

Image

Digest

sha256:1c3f6cf19…

Size

92.1 MB

Last updated

19 days ago

docker pull malasaur/mxproxy