diff --git a/.env.sample b/.env.sample old mode 100755 new mode 100644 index 2597c15..b7fc4bd --- a/.env.sample +++ b/.env.sample @@ -1,8 +1,16 @@ # uncomment in your prod env to make the repo update itself: # PYTHON_ENV=production -# comment out in your prod env to get production certs: -LE_STAGING=1 +# Trusted ips for proxy protocol, usually the ip of your router (sending to traefik) +TRUSTED_IPS_CIDR=192.168.0.0/24 + +# Traefik dashboard domain name +TRAEFIK_DOMAIN=traefik.example.com +# Traefic basic auth user+pass (htpasswd) WHICH NEEDS TO BE QUOTED! +# TRAEFIK_ADMIN='admin:$xxx/yyy' + +# Uncomment next line for prod certs +TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_CASERVER=https://acme-staging-v02.api.letsencrypt.org/directory +TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL=mail@example.com -# LE_EMAIL=you@gmail.com # API_KEY=your_api_key diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 index 3ace568..d7f0155 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ __pycache__ .venv certs/* db.yml -proxy/*.conf +proxy/nginx/*.conf +proxy/traefik/*.yml upstream diff --git a/README.md b/README.md old mode 100755 new mode 100644 diff --git a/api/main.py b/api/main.py index fa82ef9..23ec3a0 100755 --- a/api/main.py +++ b/api/main.py @@ -22,7 +22,7 @@ ) from lib.git import update_repo from lib.models import Env, PingPayload, Project, Service, WorkflowJobPayload -from lib.proxy import reload_proxy, update_proxy, write_nginx +from lib.proxy import reload_proxy, update_proxy, write_proxies from lib.upstream import check_upstream, update_upstream, write_upstreams dotenv.load_dotenv() @@ -36,7 +36,7 @@ def _after_config_change(project: str, service: str = None) -> None: """Run after a project is updated""" info("Config change detected") get_certs(project) - write_nginx() + write_proxies() write_upstreams() update_upstream(project, service, rollout=True) update_proxy() diff --git a/bin/apply.py b/bin/apply.py index 08634ec..11aa569 100755 --- a/bin/apply.py +++ b/bin/apply.py @@ -8,7 +8,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) from lib.certs import get_certs -from lib.proxy import reload_proxy, write_nginx +from lib.proxy import reload_proxy, write_proxies from lib.upstream import update_upstreams, write_upstreams load_dotenv() @@ -17,7 +17,7 @@ # if any argument is passed, we also do a rollout rollout = bool(sys.argv[1]) if len(sys.argv) > 1 else False get_certs() - write_nginx() + write_proxies() write_upstreams() update_upstreams(rollout) reload_proxy() diff --git a/bin/start-api.sh b/bin/start-api.sh index e3c0ef7..b2c6325 100755 --- a/bin/start-api.sh +++ b/bin/start-api.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh . .venv/bin/activate -kill $(fuser 8888/tcp 2>/dev/null | awk '{ print $1 }') +kill $(fuser 8888/tcp 2>/dev/null | awk '{ print $1 }') 2>/dev/null PYTHONPATH=. python api/main.py main:app >logs/error.log 2>&1 & diff --git a/bin/write-artifacts.py b/bin/write-artifacts.py index 93bc866..b486e3c 100755 --- a/bin/write-artifacts.py +++ b/bin/write-artifacts.py @@ -8,12 +8,12 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) from lib.data import validate_db -from lib.proxy import write_nginx +from lib.proxy import write_proxies from lib.upstream import write_upstreams load_dotenv() if __name__ == "__main__": validate_db() - write_nginx() + write_proxies() write_upstreams() diff --git a/lib/certs.py b/lib/certs.py old mode 100755 new mode 100644 diff --git a/lib/functions.sh b/lib/functions.sh index 7659fe0..1488f42 100644 --- a/lib/functions.sh +++ b/lib/functions.sh @@ -28,7 +28,7 @@ dcp() { docker compose --project-directory $dir -p $project -f $dir/docker-compose.yml pull cmd="$cmd --remove-orphans" fi - eval "docker compose --project-directory $dir -p $project -f $dir/docker-compose.yml $cmd $@" + eval "export HOST_GID=$(id -g daemon) && docker compose --project-directory $dir -p $project -f $dir/docker-compose.yml $cmd $@" } dcpx() { diff --git a/lib/git.py b/lib/git.py index 25ea2cb..0e0c265 100644 --- a/lib/git.py +++ b/lib/git.py @@ -2,7 +2,7 @@ from dotenv import load_dotenv -from lib.proxy import reload_proxy, write_nginx +from lib.proxy import reload_proxy, write_proxies from lib.upstream import update_upstreams, write_upstreams from lib.utils import run_command @@ -15,7 +15,7 @@ def update_repo() -> None: if os.environ["PYTHON_ENV"] == "production": run_command("git fetch origin main".split(" "), cwd=".") run_command("git reset --hard origin/main".split(" "), cwd=".") - write_nginx() + write_proxies() write_upstreams() update_upstreams() reload_proxy() diff --git a/lib/models.py b/lib/models.py index dfb8346..2c99070 100644 --- a/lib/models.py +++ b/lib/models.py @@ -11,16 +11,27 @@ class Env(BaseModel): class Service(BaseModel): """Service model""" - name: str - port: int = 8080 - """When set, the service will be exposed on this domain.""" - image: str = None command: str = None + """The command to run in the service""" + env: Env = None + """A dictionary of environment variables to pass to the service""" + image: str = None + """The image name plus tag to use for the service""" + labels: List[str] = [] + """Extra labels to add to the service. Should not interfere with generated traefik labels for ingress.""" + name: str passthrough: bool = False """Wether or not traffic to this service is forwarded as-is (without terminating SSL)""" + path_prefix: str = None + """Should the service be exposed under a specific path?""" + path_remove: bool = False + """When set, the path prefix will be removed from the request before forwarding it to the service""" + port: int = 8080 + """When set, the service will be exposed on this domain.""" + proxyprotocol: bool = True + """When set, the service will be exposed using the PROXY protocol version 2""" volumes: List[str] = [] - env: Env = None - """A dictionary of environment variables to pass to the service""" + """A list of volumes to mount in the service""" class Project(BaseModel): diff --git a/lib/proxy.py b/lib/proxy.py index 256ec01..0c842ad 100644 --- a/lib/proxy.py +++ b/lib/proxy.py @@ -1,11 +1,15 @@ +import os from logging import info from typing import Dict, List +from dotenv import load_dotenv from jinja2 import Template from lib.data import get_project, get_projects from lib.utils import run_command +load_dotenv() + def get_domains(project: str = None) -> List[str]: """Get all domains in use""" @@ -44,11 +48,11 @@ def write_maps() -> None: internal = tpl.render(map=internal_map) passthrough = tpl.render(map=passthrough_map) terminate = tpl.render(map=terminate_map) - with open("proxy/map/internal.conf", "w", encoding="utf-8") as f: + with open("proxy/nginx/map/internal.conf", "w", encoding="utf-8") as f: f.write(internal) - with open("proxy/map/passthrough.conf", "w", encoding="utf-8") as f: + with open("proxy/nginx/map/passthrough.conf", "w", encoding="utf-8") as f: f.write(passthrough) - with open("proxy/map/terminate.conf", "w", encoding="utf-8") as f: + with open("proxy/nginx/map/terminate.conf", "w", encoding="utf-8") as f: f.write(terminate) @@ -58,7 +62,7 @@ def write_proxy() -> None: t = f.read() tpl = Template(t) terminate = tpl.render(project=project) - with open("proxy/proxy.conf", "w", encoding="utf-8") as f: + with open("proxy/nginx/proxy.conf", "w", encoding="utf-8") as f: f.write(terminate) @@ -68,14 +72,47 @@ def write_terminate() -> None: t = f.read() tpl = Template(t) terminate = tpl.render(domains=domains) - with open("proxy/terminate.conf", "w", encoding="utf-8") as f: + with open("proxy/nginx/terminate.conf", "w", encoding="utf-8") as f: f.write(terminate) -def write_nginx() -> None: +def write_routers() -> None: + projects = get_projects(filter=lambda p, s: not bool(p.entrypoint) or p.entrypoint == s.name) + with open("proxy/tpl/routers-web.yml.j2", encoding="utf-8") as f: + t = f.read() + tpl_routers_web = Template(t) + domain = os.environ.get("TRAEFIK_DOMAIN") + traefik_admin = os.environ.get("TRAEFIK_ADMIN") + routers_web = tpl_routers_web.render( + projects=projects, traefik_rule=f"Host(`{domain}`)", traefik_admin=traefik_admin + ) + with open("proxy/traefik/routers-web.yml", "w", encoding="utf-8") as f: + f.write(routers_web) + with open("proxy/tpl/routers-tcp.yml.j2", encoding="utf-8") as f: + t = f.read() + tpl_routers_tcp = Template(t) + routers_tcp = tpl_routers_tcp.render(projects=projects, traefik_rule=f"HostSNI(`{domain}`)") + with open("proxy/traefik/routers-tcp.yml", "w", encoding="utf-8") as f: + f.write(routers_tcp) + with open("proxy/tpl/config-tcp.yml.j2", encoding="utf-8") as f: + t = f.read() + tpl_config_tcp = Template(t) + config_tcp = tpl_config_tcp.render(trusted_ips_cidr=os.environ.get("TRUSTED_IPS_CIDR")) + with open("proxy/traefik/config-tcp.yml", "w", encoding="utf-8") as f: + f.write(config_tcp) + with open("proxy/tpl/config-web.yml.j2", encoding="utf-8") as f: + t = f.read() + tpl_config_web = Template(t) + config_web = tpl_config_web.render(trusted_ips_cidr=os.environ.get("TRUSTED_IPS_CIDR")) + with open("proxy/traefik/config-web.yml", "w", encoding="utf-8") as f: + f.write(config_web) + + +def write_proxies() -> None: write_maps() write_proxy() write_terminate() + write_routers() def update_proxy( diff --git a/lib/proxy_test.py b/lib/proxy_test.py index c1e4035..5f6df11 100644 --- a/lib/proxy_test.py +++ b/lib/proxy_test.py @@ -14,7 +14,7 @@ get_terminate_map, reload_proxy, write_maps, - write_nginx, + write_proxies, write_proxy, write_terminate, ) @@ -117,9 +117,9 @@ def test_write_maps( mock_open.call_args_list, [ mock.call("proxy/tpl/map.conf.j2", encoding="utf-8"), - mock.call("proxy/map/internal.conf", "w", encoding="utf-8"), - mock.call("proxy/map/passthrough.conf", "w", encoding="utf-8"), - mock.call("proxy/map/terminate.conf", "w", encoding="utf-8"), + mock.call("proxy/nginx/map/internal.conf", "w", encoding="utf-8"), + mock.call("proxy/nginx/map/passthrough.conf", "w", encoding="utf-8"), + mock.call("proxy/nginx/map/terminate.conf", "w", encoding="utf-8"), ], ) @@ -135,7 +135,7 @@ def test_write_proxy(self, _: Mock, mock_open: Mock) -> None: mock_open.call_args_list, [ call("proxy/tpl/proxy.conf.j2", encoding="utf-8"), - call("proxy/proxy.conf", "w", encoding="utf-8"), + call("proxy/nginx/proxy.conf", "w", encoding="utf-8"), ], ) @@ -153,16 +153,16 @@ def test_write_terminate(self, _: Mock, mock_get_domains: Mock, mock_open: Mock) mock_open.call_args_list, [ call("proxy/tpl/terminate.conf.j2", encoding="utf-8"), - call("proxy/terminate.conf", "w", encoding="utf-8"), + call("proxy/nginx/terminate.conf", "w", encoding="utf-8"), ], ) @mock.patch("lib.proxy.write_maps") @mock.patch("lib.proxy.write_proxy") @mock.patch("lib.proxy.write_terminate") - def test_write_nginx(self, mock_write_terminate: Mock, mock_write_proxy: Mock, mock_write_maps: Mock) -> None: + def test_write_proxies(self, mock_write_terminate: Mock, mock_write_proxy: Mock, mock_write_maps: Mock) -> None: # Call the function under test - write_nginx() + write_proxies() # Assert that the write_maps, write_proxy, and write_terminate functions are called mock_write_maps.assert_called_once() diff --git a/lib/upstream.py b/lib/upstream.py index 1104760..b7847ac 100644 --- a/lib/upstream.py +++ b/lib/upstream.py @@ -1,34 +1,39 @@ import os from logging import info -from typing import List +from dotenv import load_dotenv from jinja2 import Template from lib.data import get_project, get_projects, get_service -from lib.models import Service +from lib.models import Project from lib.utils import run_command +load_dotenv() -def write_upstream(project: str, services: List[Service]) -> None: + +def write_upstream(project: Project) -> None: with open("tpl/docker-compose.yml.j2", encoding="utf-8") as f: tpl = f.read() - content = Template(tpl).render(project=project, services=services) - with open(f"upstream/{project}/docker-compose.yml", "w", encoding="utf-8") as f: + if os.environ.get("PYTHON_ENV") != "production": + content = Template(tpl).render(project=project, domain=os.environ.get("TRAEFIK_DOMAIN"), env="development") + else: + content = Template(tpl).render(project=project, domain=project.domain) + with open(f"upstream/{project.name}/docker-compose.yml", "w", encoding="utf-8") as f: f.write(content) -def write_upstream_volume_folders(project: str, services: List[Service]) -> None: - for svc in services: - for path in svc.volumes: - os.makedirs(f"upstream/{project}{path}", exist_ok=True) +def write_upstream_volume_folders(project: Project) -> None: + for s in project.services: + for path in s.volumes: + os.makedirs(f"upstream/{project.name}{path}", exist_ok=True) def write_upstreams() -> None: # iterate over projects that have an entrypoint; for p in [project for project in get_projects() if project.entrypoint]: os.makedirs(f"upstream/{p.name}", exist_ok=True) - write_upstream(p.name, p.services) - write_upstream_volume_folders(p.name, p.services) + write_upstream(p) + write_upstream_volume_folders(p) def check_upstream(project: str, service: str = None) -> None: diff --git a/lib/upstream_test.py b/lib/upstream_test.py index d8ec106..c1435ae 100644 --- a/lib/upstream_test.py +++ b/lib/upstream_test.py @@ -68,22 +68,22 @@ def test_write_upstream( mock_open: Mock, ) -> None: - services = [ - Service( - image="morriz/hello-world:main", - name="master", - port=8080, - env=Env(**{"TARGET": "cost concerned people", "INFORMANT": "http://test-informant:8080"}), - volumes=["./data/bla:/data/bla", "./etc/dida:/etc/dida"], - ), - Service(image="morriz/hello-world:main", name="informant", port=8080, env=Env(**{"TARGET": "boss"})), - ] + project = Project( + name="test", + services=[ + Service( + image="morriz/hello-world:main", + name="master", + port=8080, + env=Env(**{"TARGET": "cost concerned people", "INFORMANT": "http://test-informant:8080"}), + volumes=["./data/bla:/data/bla", "./etc/dida:/etc/dida"], + ), + Service(image="morriz/hello-world:main", name="informant", port=8080, env=Env(**{"TARGET": "boss"})), + ], + ) # Call the function under test - write_upstream( - "test", - services=services, - ) + write_upstream(project) # Assert that the subprocess.Popen was called with the correct arguments mock_open.return_value.write.assert_called_once_with(_ret_tpl) diff --git a/load-test.js b/load-test.js new file mode 100644 index 0000000..9095da4 --- /dev/null +++ b/load-test.js @@ -0,0 +1,17 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + insecureSkipTLSVerify: true, + stages: [ + // { duration: '30s', target: 20 }, + { duration: '0m20s', target: 1 }, + // { duration: '20s', target: 20 }, + ], +}; + +export default function () { + const res = http.get(`https://hello.srv.instrukt.ai`); + check(res, { 'status was 200': (r) => r.status == 200 }); + sleep(1); +} diff --git a/proxy/docker-compose-nginx.yml b/proxy/docker-compose-nginx.yml new file mode 100755 index 0000000..5cbbc6f --- /dev/null +++ b/proxy/docker-compose-nginx.yml @@ -0,0 +1,40 @@ +--- +version: '3.8' + +networks: + proxynet: + name: proxynet + driver: bridge + driver_opts: + encrypted: 'true' + +services: + proxy: + image: nginxinc/nginx-unprivileged:stable + networks: + - default + ports: + - 8443:8443 + - 8080:8080 + restart: unless-stopped + volumes: + - ../data:/data + - ./nginx/map:/etc/nginx/map:ro + - ./nginx/proxy.conf:/etc/nginx/nginx.conf:ro + + terminate: + image: nginxinc/nginx-unprivileged:stable + # user: 101:101 + depends_on: + - proxy + networks: + - default + - proxynet + expose: + - '8443' + restart: unless-stopped + volumes: + - ../certs:/certs + - ./nginx/map:/etc/nginx/map:ro + - ./nginx/terminate.conf:/etc/nginx/nginx.conf:ro + - ./nginx/snippets:/etc/nginx/snippets:ro diff --git a/proxy/docker-compose.yml b/proxy/docker-compose.yml index 946a5da..38802c0 100755 --- a/proxy/docker-compose.yml +++ b/proxy/docker-compose.yml @@ -9,32 +9,73 @@ networks: encrypted: 'true' services: - proxy: - image: nginxinc/nginx-unprivileged:stable + # watchtower: + # container_name: watchtower + # image: containrrr/watchtower:1.7.1 + # networks: + # - traefik + # restart: always + + dockerproxy: + image: wollomatic/socket-proxy:1 # see https://github.com/wollomatic/simple-traefik for reference + container_name: dockerproxy + # this image replaced https://github.com/Tecnativa/docker-socket-proxy as the socket proxy + # for an example with the tecnativa proxy refer tags before 2.10 + # depends_on: + # - watchtower + command: + # with this configuration socket-proxy acts similar to the tecnativa proxy. For additional hardening + # please refer to the documentation of the wollomatic/socket-proxy image + - '-loglevel=info' # set to debug for far more logging + - '-allowfrom=traefik-web' + - '-listenip=0.0.0.0' + - '-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)' # this regexp allows readonly access only for requests that traefik needs + - '-shutdowngracetime=5' + - '-watchdoginterval=600' + - '-stoponwatchdog' + read_only: true + mem_limit: 64M + cap_drop: + - ALL + security_opt: + - no-new-privileges + user: '0:1' # replace with the uid:gid of the host owner of the docker socket + networks: + - default + expose: + - '2375' + restart: always + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + + traefik-tcp: + image: traefik:v2.11 + container_name: traefik-tcp + env_file: + - ../.env networks: - default ports: - - 8443:8443 - 8080:8080 - restart: unless-stopped + - 8443:8443 volumes: - - ../data:/data - - ./map:/etc/nginx/map - - ./proxy.conf:/etc/nginx/nginx.conf + - ./traefik/config-tcp.yml:/etc/traefik/traefik.yml:ro + - ./traefik/routers-tcp.yml:/etc/traefik/routers-tcp.yml:ro - terminate: - image: nginxinc/nginx-unprivileged:stable - # user: 101:101 + traefik-web: + image: traefik:v2.11 + container_name: traefik-web depends_on: - - proxy + - dockerproxy + env_file: + - ../.env networks: - default - proxynet expose: + - '8080' - '8443' - restart: unless-stopped volumes: - - ../certs:/certs - - ./map:/etc/nginx/map - - ./terminate.conf:/etc/nginx/nginx.conf - - ./snippets:/etc/nginx/snippets + - ./traefik/config-web.yml:/etc/traefik/traefik.yml:ro + - ./traefik/routers-web.yml:/etc/traefik/routers-web.yml:ro + - ./traefik/acme:/etc/acme diff --git a/proxy/map/.gitignore b/proxy/nginx/map/.gitignore similarity index 100% rename from proxy/map/.gitignore rename to proxy/nginx/map/.gitignore diff --git a/proxy/snippets/headers.js b/proxy/nginx/snippets/headers.js similarity index 100% rename from proxy/snippets/headers.js rename to proxy/nginx/snippets/headers.js diff --git a/proxy/snippets/server.conf b/proxy/nginx/snippets/server.conf similarity index 100% rename from proxy/snippets/server.conf rename to proxy/nginx/snippets/server.conf diff --git a/proxy/tpl/config-tcp.yml.j2 b/proxy/tpl/config-tcp.yml.j2 new file mode 100644 index 0000000..a7ac9a4 --- /dev/null +++ b/proxy/tpl/config-tcp.yml.j2 @@ -0,0 +1,43 @@ +accessLog: + format: json + fields: + defaultMode: keep + headers: + defaultMode: keep + +entryPoints: + tcp: + address: ':8080' + forwardedHeaders: + trustedIPs: + - 127.0.0.0/32 + - {{ trusted_ips_cidr }} + proxyProtocol: + trustedIPs: + - 127.0.0.0/32 + - {{ trusted_ips_cidr }} + + tcp-secure: + address: ':8443' + forwardedHeaders: + trustedIPs: + - 127.0.0.1/32 + - {{ trusted_ips_cidr }} + proxyProtocol: + trustedIPs: + - 127.0.0.1/32 + - {{ trusted_ips_cidr }} + +log: + level: DEBUG + +providers: + file: + filename: /etc/traefik/routers-tcp.yml + watch: true + +experimental: + plugins: + fail2ban: + moduleName: 'github.com/tomMoulard/fail2ban' + version: 'v0.7.1' diff --git a/proxy/tpl/config-web.yml.j2 b/proxy/tpl/config-web.yml.j2 new file mode 100644 index 0000000..ecc9c47 --- /dev/null +++ b/proxy/tpl/config-web.yml.j2 @@ -0,0 +1,68 @@ +accessLog: + format: json + fields: + defaultMode: keep + headers: + defaultMode: keep + +entryPoints: + web: + address: ':8080' + forwardedHeaders: + trustedIPs: + - 127.0.0.0/32 + - {{ trusted_ips_cidr }} + proxyProtocol: + trustedIPs: + - 127.0.0.0/32 + - {{ trusted_ips_cidr }} + + web-secure: + address: ':8443' + # http3: + # advertisedPort: 8443 + forwardedHeaders: + trustedIPs: + - 127.0.0.1/32 + - {{ trusted_ips_cidr }} + proxyProtocol: + trustedIPs: + - 127.0.0.1/32 + - {{ trusted_ips_cidr }} + +api: + insecure: false + dashboard: true + debug: true + +providers: + docker: + exposedByDefault: false + endpoint: tcp://dockerproxy:2375 + network: default + file: + filename: /etc/traefik/routers-web.yml + watch: true + +certificatesResolvers: + letsencrypt: + acme: + caServer: https://acme-staging-v02.api.letsencrypt.org/directory + storage: /etc/acme/acme.json + # tlsChallenge: {} + httpChallenge: + entryPoint: web + +experimental: + http3: true + plugins: + traefik-get-real-ip: + moduleName: "github.com/Paxxs/traefik-get-real-ip" + version: "v1.0.3" + +global: + sendAnonymousUsage: true + +log: + level: DEBUG + diff --git a/proxy/tpl/routers-tcp.yml.j2 b/proxy/tpl/routers-tcp.yml.j2 new file mode 100644 index 0000000..aa414cb --- /dev/null +++ b/proxy/tpl/routers-tcp.yml.j2 @@ -0,0 +1,50 @@ +{%- if projects %} +tcp: + routers: + {%- for p in projects %} + {%- set s = p.services[0] %} + {%- if s.passthrough %} + {{ p.name }}: + entryPoints: + - tcp-secure + service: {{ p.name }} + rule: 'HostSNI(`{{ p.domain }}`)' + tls: + passthrough: true + {%- endif %} + {%- endfor %} + http: + entryPoints: + - tcp + service: http + rule: 'HostSNI(`*`)' + https: + entryPoints: + - tcp-secure + service: https + rule: 'HostSNI(`*`)' + tls: + passthrough: true + services: + {%- for p in projects %} + {%- set s = p.services[0] %} + {%- if s.passthrough %} + {{ p.name }}: + loadBalancer: + {%- if s.proxyprotocol %} + proxyProtocol: + version: 2 + {%- endif %} + servers: + - address: {{ s.name }}:{{ s.port }} + {%- endif %} + {%- endfor %} +{%- endif %} + http: + loadBalancer: + servers: + - address: traefik-web:8080 + https: + loadBalancer: + servers: + - address: traefik-web:8443 diff --git a/proxy/tpl/routers-web.yml.j2 b/proxy/tpl/routers-web.yml.j2 new file mode 100644 index 0000000..a06eeeb --- /dev/null +++ b/proxy/tpl/routers-web.yml.j2 @@ -0,0 +1,66 @@ +http: + routers: + http: + service: noop@internal + entryPoints: + - web + middlewares: + - redirect + rule: Host(`*`) + traefik-secure: + service: api@internal + entryPoints: + - web-secure + middlewares: + - traefik-auth + rule: {{ traefik_rule }} + tls: + certResolver: letsencrypt +{%- for p in projects %} + {%- set s = p.services[0] %} + {%- if s.passthrough %} + {{ p.name}}: + service: {{ p.name }} + entryPoints: + - web + rule: 'Host(`{{ p.domain }}`)' + {%- endif %} +{%- endfor %} + services: +{%- for p in projects %} + {%- set s = p.services[0] %} + {%- if s.passthrough %} + {{ p.name}}: + loadBalancer: + servers: + # just forwarding port 80 for doing own http challenge + - url: http://{% if p.entrypoint == s.name %}{{ p.name }}-{% endif %}{{ s.name }}:80/ + {%- endif %} +{%- endfor %} + # the rest of the services are brought up by containers with their labels + middlewares: + removeServiceSelector: + stripPrefix: + forceSlash: false + redirect: + redirectScheme: + scheme: https + traefik-auth: + basicauth: + users: {{ traefik_admin }} + my-traefik-get-real-ip: + plugin: + traefik-get-real-ip: + Proxy: + - proxyHeadername: X-From-Cdn + proxyHeadervalue: cdn1 + realIP: X-Forwarded-For + - proxyHeadername: X-From-Cdn + proxyHeadervalue: cdn2 + realIP: Client-Ip + - overwriteXFF: 'true' + proxyHeadername: X-From-Cdn + proxyHeadervalue: cdn3 + realIP: Cf-Connecting-Ip + - proxyHeadername: '*' + realIP: RemoteAddr diff --git a/proxy/traefik/acme/acme.json b/proxy/traefik/acme/acme.json new file mode 100644 index 0000000..befaaaa --- /dev/null +++ b/proxy/traefik/acme/acme.json @@ -0,0 +1,55 @@ +{ + "letsencrypt": { + "Account": { + "Email": "", + "Registration": { + "body": { + "status": "valid" + }, + "uri": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/137175483" + }, + "PrivateKey": "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", + "KeyType": "4096" + }, + "Certificates": [ + { + "domain": { + "main": "traefik.srv.instrukt.ai" + }, + "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdLekNDQlJPZ0F3SUJBZ0lTSzI4TjR3S1ZkbjFjOVlRbFpxdjQ0VXl4TUEwR0NTcUdTSWIzRFFFQkN3VUEKTUZreEN6QUpCZ05WQkFZVEFsVlRNU0F3SGdZRFZRUUtFeGNvVTFSQlIwbE9SeWtnVEdWMEozTWdSVzVqY25sdwpkREVvTUNZR0ExVUVBeE1mS0ZOVVFVZEpUa2NwSUVGeWRHbG1hV05wWVd3Z1FYQnlhV052ZENCU016QWVGdzB5Ck5EQXlNakF4TkRJMk5UQmFGdzB5TkRBMU1qQXhOREkyTkRsYU1DSXhJREFlQmdOVkJBTVRGM1J5WVdWbWFXc3UKYzNKMkxtbHVjM1J5ZFd0MExtRnBNSUlDSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQWc4QU1JSUNDZ0tDQWdFQQp5NHpJRmJkd09MdHR3VG55T2xoOHBCTmRld1JyQlROV29VQmllNUNJNExWdnRGM3dma3Z0R0wxOUVmbUhsUFVwCkwzdFovdXpYQ2tvRkx1eHdJbW5nQ0N6RjdrejBrL3JiWVYyM3ovczJQQk5nMjRyeGJBRkE4cGt3RDN3Qm84ZzIKeVpZTnlPRjRHeGhxbnM0Y3dmSitKRzI2NEhnNUJYdm16RUhsWkpkSFFScHdhYTVYWmdYVklYK1ZKblhrVnVOUwpKeUhYemxITFl5b0JiMW9PZU51Rld3blJkNHF5NUxNWmVpYWpWVlVRb2ZEVGNZekMxaUlVZFRId00zL3FKeGtXClZEeDBIdENwSG9JbldLeGcvSmlYZEJ5bi9EZDBmbytTYnpnY0FDYXNZUGZjV1dBbHVESG83RWNnZnhPWEt0RHcKQjhBUi92dkhvRkNaV3BCUHNUc0xtMld0SGdYakJtWElkVmxqS25hRUNzbXY0bGhwQVpkVlBZLzNFMzQ0b1dpaApDbjZlbEhqZUVyNGllTkUybFFvam9BRTBzc3NuZW0wU3JvNmpYSWlvWWIvbzlHbXluRzhpM1RLUnVmT1BJb2hTCkJ0aWRiOGUyaEZza1piM2NXTjJxUmI2dmdzWlNqWmtJUkhiM0tjQ2J3YWNXVURmUkhRcWI1RXBrZXBIZDRhMk0KVmF2bWM0YkN2MzBmRncwU2s1RXhLYnNHYVBhc3VIWnNza0xnZFo0SjdmdFlYTlcvMGVKUEFnLzQ5ZWdGRGN2NApjQk1Gdk44NVNxZlAwTWJ3ekFBUGxJR0k1Z1NhME5rWEhnUDdrWjFlelVTQmR5clMrcitMWGJtd1AwT3oxZVg2Ci9taHhwdVlIRHNZd0RjYU5UVnNYK1B6bzJsMlR5QlVlZEtDVmhNRS85OTBDQXdFQUFhT0NBaUl3Z2dJZU1BNEcKQTFVZER3RUIvd1FFQXdJRm9EQWRCZ05WSFNVRUZqQVVCZ2dyQmdFRkJRY0RBUVlJS3dZQkJRVUhBd0l3REFZRApWUjBUQVFIL0JBSXdBREFkQmdOVkhRNEVGZ1FVNDBmY0NmTEErbHp2aWtQT3VaVnQ1WkpwNk5Fd0h3WURWUjBqCkJCZ3dGb0FVM25KNlNOOHh3NlpRMzUrRkk5OVhOMHRkTG1Vd1hRWUlLd1lCQlFVSEFRRUVVVEJQTUNVR0NDc0cKQVFVRkJ6QUJoaGxvZEhSd09pOHZjM1JuTFhJekxtOHViR1Z1WTNJdWIzSm5NQ1lHQ0NzR0FRVUZCekFDaGhwbwpkSFJ3T2k4dmMzUm5MWEl6TG1rdWJHVnVZM0l1YjNKbkx6QWlCZ05WSFJFRUd6QVpnaGQwY21GbFptbHJMbk55CmRpNXBibk4wY25WcmRDNWhhVEFUQmdOVkhTQUVEREFLTUFnR0JtZUJEQUVDQVRDQ0FRVUdDaXNHQVFRQjFua0MKQkFJRWdmWUVnZk1BOFFCM0FLcHNzTVhKOU1TZGpZNnBERGtYNE5jSzJTSVF2d1YvUVZDVGdzdzFESmhHQUFBQgpqY2NnZUlBQUFBUURBRWd3UmdJaEFNUS9pb1QybXY2RmZBOHFrbXRpS1RpNG1uaU81ek9OTWR2SFZ5YndFK1Z1CkFpRUFtWjJwUFZ0RnhIYkhYRFppOTNVWUF2SGtBTDVVai95eVFvVTRHU3FxN1pNQWRnQ3d6SVBscGZsOWE2OTgKQ2N3b1NRU0hLc2ZvaXhNc1kxQzN4djBtNFd4c2R3QUFBWTNISUhpT0FBQUVBd0JITUVVQ0lITkFzeHhQMTlxaQpveTQ1UjNYbzFtRm5PbDVLVldxNGhmZWpmbGVjaFE4WEFpRUF5NnhlMWQwYmRpU1hJRHU1NUdiOVpsZG9DdlNBCm5scHpVMUp4Y2xaSGExY3dEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBRTBCQ1U5L3FWZnluTlBPNmhOdU95YVMKbkhjaVorSkE4L3BhL0JzNFlTZTZ3SVVranRidk45ekxwVTNGUmo5YmlGWXNOelRSb21Rckp0dG1hUHAzOGdNdApuRTRzUVFIYy9uczNETGZsMkNpY0lLdjJFczJzUFN3aHRLRjhVSzJ4WUtucHcxQi9vbnJiWEVzL3pLRWdMUEVuClV3VGdzZzZDdC9nbXNhYXFKQkhJTFN3emFXUlRoRmZXWmZmSTFTU0xudU95WWlkVVFzeFovVjVaUlBnUEZyUVAKYm1KbWpTVjdMVUIyUDVpcGEvZE1oNUZORndRaHgyQnN4L3JuRE1RdlUvZzRYVVVZVG9sVHU0TFdlb043V2JnLwp0NzFKbnlHNTYrMWc3Mlk3TTdYamNqbzkyYVduZytWbDJvdUV0Q0VqakF2UmlXZzI4L0dMNjBnenJCSGw5b0U9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlGV3pDQ0EwT2dBd0lCQWdJUVRmUXJsZEh1bXpwTUxyTTdqUkJkMWpBTkJna3Foa2lHOXcwQkFRc0ZBREJtCk1Rc3dDUVlEVlFRR0V3SlZVekV6TURFR0ExVUVDaE1xS0ZOVVFVZEpUa2NwSUVsdWRHVnlibVYwSUZObFkzVnkKYVhSNUlGSmxjMlZoY21Ob0lFZHliM1Z3TVNJd0lBWURWUVFERXhrb1UxUkJSMGxPUnlrZ1VISmxkR1Z1WkNCUQpaV0Z5SUZneE1CNFhEVEl3TURrd05EQXdNREF3TUZvWERUSTFNRGt4TlRFMk1EQXdNRm93V1RFTE1Ba0dBMVVFCkJoTUNWVk14SURBZUJnTlZCQW9URnloVFZFRkhTVTVIS1NCTVpYUW5jeUJGYm1OeWVYQjBNU2d3SmdZRFZRUUQKRXg4b1UxUkJSMGxPUnlrZ1FYSjBhV1pwWTJsaGJDQkJjSEpwWTI5MElGSXpNSUlCSWpBTkJna3Foa2lHOXcwQgpBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF1NlRSOCs3NGI0Nm1PRTFGVXdCcnZ4ekVZTGNrM2lhc21LcmNRa2IrCmd5L3o5Snk3UU5JQWwwQjlwVktwNFlVNzZKd3hGNURPWlpoaTd2SzdTYkNrSzZGYkhseVU1QmlEWUl4YmJmdk8KTC9qVkdxZHNTak5hSlFUZzNDM1hySmphL0hBNFdDRkVNVm9UMndEWm04QUJDMU4rSVFlN1E2RkVxYzhOd21UUwpubW1SUW00VFF2cjA2RFAremdGSy9NTnVieFdXRFNiU0tLVEg1aW01ajJmWmZnK2ovdE0xYkdhY3pGV3c4L2xTCm51a3luNUoyTCtOSlluY2x6a1hvaDluTUZueVBtVmJmeURQT2M0WTI1YVR6Vm9lQktYYS9jWjVNTStXZGRqZEwKYmlXdm0xOWYxc1luMWFSYUFJcmtwcHY3a2tuODN2Y3RoOFhDRzM5cUMyWnZhUUlEQVFBQm80SUJFRENDQVF3dwpEZ1lEVlIwUEFRSC9CQVFEQWdHR01CMEdBMVVkSlFRV01CUUdDQ3NHQVFVRkJ3TUNCZ2dyQmdFRkJRY0RBVEFTCkJnTlZIUk1CQWY4RUNEQUdBUUgvQWdFQU1CMEdBMVVkRGdRV0JCVGVjbnBJM3pIRHBsRGZuNFVqMzFjM1MxMHUKWlRBZkJnTlZIU01FR0RBV2dCUzE4Mlh5L3JBS2toLzdQSDN6UktDc1l5WERGREEyQmdnckJnRUZCUWNCQVFRcQpNQ2d3SmdZSUt3WUJCUVVITUFLR0dtaDBkSEE2THk5emRHY3RlREV1YVM1c1pXNWpjaTV2Y21jdk1Dc0dBMVVkCkh3UWtNQ0l3SUtBZW9CeUdHbWgwZEhBNkx5OXpkR2N0ZURFdVl5NXNaVzVqY2k1dmNtY3ZNQ0lHQTFVZElBUWIKTUJrd0NBWUdaNEVNQVFJQk1BMEdDeXNHQVFRQmd0OFRBUUVCTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElDQVFDTgpETGFtOXlOMEVGeHhuLzNwK3J1V082bi85Z29DQU01UFQ2Y0M2ZmtqTXM0dWFzNlVHWEpqcjVqN1BvVFFmM0MxCnZ1eGlJR1JKQzZxeFY3eWM2VTBYK3cwTWo4NXNISTVEblFWV041K0QxZXI3bXAxM0pKQTB4YkFiSGEzUmxjem4KeTJRODJYS3VpOFdIdVdyYTBnYjJLTHBmYm9ZajFHaGdraHIzZ2F1ODNwQy9XUThIZmt3Y3ZTd2hJWXFUcXhvWgpVcThISWYzTTgycVM5YUtPWkUwQ0VtU3lSMXpacVF4SlVUN2VtT1VhcGtVTjlwb0o5ekdjK0ZnUlp2ZHJvMFhCCnlwaFdYRGFxTVlwaDBEeFcvMTBpZzVqNHhtbU5EakNSbXFJS3NLb1dBNTJ3QlRLS1hLMW5hMnR5L2xXNWRodEEKeGt6NXJWWkZkNHNnUzRKME8rem02ZDVHUmtXc05KNGtub3RHWGw4dnRTM1g0MEtYZWIzQTUrLzNwMHFhRDIxNQpYcThvU05PUmZCMm9JMWtRdXlFQUo1eHZQVGRmd1JseVJHM2xGWW9kclJnNnBvVUJELzhmTlRYTXR6eWRwUmd5CnpVUVpoLzE4RjZCL2lXNmNiaVJOOXIySGtoMDVPbStxMC82dzBEZFplKzhZck5wZmhTT2JyLzFlVlpiS0dNSVkKcUtteVpiQk51NXlzRU5JSzVNUGMxNG1VZUttRmpwTjg0MFZSNXp1bm9VNTJscXBMRHVhL3FJTThpZGs4NnhHVwp4eDJtbDQzRE8vWWEvdFZaVm9rMG1PMFRVanpKSWZQcXl2cjQ1NUlzSXV0NFJsQ1I5SXEwRURUdmUyL1p3Q3VHCmhTanBUVUZHU2lRclIySksyRXZwK282QUVUVWtCQ08xYXcwUHBRQlBEUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K", + "key": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS1FJQkFBS0NBZ0VBeTR6SUZiZHdPTHR0d1RueU9saDhwQk5kZXdSckJUTldvVUJpZTVDSTRMVnZ0RjN3CmZrdnRHTDE5RWZtSGxQVXBMM3RaL3V6WENrb0ZMdXh3SW1uZ0NDekY3a3oway9yYllWMjN6L3MyUEJOZzI0cngKYkFGQThwa3dEM3dCbzhnMnlaWU55T0Y0R3hocW5zNGN3ZkorSkcyNjRIZzVCWHZtekVIbFpKZEhRUnB3YWE1WApaZ1hWSVgrVkpuWGtWdU5TSnlIWHpsSExZeW9CYjFvT2VOdUZXd25SZDRxeTVMTVplaWFqVlZVUW9mRFRjWXpDCjFpSVVkVEh3TTMvcUp4a1dWRHgwSHRDcEhvSW5XS3hnL0ppWGRCeW4vRGQwZm8rU2J6Z2NBQ2FzWVBmY1dXQWwKdURIbzdFY2dmeE9YS3REd0I4QVIvdnZIb0ZDWldwQlBzVHNMbTJXdEhnWGpCbVhJZFZsaktuYUVDc212NGxocApBWmRWUFkvM0UzNDRvV2loQ242ZWxIamVFcjRpZU5FMmxRb2pvQUUwc3NzbmVtMFNybzZqWElpb1liL285R215Cm5HOGkzVEtSdWZPUElvaFNCdGlkYjhlMmhGc2taYjNjV04ycVJiNnZnc1pTalprSVJIYjNLY0Nid2FjV1VEZlIKSFFxYjVFcGtlcEhkNGEyTVZhdm1jNGJDdjMwZkZ3MFNrNUV4S2JzR2FQYXN1SFpzc2tMZ2RaNEo3ZnRZWE5XLwowZUpQQWcvNDllZ0ZEY3Y0Y0JNRnZOODVTcWZQME1id3pBQVBsSUdJNWdTYTBOa1hIZ1A3a1oxZXpVU0JkeXJTCityK0xYYm13UDBPejFlWDYvbWh4cHVZSERzWXdEY2FOVFZzWCtQem8ybDJUeUJVZWRLQ1ZoTUUvOTkwQ0F3RUEKQVFLQ0FnQndpUWdDQWFYNHA1OFppamppaXhOMS93TGF4V21KdVlWMnAwc0xkN0JGVStwTE5QaUdTdUh0b0sycQpKMlFQTmErc2dhMFM1TXhLOHZ1RW90R29KUkxvVWlDb0RFSFlJb1B4ZzhmaEk4a3JVNmRrR2FBNkQwTmlITkZXCmJqL1YxaFo4UXdxaCtnM1dKUnlsYWR5NldaL2w4Z2kwbTZNaTVncUcrUk9qRm9RZGVVbVhFLzFiK1hPSkRxbGUKVnlXRDZDLytWSmFraWRKQm8vS2hKcEFEdjVJdDcvS3YvYTNFQlVUYnBNcituWVlSa2p6RWRZY3VyaTdUVUdXMQpnMDNvaWRVdmd6REw3Sjdqc0IraXhjTGxSRlBQclJ3dXRGWWl0cXZGODZaL2F1STR4akE3OVkxRkZjcnAvclQzCnJOUVJwdXhkekpIbWdEc3c1ZjZ5OUVBckF6TDVuYURXTGxYWGZwWnlYS3ArL2loaS9wOFBEUVRweFVielJvb0cKTXRVQnpmL1JydjNFOEdjWHlwN2grRTFjY1FiTFNNOEhXY1M0MHRHRWFWellERXRVYjFZUUtKWlhid2lRdU9qbgpiZ0pLbGJnLzBzYjNtcXc1bE1EWmszdEdxR3doeVE5OHJYSVZXMmQ4RmRoeURObHp2MFl2KzBkR0djU213MHRPCmd2SER1a0dOeW5xR09jcHZxNW5LNlN0M2oxZHhXZUJKTFJaK0ppVVNaUUdSV3pmRzFIeTFaNXcvR1dyK1d6TFMKakYwcG5FSGNFVnpiZSs4Ulg3ZzdkZXlqT1pIY1dvMmhBc2xXR3NwUTNmVU9aYk80ZWVoS1RVQVVsSXJYZXBhVwpPc2pRaytuWjhFcEE1dzliWnVzMHQxb3JNdDI5TDdCZUFFSlVYZkdWT1FveWNSK2t3UUtDQVFFQTNKZGt6ZXJKCm9UdVF6akxESmpqU1oxVDlPWnYvVksyakFjWHhmVmpsNVVKSDJLNmsxWGcxZkpJMWtHUlFqcmsxcVJSd0lOSGcKQTNwRXJCemhsYW9KYVpINDVieGVOakx5ak1vejVXK2lyNk9qelUveHowb2hVWFdIbE5nTktlRWdPRXllK0dyawo1ZGdnSmZzL09jSnlMMElpcnhMNmV1cnRtaThTdHNSS0hqeGp0K1NMZFkzVFI0bWVHYVlHK1p0ZERMY1g0NUFVCk5FM1FQYVU2QWgyZjB3ZlRuY0RUUGdvaXBwZ1pFNkdoS1VzcHVrOVdrVG42N3JlQ2ZjZXJmQ1F0MUYwUnRlRnIKSFBqQkZuN3grSzA5QUJYb0k0YmM4Zk5oM3A4YWFtWVhKQkdrYU96RHdDeFVOWVRHeEhCZHJ5UWJPMzBtMSt6bAp2eUxuT29Kc3JnSjRqUUtDQVFFQTdEa2RpQjZFMFVoWVFuNk9ZVzZJTGpudUpHc0ZRVGNnUWR6TGt0ZmlxWk5KCllScGtlR0Jtd0I3M0Vxek5OV2tEcHRiWHlCMU5VWHFUS3pLeU1VUGduZUZjajV2eWc2SGxNbUlWWkNWalFvSDIKaHRsMkw5aHZtM2VWLzhiMXkrTWhyK1M3ZEZNYWlMRGVvcDBkRWdhNnc5RkZyaXFPeEcrajhpMTNMNXRvR0N4LwpiaDNBMW9Zb1BsemxkaGFxRUMxVlBRVGNnQkhKbThPLzhZd29QTUJkaDZhcnZPbWFUdjA5QnM3V1N2Ziswb05qCmozRUV4bzZkekFodmlNeEFZc0xXMmNEL0s1WHFjemphZ1lBRE1qOXArYnZRR3BxMHMzMitLakxqdGVDbk1IUVcKcEpDb0tDSjdSWDhEN1BJcVlvQlcvTWUzUnNTRTIrTFg4UEVUOEl4d2tRS0NBUUVBc3VKcXNyb0ZqS1NiUnp2UApHWDNXMGtrUG1Ob3ljdDdrWTVtN3ZlU0xlaUVIMW1XNThncXJoVHVrQ0ZYeEpleDM0WWRiYnFJRHdZNHpoa2RkCmdGc0Zpd0QvRUg1ekFBek11UE40cGtTamJ6K0twQUtDbTVFSDkrRVJjOWpJdy85MmUzRlhNU2l1T3QzTEpFSGQKQWQvM2JPOTY1UExGQVV6bUpiU3F2bC9yb3gxaDJFeVQ4MTV4eVgrR0lGS05NWG1XT2F0NHllZWJHdGRIV2RaVgpka1hEeUFSK3pWMERjVG9TNDNwUVM2VTA1aUczMHdobnNSbU5ybXhPZStXaDRPZ042anNYRTZHYmxVRDhoRnVwCnFmb0NqOEUwSVN0S25VSzdTU2RJNnFPamZISjZHMFIxY2JBQkVlMThxdXhjYkowaElGcmJWeHNiTEtjSFVHRlgKOUx5NE9RS0NBUUVBcUxWcUNBdHo1Rng4M0pDeWM1ZVRueUEwbXNIc2NOVkVMb1hZeHJsMDRwT1V0c3M0c2xoMwpTZ05UaG56djJwTnp6alpteWo5VE56d0oyN0pMWGJBU3ZlOTBRZ1Nvd2FaZ29RazRLZHdoOWlBSHgrdUw0MFA3CkFkZjR3MEovemV3YjVSbkJYNlR5c0dsUnVHWHBtelEvTXhkRllzeDJSb2xOVit0czBnMTEzTDM3RE5Rbkk0K1IKTW1qNllNcHpEemF4Ui9FdTRqRFNBbk5kcE0vcTlPaVdaQWwvWWw2Y25JbDBpbVl5Mmp3Z1BJMG1FQTR4MFBXTwpjV2djOEdDZ0kyandOWGtIUlVPK3ZDZ0F0cmlNajlpR2dadkhWeGppc2NhK2lEanNOTHVMN0trY2VzL3RxU3huClNnMGE2cUZSOENPay9NaE9kelpuaFB1UUtqNmpkUXQ0WVFLQ0FRQVcwL01TSG5iTG5LeDRIWElZTUF5aHFSNmoKL3N0OCs0dzdhQUx0VXJNTm0rUExYcCtQWStORXAxZW12Y280blk4QVNFdzFzZFp6aVJ1NFZXcEduQVVMSzhjVgpsVERCL1pMc2RIMDZuWjBWRU9OUjM5ZHkya25SRjFLVUZtdnJBMnpYMVk1aFFiMVFuSm9nUWVGc1l4UVBwUk1qCjVFS3JyLzNsZGNudWFUdW8zdGQxWWR0NjdwelVQUGlGZG41cFNveisvWnkzbFNwT3V4RndiSGNJV2xKQmc4K0kKSVowQ0lwTG1waDNFNjFMUCtHclE2NWtZbDRldTJsc1orSk1qU2cxdFY2S2YxSkhMano1VHpPUXZPRVFHRDJLTwpPckdVSzJwaktTYU90aU1JRVN0aXQ1Ynhna2NHNjdLdG04UGhjenBMeWxlNHU3WnViLzFjU1NOK3ZITkoKLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K", + "Store": "default" + }, + { + "domain": { + "main": "indy-news.srv.instrukt.ai" + }, + "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdMakNDQlJhZ0F3SUJBZ0lTSzdkcXJZYlgvNVBnd3lPME9UZ0RrdXAwTUEwR0NTcUdTSWIzRFFFQkN3VUEKTUZreEN6QUpCZ05WQkFZVEFsVlRNU0F3SGdZRFZRUUtFeGNvVTFSQlIwbE9SeWtnVEdWMEozTWdSVzVqY25sdwpkREVvTUNZR0ExVUVBeE1mS0ZOVVFVZEpUa2NwSUVGeWRHbG1hV05wWVd3Z1FYQnlhV052ZENCU016QWVGdzB5Ck5EQXlNakF4TlRNME5ETmFGdzB5TkRBMU1qQXhOVE0wTkRKYU1DUXhJakFnQmdOVkJBTVRHV2x1WkhrdGJtVjMKY3k1emNuWXVhVzV6ZEhKMWEzUXVZV2t3Z2dJaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQ0R3QXdnZ0lLQW9JQwpBUURUTldJcUc4U3hEdzZoZkhZWGQwRnZaeWoyQ2lMcXd6QzdJYkxkTEMwMVoyU0h1dUluWGJXTWpPcnN2SEwwCmJldytpN0tydUlsSUQzWmhJVFI2bzk3dDA2WFJzNWVWUXp3YWlDMGlJY0Z2MkMwcFgzZ0hPOTRqZFZjRXRQbG4KazRhZDlkbGFwNThSVWlzRnB6RVYxWUQ5ZmFRSEF6bG1yeW1Cc2FFRytlRTAwWjAyTUZzUmR6MnRLM1Qxbnh4ZAoybnFpZExrWjJiOFVzbFN5c1JkVmFzVFJ1bFdCV0xwdytEQUdRbVFybmlXU0pmYWxGcmUwVjd0VXhqMGl5Z01GCjRPRWdoKzNRYWx2cGdyR2ZYTGR5Z2ZoVHJ1SWR6RnRGamZMVnRDRTVrT3hmQjZPZ2E0VGxYK2l2N1BsdzZKNFEKWXFLTUJlbEtkM2Z0cWYyVE5zamJXRFBReFJNblkvdWx3b3lwcEQyK2xsQmQyVHh1TENCYzEyR3Q2V0JvRkRhaQp5cHIwNWFqNE52WnRib0p2STVpaTRjQUpvWWpNSHBVN2wvZlRSZW5VeVNmR1c0bHZQZTlyTng0ZytLQXFLT0lvCldJeU9ycnRrMGFXWTlHOTNtSHZTLzVuYkQ2Z1A3R3VaOERGYVR4cHArdzhlT3F0UE83c2U3UkR0VE9kYTlaMVgKdlpUOVF3czlHbGtkWVFiZTJRWmVhdVdyRWhXdG9iK2twQ1hFZWlCMDkzRVN3Vm15ZmZvUk45RmhQNms5WTFJawpPVytSUlNGbmZBREpRSUJNR25LVzJaTE5pZ3BQM0tjODJ1VUtsTm5hU3lNUWl4RzlLOUU0R3pPM2NsZjV5ZXNNCkhxNFNSZzBxTzdSTXBiaGpDWFpySHdST3E1cW5VN2tsaFlNV0lkVENwbTM5bFFJREFRQUJvNElDSXpDQ0FoOHcKRGdZRFZSMFBBUUgvQkFRREFnV2dNQjBHQTFVZEpRUVdNQlFHQ0NzR0FRVUZCd01CQmdnckJnRUZCUWNEQWpBTQpCZ05WSFJNQkFmOEVBakFBTUIwR0ExVWREZ1FXQkJTVkF6RkhrZmJtdzV0SmRVVWU3bUtXMmk3WXlqQWZCZ05WCkhTTUVHREFXZ0JUZWNucEkzekhEcGxEZm40VWozMWMzUzEwdVpUQmRCZ2dyQmdFRkJRY0JBUVJSTUU4d0pRWUkKS3dZQkJRVUhNQUdHR1doMGRIQTZMeTl6ZEdjdGNqTXVieTVzWlc1amNpNXZjbWN3SmdZSUt3WUJCUVVITUFLRwpHbWgwZEhBNkx5OXpkR2N0Y2pNdWFTNXNaVzVqY2k1dmNtY3ZNQ1FHQTFVZEVRUWRNQnVDR1dsdVpIa3RibVYzCmN5NXpjbll1YVc1emRISjFhM1F1WVdrd0V3WURWUjBnQkF3d0NqQUlCZ1puZ1F3QkFnRXdnZ0VFQmdvckJnRUUKQWRaNUFnUUNCSUgxQklIeUFQQUFkd0NxYkxERnlmVEVuWTJPcVF3NUYrRFhDdGtpRUw4RmYwRlFrNExNTlF5WQpSZ0FBQVkzSFhwOWRBQUFFQXdCSU1FWUNJUURIeHVSNHd1Ykhaem1jL1JyMWdEMGI5Rk1IY3FnUzFSWkxyUEJwCm92RnphUUloQU5hT0d5TFpsZUhJUHB4UGpXWHJobmI1aEI0QnhGT09WWTdlWjlieEZSbmNBSFVBc015RDVhWDUKZld1dmZBbk1LRWtFaHlySDZJc1RMR05RdDhiOUp1RnNiSGNBQUFHTngxNmZiQUFBQkFNQVJqQkVBaUJnVWI5YwptWWlsbHhnaFV0WGN4aU1Rb3FzNjA2ZGZ4V2hUbUlKbmJUZDRoQUlnVENBV1JoWW1lL2MyMTdKUTdxUHcvU3hCCjRHRHJZTFJJckRxUEQyeDNoVjh3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUZQRUYzTUxLZXZsYXRaZDVLUnQKZ1JaZ3d4MDdLc05LWkx3NlhvOUJqVmRHSUttS1VmNjEyVkg0a0Y2a0gyWTNkSHNRQnZ0UVdxaXljSHgyNWFrNwpIa2NUNFhDYSt5S1lIeGNXaS9WNGpNY1F6U2YrQVhFMmMwK041c3dFQkFycDQ0MlB1Yzg3UTBSYlVFMkd4ckhSClUyeEhOdHFsTHFFQzVyZWMzN1pOamgzd3drQnNyRXZveTZPMWliMFlnNW1hQUhJVzFpL0RlRHJWUzh4bVo3QlkKWEYxeUIrRDBXS0tCa2JYVFlNemQ5TmdpVURGWnNzcmZCbnpxdDd5ZWlmVTNEUjdFWUJrMlBJWVp1d002eWlyLwp1bE5ORWtBQzkwZW9SVmxlbTVQaFk2S2tTdytyZXpFekd1L0JoVjluay9OOVJsalE1cUR0alI2eXZmWTB5UFhoClNhYz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQoKLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZXekNDQTBPZ0F3SUJBZ0lRVGZRcmxkSHVtenBNTHJNN2pSQmQxakFOQmdrcWhraUc5dzBCQVFzRkFEQm0KTVFzd0NRWURWUVFHRXdKVlV6RXpNREVHQTFVRUNoTXFLRk5VUVVkSlRrY3BJRWx1ZEdWeWJtVjBJRk5sWTNWeQphWFI1SUZKbGMyVmhjbU5vSUVkeWIzVndNU0l3SUFZRFZRUURFeGtvVTFSQlIwbE9SeWtnVUhKbGRHVnVaQ0JRClpXRnlJRmd4TUI0WERUSXdNRGt3TkRBd01EQXdNRm9YRFRJMU1Ea3hOVEUyTURBd01Gb3dXVEVMTUFrR0ExVUUKQmhNQ1ZWTXhJREFlQmdOVkJBb1RGeWhUVkVGSFNVNUhLU0JNWlhRbmN5QkZibU55ZVhCME1TZ3dKZ1lEVlFRRApFeDhvVTFSQlIwbE9SeWtnUVhKMGFXWnBZMmxoYkNCQmNISnBZMjkwSUZJek1JSUJJakFOQmdrcWhraUc5dzBCCkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXU2VFI4Kzc0YjQ2bU9FMUZVd0Jydnh6RVlMY2szaWFzbUtyY1FrYisKZ3kvejlKeTdRTklBbDBCOXBWS3A0WVU3Nkp3eEY1RE9aWmhpN3ZLN1NiQ2tLNkZiSGx5VTVCaURZSXhiYmZ2TwpML2pWR3Fkc1NqTmFKUVRnM0MzWHJKamEvSEE0V0NGRU1Wb1Qyd0RabThBQkMxTitJUWU3UTZGRXFjOE53bVRTCm5tbVJRbTRUUXZyMDZEUCt6Z0ZLL01OdWJ4V1dEU2JTS0tUSDVpbTVqMmZaZmcrai90TTFiR2FjekZXdzgvbFMKbnVreW41SjJMK05KWW5jbHprWG9oOW5NRm55UG1WYmZ5RFBPYzRZMjVhVHpWb2VCS1hhL2NaNU1NK1dkZGpkTApiaVd2bTE5ZjFzWW4xYVJhQUlya3Bwdjdra244M3ZjdGg4WENHMzlxQzJadmFRSURBUUFCbzRJQkVEQ0NBUXd3CkRnWURWUjBQQVFIL0JBUURBZ0dHTUIwR0ExVWRKUVFXTUJRR0NDc0dBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVMKQmdOVkhSTUJBZjhFQ0RBR0FRSC9BZ0VBTUIwR0ExVWREZ1FXQkJUZWNucEkzekhEcGxEZm40VWozMWMzUzEwdQpaVEFmQmdOVkhTTUVHREFXZ0JTMTgyWHkvckFLa2gvN1BIM3pSS0NzWXlYREZEQTJCZ2dyQmdFRkJRY0JBUVFxCk1DZ3dKZ1lJS3dZQkJRVUhNQUtHR21oMGRIQTZMeTl6ZEdjdGVERXVhUzVzWlc1amNpNXZjbWN2TUNzR0ExVWQKSHdRa01DSXdJS0Flb0J5R0dtaDBkSEE2THk5emRHY3RlREV1WXk1c1pXNWpjaTV2Y21jdk1DSUdBMVVkSUFRYgpNQmt3Q0FZR1o0RU1BUUlCTUEwR0N5c0dBUVFCZ3Q4VEFRRUJNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUNBUUNOCkRMYW05eU4wRUZ4eG4vM3ArcnVXTzZuLzlnb0NBTTVQVDZjQzZma2pNczR1YXM2VUdYSmpyNWo3UG9UUWYzQzEKdnV4aUlHUkpDNnF4Vjd5YzZVMFgrdzBNajg1c0hJNURuUVZXTjUrRDFlcjdtcDEzSkpBMHhiQWJIYTNSbGN6bgp5MlE4MlhLdWk4V0h1V3JhMGdiMktMcGZib1lqMUdoZ2tocjNnYXU4M3BDL1dROEhma3djdlN3aElZcVRxeG9aClVxOEhJZjNNODJxUzlhS09aRTBDRW1TeVIxelpxUXhKVVQ3ZW1PVWFwa1VOOXBvSjl6R2MrRmdSWnZkcm8wWEIKeXBoV1hEYXFNWXBoMER4Vy8xMGlnNWo0eG1tTkRqQ1JtcUlLc0tvV0E1MndCVEtLWEsxbmEydHkvbFc1ZGh0QQp4a3o1clZaRmQ0c2dTNEowTyt6bTZkNUdSa1dzTko0a25vdEdYbDh2dFMzWDQwS1hlYjNBNSsvM3AwcWFEMjE1ClhxOG9TTk9SZkIyb0kxa1F1eUVBSjV4dlBUZGZ3Umx5UkczbEZZb2RyUmc2cG9VQkQvOGZOVFhNdHp5ZHBSZ3kKelVRWmgvMThGNkIvaVc2Y2JpUk45cjJIa2gwNU9tK3EwLzZ3MERkWmUrOFlyTnBmaFNPYnIvMWVWWmJLR01JWQpxS215WmJCTnU1eXNFTklLNU1QYzE0bVVlS21GanBOODQwVlI1enVub1U1MmxxcExEdWEvcUlNOGlkazg2eEdXCnh4Mm1sNDNETy9ZYS90VlpWb2swbU8wVFVqekpJZlBxeXZyNDU1SXNJdXQ0UmxDUjlJcTBFRFR2ZTIvWndDdUcKaFNqcFRVRkdTaVFyUjJKSzJFdnArbzZBRVRVa0JDTzFhdzBQcFFCUERRPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=", + "key": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS1FJQkFBS0NBZ0VBMHpWaUtodkVzUThPb1h4MkYzZEJiMmNvOWdvaTZzTXd1eUd5M1N3dE5XZGtoN3JpCkoxMjFqSXpxN0x4eTlHM3NQb3V5cTdpSlNBOTJZU0UwZXFQZTdkT2wwYk9YbFVNOEdvZ3RJaUhCYjlndEtWOTQKQnp2ZUkzVlhCTFQ1WjVPR25mWFpXcWVmRVZJckJhY3hGZFdBL1gya0J3TTVacThwZ2JHaEJ2bmhOTkdkTmpCYgpFWGM5clN0MDlaOGNYZHA2b25TNUdkbS9GTEpVc3JFWFZXckUwYnBWZ1ZpNmNQZ3dCa0prSzU0bGtpWDJwUmEzCnRGZTdWTVk5SXNvREJlRGhJSWZ0MEdwYjZZS3huMXkzY29INFU2N2lIY3hiUlkzeTFiUWhPWkRzWHdlam9HdUUKNVYvb3IrejVjT2llRUdLaWpBWHBTbmQzN2FuOWt6YkkyMWd6ME1VVEoyUDdwY0tNcWFROXZwWlFYZGs4Yml3ZwpYTmRocmVsZ2FCUTJvc3FhOU9XbytEYjJiVzZDYnlPWW91SEFDYUdJekI2Vk81ZjMwMFhwMU1rbnhsdUpiejN2CmF6Y2VJUGlnS2lqaUtGaU1qcTY3Wk5HbG1QUnZkNWg3MHYrWjJ3K29EK3hybWZBeFdrOGFhZnNQSGpxclR6dTcKSHUwUTdVem5XdldkVjcyVS9VTUxQUnBaSFdFRzN0a0dYbXJscXhJVnJhRy9wS1FseEhvZ2RQZHhFc0Zac24zNgpFVGZSWVQrcFBXTlNKRGx2a1VVaFozd0F5VUNBVEJweWx0bVN6WW9LVDl5blBOcmxDcFRaMmtzakVJc1J2U3ZSCk9Cc3p0M0pYK2NuckRCNnVFa1lOS2p1MFRLVzRZd2wyYXg4RVRxdWFwMU81SllXREZpSFV3cVp0L1pVQ0F3RUEKQVFLQ0FnQnJFbjBaYThwakdJY0tSdFozUHZYbFRCN3YzR09uTUJ6Y1FWRXozdGxzWVdZTmlTaHRYSXhWSEh0YwpXSXpPYll0K2ljT2lXb0wrRFJpdVZPOE4zVlYrcS9VOFFoZHVqQTlFUkJUZFlNOTNxOWY1U1hUSDlnbS9ZQlVsClJibktVSGZHVElSTCtQNGpBY3pkcU15eGpQTS9qeHBBazluZHZKOEdLTTdCZ3B3dzhyTUliYXM5UjMzMDlEcnUKNmlLdlVic3A4SktHRUdWV1VMR3ZoU2VYSGZGNWdVVW50WnVPK2NNZTZUbHZBMVY0cWRaSUN5UkdRTk92UG5Cdgoyb2w5eVd0WDJwRXRQejRwT3lCZzhkZnVMUVYvSndGcXFkYjdnK0svZy9uei9ReFhnTC9xNjVjTDBYdGM5SDNnClI0MGFNV2JHSlprSEwxbVZHc0I2aVNONlQxUzBiZUJuVXl5c3IzckluY1hSUXlPd2l6dlh6L1hRbGpTK0x2QlAKc0hzKzVPR1lXK09WOW9zdlh3cmlNaUZhbjArVHBzeWIrSGtNWmVtR1ZrSm9IWUlEU3MrZCtOMnRnd0N0Uy92VwpKM1JtOWc1SDZNUFpNZWNOWnozOTE5dHdkKzQzTjRkcnRGUWN1M3NJRC9LYnJoV2pnWUdrSmlFLzVnRnZBRmVkCmQ1ZnBKVkpIODJPdHFaWkp2QklPNHFrVVhxSDRvcW1pVUdYZkFUeC9xcWxiSFhzRTk3YW9paUFObmE5clpLeGwKMVBGbVJNZytkYWpaR3Via04ybXZlNlg4T2FHdzUyei9zemh5U3QrRVFKWmJzOGVZM3RVbi9IY1BkTHJUeUlNdAplZkJveHhIZUJmVXR5Z2t3VkRZUkQ4M3A1MmxZR3NocmpPaElzc2JudTNicER0anZBUUtDQVFFQTR6VVZjRmdOCnVrQzZHQ0ZZMVVRRVZBTXhLZkFWbnhJakcvMEdxRUhKL3dqa3VNbnZkaE84a3dJc0xkTzRxbjVNNnVld3NiMTkKNk12MmRlVHlvSHJQT3dHUktlZlZ5dGNYRFVVWHFRT3plZlEvRXlWeGdNWXBYUjQ5UTk0WHZ6SnZtOVV3ZGt5agpsQmRySXZSM1BCZE1qREVKdjkvTFdOc0kyUVoyT0VVd3dHVW5uQTUyYVM1TTdFTkxOR1h0bUJyUUo0Ym1PSE82Cm1BYWFhLzZrQkFzM1IraHRJQXB5aHVvYkRCazd1bzM2VU0wb0ZhU2x4SXhpUHVDOTlKQjNOSHF3Sm43aTJqU0cKMmdoR0dDTnZlNk1kMGFhOVN3a1NTZ3EvTVN3Nis5TFZtTktReU9QNWtWMVNKSnJybk1NRmM4dlp4OTVDUEFJZgpaWllsMXB3aXhsZlF6UUtDQVFFQTdmbEdvTktnd090SHNQd3Fnakphd3ZTTG9ubi9mOFpLK1dtT0t2YUVHUTBQCmhEL2NHNnowZGpTZDk0ZUxWK2tDMysxMTRISGUyeE9hdTlzNTBKS1RpYXQ5bXNCV2V5V3dJZVo5aSs1ZlZtTVIKVzBPVTFmVFV3bEZBRkcvT05oNndRekxUS216K1FNdXUyMUNUb0hIcmhDYkRxc2xYbDJmWWV2cGVYYUpkSWRacApyRktESno1RWdwcXZhRWQvbnFmbzM1RmtKdGhITWhENnlEalRiT2kwYnc4QzZSNlgzYjE0OW9uU3JYVDlpbFZWClNvMjN3Mm5PaEVKY1hKUlZCNjBWblFMbEpnTzJhTWJ6ODJwUlZmcEZwU2NqL09Ya0EvOUxzVDZQeTFaRUJzcmkKZzlydjhuZ1A2Z3JmSDRoYmxJTGN5VmxaUnlaZVA5djN1L21uS3BpLzZRS0NBUUJGMDNHVVJpdHAyeXlWNytHcQpJUzRuVjVmTGFMSnNZWm1TcDI5ZFZHS3MwMkhmZklmd1JONkdXM3VTVmVnQjFiRHozR0tNTFUwOXR6azJRRGRrCnBIbDlpOXkrQVRiT0VNNVAwTFVWeDBKb0wyMFhDRUhhcm92VngwNVN4aldNR0FiKzRFVDFobFliVkJzajhmZVcKNng0bVU0ZkloUkdzYkJ4Sy9sWmtzcmJwMmV5VFpFcDF4cm56UGpjbGtsR3psa0gwYzl2YTY0NGozSnVtcHVwbgpKNzlFaFNUTVhRbVhkdWo3RGVJeVVpSGtwVGcxZHBWUFUwbDZRMzE5Wk15TnYwclZlZGRqOGhLamZUbDFmNlpxClc5WDFNWVlwUWs5eG05M29VY3lLeWQvRkNLbGdZOWUyR2ZUOXRQTEdDWlZxWWZLN0h3NUIvUEVET21MRWt0V0oKR1VIZEFvSUJBUUNBNEZMam5hMzVUSElTNHoyU0xQc0NRYld0VkN5T1lqRVFTWjM0bm5DRHNKTUtxQnJuMGc3QwpSVlNYVFF5bG0wYzNSajVrUXNiU0ZoZmlUS05PMVZLWmFBb3AyS2Y5SVRmMTVBRngyckkrOG4vZjBRVzFxVlhmCnRtamhxSHNnekJYaERwRlZsSmxOTmdHSVBHYjJxVHRHNGtkdElvVVJOZWdERWJ3eE1WRUdsanU4V3lNMENZREkKeWtUY1VIeEl5OTVOMVV0THppMGdiQm1rZlVIaTVWTnlJY3NOdk1lNkNuYUw3YS91eUVEV3ZjZm5ERURNejJ4TQpMRFFnM3lQOWVEc1FRUlVnTlRxQUc5N1pvR1pVRGFuUit4UitUWEhpeUV1UnZiUGFCdW5vVTgvQVlMMDVydFBQCmgvdTE4LzdFdXVvZkVRaE1nL2JlMXAyZmEyQ2dyUUNoQW9JQkFRQzRiSGQ2NjU1dWw0dXZib2RoMHlOUnNuQkcKN1puOGd2di9obE11K2VaSStrOHhYMXdvWkdpSlFTZXA1Y0lCZno4Y2g5T3VTYTBLbmZRNklTa0ZzZ3M4VTVBTApnWXRTKzBQemlGYStlcHZZK2creW1KV29pMjBLalNlQnVVVTdnOWJmMEZPd05LeUk5b1B5VkRDcmJWZUUrZ1EwCkg4T1MvVDNuUHVObC9PMHVHMnY2bkJnQkJrOTJDcE95RityVEJVNGNXVmRrVE1xNk4xbVU3RkNQMXJHNmhjMjcKL1l3N0xtZmhpNHJlNmIrSlRtUElWOUNLc291a3czRm5oQjhPdWtHd2tsSisxTklMWU05bkJmeUlUM2ErQ3gzcworSk5nNU82RmVEVHFTamtEWmJ0UlJnTjFjcm9lMklDWS9WblpWTm01aExlTDJweDdXSWJKNGRlb1RSSk0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K", + "Store": "default" + }, + { + "domain": { + "main": "hello.srv.instrukt.ai" + }, + "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdMRENDQlJTZ0F3SUJBZ0lTSzlJYndNam5SUUswQU9FdUNXV2hVQ25yTUEwR0NTcUdTSWIzRFFFQkN3VUEKTUZreEN6QUpCZ05WQkFZVEFsVlRNU0F3SGdZRFZRUUtFeGNvVTFSQlIwbE9SeWtnVEdWMEozTWdSVzVqY25sdwpkREVvTUNZR0ExVUVBeE1mS0ZOVVFVZEpUa2NwSUVGeWRHbG1hV05wWVd3Z1FYQnlhV052ZENCU016QWVGdzB5Ck5EQXlNakF4TlRNMU1ERmFGdzB5TkRBMU1qQXhOVE0xTURCYU1DQXhIakFjQmdOVkJBTVRGV2hsYkd4dkxuTnkKZGk1cGJuTjBjblZyZEM1aGFUQ0NBaUl3RFFZSktvWklodmNOQVFFQkJRQURnZ0lQQURDQ0Fnb0NnZ0lCQVBCaApodW51QUFZY3oyd2NLakhISjg0cjFXSkNsdjVJMC9UckhWREhXYnRzYWo3Sys2Q0tyQTNmbm9mRVZSM3gwZVFzCkpDYUZ2L1E4bDhQdExZZkNoUlp6Mkxra21yUUx3RVlSaitWcWt2Y28vZHcxaGI4YVQyZ2l5RDd2c29KcCt2TncKQ0s1V3pLSUprVDc0TFhqMmN1SGRDdzQ0b21LczYwaWxOaGEzY2I4WnB4VDU4bUpPNlVmelVLRjc4Tk40RWtpSQowdERKWVQ0dnF6RWZqb0UvaUFXdThFaVhPL3UwL3BvQUdJTVNKc0hDeWMwZXVZVjJ3TnFGRGV6S1FITFVGRlNrCmJ4RXlwaXJ0KzB2SE55dWE3YXd1eDBNdlFvc0lndnFLNFd0ZEptdnJDNlJUV1RWcXpVemNMQnpvR2RBVlh6ajMKT2xMV3Nabyt2TFBRU21nL1MzSXIwOFNKVEpOTUwxK2FMRWlMVHVoWVJhMjF5TmErWUUyMWcvWmtUOGRFSG5NWgpMNEV0RzR1K1l3cm81dS9QRjkxYy9VcDY5Y05VUVRmMWFycjdLZ202R0dRYkdBcFBnUmE3UWtsZk5BOW9nM2xYClhEUXlqR3ZoYXBLTUtmcE83NkV5SDVJZTlXbC92WGJyWFZTeUVzTXVUcDZTbDFObjNQRlluYTg0WTRwOVBuTWgKSndZRW1CRGJNNVVIY1B0eHdQSjJhTk1pNHRsRGVaZTJZaVcyZGFkL0QyQWFZNjFUYWVncnRaRU9sS3FTWktjdApTak5TTlJEdG9XRU5oNDBQbUx3UUM5c1oxSkh0WkY5Y3BnMnV1TThQUmNPdmJkVkNlOFFuanBHM1hqNlJYK0MyCnRZWUl1c1A1d2UvSHNBMXRxb2dTTlF1TVFhNmlFSHNHUkU4bHJwRC9BZ01CQUFHamdnSWxNSUlDSVRBT0JnTlYKSFE4QkFmOEVCQU1DQmFBd0hRWURWUjBsQkJZd0ZBWUlLd1lCQlFVSEF3RUdDQ3NHQVFVRkJ3TUNNQXdHQTFVZApFd0VCL3dRQ01BQXdIUVlEVlIwT0JCWUVGQ2FqZXA0VkJvSmhmSzBtMUpXZ0N1UXhDdTNoTUI4R0ExVWRJd1FZCk1CYUFGTjV5ZWtqZk1jT21VTitmaFNQZlZ6ZExYUzVsTUYwR0NDc0dBUVVGQndFQkJGRXdUekFsQmdnckJnRUYKQlFjd0FZWVphSFIwY0RvdkwzTjBaeTF5TXk1dkxteGxibU55TG05eVp6QW1CZ2dyQmdFRkJRY3dBb1lhYUhSMApjRG92TDNOMFp5MXlNeTVwTG14bGJtTnlMbTl5Wnk4d0lBWURWUjBSQkJrd0Y0SVZhR1ZzYkc4dWMzSjJMbWx1CmMzUnlkV3QwTG1GcE1CTUdBMVVkSUFRTU1Bb3dDQVlHWjRFTUFRSUJNSUlCQ2dZS0t3WUJCQUhXZVFJRUFnU0IKK3dTQitBRDJBSDBBN1RESWsvb3ZkbUlReTl6eUJ4alBma1JXblVvdDJldUl0ZWplMnNFMUE1a0FBQUdOeDE3ago5UUFJQUFBRkFBQldaRjhFQXdCR01FUUNJQXNpNnNhbHB0K1ZQZjc2WmdJNHVDeEVjL09xQWZ4WnVCcytWaCtQCmI3cWpBaUFQYjM4V3hOOVJTanFiSy9yRkxRUktJd0Y1UCtEQzIrYnVTYWNINTFacUtRQjFBTERNZytXbCtYMXIKcjN3SnpDaEpCSWNxeCtpTEV5eGpVTGZHL1NiaGJHeDNBQUFCamNkZTViTUFBQVFEQUVZd1JBSWdVbklwQTljdwpubFV5NythZVVNTmhHYnJsenE5NnhUNy9DYUhNalYyQWRsY0NJRlNwSndqRWkyb3luYnJhOXF6U29tNy9aMXpUCkpBU3dqV3BHSTFxb3pEWTBNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUFjbU1hWDYxTjl6amIvUlNjYzVZaVgKRzhGYUFIU1M2Ti9CYWZWempyalJGcHZ4WXhCbGtoTUhZY1pGa29DREhIcmQrcWNEVzJQYi9NbXEvRW5OMnoxLwo4LzJkSC8rSFNJaE5UYVF5aXRIQklnL3puYWFaYXZNNDZpQlp1c3liS0ZjcUxZYUprek5NK3RRaWVtRnhHN1dPCnV1YnhMSnpKcU5yQThBWHc3eVBxblZkakZ4OGhkRXd4Q0Y5VFhOUUFGVFBSTHlrTlVySTE2dk9COUVTbE55ajYKVDJSSXhmSlBKaHdqWFY1SVhvTzlSRitjY3p0Z01pOWFyd2c0ZnUzcDMwUFRBc3pYaURVQ1Q5d3ByeWdiUEM2MApwd25BK0g4WWlhSVdGYmhuOGRiRUUxZHBnb1BGczdFbGkxK3hYUS9vd3J2NXdIZHVzeFBNTXNjbFlYSjMzWG9rCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlGV3pDQ0EwT2dBd0lCQWdJUVRmUXJsZEh1bXpwTUxyTTdqUkJkMWpBTkJna3Foa2lHOXcwQkFRc0ZBREJtCk1Rc3dDUVlEVlFRR0V3SlZVekV6TURFR0ExVUVDaE1xS0ZOVVFVZEpUa2NwSUVsdWRHVnlibVYwSUZObFkzVnkKYVhSNUlGSmxjMlZoY21Ob0lFZHliM1Z3TVNJd0lBWURWUVFERXhrb1UxUkJSMGxPUnlrZ1VISmxkR1Z1WkNCUQpaV0Z5SUZneE1CNFhEVEl3TURrd05EQXdNREF3TUZvWERUSTFNRGt4TlRFMk1EQXdNRm93V1RFTE1Ba0dBMVVFCkJoTUNWVk14SURBZUJnTlZCQW9URnloVFZFRkhTVTVIS1NCTVpYUW5jeUJGYm1OeWVYQjBNU2d3SmdZRFZRUUQKRXg4b1UxUkJSMGxPUnlrZ1FYSjBhV1pwWTJsaGJDQkJjSEpwWTI5MElGSXpNSUlCSWpBTkJna3Foa2lHOXcwQgpBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF1NlRSOCs3NGI0Nm1PRTFGVXdCcnZ4ekVZTGNrM2lhc21LcmNRa2IrCmd5L3o5Snk3UU5JQWwwQjlwVktwNFlVNzZKd3hGNURPWlpoaTd2SzdTYkNrSzZGYkhseVU1QmlEWUl4YmJmdk8KTC9qVkdxZHNTak5hSlFUZzNDM1hySmphL0hBNFdDRkVNVm9UMndEWm04QUJDMU4rSVFlN1E2RkVxYzhOd21UUwpubW1SUW00VFF2cjA2RFAremdGSy9NTnVieFdXRFNiU0tLVEg1aW01ajJmWmZnK2ovdE0xYkdhY3pGV3c4L2xTCm51a3luNUoyTCtOSlluY2x6a1hvaDluTUZueVBtVmJmeURQT2M0WTI1YVR6Vm9lQktYYS9jWjVNTStXZGRqZEwKYmlXdm0xOWYxc1luMWFSYUFJcmtwcHY3a2tuODN2Y3RoOFhDRzM5cUMyWnZhUUlEQVFBQm80SUJFRENDQVF3dwpEZ1lEVlIwUEFRSC9CQVFEQWdHR01CMEdBMVVkSlFRV01CUUdDQ3NHQVFVRkJ3TUNCZ2dyQmdFRkJRY0RBVEFTCkJnTlZIUk1CQWY4RUNEQUdBUUgvQWdFQU1CMEdBMVVkRGdRV0JCVGVjbnBJM3pIRHBsRGZuNFVqMzFjM1MxMHUKWlRBZkJnTlZIU01FR0RBV2dCUzE4Mlh5L3JBS2toLzdQSDN6UktDc1l5WERGREEyQmdnckJnRUZCUWNCQVFRcQpNQ2d3SmdZSUt3WUJCUVVITUFLR0dtaDBkSEE2THk5emRHY3RlREV1YVM1c1pXNWpjaTV2Y21jdk1Dc0dBMVVkCkh3UWtNQ0l3SUtBZW9CeUdHbWgwZEhBNkx5OXpkR2N0ZURFdVl5NXNaVzVqY2k1dmNtY3ZNQ0lHQTFVZElBUWIKTUJrd0NBWUdaNEVNQVFJQk1BMEdDeXNHQVFRQmd0OFRBUUVCTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElDQVFDTgpETGFtOXlOMEVGeHhuLzNwK3J1V082bi85Z29DQU01UFQ2Y0M2ZmtqTXM0dWFzNlVHWEpqcjVqN1BvVFFmM0MxCnZ1eGlJR1JKQzZxeFY3eWM2VTBYK3cwTWo4NXNISTVEblFWV041K0QxZXI3bXAxM0pKQTB4YkFiSGEzUmxjem4KeTJRODJYS3VpOFdIdVdyYTBnYjJLTHBmYm9ZajFHaGdraHIzZ2F1ODNwQy9XUThIZmt3Y3ZTd2hJWXFUcXhvWgpVcThISWYzTTgycVM5YUtPWkUwQ0VtU3lSMXpacVF4SlVUN2VtT1VhcGtVTjlwb0o5ekdjK0ZnUlp2ZHJvMFhCCnlwaFdYRGFxTVlwaDBEeFcvMTBpZzVqNHhtbU5EakNSbXFJS3NLb1dBNTJ3QlRLS1hLMW5hMnR5L2xXNWRodEEKeGt6NXJWWkZkNHNnUzRKME8rem02ZDVHUmtXc05KNGtub3RHWGw4dnRTM1g0MEtYZWIzQTUrLzNwMHFhRDIxNQpYcThvU05PUmZCMm9JMWtRdXlFQUo1eHZQVGRmd1JseVJHM2xGWW9kclJnNnBvVUJELzhmTlRYTXR6eWRwUmd5CnpVUVpoLzE4RjZCL2lXNmNiaVJOOXIySGtoMDVPbStxMC82dzBEZFplKzhZck5wZmhTT2JyLzFlVlpiS0dNSVkKcUtteVpiQk51NXlzRU5JSzVNUGMxNG1VZUttRmpwTjg0MFZSNXp1bm9VNTJscXBMRHVhL3FJTThpZGs4NnhHVwp4eDJtbDQzRE8vWWEvdFZaVm9rMG1PMFRVanpKSWZQcXl2cjQ1NUlzSXV0NFJsQ1I5SXEwRURUdmUyL1p3Q3VHCmhTanBUVUZHU2lRclIySksyRXZwK282QUVUVWtCQ08xYXcwUHBRQlBEUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K", + "key": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS1FJQkFBS0NBZ0VBOEdHRzZlNEFCaHpQYkJ3cU1jY256aXZWWWtLVy9ralQ5T3NkVU1kWnUyeHFQc3I3Cm9JcXNEZCtlaDhSVkhmSFI1Q3drSm9XLzlEeVh3KzB0aDhLRkZuUFl1U1NhdEF2QVJoR1A1V3FTOXlqOTNEV0YKdnhwUGFDTElQdSt5Z21uNjgzQUlybGJNb2dtUlB2Z3RlUFp5NGQwTERqaWlZcXpyU0tVMkZyZHh2eG1uRlBueQpZazdwUi9OUW9YdncwM2dTU0lqUzBNbGhQaStyTVIrT2dUK0lCYTd3U0pjNys3VCttZ0FZZ3hJbXdjTEp6UjY1CmhYYkEyb1VON01wQWN0UVVWS1J2RVRLbUt1MzdTOGMzSzVydHJDN0hReTlDaXdpQytvcmhhMTBtYStzTHBGTloKTldyTlROd3NIT2daMEJWZk9QYzZVdGF4bWo2OHM5QkthRDlMY2l2VHhJbE1rMHd2WDVvc1NJdE82RmhGcmJYSQoxcjVnVGJXRDltUlB4MFFlY3hrdmdTMGJpNzVqQ3VqbTc4OFgzVno5U25yMXcxUkJOL1ZxdXZzcUNib1laQnNZCkNrK0JGcnRDU1Y4MEQyaURlVmRjTkRLTWErRnFrb3dwK2s3dm9USWZraDcxYVgrOWR1dGRWTElTd3k1T25wS1gKVTJmYzhWaWRyemhqaW4wK2N5RW5CZ1NZRU5zemxRZHcrM0hBOG5abzB5TGkyVU41bDdaaUpiWjFwMzhQWUJwagpyVk5wNkN1MWtRNlVxcEprcHkxS00xSTFFTzJoWVEySGpRK1l2QkFMMnhuVWtlMWtYMXltRGE2NHp3OUZ3Njl0CjFVSjd4Q2VPa2JkZVBwRmY0TGExaGdpNncvbkI3OGV3RFcycWlCSTFDNHhCcnFJUWV3WkVUeVd1a1A4Q0F3RUEKQVFLQ0FnQXpCd0QwdDZrWm94UUZCeFdONjVEdm15NTQ1Vm5ZTU8yTm1EdG80bWlSK0xtMUNySFBxcUJMY2FUZApmVGYwVzFrSzdyUGQyWmFkNmg3dFlIUEIxZkVhaXd6K2dGNjJ5YWZQTi9zcjRDdkNlOWtuM2RTK2RIOVd3OHR1ClVNTzhHM05DS2c0dHRBOU1kbXQyeW9nY25YUlZyZTZpM2pMYWI0cWlxUytLTXZiYWNqK2RuRkZ2dlFDWVhYWkkKVVY4V0JPOGNwcTdaMDJYcEpZVlpzREU0RHB4NVdHRjVoMkVjVlplV2FWN0t0QmUyR2Z0c3A1RUtXK01rZ0FvcwpzZUpzZmNpekR4VGt1Yk84ZVRoWEpicjRickwxVWhqS2trVzE3ZUNmL3N0TFYwTEl0ZVNLeFlheWFER2NsMEJtCkFuODRSTkVwN3BGYTlHaFBMZkl3Mlk1eUFyMm8zWDNnbm9QME5mckdiZ0JFLzA4K3Rwc2ZnalFRU0dZekxXU2UKVWZhUythOExQV2U0RHpncUxIdUtxUkJNN3llcHpEMTB3WkNKNHpkRS96bkNUR0VOTEpCaTlFY2NxM0IxbWxQbwpodmFVbWZybWVvSlVVaWhocmVabGp6ODBKZE5GbW5FMkp3R0hqdnJjam90T0RpejJ4REpvQ0dxZm1pQ2R2ajYyCjNOemdFaU9ZL096RDc2WVpWTzRoSHRWUjVISGVGdmpOL3d0S0RjWVVqcHpsRlJCbE5NdzdQclQrd3FhSVlvUUMKZHZ4NVljcjYxTlZ5VDhSTVRFaW5UK0hIQy9ZSnlERVgwMDgvNlJoaWVFUkxmZ0xDMTlWMEtSMVptNW4wYUtkYQp4RFlQaW5zb3JENzlrbHIwUXpyVmNnTzd3cXlJNDBFUFNNdVg5VTZHc0RML3krT0x3UUtDQVFFQS9QWmNieG55Cnh1NW84L3pNdlVmVUdKNkRpM2pwUThXejRLTklwUVZtRWR4eFo2emJuSWxRR3UxdDdmSjAzWnNhTDIyRzFDZVQKb2krQ25WZXVxYk1peERkZlgxR21uS3BHb3grckR4NjZJVWdnVWM5Y2ZoaGNkUzRBUU9iWktOYkgzWi8wcDZ0VwpnN2dkemdTWWRpNTRhNE4xbEhIMFE4MXUvRTQ5eEFZclpaeUpzbXNmR0VyNGxMS1lENFgyekFMbnhSK3pqYjkwClVvdWp0aWY4aVZYZ0RCTUF1amdNdmIxT2V3MS9oQWNWYkw4YW52K2htbEJKV1NQVjRjWFllUU1EOUR0OWJIZXcKUElKb25iNXF0US9zTEo3RndEQmRLNGg2Qlc4RjVVZjd2Y0hoS21Qam9taS9lZDZmN2FxWTlUYXFtWUJoa05neQpkUUZqS24zdTZXTXNJd0tDQVFFQTgwUjlPRXE0TDVoNUVGazhnd1RvUmFla0d1L0F5cmJ5WXZDb0NEZVh6bzhOCitpMmEvQzJwaFFkZUx3TmlxZHV5TFVmb0xZMms5Q1ZJbFRDY0I4d2IybVU1RGdjbEZrSm5xNXpsODZhMlI2N0EKdExkb2VsMmRFa29OSElJNGRud25QeXk1YnB3WTNRL2gyUXNTQjk3bXlJTmorUEhZOGJYY0xpYTRETTU4Wjh1RAoyT29jUjRqWUx0QVRqREF4cVZwNi84VDcwQ1Z2WDZFMm1lT0lZc0ZnQXU2SXNCb0NNUzNMdVBkNGlLaXBwQ3I3Ci8zM1FlMFN2dUp6N0NwMDNnQjE5UWIzUktDZGlmb2Z6eWgyWUNnaERhSFl4Q3drRENoUFNzbmZQVXkwQ2ZFVjMKWTR3dzRBM2lZVWpnQjN6aVVic011dW96dk10ZDFYOE1HdVc3WVM3WGRRS0NBUUVBOW14OHdWSnkyZTM0V2U3agorR1FYcnVBRVg0TXJJZlZmZEs4OEhsM3kvK1ZTcUYrUkZKNERaaWZ6bGdCQ2Juc2crQ0RuWHBjclQ2eWV2ZTVqCjNUYi80RXZjNGJ0bDVtcE9JNnkrOUJ6SHo1STJ3ZHVTUzlncGdaR05kNndxMG9qZzdQcGMyRldtRXV6eVEvYmcKR3dqTUVGaGcyM3BqTFFYY1gzZE0zai93V0swMHlwVm1Ga0U2aFlpeGJ6WGVwUUVUalVtRG5jdHJZdHBiWWlEego5MG1yMTdkUFZ4a3crRW1KUVlCMXYwVXpUdHVmOEZJd3VHWUwvZ1ZWM3ZuZ0ZvcWtlSERYanluTmlqU3oreTg3ClBpVkR1bmYxUm5NVzRFaTBMY1RvNW1ISVVDT2wyS2kwWVV0UGRFWTJYODEzOGpFTjRqbFpVa1A2YTFjdHBvQkYKbENBakp3S0NBUUJwYjlUcU1oQjBxSnhJUnJVZ2dGNy8yYklaN3JKYjZrRlNPc1pKa1hiZGswbFdscSttLzArTApGZTl2WndNN1picHJQOUJVOWlCNXBUTTVYbkV5Ty9nU2gyN2NNcUtUYkVLc3NmdEV5SXdXczVwWWVXbnlKMkx0CnkyUzFaNGQ3YTBKRGNtb0lrK1YrNHVSYmd4M1VoWHIzaTJNb1FnSnFlUTB1ZjUra0JWY01VYndhcHhXTXQ3WWoKVEJJODM2S1dxVjNJUDZONHl0RGozT2JpZWlqaHpMaUZiTWVYdEI0aEhKMUExUm5MK1haVmJqLzhzNVpKNXpKVwpUTXZPQk9oS1FmM0pJYkZtdXZOSXJqb3U3dmEzMm04RTlyMDQ4RFgrQnl0NktNMldXbHJLYS8xY0N5WWY1a1NwCkw4SFIzUUhtdExER3pyMlA1WUVOQ056QlI3TVlZd29GQW9JQkFRQ01wOEoxWHE1WFNWQnJOR3NteTh5ZFlldjkKZkVPVE02M3VTSkpJN2RjWVpBOXhucUlWRDdZV0lOSmF0QlFNc2V4Q3lrWGxIamtoSnJsMWxzMk9lUUZrZkNtZwo0WEhoeWdGUk4xY1VXblE0eGxFQmJZRDkvRDhVYTJodVB2b2RlMnpndHhzWlVvV1pGeCtWRDRqMXZEa05TNG02Cjh4NGdNZ3dvaWwybDVDaDZJcGFUZEkwSkFGc3lGYWF0R2RGZ3NSalRFS0toNVpNcG9CdlpZdU1Cem90RDZjWXYKVnE3VStDcGwxOVBDZ1QzME16VzB5VlF0akNKdEg4SUdZWjN2eTZzQmtmUXB3RWgrclBEeEdiNjZOTTI0bmxTawpqNlJVRHo2TnA5dlgydG8xdzVXMDlrZ3ZFZHBSd1YwTllaVE5nSGdFeGRtNTRleFJnOUVCR210Y21kaVAKLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K", + "Store": "default" + } + ] + }, + "tls": { + "Account": { + "Email": "", + "Registration": { + "body": { + "status": "valid" + }, + "uri": "https://acme-v02.api.letsencrypt.org/acme/acct/1579757127" + }, + "PrivateKey": "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", + "KeyType": "4096" + }, + "Certificates": null + } +} \ No newline at end of file diff --git a/requirements-prod.txt b/requirements-prod.txt old mode 100755 new mode 100644 diff --git a/requirements-test.txt b/requirements-test.txt old mode 100755 new mode 100644 diff --git a/requirements.txt b/requirements.txt old mode 100755 new mode 100644 diff --git a/tpl/docker-compose.yml.j2 b/tpl/docker-compose.yml.j2 index 6cee735..5203830 100755 --- a/tpl/docker-compose.yml.j2 +++ b/tpl/docker-compose.yml.j2 @@ -7,31 +7,51 @@ networks: external: true services: -{% for s in services %} - {{ project }}-{{ s.name }}: - image: {{ s.image }} - {% if s.command -%} +{%- for s in project.services %} + {%- set p = project %} + {{ project.name }}-{{ s.name }}: + {%- if s.command %} command: {{ s.command }} - {% endif -%} - networks: - - default - - proxynet - restart: unless-stopped - {% if s.env -%} + {%- endif %} + {%- if s.env %} environment: - {% for k, v in s.env -%} + {%- for k, v in s.env %} {{ k }}: {{ v }} - {% endfor -%} - {% endif %} - {% if s.port -%} + {%- endfor %} + {%- endif %} + {%- if p.entrypoint == s.name %} expose: - '{{ s.port }}' - {% endif -%} - {% if s.volumes %} + {%- endif %} + image: {{ s.image }} + {%- if p.entrypoint == s.name %} + labels: + - traefik.enable=true + - traefik.docker.network=proxynet + - traefik.http.routers.{{ p.name }}.entrypoints=web-secure + - traefik.http.routers.{{ p.name }}.rule=Host(`{{ p.domain }}`){%- if s.path_prefix %} && PathPrefix(`{{ s.path_prefix }}`){%- endif %} + - traefik.http.routers.{{ p.name }}.tls.certresolver=letsencrypt + {%- if path_prefix and path_remove %} + - traefik.http.middlewares.removeServiceSelector.stripPrefix.prefixes={{ path_prefix }} + {%- endif %} + - traefik.http.services.{{ p.name }}.loadbalancer.server.port={{ s.port }} + {%- for k, v in s.labels %} + - {{ k }}={{ v }} + {%- endfor %} + {%- endif %} + networks: + {%- if p.services | length > 1 %} + - default + {%- endif %} + {%- if p.entrypoint == s.name %} + - proxynet + {%- endif %} + restart: unless-stopped + {%- if s.volumes %} volumes: - {% for v in s.volumes -%} + {%- for v in s.volumes %} - .{{ v }}:{{ v }} - {% endfor -%} - {% endif -%} -{% endfor -%} + {%- endfor %} + {%- endif %} +{%- endfor %}