feat(haproxy): Initial commit
This commit is contained in:
parent
b4bbb66dce
commit
867e1ff919
200
README.md
200
README.md
@ -1,3 +1,199 @@
|
|||||||
# haproxy
|
# HAProxy Docker Compose files
|
||||||
|
|
||||||
HAProxy container setup
|
Docker Compose files to spin up an instance of HAProxy.
|
||||||
|
|
||||||
|
# How to run
|
||||||
|
|
||||||
|
Add a `COMPOSE_ENV` file and save its location as a shell variable along with the location where this repo lives, here for example `/opt/containers/haproxy` plus all other variables. At [env/fqdn_context.env.example](env/fqdn_context.env.example) you'll find an example environment file.
|
||||||
|
|
||||||
|
When everything's ready start HAProxy with Docker Compose, otherwise head down to [Initial setup](#initial-setup) first.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
```
|
||||||
|
export COMPOSE_DIR='/opt/containers/haproxy'
|
||||||
|
export COMPOSE_CTX='ux_vilnius'
|
||||||
|
export COMPOSE_PROJECT='haproxy-'"${COMPOSE_CTX}"
|
||||||
|
export COMPOSE_FILE="${COMPOSE_DIR}"'/docker-compose.yml'
|
||||||
|
export COMPOSE_ENV=<add accordingly>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
On your deployment machine create the necessary Docker context to connect to and control the Docker daemon on whatever target host you'll be using, for example:
|
||||||
|
```
|
||||||
|
docker context create fully.qualified.domain.name --docker 'host=ssh://root@fully.qualified.domain.name'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Start
|
||||||
|
|
||||||
|
```
|
||||||
|
docker --context 'fully.qualified.domain.name' compose --project-name "${COMPOSE_PROJECT}" --file "${COMPOSE_FILE}" --env-file "${COMPOSE_ENV}" up --detach
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reload
|
||||||
|
|
||||||
|
HAProxy lends itself well to graceful reloads in a similar fashion to Nginx. Considering a running HAProxy container named `containername` check config file syntax:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker exec -i containername haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg
|
||||||
|
```
|
||||||
|
|
||||||
|
Reload HAProxy
|
||||||
|
|
||||||
|
```
|
||||||
|
docker kill --signal SIGHUP containername
|
||||||
|
```
|
||||||
|
|
||||||
|
# Initial setup
|
||||||
|
|
||||||
|
We're assuming you run Docker Compose workloads with ZFS-based bind mounts. ZFS management, creating a zpool and setting adequate properties for its datasets is out of scope of this document.
|
||||||
|
|
||||||
|
## Datasets
|
||||||
|
|
||||||
|
Create ZFS datasets.
|
||||||
|
|
||||||
|
* Parent dateset
|
||||||
|
```
|
||||||
|
zfs create -o mountpoint=/opt/docker-data 'zpool/docker-data'
|
||||||
|
```
|
||||||
|
|
||||||
|
* Container-specific dataset
|
||||||
|
```
|
||||||
|
zfs create -p 'zpool/docker-data/haproxy-'"${COMPOSE_CTX}"'/haproxy/config'
|
||||||
|
```
|
||||||
|
|
||||||
|
* Create a subdir for certificates
|
||||||
|
```
|
||||||
|
mkdir -p '/opt/docker-data/haproxy-'"${COMPOSE_CTX}"'/haproxy/config/certs'
|
||||||
|
```
|
||||||
|
|
||||||
|
A change in file or directory ownership is typically not needed. HAProxy runs by default as user ID 99 and group ID 99, it has no problem accessing a bind-mounted file owned by `root:root` with typical `0644` (`rw-r--r--`) permissions.
|
||||||
|
|
||||||
|
## Additional files
|
||||||
|
|
||||||
|
Place the following files on target server. Use the directory structure at [build-context](build-context) as a guide, specifically at `docker-data`.
|
||||||
|
|
||||||
|
```
|
||||||
|
build-context/
|
||||||
|
├── docker-data
|
||||||
|
│ └── config
|
||||||
|
│ ├── certs
|
||||||
|
│ │ ├── example.com.pem
|
||||||
|
│ │ ├── example.com.pem.key
|
||||||
|
│ │ ├── example.net.pem
|
||||||
|
│ │ └── example.net.pem.key
|
||||||
|
│ ├── haproxy.cfg
|
||||||
|
│ └── hosts.map
|
||||||
|
├── Dockerfile
|
||||||
|
└── extras
|
||||||
|
```
|
||||||
|
|
||||||
|
In our example config (see next section [HAProxy config example](#haproxy-config-example)) each certificate is stored in what HAProxy calls a cert bundle: one complete PEM-formatted plain text SSL certificate chain file `something.pem` plus its matching key named `something.pem.key`. The chain file from top to bottom contains domain certificate, optionally intermediate certificate and lastly Certificate Authority certificate. With this naming scheme HAProxy finds keys and matching certs automatically.
|
||||||
|
|
||||||
|
When done head back up to [How to run](#how-to-run).
|
||||||
|
|
||||||
|
# HAProxy config example
|
||||||
|
|
||||||
|
Proper config for your HAProxy use case is beyond the scope of this document. The example [build-context/docker-data/config/haproxy.cfg](build-context/docker-data/config/haproxy.cfg) file is a courtesy config to get you started. Your real world file will likely turn out differently.
|
||||||
|
|
||||||
|
Here's a quick rundown of what you see in the example file.
|
||||||
|
|
||||||
|
## Section `global`
|
||||||
|
|
||||||
|
```
|
||||||
|
global
|
||||||
|
daemon
|
||||||
|
maxconn 200
|
||||||
|
log stdout format raw local0
|
||||||
|
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
|
||||||
|
ssl-default-bind-options prefer-client-ciphers no-tls-tickets ssl-min-ver TLSv1.3
|
||||||
|
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
|
||||||
|
ssl-default-server-options no-tls-tickets ssl-min-ver TLSv1.3
|
||||||
|
```
|
||||||
|
|
||||||
|
We follow recommendations from [Mozilla's SSL Configuration Generator](https://ssl-config.mozilla.org/) for SSL settings. The container outputs logging data to `stdout`. We're setting a global connection limit of `maxconn 200`. Keeping some limit in place makes HAProxy behavior predictable, it doesn't have to be this low. This argument goes hand in hand with the `ulimits` directive in [docker-compose.yml](docker-compose.yml).
|
||||||
|
|
||||||
|
```
|
||||||
|
...
|
||||||
|
...
|
||||||
|
ulimits:
|
||||||
|
nproc: ${ULIMIT_NPROC-65535}
|
||||||
|
nofile:
|
||||||
|
soft: ${ULIMIT_NPROC-65535}
|
||||||
|
hard: ${ULIMIT_NPROC-65535}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you don't specify a sensible limit HAProxy will try and allocate whatever your platform allows.
|
||||||
|
|
||||||
|
## Sections `defaults`
|
||||||
|
|
||||||
|
```
|
||||||
|
defaults generic_defaults
|
||||||
|
log global
|
||||||
|
maxconn 50
|
||||||
|
|
||||||
|
# Irrelevant on frontends
|
||||||
|
timeout connect 5s
|
||||||
|
timeout server 5s
|
||||||
|
|
||||||
|
# Irrelevant on backends
|
||||||
|
timeout client 30s
|
||||||
|
|
||||||
|
defaults http_defaults from generic_defaults
|
||||||
|
mode http
|
||||||
|
option forwardfor
|
||||||
|
option httplog
|
||||||
|
```
|
||||||
|
|
||||||
|
Set generic defaults and supplement them with HTTP-specific defaults.
|
||||||
|
|
||||||
|
## Statistics
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend fe_stats from http_defaults
|
||||||
|
bind :"${STATS_PORT-61000}"
|
||||||
|
stats enable
|
||||||
|
stats uri /
|
||||||
|
stats refresh 10s
|
||||||
|
acl ips_allowed src 10.10.10.0/24 10.10.1.0/24
|
||||||
|
http-request deny if !ips_allowed
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable statistics output. Access to this endpoint is permitted only from `acl ips_allowed` which can be for example network prefixes in your local network.
|
||||||
|
|
||||||
|
## A proxy example
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend fe_https-proxy from http_defaults
|
||||||
|
bind :80
|
||||||
|
bind :443 ssl strict-sni crt /usr/local/etc/haproxy/certs alpn h2,http/1.1
|
||||||
|
redirect scheme https code 301 if !{ ssl_fc }
|
||||||
|
http-response set-header Strict-Transport-Security max-age=63072000
|
||||||
|
use_backend %[req.hdr(host),lower,map_dom(/usr/local/etc/haproxy/hosts.map,be_no-match)]
|
||||||
|
```
|
||||||
|
|
||||||
|
We use a `hosts.map` file to map HTTP `host` header information for incoming requests to the backend that should serve them. Check out [build-context/docker-data/config/hosts.map](build-context/docker-data/config/hosts.map) for an example file.
|
||||||
|
|
||||||
|
Also HTTP traffic gets redirected to HTTPS and we specify more best practices suggested by [Mozilla's SSL Configuration Generator](https://ssl-config.mozilla.org/).
|
||||||
|
|
||||||
|
```
|
||||||
|
backend be_example.net from http_defaults
|
||||||
|
compression algo gzip
|
||||||
|
compression type text/plain text/css
|
||||||
|
option httpchk
|
||||||
|
http-check send meth HEAD uri /status hdr Host example.net hdr User-Agent "${HEALTH_CHECK_USER_AGENT-HAProxy} health check"
|
||||||
|
server example.net example.net:8080 check inter 10s fall 3 rise 3
|
||||||
|
```
|
||||||
|
|
||||||
|
In our backend example we do HTTP health checks by sending a `HEAD` request every 10 seconds. When doing so we identify ourselves as an HAProxy instance so the downstream application can recognize legitimate health checks in its logs.
|
||||||
|
|
||||||
|
Note that both `frontend` and `backend` inherit settings from the `http_defaults` section defined earlier.
|
||||||
|
|
||||||
|
## No match
|
||||||
|
|
||||||
|
```
|
||||||
|
backend be_no-match from http_defaults
|
||||||
|
http-request deny deny_status 404
|
||||||
|
```
|
||||||
|
|
||||||
|
To requests without a match we serve an HTTP status code 404.
|
||||||
|
14
build-context/Dockerfile
Normal file
14
build-context/Dockerfile
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# For the remainder of this Dockerfile EXAMPLE_ARG_FOR_DOCKERFILE will be
|
||||||
|
# available with a value of 'must_be_available_in_dockerfile', check out the env
|
||||||
|
# file at 'env/fully.qualified.domain.name.example' for reference.
|
||||||
|
# ARG EXAMPLE_ARG_FOR_DOCKERFILE
|
||||||
|
|
||||||
|
# Another env var, this one's needed in the example build step below:
|
||||||
|
# ARG HAPROXY_VERSION
|
||||||
|
|
||||||
|
# Example
|
||||||
|
# FROM "haproxy:${HAPROXY_VERSION}"
|
||||||
|
# RUN apt-get update && \
|
||||||
|
# apt-get -y install \
|
||||||
|
# somepackage-6.q16-6-extra && \
|
||||||
|
# rm -rf /var/lib/apt/lists/*
|
0
build-context/docker-data/.gitkeep
Normal file
0
build-context/docker-data/.gitkeep
Normal file
49
build-context/docker-data/config/haproxy.cfg
Normal file
49
build-context/docker-data/config/haproxy.cfg
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
global
|
||||||
|
daemon
|
||||||
|
maxconn 200
|
||||||
|
log stdout format raw local0
|
||||||
|
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
|
||||||
|
ssl-default-bind-options prefer-client-ciphers no-tls-tickets ssl-min-ver TLSv1.3
|
||||||
|
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
|
||||||
|
ssl-default-server-options no-tls-tickets ssl-min-ver TLSv1.3
|
||||||
|
|
||||||
|
defaults generic_defaults
|
||||||
|
log global
|
||||||
|
maxconn 50
|
||||||
|
|
||||||
|
# Irrelevant on frontends
|
||||||
|
timeout connect 5s
|
||||||
|
timeout server 5s
|
||||||
|
|
||||||
|
# Irrelevant on backends
|
||||||
|
timeout client 30s
|
||||||
|
|
||||||
|
defaults http_defaults from generic_defaults
|
||||||
|
mode http
|
||||||
|
option forwardfor
|
||||||
|
option httplog
|
||||||
|
|
||||||
|
frontend fe_stats from http_defaults
|
||||||
|
bind :"${STATS_PORT-61000}"
|
||||||
|
stats enable
|
||||||
|
stats uri /
|
||||||
|
stats refresh 10s
|
||||||
|
acl ips_allowed src 10.10.10.0/24 10.10.1.0/24
|
||||||
|
http-request deny if !ips_allowed
|
||||||
|
|
||||||
|
frontend fe_https-proxy from http_defaults
|
||||||
|
bind :80
|
||||||
|
bind :443 ssl strict-sni crt /usr/local/etc/haproxy/certs alpn h2,http/1.1
|
||||||
|
redirect scheme https code 301 if !{ ssl_fc }
|
||||||
|
http-response set-header Strict-Transport-Security max-age=63072000
|
||||||
|
use_backend %[req.hdr(host),lower,map_dom(/usr/local/etc/haproxy/hosts.map,be_no-match)]
|
||||||
|
|
||||||
|
backend be_example.net from http_defaults
|
||||||
|
compression algo gzip
|
||||||
|
compression type text/plain text/css
|
||||||
|
option httpchk
|
||||||
|
http-check send meth HEAD uri /status hdr Host example.net hdr User-Agent "${HEALTH_CHECK_USER_AGENT-HAProxy} health check"
|
||||||
|
server nextcloud-loft example.net:8080 check inter 10s fall 3 rise 3
|
||||||
|
|
||||||
|
backend be_no-match from http_defaults
|
||||||
|
http-request deny deny_status 404
|
6
build-context/docker-data/config/hosts.map
Normal file
6
build-context/docker-data/config/hosts.map
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# See https://www.haproxy.com/blog/how-to-map-domain-names-to-backend-server-pools-with-haproxy
|
||||||
|
#
|
||||||
|
# domain backend
|
||||||
|
fully.qualified.domain.name be_example.net
|
||||||
|
example.net be_example.net
|
||||||
|
another.domain.com be_example.net
|
0
build-context/extras/.gitkeep
Normal file
0
build-context/extras/.gitkeep
Normal file
11
common-settings.yml
Normal file
11
common-settings.yml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
common-settings:
|
||||||
|
environment:
|
||||||
|
TZ: "${TIMEZONE:-Etc/UTC}"
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "10"
|
||||||
|
compress: "true"
|
||||||
|
restart: "${RESTARTPOLICY:-unless-stopped}"
|
10
docker-compose.override.yml
Normal file
10
docker-compose.override.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
haproxy-build:
|
||||||
|
image: "haproxy:${HAPROXY_VERSION}"
|
||||||
|
profiles: ["build"]
|
||||||
|
build:
|
||||||
|
context: "build-context/haproxy"
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
EXAMPLE_ARG_FOR_DOCKERFILE: "${EXAMPLE_ARG_FROM_ENV_FILE}"
|
||||||
|
HAPROXY_VERSION: "${HAPROXY_VERSION}"
|
35
docker-compose.yml
Normal file
35
docker-compose.yml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
services:
|
||||||
|
haproxy:
|
||||||
|
image: "haproxy:${HAPROXY_VERSION}"
|
||||||
|
container_name: "haproxy-${CONTEXT}"
|
||||||
|
networks:
|
||||||
|
haproxy-default:
|
||||||
|
ulimits:
|
||||||
|
nproc: ${ULIMIT_NPROC-65535}
|
||||||
|
nofile:
|
||||||
|
soft: ${ULIMIT_NPROC-65535}
|
||||||
|
hard: ${ULIMIT_NPROC-65535}
|
||||||
|
extends:
|
||||||
|
file: common-settings.yml
|
||||||
|
service: common-settings
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 443:443
|
||||||
|
- ${STATS_PORT}:${STATS_PORT}
|
||||||
|
volumes:
|
||||||
|
- /opt/docker-data/haproxy-${CONTEXT}/haproxy/config/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg
|
||||||
|
- /opt/docker-data/haproxy-${CONTEXT}/haproxy/config/hosts.map:/usr/local/etc/haproxy/hosts.map
|
||||||
|
- /opt/docker-data/haproxy-${CONTEXT}/haproxy/config/certs:/usr/local/etc/haproxy/certs
|
||||||
|
environment:
|
||||||
|
STATS_PORT: ${STATS_PORT}
|
||||||
|
HEALTH_CHECK_USER_AGENT: "haproxy-${CONTEXT}"
|
||||||
|
networks:
|
||||||
|
haproxy-default:
|
||||||
|
name: haproxy-${CONTEXT}
|
||||||
|
driver: bridge
|
||||||
|
driver_opts:
|
||||||
|
com.docker.network.enable_ipv6: "false"
|
||||||
|
ipam:
|
||||||
|
driver: default
|
||||||
|
config:
|
||||||
|
- subnet: ${SUBNET}
|
36
env/fqdn_context.env.example
vendored
Normal file
36
env/fqdn_context.env.example
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
CONTEXT=ux_vilnius
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Set something sensible here and uncomment
|
||||||
|
# ---
|
||||||
|
# HAPROXY_VERSION=x.y.z
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# On which port do you want HAProxy's statistics page exposed?
|
||||||
|
STATS_PORT=61000
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Feel free to leave defaults. They apply while these vars are commented out
|
||||||
|
# ---
|
||||||
|
# RESTARTPOLICY=unless-stopped
|
||||||
|
# TIMEZONE=Etc/UTC
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Subnet to use for this Docker Compose project. Docker defaults to
|
||||||
|
# container networks in prefix 172.16.0.0/12 which is 1 million addresses in
|
||||||
|
# the range from 172.16.0.0 to 172.31.255.255. Docker uses 172.17.0.0/16 for
|
||||||
|
# itself. Use any sensible prefix in 172.16.0.0/12 here except for Docker's
|
||||||
|
# own 172.17.0.0/16.
|
||||||
|
# ---
|
||||||
|
SUBNET=172.30.95.0/24
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# See 'docker-compose.override.yml' for how to make a variable available in
|
||||||
|
# a Dockerfile
|
||||||
|
# ---
|
||||||
|
# EXAMPLE_ARG_FROM_ENV_FILE=must_be_available_in_dockerfile
|
Loading…
x
Reference in New Issue
Block a user