How to serve single file using HAProxy

Use simple HAProxy configuration hacks to serve single static file.

HAProxy version.

$ sudo haproxy -v
HA-Proxy version 1.8.19-1 2019/02/12
Copyright 2000-2019 Willy Tarreau <willy at haproxy.org>
Beware, these static files will be served from memory, you need to reload HAProxy service to refresh their contents.

Serve single static file

This is the simplest case. HAProxy will not log these requests.

Create /etc/haproxy/static/index.static.html HTML file including HTTP headers.

HTTP/1.0 200 OK
Cache-Control: no-cache
Connection: close
Content-Type: text/html

<html>
  <body>
    <p>Static file</p>
  </body>
</html>

Use the following HAProxy configuration.

global
  log /dev/log  local0
  log /dev/log  local1 notice
  chroot /var/lib/haproxy
  stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
  stats timeout 30s
  user haproxy
  group haproxy
  daemon

  # Default SSL material locations
  ca-base /etc/ssl/certs
  crt-base /etc/ssl/private

  # Default ciphers to use on SSL-enabled listening sockets.
  # For more information, see ciphers(1SSL). This list is from:
  #  https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
  # An alternative list with additional directives can be obtained from
  #  https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=haproxy
  ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
  ssl-default-bind-options no-sslv3

defaults
  log global
  mode  http
  option  httplog
  option  dontlognull
  timeout connect 5000
  timeout client  50000
  timeout server  50000
  errorfile 400 /etc/haproxy/errors/400.http
  errorfile 403 /etc/haproxy/errors/403.http
  errorfile 408 /etc/haproxy/errors/408.http
  errorfile 500 /etc/haproxy/errors/500.http
  errorfile 502 /etc/haproxy/errors/502.http
  errorfile 503 /etc/haproxy/errors/503.http
  errorfile 504 /etc/haproxy/errors/504.http

listen example-single-static-file
  bind  127.0.0.1:7000
  mode  http

  monitor-uri /static/
  errorfile 200 /etc/haproxy/static/index.static.html

Reload HAProxy service.

$ sudo systemctl reload haproxy

Verify the URL of the static file.

$ curl -v -w "* Received HTTP status code: %{http_code}\n" http://127.0.0.1:7000/static/ 
* Expire in 0 ms for 6 (transfer 0x55f8e8f21f50)
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x55f8e8f21f50)
* Connected to 127.0.0.1 (127.0.0.1) port 7000 (#0)
> GET /static/ HTTP/1.1
> Host: 127.0.0.1:7000
> User-Agent: curl/7.64.0
> Accept: */*
> 
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Cache-Control: no-cache
< Connection: close
< Content-Type: text/html
< 
<html>
  <body>
    <p>Static file</p>
  </body>
</html>
* Closing connection 0
* Received HTTP status code: 200

Anything else will end up returning 503 Service Unavailable HTTP status code.

$ curl -v -w "* Received HTTP status code: %{http_code}\n" http://127.0.0.1:7000/
* Expire in 0 ms for 6 (transfer 0x562ba6b70f50)
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x562ba6b70f50)
* Connected to 127.0.0.1 (127.0.0.1) port 7000 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:7000
> User-Agent: curl/7.64.0
> Accept: */*
> 
* HTTP 1.0, assume close after body
< HTTP/1.0 503 Service Unavailable
< Cache-Control: no-cache
< Connection: close
< Content-Type: text/html
< 
<html><body><h1>503 Service Unavailable</h1>
No server is available to handle this request.
</body></html>

* Closing connection 0
* Received HTTP status code: 503

Serve multiple static files

This is more interesting as it requires a slightly different approach. HAProxy will log these requests using 503 HTTP status code, so it needs to be silenced.

Create /etc/haproxy/static/location.txt text file including HTTP headers.

HTTP/1.0 200 OK
Cache-Control: no-cache
Connection: close
Content-Type: text/plain

location: eu

Create /etc/haproxy/static/serverid.txt text file including HTTP headers.

HTTP/1.0 200 OK
Cache-Control: no-cache
Connection: close
Content-Type: text/plain

serverid: kraken

Create /etc/haproxy/static/environment.txt text file including HTTP headers.

HTTP/1.0 200 OK
Cache-Control: no-cache
Connection: close
Content-Type: text/plain

environment: staging

Use the following HAProxy configuration.

global
  log /dev/log  local0
  log /dev/log  local1 notice
  chroot /var/lib/haproxy
  stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
  stats timeout 30s
  user haproxy
  group haproxy
  daemon

  # Default SSL material locations
  ca-base /etc/ssl/certs
  crt-base /etc/ssl/private

  # Default ciphers to use on SSL-enabled listening sockets.
  # For more information, see ciphers(1SSL). This list is from:
  #  https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
  # An alternative list with additional directives can be obtained from
  #  https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=haproxy
  ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
  ssl-default-bind-options no-sslv3

defaults
  log global
  mode  http
  option  httplog
  option  dontlognull
  timeout connect 5000
  timeout client  50000
  timeout server  50000
  errorfile 400 /etc/haproxy/errors/400.http
  errorfile 403 /etc/haproxy/errors/403.http
  errorfile 408 /etc/haproxy/errors/408.http
  errorfile 500 /etc/haproxy/errors/500.http
  errorfile 502 /etc/haproxy/errors/502.http
  errorfile 503 /etc/haproxy/errors/503.http
  errorfile 504 /etc/haproxy/errors/504.http

userlist static-auth-list
  user static insecure-password static

frontend example-multiple-static-files
  bind 127.0.0.1:8000
  mode http

  acl static-auth http_auth(static-auth-list)
  http-request auth realm static unless static-auth

  acl is-path-static-location-txt    path /location.txt
  acl is-path-static-serverid-txt    path /serverid.txt
  acl is-path-static-environment-txt path /environment.txt

  use_backend example-backend-location-txt    if is-path-static-location-txt
  use_backend example-backend-serverid-txt    if is-path-static-serverid-txt
  use_backend example-backend-environment-txt if is-path-static-environment-txt

backend example-backend-location-txt
  mode http
  http-request set-log-level silent
  errorfile 503 /etc/haproxy/static/location.txt

backend example-backend-serverid-txt
  mode http
  http-request set-log-level silent
  errorfile 503 /etc/haproxy/static/serverid.txt

backend example-backend-environment-txt
  mode http
  http-request set-log-level silent
  errorfile 503 /etc/haproxy/static/environment.txt

Verify the location.txt static file.

$ curl -v -w "* Received HTTP status code: %{http_code}\n" -u static:static http://127.0.0.1:8000/location.txt
* Expire in 0 ms for 6 (transfer 0x55f2385c6f50)
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x55f2385c6f50)
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
* Server auth using Basic with user 'static'
> GET /location.txt HTTP/1.1
> Host: 127.0.0.1:8000
> Authorization: Basic c3RhdGljOnN0YXRpYw==
> User-Agent: curl/7.64.0
> Accept: */*
> 
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Cache-Control: no-cache
< Connection: close
< Content-Type: text/plain
<
location: eu
* Closing connection 0
* Received HTTP status code: 200

Verify the serverid.txt static file.

$ curl -v -w "* Received HTTP status code: %{http_code}\n" -u static:static http://127.0.0.1:8000/serverid.txt
* Expire in 0 ms for 6 (transfer 0x56356a20ef50)
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x56356a20ef50)
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
* Server auth using Basic with user 'static'
> GET /serverid.txt HTTP/1.1
> Host: 127.0.0.1:8000
> Authorization: Basic c3RhdGljOnN0YXRpYw==
> User-Agent: curl/7.64.0
> Accept: */*
> 
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Cache-Control: no-cache
< Connection: close
< Content-Type: text/plain
<
serverid: kraken
* Closing connection 0
* Received HTTP status code: 200

Verify the environment.txt static file.

$ curl -v -w "* Received HTTP status code: %{http_code}\n" -u static:static http://127.0.0.1:8000/environment.txt
* Expire in 0 ms for 6 (transfer 0x5634941faf50)
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x5634941faf50)
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
* Server auth using Basic with user 'static'
> GET /environment.txt HTTP/1.1
> Host: 127.0.0.1:8000
> Authorization: Basic c3RhdGljOnN0YXRpYw==
> User-Agent: curl/7.64.0
> Accept: */*
> 
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Cache-Control: no-cache
< Connection: close
< Content-Type: text/plain
<
environment: staging
* Closing connection 0
* Received HTTP status code: 200

Anything else will end up returning 503 Service Unavailable HTTP status code or 401 Unauthorized if user credentials are missing or incorrect.

$ curl -v -w "* Received HTTP status code: %{http_code}\n" -u static:static http://127.0.0.1:8000/other.txt
* Expire in 0 ms for 6 (transfer 0x55fd8fe78f50)
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x55fd8fe78f50)
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
* Server auth using Basic with user 'static'
> GET /other.txt HTTP/1.1
> Host: 127.0.0.1:8000
> Authorization: Basic c3RhdGljOnN0YXRpYw==
> User-Agent: curl/7.64.0
> Accept: */*
> 
* HTTP 1.0, assume close after body
< HTTP/1.0 503 Service Unavailable
< Cache-Control: no-cache
< Connection: close
< Content-Type: text/html
< 
<html><body><h1>503 Service Unavailable</h1>
No server is available to handle this request.
</body></html>

* Closing connection 0
* Received HTTP status code: 503
$ curl -v -w "* Received HTTP status code: %{http_code}\n" http://127.0.0.1:8000/location.txt
* Expire in 0 ms for 6 (transfer 0x55e7ad280f50)
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x55e7ad280f50)
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> GET /location.txt HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.64.0
> Accept: */*
> 
* HTTP 1.0, assume close after body
< HTTP/1.0 401 Unauthorized
< Cache-Control: no-cache
< Connection: close
< Content-Type: text/html
< WWW-Authenticate: Basic realm="static"
< 
<html><body><h1>401 Unauthorized</h1>
You need a valid user and password to access this content.
</body></html>
* Closing connection 0
* Received HTTP status code: 401

These examples should give you some ideas about how to use the above-mentioned configuration hacks, but please do not use these in production environment.