├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── applications ├── reconcile.sh ├── watchdog-down.sh └── watchdog-up.sh ├── architecture.excalidraw ├── architecture.svg ├── config.sh.example ├── docker-compose.yaml ├── docs └── postgres.md ├── init ├── create-empty-configuration-structure.sh ├── install-dependencies.sh ├── template │ ├── .gitignore.template │ ├── applications-internal │ │ ├── monitoring-grafana.yaml │ │ ├── postgres.yaml │ │ ├── prometheus.yaml │ │ ├── registry-docker.yaml │ │ └── registry-npm.yaml │ ├── applications-public │ │ └── example-application.yaml │ ├── config.sh │ ├── docker │ │ └── storage │ │ │ └── .gitkeep │ ├── grafana │ │ └── .gitkeep │ ├── haproxy-internal-ssl │ │ └── invalid.pem │ ├── haproxy-internal │ │ ├── directory.html │ │ └── haproxy.cfg.template │ ├── haproxy-public-ssl │ │ └── invalid.pem │ ├── haproxy-public-stats │ │ └── .gitkeep │ ├── haproxy-public │ │ └── haproxy.cfg.template │ ├── letsencrypt │ │ ├── etc │ │ │ └── .gitkeep │ │ └── var │ │ │ └── .gitkeep │ ├── prometheus │ │ ├── conf │ │ │ └── prometheus.yml │ │ └── data │ │ │ └── .gitkeep │ └── verdaccio │ │ ├── conf │ │ ├── config.yaml │ │ └── htpasswd │ │ └── storage │ │ └── .gitkeep └── test-example-application.sh ├── lib ├── js-yaml.js └── load-applications.js ├── proxy ├── haproxy-generate.js └── reload-haproxy-config.sh ├── ssl-internal ├── issue-new-ssl-cert.sh ├── push-cert-to-haproxy.sh └── renew-ssl-cert.sh ├── ssl-public ├── generate-issue-all-ssl-certs.js ├── generate-renew-all-ssl-certs.js ├── issue-all-ssl-certs.sh ├── issue-new-ssl-cert.sh ├── issue-new-ssl-cert.staging.sh ├── push-cert-to-haproxy.sh └── renew-all-ssl-certs.sh ├── start.sh ├── stop.sh └── util └── backup.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | /config.sh 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Michael Nahkies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shoe-string cluster 2 | * [Introduction](#introduction) 3 | * [Guiding principals](#guiding-principals) 4 | * [Contributing](#contributing) 5 | * [Features](#features) 6 | * [Considerations](#considerations) 7 | * [Setup](#setup) 8 | * [Usage / Scripts reference](#usage--scripts-reference) 9 | + [`./config.sh`](#configsh) 10 | + [`./start.sh`](#startsh) 11 | + [`./stop.sh`](#stopsh) 12 | + [`./proxy/reload-haproxy-config.sh`](#proxyreload-haproxy-configsh) 13 | + [`./applications/watchdog-up.sh`](#applicationswatchdog-upsh) 14 | + [`./ssl/*`](#ssl) 15 | * [Accessing internal services](#accessing-internal-services) 16 | + [VPN Wireguard example](#vpn-wireguard-example) 17 | + [SSH Tunnel example](#ssh-tunnel-example) 18 | * [Core Services](#core-services) 19 | + [Influxdb](#influxdb) 20 | + [Telegraf](#telegraf) 21 | + [HAProxy](#haproxy) 22 | - [haproxy-public-ssl](#haproxy-public-ssl) 23 | * [Default internal services](#default-internal-services) 24 | + [Grafana](#grafana) 25 | + [Docker Registry (`registry:2`)](#docker-registry-registry2) 26 | + [NPM Registry (`verdaccio`)](#npm-registry-verdaccio) 27 | * [TODO:](#todo) 28 | * [Future / TODO](#future--todo) 29 | 30 | ## Introduction 31 | 32 | This project is an attempt to create a turn-key "cluster" that is suitable for 33 | small servers with limited RAM. 34 | 35 | The only OS level dependencies it needs to be operated are `docker` and `docker-compose` 36 | and I use it on a AWS Lightsail host with 1GB of RAM. 37 | 38 | I wrote this after attempting to get "light weight" kubernetes environments going like K3s 39 | or KIND, and finding that the resource overhead was simply too high for my requirements 40 | (`512mb` minimum, `1gb` recommended) 41 | 42 | **Current architecture** 43 | ![architecture](./architecture.svg) 44 | 45 | ## Guiding principals 46 | - Every application runs in a `docker` container 47 | 48 | - All configuration and data is stored under a single directory structure, to make backups simple 49 | 50 | - Portable across different cloud providers, minimal host pre-requisites 51 | 52 | - Provides the software that I typically want available to me when experimenting with personal projects 53 | (eg: `npm` registry, `postgres` instance) 54 | 55 | ## Contributing 56 | I welcome feedback and improvements, especially around any security concerns. However please note, 57 | this project is pretty particular to my personal preferences and needs - I hope it might be of use 58 | to others, but I might not accept PR's that don't align with my requirements. 59 | 60 | Please feel free to fork and customize to your hearts desire though :) 61 | 62 | ## Features 63 | - Public ingress on port 80 using haproxy 64 | 65 | - Internal/private service ingress on configured port and ip address using haproxy 66 | - I recommend using `wireguard` or similar to bind this to a private ip address 67 | 68 | - Private Docker registry 69 | 70 | - Private NPM registry 71 | 72 | - Metrics collected by `prometheus`, with `grafana` for dashboards 73 | 74 | - Letsencrypt certbot for automatic SSL provisioning, and renewal 75 | 76 | - "Watchdog" script suitable for running on demand, or as `CronJob` to reconcile 77 | running applications with the configuration on filesystem 78 | 79 | - Helper scripts for updating application configuration with new docker tags to 80 | deploy 81 | 82 | ## Considerations 83 | - Letsencrypt will ban domains that make invalid requests to its production environment 84 | it's worthwhile testing this part of things using their staging environment before running 85 | 86 | - This is held together with string, a collection of bash scripts that may or may not be portable, 87 | it has been tested on Fedora 34 and AWS Linux 2. 88 | 89 | - Whilst every effort is made to limit RAM consumption, you may still want to add swap to avoid OOM killer. Influxdb 90 | in particular can consume a fair chunk of RAM. Instructions here: https://aws.amazon.com/premiumsupport/knowledge-center/ec2-memory-swap-file/ 91 | 92 | ## Setup 93 | 1. Clone repo to somewhere on host or otherwise place the contents on the server 94 | 2. Install `docker` / `docker-compose` (`./init/install-dependencies.sh`, or manually) 95 | 3. Bootstrap data/configuration structure using `./init/create-empty-configuration-structure.sh /path/you/want/config-and-data-to-be-stored` 96 | 4. Configure generated files 97 | 1. Update config.sh with correct ip addresses, ports etc 98 | 2. Add, remove, or customise applications in `applications-internal` / `applications-public` 99 | 5. Run ./start.sh 100 | 101 | ## Usage / Scripts reference 102 | There are a number of bash scripts in this project, I give a brief overview below, 103 | but you should probably read through them before attempting to use in production. 104 | 105 | ### `./config.sh` 106 | - Contains the path to the configuration directory. Generated by `./init/create-empty-configuration-structure.sh` 107 | 108 | ### `./start.sh` 109 | - Generates haproxy configuration 110 | - Creates the docker networks 111 | - Start services defined in `docker-compose.yaml` 112 | - Starts application using `applications/watchdog-up.sh` 113 | 114 | ### `./stop.sh` 115 | - Stop services defined in `docker-compose.yaml` 116 | - Stop applications using `applications/watchdog-down.sh` 117 | - Clean up unused networks 118 | 119 | ### `./proxy/reload-haproxy-config.sh` 120 | - Called automatically by `start.sh` and ssl certificate scripts 121 | - Re-generates `haproxy.cfg` files, and sends a signal to the containers to reload their config. 122 | - Can be called manually if changes to the template or applications yaml files have been made 123 | 124 | ### `./applications/watchdog-up.sh` 125 | - Suitable for calling as a `CronJob` or manually after making changes to the application yaml 126 | configuration files, attempts to reconcile running containers with the configuration state. 127 | 128 | ### `./ssl/*` 129 | 130 | TODO: write documentation 131 | 132 | ## Accessing internal services 133 | There are two main options for configuring this securely: 134 | - Bind to private ip address and access via VPN 135 | - Bind to localhost / **firewalled** port and access via SSH tunnel 136 | 137 | **Note:** these services are only exposed over `http` but I might add support for 138 | exposing them over `https` as well since `docker` in particular gets a bit naggy about "insecure" 139 | registries. It is assumed that you will only make them accessible via secure channels 140 | like the examples below. 141 | 142 | ### VPN Wireguard example 143 | **Prerequisites:** `wireguard-tools` installed, Linux kernel >= 5.6 or `wireguard` installed as a kernel module. 144 | If you're not using Linux on both ends then you'll need to consult your platforms documentation. 145 | 146 | 1. On both host and client generate public/private key pairs: 147 | (you'll want to delete these files at the end) 148 | ```shell 149 | wg genkey | tee privatekey | wg pubkey > publickey 150 | ``` 151 | 152 | 2. Create server config: 153 | 154 | Create file /etc/wireguard/wg0.conf: 155 | ```shell 156 | [Interface] 157 | Address = 10.12.0.1 158 | PrivateKey = 159 | ListenPort = 51820 160 | 161 | [Peer] 162 | PublicKey = 163 | AllowedIPs = 10.12.0.1/24 164 | ``` 165 | 3. Open port `51820` on your servers firewall 166 | 167 | 4. Bring interface up on server 168 | ```shell 169 | sudo wg-quick up wg0 170 | sudo systemctl enable wg-quick@wg0 # optional, enable bringing up at boot-time 171 | ``` 172 | 173 | 5. Create client config 174 | 175 | Create file /etc/wireguard/wg0.conf: 176 | ```shell 177 | [Interface] 178 | Address = 10.12.0.2 179 | PrivateKey = 180 | 181 | [Peer] 182 | PublicKey = 183 | Endpoint = :51820 184 | AllowedIPs = 10.12.0.2/24 185 | 186 | PersistentKeepalive = 25 187 | ``` 188 | 189 | 6. Bring interface up on client and test 190 | ```shell 191 | sudo wg-quick up wg0 192 | sudo systemctl enable wg-quick@wg0 # optional, enable bringing up at boot-time 193 | ping 10.12.0.1 194 | ``` 195 | 196 | ### SSH Tunnel example 197 | **Assumptions:** ingress configured to 127.0.0.1:8080 198 | 199 | Create tunnel using ssh, eg: 200 | ```shell 201 | ssh user@host.com -L 127.0.0.1:8080:127.0.0.1:8080 202 | ``` 203 | 204 | Then services are available via port 8080, with the domains configured in the `haproxy-internal/applications.js` 205 | file. The easiest way to make this available in your browser is to add entries to your 206 | hosts file, or use something like `dnsmasq` 207 | 208 | Eg: `/etc/hosts` file 209 | ```shell 210 | 127.0.0.1 grafana.internal.example.com 211 | 127.0.0.1 npm.internal.example.com 212 | 127.0.0.1 docker.internal.example.com 213 | ``` 214 | 215 | ## Core Services 216 | There are a number of "core" services managed by `docker-compose`. This is distinct from application services 217 | that you want to deploy and make available. 218 | 219 | ### HAProxy 220 | There are two instances of haproxy in use, one for public ingress, and one for private/internal ingress. 221 | 222 | The configuration is generated from the docker-compose yaml files in the `applications-public` / `applications-internal` 223 | directories. Specifically: 224 | 225 | - A custom top level property `x-external-host-names` is used to know which vhosts to proxy 226 | to that application. 227 | 228 | - A service named `application` is expected to exist, and the `hostname` of this is used to 229 | know which container to proxy to. 230 | 231 | - A custom top level property `x-container-port` is used to know which port to proxy to, or 232 | default to 80. 233 | 234 | If there are no external host names declared, or no application service is found, the file 235 | is skipped, and not included in the proxy. 236 | 237 | Any customizations you need to make should be made to the template rather to avoid them 238 | being overwritten. 239 | 240 | The public proxy exposes stats using a unix socket, mounted to `haproxy-public-stats` and 241 | read from by telegraf. 242 | 243 | For the public ingress, SSL certificates are read from `haproxy-public-ssl`, noting that haproxy 244 | requires the public and private portion to be stored in the same file. 245 | 246 | Originals are managed by certbot and stored in `letsencrypt/etc` 247 | 248 | #### haproxy-public-ssl 249 | This folder is where the concatenated ssl certificate + private keys will be 250 | stored, and loaded by the public facing haproxy instance. 251 | 252 | Due to an annoying "feature" of haproxy where it will refuse to start if this 253 | directly is empty, which is likely is when first bootstrapping your server, 254 | there is a `invalid.pem` file which contains a self-signed certificate for the 255 | domain `invalid.` 256 | 257 | This avoids the chicken and egg situation where you either need to first start 258 | with ssl disabled then restart after certificates have been populated, etc 259 | 260 | As per [RFC 6761](https://datatracker.ietf.org/doc/html/rfc6761) section 6.4, 261 | `invalid.` is guaranteed to never exist, and once you have your own certificates 262 | in this folder, it is safe to delete this placeholder certificate. 263 | 264 | ## Default internal services 265 | The configuration template defines some default internal services, including: 266 | - Prometheus / Node Exporter / Postgres Exporter / cadvisor (metrics collection) 267 | - Grafana (dashboards / monitoring) 268 | - Private NPM Registry (`verdaccio`) 269 | - Private Docker Registry (`registry:2`) 270 | - Postgres 271 | 272 | **You'll need to modify the external hostnames in the yaml files to suit your environment** 273 | 274 | **Disabling a service:** simply delete it's yaml file from `applications-internal` 275 | 276 | ### Grafana 277 | At first start you will need to configure grafana with a connection to the prometheus datasource, and 278 | create / import some dashboards. 279 | 280 | Datasource configuration: 281 | - Type `prometheus` 282 | - URL: `http://monitoring_prometheus:9090` 283 | - Authentication disabled by default 284 | 285 | I recommend importing these dashboards to get started: 286 | - System: https://grafana.com/grafana/dashboards/1860-node-exporter-full/ 287 | - Docker: https://grafana.com/grafana/dashboards/16527-docker-monitoring/ 288 | - Postgres: https://grafana.com/grafana/dashboards/14114-postgres-overview/ 289 | - HAProxy: https://grafana.com/grafana/dashboards/12693-haproxy-2-full/ 290 | 291 | #### Postgres Exporter configuration 292 | You'll need to create a user for the exporter to use, and then configure it's credentials in `prometheus.yaml` 293 | ```sql 294 | create user postgres_exporter with login password ''; 295 | grant pg_monitor to postgres_exporter; 296 | grant connect on database postgres to postgres_exporter; 297 | ``` 298 | 299 | ### Docker Registry (`registry:2`) 300 | 301 | TODO: write documentation 302 | 303 | ### NPM Registry (`verdaccio`) 304 | 305 | TODO: write documentation 306 | 307 | 308 | ## Future / TODO 309 | - Get rid of the certificate concatenation, haproxy no longer requires this 310 | since version 2.2 🥳 311 | - find a way to allow issuing of SSL certs for private/internal services? 312 | - would probably have to go the DNS TXT record route, but AFAIK there is not 313 | a standardised API for this that can be reasonably expected to work across providers 😢 314 | - rework data directory structure by be split by configuration / data, eg: 315 | ```shell 316 | /data/conf/ 317 | /data/data/ 318 | ``` 319 | -------------------------------------------------------------------------------- /applications/reconcile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | source "${__dir}"/../config.sh 7 | 8 | pushd "$DATA_BASE_PATH" 9 | git fetch 10 | 11 | if [ $(git rev-parse HEAD) = $(git rev-parse @{u}) ]; then 12 | echo "Up to date - no changes to reconcile" 13 | popd 14 | else 15 | echo "Not up to date - running reconcile" 16 | git pull 17 | popd 18 | "$__dir"/watchdog-up.sh 19 | fi 20 | 21 | -------------------------------------------------------------------------------- /applications/watchdog-down.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | source "${__dir}"/../config.sh 7 | 8 | INTERNAL_APPLICATIONS="${DATA_BASE_PATH}/applications-internal/*.yaml" 9 | 10 | for APP in $INTERNAL_APPLICATIONS; do 11 | echo $APP 12 | APP_NAME=$(basename $APP ".yaml") 13 | docker compose --file "$APP" -p "$APP_NAME" down --remove-orphans 14 | done 15 | 16 | PUBLIC_APPLICATIONS="${DATA_BASE_PATH}/applications-public/*.yaml" 17 | 18 | for APP in $PUBLIC_APPLICATIONS; do 19 | echo $APP 20 | APP_NAME=$(basename $APP ".yaml") 21 | docker compose --file "$APP" -p "$APP_NAME" down --remove-orphans 22 | done 23 | -------------------------------------------------------------------------------- /applications/watchdog-up.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | source "${__dir}"/../config.sh 7 | 8 | INTERNAL_APPLICATIONS="${DATA_BASE_PATH}/applications-internal/*.yaml" 9 | 10 | for APP in $INTERNAL_APPLICATIONS; do 11 | echo $APP 12 | APP_NAME=$(basename $APP ".yaml") 13 | docker compose --file "$APP" -p $APP_NAME up -d --force-recreate --remove-orphans 14 | done 15 | 16 | PUBLIC_APPLICATIONS="${DATA_BASE_PATH}/applications-public/*.yaml" 17 | 18 | for APP in $PUBLIC_APPLICATIONS; do 19 | echo $APP 20 | APP_NAME=$(basename $APP ".yaml") 21 | docker compose --file "$APP" -p $APP_NAME up -d --force-recreate --remove-orphans 22 | done 23 | 24 | "$__dir"/../proxy/reload-haproxy-config.sh 25 | -------------------------------------------------------------------------------- /architecture.excalidraw: -------------------------------------------------------------------------------- 1 | { 2 | "type": "excalidraw", 3 | "version": 2, 4 | "source": "https://excalidraw.com", 5 | "elements": [ 6 | { 7 | "type": "rectangle", 8 | "version": 179, 9 | "versionNonce": 15495813, 10 | "index": "a6", 11 | "isDeleted": false, 12 | "id": "oku39lmcTHULJyZT4htNh", 13 | "fillStyle": "hachure", 14 | "strokeWidth": 1, 15 | "strokeStyle": "solid", 16 | "roughness": 1, 17 | "opacity": 100, 18 | "angle": 0, 19 | "x": 797.6666666666667, 20 | "y": 335.33333333333326, 21 | "strokeColor": "#000000", 22 | "backgroundColor": "transparent", 23 | "width": 96, 24 | "height": 72, 25 | "seed": 2022130728, 26 | "groupIds": [ 27 | "CtRJzMG4Ab-NNUBWGX3is" 28 | ], 29 | "frameId": null, 30 | "roundness": null, 31 | "boundElements": [ 32 | { 33 | "id": "NKUpIQQa0D5Zp7CKDBYWc", 34 | "type": "arrow" 35 | } 36 | ], 37 | "updated": 1726931135433, 38 | "link": null, 39 | "locked": false 40 | }, 41 | { 42 | "type": "text", 43 | "version": 185, 44 | "versionNonce": 1500505797, 45 | "index": "a7", 46 | "isDeleted": false, 47 | "id": "-dTtv9tfrIwGspxhYUH3u", 48 | "fillStyle": "hachure", 49 | "strokeWidth": 1, 50 | "strokeStyle": "solid", 51 | "roughness": 1, 52 | "opacity": 100, 53 | "angle": 0, 54 | "x": 806.6666666666667, 55 | "y": 358.33333333333326, 56 | "strokeColor": "#000000", 57 | "backgroundColor": "transparent", 58 | "width": 78, 59 | "height": 26, 60 | "seed": 698118744, 61 | "groupIds": [ 62 | "CtRJzMG4Ab-NNUBWGX3is" 63 | ], 64 | "frameId": null, 65 | "roundness": null, 66 | "boundElements": [ 67 | { 68 | "type": "arrow", 69 | "id": "Aol8k-jjQIKsY_hPqtc91" 70 | } 71 | ], 72 | "updated": 1726931061461, 73 | "link": null, 74 | "locked": false, 75 | "fontSize": 20, 76 | "fontFamily": 1, 77 | "text": "grafana", 78 | "textAlign": "center", 79 | "verticalAlign": "middle", 80 | "containerId": null, 81 | "originalText": "grafana", 82 | "autoResize": true, 83 | "lineHeight": 1.3 84 | }, 85 | { 86 | "type": "rectangle", 87 | "version": 364, 88 | "versionNonce": 531291013, 89 | "index": "aA", 90 | "isDeleted": false, 91 | "id": "Wyx8orBwD_XrrO4sIDQj0", 92 | "fillStyle": "hachure", 93 | "strokeWidth": 1, 94 | "strokeStyle": "solid", 95 | "roughness": 1, 96 | "opacity": 100, 97 | "angle": 0, 98 | "x": 388, 99 | "y": 173.66666666666669, 100 | "strokeColor": "#000000", 101 | "backgroundColor": "transparent", 102 | "width": 739.9999999999999, 103 | "height": 405.6666666666666, 104 | "seed": 1397681704, 105 | "groupIds": [], 106 | "frameId": null, 107 | "roundness": null, 108 | "boundElements": [], 109 | "updated": 1726930958638, 110 | "link": null, 111 | "locked": false 112 | }, 113 | { 114 | "type": "text", 115 | "version": 221, 116 | "versionNonce": 1837349675, 117 | "index": "aB", 118 | "isDeleted": false, 119 | "id": "C0YhLHuouTN8LXKgNNgKr", 120 | "fillStyle": "hachure", 121 | "strokeWidth": 1, 122 | "strokeStyle": "solid", 123 | "roughness": 1, 124 | "opacity": 100, 125 | "angle": 0, 126 | "x": 427.33333333333337, 127 | "y": 192, 128 | "strokeColor": "#000000", 129 | "backgroundColor": "transparent", 130 | "width": 171, 131 | "height": 26, 132 | "seed": 596940376, 133 | "groupIds": [], 134 | "frameId": null, 135 | "roundness": null, 136 | "boundElements": [], 137 | "updated": 1726930964555, 138 | "link": null, 139 | "locked": false, 140 | "fontSize": 20, 141 | "fontFamily": 1, 142 | "text": "Network: Internal", 143 | "textAlign": "left", 144 | "verticalAlign": "top", 145 | "containerId": null, 146 | "originalText": "Network: Internal", 147 | "autoResize": true, 148 | "lineHeight": 1.3 149 | }, 150 | { 151 | "type": "rectangle", 152 | "version": 145, 153 | "versionNonce": 139451013, 154 | "index": "aC", 155 | "isDeleted": false, 156 | "id": "U3QqFyMb3fG-ylqyM78qB", 157 | "fillStyle": "hachure", 158 | "strokeWidth": 1, 159 | "strokeStyle": "solid", 160 | "roughness": 1, 161 | "opacity": 100, 162 | "angle": 0, 163 | "x": 595, 164 | "y": 438, 165 | "strokeColor": "#000000", 166 | "backgroundColor": "transparent", 167 | "width": 205, 168 | "height": 69, 169 | "seed": 21825624, 170 | "groupIds": [], 171 | "frameId": null, 172 | "roundness": null, 173 | "boundElements": [ 174 | { 175 | "id": "qUS0ZcHjghPbQaCueV54I", 176 | "type": "arrow" 177 | } 178 | ], 179 | "updated": 1726931209544, 180 | "link": null, 181 | "locked": false 182 | }, 183 | { 184 | "type": "text", 185 | "version": 164, 186 | "versionNonce": 1159054693, 187 | "index": "aD", 188 | "isDeleted": false, 189 | "id": "ag1eMENCTVyzgZiMWbi7n", 190 | "fillStyle": "hachure", 191 | "strokeWidth": 1, 192 | "strokeStyle": "solid", 193 | "roughness": 1, 194 | "opacity": 100, 195 | "angle": 0, 196 | "x": 625, 197 | "y": 459, 198 | "strokeColor": "#000000", 199 | "backgroundColor": "transparent", 200 | "width": 156, 201 | "height": 26, 202 | "seed": 1923742504, 203 | "groupIds": [], 204 | "frameId": null, 205 | "roundness": null, 206 | "boundElements": [ 207 | { 208 | "type": "arrow", 209 | "id": "lW77lvnm58FfeT3-5-ykF" 210 | }, 211 | { 212 | "type": "arrow", 213 | "id": "BIYrlCEaeRWXYWxS4xs8l" 214 | }, 215 | { 216 | "type": "arrow", 217 | "id": "Aol8k-jjQIKsY_hPqtc91" 218 | } 219 | ], 220 | "updated": 1726930288291, 221 | "link": null, 222 | "locked": false, 223 | "fontSize": 20, 224 | "fontFamily": 1, 225 | "text": "haproxy internal", 226 | "textAlign": "left", 227 | "verticalAlign": "top", 228 | "containerId": null, 229 | "originalText": "haproxy internal", 230 | "autoResize": true, 231 | "lineHeight": 1.3 232 | }, 233 | { 234 | "type": "text", 235 | "version": 114, 236 | "versionNonce": 337604459, 237 | "index": "aE", 238 | "isDeleted": false, 239 | "id": "_MtLZSxU2CtZfsXyZSyAN", 240 | "fillStyle": "hachure", 241 | "strokeWidth": 1, 242 | "strokeStyle": "solid", 243 | "roughness": 1, 244 | "opacity": 100, 245 | "angle": 0, 246 | "x": 610, 247 | "y": 351, 248 | "strokeColor": "#000000", 249 | "backgroundColor": "transparent", 250 | "width": 41, 251 | "height": 26, 252 | "seed": 1880605016, 253 | "groupIds": [], 254 | "frameId": null, 255 | "roundness": null, 256 | "boundElements": [ 257 | { 258 | "type": "arrow", 259 | "id": "lW77lvnm58FfeT3-5-ykF" 260 | } 261 | ], 262 | "updated": 1726930288291, 263 | "link": null, 264 | "locked": false, 265 | "fontSize": 20, 266 | "fontFamily": 1, 267 | "text": "NPM", 268 | "textAlign": "left", 269 | "verticalAlign": "top", 270 | "containerId": null, 271 | "originalText": "NPM", 272 | "autoResize": true, 273 | "lineHeight": 1.3 274 | }, 275 | { 276 | "type": "text", 277 | "version": 144, 278 | "versionNonce": 1398919365, 279 | "index": "aF", 280 | "isDeleted": false, 281 | "id": "NTB0GJry9fw8wn3jvJuGs", 282 | "fillStyle": "hachure", 283 | "strokeWidth": 1, 284 | "strokeStyle": "solid", 285 | "roughness": 1, 286 | "opacity": 100, 287 | "angle": 0, 288 | "x": 697.5, 289 | "y": 339.5, 290 | "strokeColor": "#000000", 291 | "backgroundColor": "transparent", 292 | "width": 79, 293 | "height": 51, 294 | "seed": 811619672, 295 | "groupIds": [], 296 | "frameId": null, 297 | "roundness": null, 298 | "boundElements": [ 299 | { 300 | "type": "arrow", 301 | "id": "kwIukmr6EqByfGw18oVwD" 302 | } 303 | ], 304 | "updated": 1726930288291, 305 | "link": null, 306 | "locked": false, 307 | "fontSize": 20, 308 | "fontFamily": 1, 309 | "text": "Docker\nRegistry", 310 | "textAlign": "center", 311 | "verticalAlign": "middle", 312 | "containerId": null, 313 | "originalText": "Docker\nRegistry", 314 | "autoResize": true, 315 | "lineHeight": 1.275 316 | }, 317 | { 318 | "type": "rectangle", 319 | "version": 120, 320 | "versionNonce": 1760840203, 321 | "index": "aG", 322 | "isDeleted": false, 323 | "id": "lvElMVL7PgIuq5rTYxBaQ", 324 | "fillStyle": "hachure", 325 | "strokeWidth": 1, 326 | "strokeStyle": "solid", 327 | "roughness": 1, 328 | "opacity": 100, 329 | "angle": 0, 330 | "x": 593, 331 | "y": 339, 332 | "strokeColor": "#000000", 333 | "backgroundColor": "transparent", 334 | "width": 72, 335 | "height": 55, 336 | "seed": 1621951528, 337 | "groupIds": [], 338 | "frameId": null, 339 | "roundness": null, 340 | "boundElements": [], 341 | "updated": 1726930288291, 342 | "link": null, 343 | "locked": false 344 | }, 345 | { 346 | "type": "rectangle", 347 | "version": 124, 348 | "versionNonce": 114651173, 349 | "index": "aH", 350 | "isDeleted": false, 351 | "id": "gjLgbU1fvU3g5_dBIb6Or", 352 | "fillStyle": "hachure", 353 | "strokeWidth": 1, 354 | "strokeStyle": "solid", 355 | "roughness": 1, 356 | "opacity": 100, 357 | "angle": 0, 358 | "x": 683, 359 | "y": 337, 360 | "strokeColor": "#000000", 361 | "backgroundColor": "transparent", 362 | "width": 100, 363 | "height": 65, 364 | "seed": 1211879256, 365 | "groupIds": [], 366 | "frameId": null, 367 | "roundness": null, 368 | "boundElements": [], 369 | "updated": 1726930288291, 370 | "link": null, 371 | "locked": false 372 | }, 373 | { 374 | "type": "arrow", 375 | "version": 85, 376 | "versionNonce": 261410987, 377 | "index": "aI", 378 | "isDeleted": false, 379 | "id": "lW77lvnm58FfeT3-5-ykF", 380 | "fillStyle": "hachure", 381 | "strokeWidth": 1, 382 | "strokeStyle": "solid", 383 | "roughness": 1, 384 | "opacity": 100, 385 | "angle": 0, 386 | "x": 633, 387 | "y": 389.53125, 388 | "strokeColor": "#000000", 389 | "backgroundColor": "transparent", 390 | "width": 2, 391 | "height": 61, 392 | "seed": 1161152600, 393 | "groupIds": [], 394 | "frameId": null, 395 | "roundness": { 396 | "type": 2 397 | }, 398 | "boundElements": [], 399 | "updated": 1726930288291, 400 | "link": null, 401 | "locked": false, 402 | "startBinding": { 403 | "elementId": "_MtLZSxU2CtZfsXyZSyAN", 404 | "focus": -0.15946925186055622, 405 | "gap": 12.53125, 406 | "fixedPoint": null 407 | }, 408 | "endBinding": { 409 | "elementId": "ag1eMENCTVyzgZiMWbi7n", 410 | "focus": -0.9270354306020069, 411 | "gap": 8.46875, 412 | "fixedPoint": null 413 | }, 414 | "lastCommittedPoint": null, 415 | "startArrowhead": null, 416 | "endArrowhead": "arrow", 417 | "points": [ 418 | [ 419 | 0, 420 | 0 421 | ], 422 | [ 423 | -2, 424 | 61 425 | ] 426 | ] 427 | }, 428 | { 429 | "type": "arrow", 430 | "version": 62, 431 | "versionNonce": 2103321477, 432 | "index": "aJ", 433 | "isDeleted": false, 434 | "id": "kwIukmr6EqByfGw18oVwD", 435 | "fillStyle": "hachure", 436 | "strokeWidth": 1, 437 | "strokeStyle": "solid", 438 | "roughness": 1, 439 | "opacity": 100, 440 | "angle": 0, 441 | "x": 739, 442 | "y": 395.53125, 443 | "strokeColor": "#000000", 444 | "backgroundColor": "transparent", 445 | "width": 0, 446 | "height": 47, 447 | "seed": 791015512, 448 | "groupIds": [], 449 | "frameId": null, 450 | "roundness": { 451 | "type": 2 452 | }, 453 | "boundElements": [], 454 | "updated": 1726930288291, 455 | "link": null, 456 | "locked": false, 457 | "startBinding": { 458 | "elementId": "NTB0GJry9fw8wn3jvJuGs", 459 | "focus": -0.05063291139240506, 460 | "gap": 5.03125, 461 | "fixedPoint": null 462 | }, 463 | "endBinding": null, 464 | "lastCommittedPoint": null, 465 | "startArrowhead": null, 466 | "endArrowhead": "arrow", 467 | "points": [ 468 | [ 469 | 0, 470 | 0 471 | ], 472 | [ 473 | 0, 474 | 47 475 | ] 476 | ] 477 | }, 478 | { 479 | "type": "rectangle", 480 | "version": 89, 481 | "versionNonce": 699106123, 482 | "index": "aK", 483 | "isDeleted": false, 484 | "id": "XPwqRUXYrfF_oJXPJPRkq", 485 | "fillStyle": "hachure", 486 | "strokeWidth": 1, 487 | "strokeStyle": "solid", 488 | "roughness": 1, 489 | "opacity": 100, 490 | "angle": 0, 491 | "x": 573, 492 | "y": 595.53125, 493 | "strokeColor": "#000000", 494 | "backgroundColor": "transparent", 495 | "width": 261.00000000000006, 496 | "height": 73, 497 | "seed": 1101022040, 498 | "groupIds": [], 499 | "frameId": null, 500 | "roundness": null, 501 | "boundElements": [], 502 | "updated": 1726930288291, 503 | "link": null, 504 | "locked": false 505 | }, 506 | { 507 | "type": "text", 508 | "version": 104, 509 | "versionNonce": 993618661, 510 | "index": "aL", 511 | "isDeleted": false, 512 | "id": "RXBWyy3bdHw7wyB1Xv_Zp", 513 | "fillStyle": "hachure", 514 | "strokeWidth": 1, 515 | "strokeStyle": "solid", 516 | "roughness": 1, 517 | "opacity": 100, 518 | "angle": 0, 519 | "x": 612, 520 | "y": 623.53125, 521 | "strokeColor": "#000000", 522 | "backgroundColor": "transparent", 523 | "width": 197, 524 | "height": 26, 525 | "seed": 372674856, 526 | "groupIds": [], 527 | "frameId": null, 528 | "roundness": null, 529 | "boundElements": [], 530 | "updated": 1726930288291, 531 | "link": null, 532 | "locked": false, 533 | "fontSize": 20, 534 | "fontFamily": 1, 535 | "text": ":::80", 536 | "textAlign": "left", 537 | "verticalAlign": "top", 538 | "containerId": null, 539 | "originalText": ":::80", 540 | "autoResize": true, 541 | "lineHeight": 1.3 542 | }, 543 | { 544 | "type": "arrow", 545 | "version": 67, 546 | "versionNonce": 1066590699, 547 | "index": "aM", 548 | "isDeleted": false, 549 | "id": "BIYrlCEaeRWXYWxS4xs8l", 550 | "fillStyle": "hachure", 551 | "strokeWidth": 1, 552 | "strokeStyle": "solid", 553 | "roughness": 1, 554 | "opacity": 100, 555 | "angle": 0, 556 | "x": 693, 557 | "y": 490.53125, 558 | "strokeColor": "#000000", 559 | "backgroundColor": "transparent", 560 | "width": 2, 561 | "height": 113, 562 | "seed": 850004824, 563 | "groupIds": [], 564 | "frameId": null, 565 | "roundness": { 566 | "type": 2 567 | }, 568 | "boundElements": [], 569 | "updated": 1726930288291, 570 | "link": null, 571 | "locked": false, 572 | "startBinding": { 573 | "elementId": "ag1eMENCTVyzgZiMWbi7n", 574 | "focus": 0.12363546380090498, 575 | "gap": 5.53125, 576 | "fixedPoint": null 577 | }, 578 | "endBinding": null, 579 | "lastCommittedPoint": null, 580 | "startArrowhead": null, 581 | "endArrowhead": "arrow", 582 | "points": [ 583 | [ 584 | 0, 585 | 0 586 | ], 587 | [ 588 | -2, 589 | 113 590 | ] 591 | ] 592 | }, 593 | { 594 | "type": "arrow", 595 | "version": 119, 596 | "versionNonce": 246466085, 597 | "index": "aN", 598 | "isDeleted": false, 599 | "id": "Aol8k-jjQIKsY_hPqtc91", 600 | "fillStyle": "hachure", 601 | "strokeWidth": 1, 602 | "strokeStyle": "solid", 603 | "roughness": 1, 604 | "opacity": 100, 605 | "angle": 0, 606 | "x": 838.0666366638429, 607 | "y": 391.33333333333326, 608 | "strokeColor": "#000000", 609 | "backgroundColor": "transparent", 610 | "width": 43.06663666384293, 611 | "height": 91.19791666666674, 612 | "seed": 795534376, 613 | "groupIds": [], 614 | "frameId": null, 615 | "roundness": { 616 | "type": 2 617 | }, 618 | "boundElements": [], 619 | "updated": 1726931061461, 620 | "link": null, 621 | "locked": false, 622 | "startBinding": { 623 | "elementId": "-dTtv9tfrIwGspxhYUH3u", 624 | "focus": -0.040865384615384616, 625 | "gap": 7, 626 | "fixedPoint": null 627 | }, 628 | "endBinding": { 629 | "elementId": "ag1eMENCTVyzgZiMWbi7n", 630 | "focus": 0.8100961538461539, 631 | "gap": 14, 632 | "fixedPoint": null 633 | }, 634 | "lastCommittedPoint": null, 635 | "startArrowhead": null, 636 | "endArrowhead": "arrow", 637 | "points": [ 638 | [ 639 | 0, 640 | 0 641 | ], 642 | [ 643 | -43.06663666384293, 644 | 91.19791666666674 645 | ] 646 | ] 647 | }, 648 | { 649 | "type": "text", 650 | "version": 57, 651 | "versionNonce": 1783983243, 652 | "index": "aO", 653 | "isDeleted": false, 654 | "id": "M_MYSQyBEHgi9owCbqUI1", 655 | "fillStyle": "hachure", 656 | "strokeWidth": 1, 657 | "strokeStyle": "solid", 658 | "roughness": 1, 659 | "opacity": 100, 660 | "angle": 0, 661 | "x": 1228, 662 | "y": 184.53125, 663 | "strokeColor": "#000000", 664 | "backgroundColor": "transparent", 665 | "width": 129, 666 | "height": 26, 667 | "seed": 2137621336, 668 | "groupIds": [], 669 | "frameId": null, 670 | "roundness": null, 671 | "boundElements": [], 672 | "updated": 1726930288291, 673 | "link": null, 674 | "locked": false, 675 | "fontSize": 20, 676 | "fontFamily": 1, 677 | "text": "Network Main", 678 | "textAlign": "left", 679 | "verticalAlign": "top", 680 | "containerId": null, 681 | "originalText": "Network Main", 682 | "autoResize": true, 683 | "lineHeight": 1.3 684 | }, 685 | { 686 | "type": "rectangle", 687 | "version": 92, 688 | "versionNonce": 560838053, 689 | "index": "aP", 690 | "isDeleted": false, 691 | "id": "PDuC2sDgEeav60n3pzY8w", 692 | "fillStyle": "hachure", 693 | "strokeWidth": 1, 694 | "strokeStyle": "solid", 695 | "roughness": 1, 696 | "opacity": 100, 697 | "angle": 0, 698 | "x": 1159, 699 | "y": 170.53125, 700 | "strokeColor": "#000000", 701 | "backgroundColor": "transparent", 702 | "width": 438.0000000000001, 703 | "height": 301, 704 | "seed": 1305057320, 705 | "groupIds": [], 706 | "frameId": null, 707 | "roundness": null, 708 | "boundElements": [], 709 | "updated": 1726930288291, 710 | "link": null, 711 | "locked": false 712 | }, 713 | { 714 | "type": "rectangle", 715 | "version": 275, 716 | "versionNonce": 1034789675, 717 | "index": "aQ", 718 | "isDeleted": false, 719 | "id": "HBNWbnCdRc-afDfzZGEgi", 720 | "fillStyle": "hachure", 721 | "strokeWidth": 1, 722 | "strokeStyle": "solid", 723 | "roughness": 1, 724 | "opacity": 100, 725 | "angle": 0, 726 | "x": 1175.5, 727 | "y": 358.03125, 728 | "strokeColor": "#000000", 729 | "backgroundColor": "transparent", 730 | "width": 143, 731 | "height": 69, 732 | "seed": 1219505448, 733 | "groupIds": [ 734 | "IopXauWAD3cnLqxKCvU0N" 735 | ], 736 | "frameId": null, 737 | "roundness": null, 738 | "boundElements": [ 739 | { 740 | "type": "arrow", 741 | "id": "U-j9K3uEf0_LPzlwtwIiW" 742 | } 743 | ], 744 | "updated": 1726930288291, 745 | "link": null, 746 | "locked": false 747 | }, 748 | { 749 | "type": "text", 750 | "version": 282, 751 | "versionNonce": 1467135525, 752 | "index": "aR", 753 | "isDeleted": false, 754 | "id": "3osZzCbgQyZWgyhYkopbp", 755 | "fillStyle": "hachure", 756 | "strokeWidth": 1, 757 | "strokeStyle": "solid", 758 | "roughness": 1, 759 | "opacity": 100, 760 | "angle": 0, 761 | "x": 1181.5, 762 | "y": 380.03125, 763 | "strokeColor": "#000000", 764 | "backgroundColor": "transparent", 765 | "width": 134, 766 | "height": 26, 767 | "seed": 2073205080, 768 | "groupIds": [ 769 | "IopXauWAD3cnLqxKCvU0N" 770 | ], 771 | "frameId": null, 772 | "roundness": null, 773 | "boundElements": [ 774 | { 775 | "type": "arrow", 776 | "id": "lW77lvnm58FfeT3-5-ykF" 777 | }, 778 | { 779 | "type": "arrow", 780 | "id": "BIYrlCEaeRWXYWxS4xs8l" 781 | }, 782 | { 783 | "type": "arrow", 784 | "id": "Aol8k-jjQIKsY_hPqtc91" 785 | }, 786 | { 787 | "type": "arrow", 788 | "id": "h6iF5RRgvBek8lTWidTXF" 789 | }, 790 | { 791 | "type": "arrow", 792 | "id": "Dt21UqvhrtYOthrKavJcF" 793 | }, 794 | { 795 | "type": "arrow", 796 | "id": "XLceGnj_ztNDOyPs1ZSiL" 797 | } 798 | ], 799 | "updated": 1726930936528, 800 | "link": null, 801 | "locked": false, 802 | "fontSize": 20, 803 | "fontFamily": 1, 804 | "text": "haproxy public", 805 | "textAlign": "left", 806 | "verticalAlign": "top", 807 | "containerId": null, 808 | "originalText": "haproxy public", 809 | "autoResize": true, 810 | "lineHeight": 1.3 811 | }, 812 | { 813 | "type": "text", 814 | "version": 39, 815 | "versionNonce": 1912997323, 816 | "index": "aS", 817 | "isDeleted": false, 818 | "id": "2tnA6doKWXbyT0UB7aiLz", 819 | "fillStyle": "hachure", 820 | "strokeWidth": 1, 821 | "strokeStyle": "solid", 822 | "roughness": 1, 823 | "opacity": 100, 824 | "angle": 0, 825 | "x": 1196, 826 | "y": 262.53125, 827 | "strokeColor": "#000000", 828 | "backgroundColor": "transparent", 829 | "width": 100, 830 | "height": 26, 831 | "seed": 1541506136, 832 | "groupIds": [], 833 | "frameId": null, 834 | "roundness": null, 835 | "boundElements": [], 836 | "updated": 1726930288291, 837 | "link": null, 838 | "locked": false, 839 | "fontSize": 20, 840 | "fontFamily": 1, 841 | "text": "Your App 1", 842 | "textAlign": "left", 843 | "verticalAlign": "top", 844 | "containerId": null, 845 | "originalText": "Your App 1", 846 | "autoResize": true, 847 | "lineHeight": 1.3 848 | }, 849 | { 850 | "type": "rectangle", 851 | "version": 46, 852 | "versionNonce": 496914533, 853 | "index": "aT", 854 | "isDeleted": false, 855 | "id": "KofZHUuZx0SoIKM1oYIgm", 856 | "fillStyle": "hachure", 857 | "strokeWidth": 1, 858 | "strokeStyle": "solid", 859 | "roughness": 1, 860 | "opacity": 100, 861 | "angle": 0, 862 | "x": 1177, 863 | "y": 251.53125, 864 | "strokeColor": "#000000", 865 | "backgroundColor": "transparent", 866 | "width": 142, 867 | "height": 52, 868 | "seed": 1105074728, 869 | "groupIds": [], 870 | "frameId": null, 871 | "roundness": null, 872 | "boundElements": [ 873 | { 874 | "type": "arrow", 875 | "id": "h6iF5RRgvBek8lTWidTXF" 876 | } 877 | ], 878 | "updated": 1726930288291, 879 | "link": null, 880 | "locked": false 881 | }, 882 | { 883 | "type": "text", 884 | "version": 88, 885 | "versionNonce": 1316557931, 886 | "index": "aU", 887 | "isDeleted": false, 888 | "id": "Ivoj7iab39w2zdHX85qOk", 889 | "fillStyle": "hachure", 890 | "strokeWidth": 1, 891 | "strokeStyle": "solid", 892 | "roughness": 1, 893 | "opacity": 100, 894 | "angle": 0, 895 | "x": 1353, 896 | "y": 266.53125, 897 | "strokeColor": "#000000", 898 | "backgroundColor": "transparent", 899 | "width": 108, 900 | "height": 26, 901 | "seed": 1500243240, 902 | "groupIds": [ 903 | "h-erHJuEFgfi2zv4_xXE4" 904 | ], 905 | "frameId": null, 906 | "roundness": null, 907 | "boundElements": [ 908 | { 909 | "type": "arrow", 910 | "id": "Dt21UqvhrtYOthrKavJcF" 911 | } 912 | ], 913 | "updated": 1726930288291, 914 | "link": null, 915 | "locked": false, 916 | "fontSize": 20, 917 | "fontFamily": 1, 918 | "text": "Your App 2", 919 | "textAlign": "left", 920 | "verticalAlign": "top", 921 | "containerId": null, 922 | "originalText": "Your App 2", 923 | "autoResize": true, 924 | "lineHeight": 1.3 925 | }, 926 | { 927 | "type": "rectangle", 928 | "version": 95, 929 | "versionNonce": 1084292037, 930 | "index": "aV", 931 | "isDeleted": false, 932 | "id": "zvlT5sxhcae9ZJua3Tw1A", 933 | "fillStyle": "hachure", 934 | "strokeWidth": 1, 935 | "strokeStyle": "solid", 936 | "roughness": 1, 937 | "opacity": 100, 938 | "angle": 0, 939 | "x": 1331, 940 | "y": 244.53125, 941 | "strokeColor": "#000000", 942 | "backgroundColor": "transparent", 943 | "width": 142, 944 | "height": 57, 945 | "seed": 1605097816, 946 | "groupIds": [ 947 | "h-erHJuEFgfi2zv4_xXE4" 948 | ], 949 | "frameId": null, 950 | "roundness": null, 951 | "boundElements": [], 952 | "updated": 1726930288291, 953 | "link": null, 954 | "locked": false 955 | }, 956 | { 957 | "type": "text", 958 | "version": 80, 959 | "versionNonce": 1389448971, 960 | "index": "aW", 961 | "isDeleted": false, 962 | "id": "TkdmwodezkbT5ddS3HsU2", 963 | "fillStyle": "hachure", 964 | "strokeWidth": 1, 965 | "strokeStyle": "solid", 966 | "roughness": 1, 967 | "opacity": 100, 968 | "angle": 0, 969 | "x": 1388, 970 | "y": 374.53125, 971 | "strokeColor": "#000000", 972 | "backgroundColor": "transparent", 973 | "width": 73, 974 | "height": 26, 975 | "seed": 91689768, 976 | "groupIds": [ 977 | "_BgAPsiC049U7Jb4CrPvz" 978 | ], 979 | "frameId": null, 980 | "roundness": null, 981 | "boundElements": [], 982 | "updated": 1726930288291, 983 | "link": null, 984 | "locked": false, 985 | "fontSize": 20, 986 | "fontFamily": 1, 987 | "text": "certbot", 988 | "textAlign": "left", 989 | "verticalAlign": "top", 990 | "containerId": null, 991 | "originalText": "certbot", 992 | "autoResize": true, 993 | "lineHeight": 1.3 994 | }, 995 | { 996 | "type": "rectangle", 997 | "version": 95, 998 | "versionNonce": 2029468453, 999 | "index": "aX", 1000 | "isDeleted": false, 1001 | "id": "uWaihXZDwj7PhfceInAEa", 1002 | "fillStyle": "hachure", 1003 | "strokeWidth": 1, 1004 | "strokeStyle": "dashed", 1005 | "roughness": 1, 1006 | "opacity": 100, 1007 | "angle": 0, 1008 | "x": 1359, 1009 | "y": 367.53125, 1010 | "strokeColor": "#000000", 1011 | "backgroundColor": "transparent", 1012 | "width": 136, 1013 | "height": 49, 1014 | "seed": 331706664, 1015 | "groupIds": [ 1016 | "_BgAPsiC049U7Jb4CrPvz" 1017 | ], 1018 | "frameId": null, 1019 | "roundness": null, 1020 | "boundElements": [], 1021 | "updated": 1726930288292, 1022 | "link": null, 1023 | "locked": false 1024 | }, 1025 | { 1026 | "type": "arrow", 1027 | "version": 82, 1028 | "versionNonce": 562640299, 1029 | "index": "aY", 1030 | "isDeleted": false, 1031 | "id": "h6iF5RRgvBek8lTWidTXF", 1032 | "fillStyle": "hachure", 1033 | "strokeWidth": 1, 1034 | "strokeStyle": "solid", 1035 | "roughness": 1, 1036 | "opacity": 100, 1037 | "angle": 0, 1038 | "x": 1240, 1039 | "y": 303.53125, 1040 | "strokeColor": "#000000", 1041 | "backgroundColor": "transparent", 1042 | "width": 1, 1043 | "height": 62, 1044 | "seed": 543946072, 1045 | "groupIds": [], 1046 | "frameId": null, 1047 | "roundness": { 1048 | "type": 2 1049 | }, 1050 | "boundElements": [], 1051 | "updated": 1726930288292, 1052 | "link": null, 1053 | "locked": false, 1054 | "startBinding": { 1055 | "elementId": "KofZHUuZx0SoIKM1oYIgm", 1056 | "focus": 0.10614272809394762, 1057 | "gap": 1, 1058 | "fixedPoint": null 1059 | }, 1060 | "endBinding": { 1061 | "elementId": "3osZzCbgQyZWgyhYkopbp", 1062 | "focus": -0.14794816414686826, 1063 | "gap": 14.5, 1064 | "fixedPoint": null 1065 | }, 1066 | "lastCommittedPoint": null, 1067 | "startArrowhead": null, 1068 | "endArrowhead": "arrow", 1069 | "points": [ 1070 | [ 1071 | 0, 1072 | 0 1073 | ], 1074 | [ 1075 | -1, 1076 | 62 1077 | ] 1078 | ] 1079 | }, 1080 | { 1081 | "type": "arrow", 1082 | "version": 86, 1083 | "versionNonce": 2131660421, 1084 | "index": "aZ", 1085 | "isDeleted": false, 1086 | "id": "Dt21UqvhrtYOthrKavJcF", 1087 | "fillStyle": "hachure", 1088 | "strokeWidth": 1, 1089 | "strokeStyle": "solid", 1090 | "roughness": 1, 1091 | "opacity": 100, 1092 | "angle": 0, 1093 | "x": 1371, 1094 | "y": 300.53125, 1095 | "strokeColor": "#000000", 1096 | "backgroundColor": "transparent", 1097 | "width": 65, 1098 | "height": 67, 1099 | "seed": 600696920, 1100 | "groupIds": [], 1101 | "frameId": null, 1102 | "roundness": { 1103 | "type": 2 1104 | }, 1105 | "boundElements": [], 1106 | "updated": 1726930288292, 1107 | "link": null, 1108 | "locked": false, 1109 | "startBinding": { 1110 | "elementId": "Ivoj7iab39w2zdHX85qOk", 1111 | "focus": 0.2345955635222944, 1112 | "gap": 8, 1113 | "fixedPoint": null 1114 | }, 1115 | "endBinding": { 1116 | "elementId": "3osZzCbgQyZWgyhYkopbp", 1117 | "focus": 0.41151106111736035, 1118 | "gap": 12.5, 1119 | "fixedPoint": null 1120 | }, 1121 | "lastCommittedPoint": null, 1122 | "startArrowhead": null, 1123 | "endArrowhead": "arrow", 1124 | "points": [ 1125 | [ 1126 | 0, 1127 | 0 1128 | ], 1129 | [ 1130 | -65, 1131 | 67 1132 | ] 1133 | ] 1134 | }, 1135 | { 1136 | "type": "arrow", 1137 | "version": 67, 1138 | "versionNonce": 701188171, 1139 | "index": "aa", 1140 | "isDeleted": false, 1141 | "id": "XLceGnj_ztNDOyPs1ZSiL", 1142 | "fillStyle": "hachure", 1143 | "strokeWidth": 1, 1144 | "strokeStyle": "solid", 1145 | "roughness": 1, 1146 | "opacity": 100, 1147 | "angle": 0, 1148 | "x": 1361, 1149 | "y": 388.53125, 1150 | "strokeColor": "#000000", 1151 | "backgroundColor": "transparent", 1152 | "width": 48, 1153 | "height": 9, 1154 | "seed": 455755096, 1155 | "groupIds": [], 1156 | "frameId": null, 1157 | "roundness": { 1158 | "type": 2 1159 | }, 1160 | "boundElements": [], 1161 | "updated": 1726930288292, 1162 | "link": null, 1163 | "locked": false, 1164 | "startBinding": null, 1165 | "endBinding": { 1166 | "elementId": "3osZzCbgQyZWgyhYkopbp", 1167 | "focus": -1.0012224938875305, 1168 | "gap": 1, 1169 | "fixedPoint": null 1170 | }, 1171 | "lastCommittedPoint": null, 1172 | "startArrowhead": null, 1173 | "endArrowhead": "arrow", 1174 | "points": [ 1175 | [ 1176 | 0, 1177 | 0 1178 | ], 1179 | [ 1180 | -48, 1181 | -9 1182 | ] 1183 | ] 1184 | }, 1185 | { 1186 | "type": "rectangle", 1187 | "version": 129, 1188 | "versionNonce": 564426213, 1189 | "index": "ab", 1190 | "isDeleted": false, 1191 | "id": "07-PY8TbxglrbON9dytu0", 1192 | "fillStyle": "hachure", 1193 | "strokeWidth": 1, 1194 | "strokeStyle": "solid", 1195 | "roughness": 1, 1196 | "opacity": 100, 1197 | "angle": 0, 1198 | "x": 1163, 1199 | "y": 512.03125, 1200 | "strokeColor": "#000000", 1201 | "backgroundColor": "transparent", 1202 | "width": 433.99999999999994, 1203 | "height": 73, 1204 | "seed": 1293943128, 1205 | "groupIds": [], 1206 | "frameId": null, 1207 | "roundness": null, 1208 | "boundElements": [], 1209 | "updated": 1726930288292, 1210 | "link": null, 1211 | "locked": false 1212 | }, 1213 | { 1214 | "type": "text", 1215 | "version": 114, 1216 | "versionNonce": 292685547, 1217 | "index": "ac", 1218 | "isDeleted": false, 1219 | "id": "HQiw53rzu9sQMDPsU4Zo-", 1220 | "fillStyle": "hachure", 1221 | "strokeWidth": 1, 1222 | "strokeStyle": "solid", 1223 | "roughness": 1, 1224 | "opacity": 100, 1225 | "angle": 0, 1226 | "x": 1199, 1227 | "y": 537.03125, 1228 | "strokeColor": "#000000", 1229 | "backgroundColor": "transparent", 1230 | "width": 388, 1231 | "height": 26, 1232 | "seed": 366829608, 1233 | "groupIds": [], 1234 | "frameId": null, 1235 | "roundness": null, 1236 | "boundElements": [], 1237 | "updated": 1726930288292, 1238 | "link": null, 1239 | "locked": false, 1240 | "fontSize": 20, 1241 | "fontFamily": 1, 1242 | "text": ":::80 :::443", 1243 | "textAlign": "left", 1244 | "verticalAlign": "top", 1245 | "containerId": null, 1246 | "originalText": ":::80 :::443", 1247 | "autoResize": true, 1248 | "lineHeight": 1.3 1249 | }, 1250 | { 1251 | "type": "arrow", 1252 | "version": 64, 1253 | "versionNonce": 2076602693, 1254 | "index": "ad", 1255 | "isDeleted": false, 1256 | "id": "U-j9K3uEf0_LPzlwtwIiW", 1257 | "fillStyle": "hachure", 1258 | "strokeWidth": 1, 1259 | "strokeStyle": "solid", 1260 | "roughness": 1, 1261 | "opacity": 100, 1262 | "angle": 0, 1263 | "x": 1246, 1264 | "y": 427.53125, 1265 | "strokeColor": "#000000", 1266 | "backgroundColor": "transparent", 1267 | "width": 1, 1268 | "height": 90, 1269 | "seed": 508428072, 1270 | "groupIds": [], 1271 | "frameId": null, 1272 | "roundness": { 1273 | "type": 2 1274 | }, 1275 | "boundElements": [], 1276 | "updated": 1726930288292, 1277 | "link": null, 1278 | "locked": false, 1279 | "startBinding": { 1280 | "elementId": "HBNWbnCdRc-afDfzZGEgi", 1281 | "focus": 0.019321431331633047, 1282 | "gap": 1, 1283 | "fixedPoint": null 1284 | }, 1285 | "endBinding": null, 1286 | "lastCommittedPoint": null, 1287 | "startArrowhead": null, 1288 | "endArrowhead": "arrow", 1289 | "points": [ 1290 | [ 1291 | 0, 1292 | 0 1293 | ], 1294 | [ 1295 | 1, 1296 | 90 1297 | ] 1298 | ] 1299 | }, 1300 | { 1301 | "type": "rectangle", 1302 | "version": 134, 1303 | "versionNonce": 1012052229, 1304 | "index": "ag", 1305 | "isDeleted": false, 1306 | "id": "wfKiuOjQscB6QZUl18tGT", 1307 | "fillStyle": "hachure", 1308 | "strokeWidth": 1, 1309 | "strokeStyle": "dashed", 1310 | "roughness": 1, 1311 | "opacity": 100, 1312 | "angle": 0, 1313 | "x": 335.6666666666667, 1314 | "y": 145.53125, 1315 | "strokeColor": "#000000", 1316 | "backgroundColor": "transparent", 1317 | "width": 1325, 1318 | "height": 553, 1319 | "seed": 1100062040, 1320 | "groupIds": [], 1321 | "frameId": null, 1322 | "roundness": null, 1323 | "boundElements": [], 1324 | "updated": 1726931222337, 1325 | "link": null, 1326 | "locked": false 1327 | }, 1328 | { 1329 | "type": "rectangle", 1330 | "version": 216, 1331 | "versionNonce": 1434798021, 1332 | "index": "ah", 1333 | "isDeleted": false, 1334 | "id": "c54H7yuX8DwMbI-AoBEau", 1335 | "fillStyle": "hachure", 1336 | "strokeWidth": 1, 1337 | "strokeStyle": "solid", 1338 | "roughness": 1, 1339 | "opacity": 100, 1340 | "angle": 0, 1341 | "x": 400, 1342 | "y": 336.53125, 1343 | "strokeColor": "#000000", 1344 | "backgroundColor": "transparent", 1345 | "width": 175.99999999999997, 1346 | "height": 73, 1347 | "seed": 251464011, 1348 | "groupIds": [], 1349 | "frameId": null, 1350 | "roundness": null, 1351 | "boundElements": [ 1352 | { 1353 | "id": "qUS0ZcHjghPbQaCueV54I", 1354 | "type": "arrow" 1355 | } 1356 | ], 1357 | "updated": 1726931209544, 1358 | "link": null, 1359 | "locked": false 1360 | }, 1361 | { 1362 | "type": "text", 1363 | "version": 144, 1364 | "versionNonce": 1908455691, 1365 | "index": "ai", 1366 | "isDeleted": false, 1367 | "id": "UDq9w81EJ6dkqFJVcaPRf", 1368 | "fillStyle": "hachure", 1369 | "strokeWidth": 1, 1370 | "strokeStyle": "solid", 1371 | "roughness": 1, 1372 | "opacity": 100, 1373 | "angle": 0, 1374 | "x": 401.66666666666674, 1375 | "y": 355.86458333333337, 1376 | "strokeColor": "#000000", 1377 | "backgroundColor": "transparent", 1378 | "width": 174.25, 1379 | "height": 25.5, 1380 | "seed": 786688965, 1381 | "groupIds": [], 1382 | "frameId": null, 1383 | "roundness": null, 1384 | "boundElements": [], 1385 | "updated": 1726931203468, 1386 | "link": null, 1387 | "locked": false, 1388 | "fontSize": 20, 1389 | "fontFamily": 1, 1390 | "text": "Your private apps", 1391 | "textAlign": "left", 1392 | "verticalAlign": "top", 1393 | "containerId": null, 1394 | "originalText": "Your private apps", 1395 | "autoResize": true, 1396 | "lineHeight": 1.275 1397 | }, 1398 | { 1399 | "type": "rectangle", 1400 | "version": 518, 1401 | "versionNonce": 2075761835, 1402 | "index": "ak", 1403 | "isDeleted": false, 1404 | "id": "8Lbij8u2KRYsdYCUeLw00", 1405 | "fillStyle": "hachure", 1406 | "strokeWidth": 1, 1407 | "strokeStyle": "solid", 1408 | "roughness": 1, 1409 | "opacity": 100, 1410 | "angle": 0, 1411 | "x": 935.5606647305585, 1412 | "y": 280.7096786671356, 1413 | "strokeColor": "#000000", 1414 | "backgroundColor": "transparent", 1415 | "width": 119.7909002904162, 1416 | "height": 57.17650855114549, 1417 | "seed": 33909739, 1418 | "groupIds": [], 1419 | "frameId": null, 1420 | "roundness": null, 1421 | "boundElements": [], 1422 | "updated": 1726931186128, 1423 | "link": null, 1424 | "locked": false 1425 | }, 1426 | { 1427 | "type": "text", 1428 | "version": 556, 1429 | "versionNonce": 1150845701, 1430 | "index": "al", 1431 | "isDeleted": false, 1432 | "id": "L5dBGw_FGtYAsMRE7G82f", 1433 | "fillStyle": "hachure", 1434 | "strokeWidth": 1, 1435 | "strokeStyle": "solid", 1436 | "roughness": 1, 1437 | "opacity": 100, 1438 | "angle": 0, 1439 | "x": 943.2083333333336, 1440 | "y": 293.64720690205013, 1441 | "strokeColor": "#000000", 1442 | "backgroundColor": "transparent", 1443 | "width": 102.93749999999966, 1444 | "height": 24.872410454985395, 1445 | "seed": 300332683, 1446 | "groupIds": [], 1447 | "frameId": null, 1448 | "roundness": null, 1449 | "boundElements": [ 1450 | { 1451 | "id": "NKUpIQQa0D5Zp7CKDBYWc", 1452 | "type": "arrow" 1453 | }, 1454 | { 1455 | "id": "WhKU-kPA4EaGZZVcrdHDO", 1456 | "type": "arrow" 1457 | }, 1458 | { 1459 | "id": "SnGpzbHR6ss5fbsrisSJ4", 1460 | "type": "arrow" 1461 | }, 1462 | { 1463 | "id": "EKEgQCqeiZMaPkHOoXklp", 1464 | "type": "arrow" 1465 | } 1466 | ], 1467 | "updated": 1726931189943, 1468 | "link": null, 1469 | "locked": false, 1470 | "fontSize": 19.13262342691184, 1471 | "fontFamily": 1, 1472 | "text": "prometheus", 1473 | "textAlign": "center", 1474 | "verticalAlign": "middle", 1475 | "containerId": null, 1476 | "originalText": "prometheus", 1477 | "autoResize": true, 1478 | "lineHeight": 1.3 1479 | }, 1480 | { 1481 | "type": "rectangle", 1482 | "version": 355, 1483 | "versionNonce": 955093669, 1484 | "index": "am", 1485 | "isDeleted": false, 1486 | "id": "ILy8dUQ6hQqrMWWA4td_n", 1487 | "fillStyle": "hachure", 1488 | "strokeWidth": 1, 1489 | "strokeStyle": "solid", 1490 | "roughness": 1, 1491 | "opacity": 100, 1492 | "angle": 0, 1493 | "x": 720.9378831881253, 1494 | "y": 193.37634533380225, 1495 | "strokeColor": "#000000", 1496 | "backgroundColor": "transparent", 1497 | "width": 143.79090029041606, 1498 | "height": 53.17650855114548, 1499 | "seed": 1026930341, 1500 | "groupIds": [], 1501 | "frameId": null, 1502 | "roundness": null, 1503 | "boundElements": [], 1504 | "updated": 1726931109831, 1505 | "link": null, 1506 | "locked": false 1507 | }, 1508 | { 1509 | "type": "text", 1510 | "version": 410, 1511 | "versionNonce": 281210853, 1512 | "index": "an", 1513 | "isDeleted": false, 1514 | "id": "4fi_qMiRyri4xYJONPZJm", 1515 | "fillStyle": "hachure", 1516 | "strokeWidth": 1, 1517 | "strokeStyle": "solid", 1518 | "roughness": 1, 1519 | "opacity": 100, 1520 | "angle": 0, 1521 | "x": 728.7626351242337, 1522 | "y": 200.9805402353834, 1523 | "strokeColor": "#000000", 1524 | "backgroundColor": "transparent", 1525 | "width": 128.08333333333331, 1526 | "height": 24.872410454985395, 1527 | "seed": 1022466565, 1528 | "groupIds": [], 1529 | "frameId": null, 1530 | "roundness": null, 1531 | "boundElements": [ 1532 | { 1533 | "id": "WhKU-kPA4EaGZZVcrdHDO", 1534 | "type": "arrow" 1535 | } 1536 | ], 1537 | "updated": 1726931147066, 1538 | "link": null, 1539 | "locked": false, 1540 | "fontSize": 19.13262342691184, 1541 | "fontFamily": 1, 1542 | "text": "node-exporter", 1543 | "textAlign": "center", 1544 | "verticalAlign": "middle", 1545 | "containerId": null, 1546 | "originalText": "node-exporter", 1547 | "autoResize": true, 1548 | "lineHeight": 1.3 1549 | }, 1550 | { 1551 | "id": "NKUpIQQa0D5Zp7CKDBYWc", 1552 | "type": "arrow", 1553 | "x": 934.8333333333333, 1554 | "y": 313.97250283458555, 1555 | "width": 39.999999999999886, 1556 | "height": 29.65876344145613, 1557 | "angle": 0, 1558 | "strokeColor": "#1e1e1e", 1559 | "backgroundColor": "transparent", 1560 | "fillStyle": "solid", 1561 | "strokeWidth": 2, 1562 | "strokeStyle": "solid", 1563 | "roughness": 1, 1564 | "opacity": 100, 1565 | "groupIds": [], 1566 | "frameId": null, 1567 | "index": "ao", 1568 | "roundness": { 1569 | "type": 2 1570 | }, 1571 | "seed": 1712100043, 1572 | "version": 79, 1573 | "versionNonce": 703901163, 1574 | "isDeleted": false, 1575 | "boundElements": null, 1576 | "updated": 1726931186128, 1577 | "link": null, 1578 | "locked": false, 1579 | "points": [ 1580 | [ 1581 | 0, 1582 | 0 1583 | ], 1584 | [ 1585 | -39.999999999999886, 1586 | 29.65876344145613 1587 | ] 1588 | ], 1589 | "lastCommittedPoint": null, 1590 | "startBinding": { 1591 | "elementId": "L5dBGw_FGtYAsMRE7G82f", 1592 | "focus": 0.7210304909186734, 1593 | "gap": 8.375000000000284, 1594 | "fixedPoint": null 1595 | }, 1596 | "endBinding": { 1597 | "elementId": "oku39lmcTHULJyZT4htNh", 1598 | "focus": 0.3000975916625345, 1599 | "gap": 1.1666666666666288, 1600 | "fixedPoint": null 1601 | }, 1602 | "startArrowhead": null, 1603 | "endArrowhead": "arrow", 1604 | "elbowed": false 1605 | }, 1606 | { 1607 | "id": "WhKU-kPA4EaGZZVcrdHDO", 1608 | "type": "arrow", 1609 | "x": 864.1666666666666, 1610 | "y": 234.964599609375, 1611 | "width": 81.53382201483907, 1612 | "height": 54.97427395934159, 1613 | "angle": 0, 1614 | "strokeColor": "#1e1e1e", 1615 | "backgroundColor": "transparent", 1616 | "fillStyle": "solid", 1617 | "strokeWidth": 2, 1618 | "strokeStyle": "solid", 1619 | "roughness": 1, 1620 | "opacity": 100, 1621 | "groupIds": [], 1622 | "frameId": null, 1623 | "index": "ap", 1624 | "roundness": { 1625 | "type": 2 1626 | }, 1627 | "seed": 41637189, 1628 | "version": 58, 1629 | "versionNonce": 1050523787, 1630 | "isDeleted": false, 1631 | "boundElements": null, 1632 | "updated": 1726931186128, 1633 | "link": null, 1634 | "locked": false, 1635 | "points": [ 1636 | [ 1637 | 0, 1638 | 0 1639 | ], 1640 | [ 1641 | 81.53382201483907, 1642 | 54.97427395934159 1643 | ] 1644 | ], 1645 | "lastCommittedPoint": null, 1646 | "startBinding": { 1647 | "elementId": "4fi_qMiRyri4xYJONPZJm", 1648 | "focus": -0.2196079355908853, 1649 | "gap": 9.111648919006242, 1650 | "fixedPoint": null 1651 | }, 1652 | "endBinding": { 1653 | "elementId": "L5dBGw_FGtYAsMRE7G82f", 1654 | "focus": -0.35804667529588025, 1655 | "gap": 3.7083333333335418, 1656 | "fixedPoint": null 1657 | }, 1658 | "startArrowhead": null, 1659 | "endArrowhead": "arrow", 1660 | "elbowed": false 1661 | }, 1662 | { 1663 | "type": "rectangle", 1664 | "version": 415, 1665 | "versionNonce": 963923365, 1666 | "index": "aq", 1667 | "isDeleted": false, 1668 | "id": "brP4g7llJlxpoF4Y2hghB", 1669 | "fillStyle": "hachure", 1670 | "strokeWidth": 1, 1671 | "strokeStyle": "solid", 1672 | "roughness": 1, 1673 | "opacity": 100, 1674 | "angle": 0, 1675 | "x": 934.2712165214585, 1676 | "y": 182.3763453338023, 1677 | "strokeColor": "#000000", 1678 | "backgroundColor": "transparent", 1679 | "width": 143.79090029041606, 1680 | "height": 53.17650855114548, 1681 | "seed": 263775883, 1682 | "groupIds": [], 1683 | "frameId": null, 1684 | "roundness": null, 1685 | "boundElements": [], 1686 | "updated": 1726931182229, 1687 | "link": null, 1688 | "locked": false 1689 | }, 1690 | { 1691 | "type": "text", 1692 | "version": 458, 1693 | "versionNonce": 2114326597, 1694 | "index": "ar", 1695 | "isDeleted": false, 1696 | "id": "XYpSwssbc6wKmYlB5XItc", 1697 | "fillStyle": "hachure", 1698 | "strokeWidth": 1, 1699 | "strokeStyle": "solid", 1700 | "roughness": 1, 1701 | "opacity": 100, 1702 | "angle": 0, 1703 | "x": 975.5438851242336, 1704 | "y": 193.31387356871676, 1705 | "strokeColor": "#000000", 1706 | "backgroundColor": "transparent", 1707 | "width": 77.1875, 1708 | "height": 24.872410454985395, 1709 | "seed": 479321387, 1710 | "groupIds": [], 1711 | "frameId": null, 1712 | "roundness": null, 1713 | "boundElements": [ 1714 | { 1715 | "id": "EKEgQCqeiZMaPkHOoXklp", 1716 | "type": "arrow" 1717 | } 1718 | ], 1719 | "updated": 1726931189943, 1720 | "link": null, 1721 | "locked": false, 1722 | "fontSize": 19.13262342691184, 1723 | "fontFamily": 1, 1724 | "text": "cadvisor", 1725 | "textAlign": "center", 1726 | "verticalAlign": "middle", 1727 | "containerId": null, 1728 | "originalText": "cadvisor", 1729 | "autoResize": true, 1730 | "lineHeight": 1.3 1731 | }, 1732 | { 1733 | "type": "rectangle", 1734 | "version": 369, 1735 | "versionNonce": 2112262245, 1736 | "index": "as", 1737 | "isDeleted": false, 1738 | "id": "KMmH1-IuiqhorpBHciLsy", 1739 | "fillStyle": "hachure", 1740 | "strokeWidth": 1, 1741 | "strokeStyle": "solid", 1742 | "roughness": 1, 1743 | "opacity": 100, 1744 | "angle": 0, 1745 | "x": 638.9378831881255, 1746 | "y": 262.37634533380225, 1747 | "strokeColor": "#000000", 1748 | "backgroundColor": "transparent", 1749 | "width": 171.12423362374932, 1750 | "height": 53.17650855114548, 1751 | "seed": 1345172523, 1752 | "groupIds": [], 1753 | "frameId": null, 1754 | "roundness": null, 1755 | "boundElements": [], 1756 | "updated": 1726931173368, 1757 | "link": null, 1758 | "locked": false 1759 | }, 1760 | { 1761 | "type": "text", 1762 | "version": 446, 1763 | "versionNonce": 1922779141, 1764 | "index": "at", 1765 | "isDeleted": false, 1766 | "id": "j7KAo4MWazXyzWPQCk28R", 1767 | "fillStyle": "hachure", 1768 | "strokeWidth": 1, 1769 | "strokeStyle": "solid", 1770 | "roughness": 1, 1771 | "opacity": 100, 1772 | "angle": 0, 1773 | "x": 639.3355517909006, 1774 | "y": 275.3138735687167, 1775 | "strokeColor": "#000000", 1776 | "backgroundColor": "transparent", 1777 | "width": 166.9375, 1778 | "height": 24.872410454985395, 1779 | "seed": 1012659915, 1780 | "groupIds": [], 1781 | "frameId": null, 1782 | "roundness": null, 1783 | "boundElements": [ 1784 | { 1785 | "id": "SnGpzbHR6ss5fbsrisSJ4", 1786 | "type": "arrow" 1787 | }, 1788 | { 1789 | "id": "Jyb84Q39oHJQW6HxDTzot", 1790 | "type": "arrow" 1791 | } 1792 | ], 1793 | "updated": 1726931232982, 1794 | "link": null, 1795 | "locked": false, 1796 | "fontSize": 19.13262342691184, 1797 | "fontFamily": 1, 1798 | "text": "postgres-exporter", 1799 | "textAlign": "center", 1800 | "verticalAlign": "middle", 1801 | "containerId": null, 1802 | "originalText": "postgres-exporter", 1803 | "autoResize": true, 1804 | "lineHeight": 1.3 1805 | }, 1806 | { 1807 | "id": "SnGpzbHR6ss5fbsrisSJ4", 1808 | "type": "arrow", 1809 | "x": 813.5, 1810 | "y": 287.6312662760417, 1811 | "width": 118, 1812 | "height": 8.213433488542648, 1813 | "angle": 0, 1814 | "strokeColor": "#1e1e1e", 1815 | "backgroundColor": "transparent", 1816 | "fillStyle": "solid", 1817 | "strokeWidth": 2, 1818 | "strokeStyle": "solid", 1819 | "roughness": 1, 1820 | "opacity": 100, 1821 | "groupIds": [], 1822 | "frameId": null, 1823 | "index": "au", 1824 | "roundness": { 1825 | "type": 2 1826 | }, 1827 | "seed": 1087644843, 1828 | "version": 51, 1829 | "versionNonce": 2064412459, 1830 | "isDeleted": false, 1831 | "boundElements": null, 1832 | "updated": 1726931186128, 1833 | "link": null, 1834 | "locked": false, 1835 | "points": [ 1836 | [ 1837 | 0, 1838 | 0 1839 | ], 1840 | [ 1841 | 118, 1842 | 8.213433488542648 1843 | ] 1844 | ], 1845 | "lastCommittedPoint": null, 1846 | "startBinding": { 1847 | "elementId": "j7KAo4MWazXyzWPQCk28R", 1848 | "focus": 0.3636752350550559, 1849 | "gap": 7.226948209099419, 1850 | "fixedPoint": null 1851 | }, 1852 | "endBinding": { 1853 | "elementId": "L5dBGw_FGtYAsMRE7G82f", 1854 | "focus": 0.3646516765427601, 1855 | "gap": 11.708333333333542, 1856 | "fixedPoint": null 1857 | }, 1858 | "startArrowhead": null, 1859 | "endArrowhead": "arrow", 1860 | "elbowed": false 1861 | }, 1862 | { 1863 | "id": "EKEgQCqeiZMaPkHOoXklp", 1864 | "type": "arrow", 1865 | "x": 1008.1666666666667, 1866 | "y": 233.63126627604169, 1867 | "width": 8, 1868 | "height": 48.66666666666663, 1869 | "angle": 0, 1870 | "strokeColor": "#1e1e1e", 1871 | "backgroundColor": "transparent", 1872 | "fillStyle": "solid", 1873 | "strokeWidth": 2, 1874 | "strokeStyle": "solid", 1875 | "roughness": 1, 1876 | "opacity": 100, 1877 | "groupIds": [], 1878 | "frameId": null, 1879 | "index": "av", 1880 | "roundness": { 1881 | "type": 2 1882 | }, 1883 | "seed": 60088427, 1884 | "version": 24, 1885 | "versionNonce": 1640349605, 1886 | "isDeleted": false, 1887 | "boundElements": null, 1888 | "updated": 1726931189943, 1889 | "link": null, 1890 | "locked": false, 1891 | "points": [ 1892 | [ 1893 | 0, 1894 | 0 1895 | ], 1896 | [ 1897 | -8, 1898 | 48.66666666666663 1899 | ] 1900 | ], 1901 | "lastCommittedPoint": null, 1902 | "startBinding": { 1903 | "elementId": "XYpSwssbc6wKmYlB5XItc", 1904 | "focus": 0.03414925897105192, 1905 | "gap": 15.444982252339557, 1906 | "fixedPoint": null 1907 | }, 1908 | "endBinding": { 1909 | "elementId": "L5dBGw_FGtYAsMRE7G82f", 1910 | "focus": 0.029518805630524936, 1911 | "gap": 11.349273959341787, 1912 | "fixedPoint": null 1913 | }, 1914 | "startArrowhead": null, 1915 | "endArrowhead": "arrow", 1916 | "elbowed": false 1917 | }, 1918 | { 1919 | "id": "qUS0ZcHjghPbQaCueV54I", 1920 | "type": "arrow", 1921 | "x": 566.8333333333334, 1922 | "y": 400.29793294270837, 1923 | "width": 35.33333333333326, 1924 | "height": 55.33333333333326, 1925 | "angle": 0, 1926 | "strokeColor": "#1e1e1e", 1927 | "backgroundColor": "transparent", 1928 | "fillStyle": "solid", 1929 | "strokeWidth": 2, 1930 | "strokeStyle": "solid", 1931 | "roughness": 1, 1932 | "opacity": 100, 1933 | "groupIds": [], 1934 | "frameId": null, 1935 | "index": "aw", 1936 | "roundness": { 1937 | "type": 2 1938 | }, 1939 | "seed": 1869099595, 1940 | "version": 26, 1941 | "versionNonce": 57804581, 1942 | "isDeleted": false, 1943 | "boundElements": null, 1944 | "updated": 1726931209544, 1945 | "link": null, 1946 | "locked": false, 1947 | "points": [ 1948 | [ 1949 | 0, 1950 | 0 1951 | ], 1952 | [ 1953 | 35.33333333333326, 1954 | 55.33333333333326 1955 | ] 1956 | ], 1957 | "lastCommittedPoint": null, 1958 | "startBinding": { 1959 | "elementId": "c54H7yuX8DwMbI-AoBEau", 1960 | "focus": -0.5518246978084245, 1961 | "gap": 1, 1962 | "fixedPoint": null 1963 | }, 1964 | "endBinding": { 1965 | "elementId": "U3QqFyMb3fG-ylqyM78qB", 1966 | "focus": -0.6790464182756266, 1967 | "gap": 1, 1968 | "fixedPoint": null 1969 | }, 1970 | "startArrowhead": null, 1971 | "endArrowhead": "arrow", 1972 | "elbowed": false 1973 | }, 1974 | { 1975 | "id": "OUfR-0bVvYgDpWVVB6UvD", 1976 | "type": "rectangle", 1977 | "x": 410.1666666666667, 1978 | "y": 236.964599609375, 1979 | "width": 159.33333333333331, 1980 | "height": 53.333333333333314, 1981 | "angle": 0, 1982 | "strokeColor": "#1e1e1e", 1983 | "backgroundColor": "transparent", 1984 | "fillStyle": "solid", 1985 | "strokeWidth": 1, 1986 | "strokeStyle": "solid", 1987 | "roughness": 1, 1988 | "opacity": 100, 1989 | "groupIds": [], 1990 | "frameId": null, 1991 | "index": "ax", 1992 | "roundness": null, 1993 | "seed": 605756293, 1994 | "version": 45, 1995 | "versionNonce": 579212613, 1996 | "isDeleted": false, 1997 | "boundElements": [ 1998 | { 1999 | "type": "text", 2000 | "id": "lqZt-kbi7Sv3S2FBXYGZm" 2001 | }, 2002 | { 2003 | "id": "Jyb84Q39oHJQW6HxDTzot", 2004 | "type": "arrow" 2005 | } 2006 | ], 2007 | "updated": 1726931232982, 2008 | "link": null, 2009 | "locked": false 2010 | }, 2011 | { 2012 | "id": "lqZt-kbi7Sv3S2FBXYGZm", 2013 | "type": "text", 2014 | "x": 446.65625, 2015 | "y": 251.13126627604166, 2016 | "width": 86.35416666666666, 2017 | "height": 25, 2018 | "angle": 0, 2019 | "strokeColor": "#1e1e1e", 2020 | "backgroundColor": "transparent", 2021 | "fillStyle": "solid", 2022 | "strokeWidth": 1, 2023 | "strokeStyle": "solid", 2024 | "roughness": 1, 2025 | "opacity": 100, 2026 | "groupIds": [], 2027 | "frameId": null, 2028 | "index": "ay", 2029 | "roundness": null, 2030 | "seed": 2088448805, 2031 | "version": 16, 2032 | "versionNonce": 1138395051, 2033 | "isDeleted": false, 2034 | "boundElements": null, 2035 | "updated": 1726931228713, 2036 | "link": null, 2037 | "locked": false, 2038 | "text": "postgres", 2039 | "fontSize": 20, 2040 | "fontFamily": 5, 2041 | "textAlign": "center", 2042 | "verticalAlign": "middle", 2043 | "containerId": "OUfR-0bVvYgDpWVVB6UvD", 2044 | "originalText": "postgres", 2045 | "autoResize": true, 2046 | "lineHeight": 1.25 2047 | }, 2048 | { 2049 | "id": "Jyb84Q39oHJQW6HxDTzot", 2050 | "type": "arrow", 2051 | "x": 567.5, 2052 | "y": 264.964599609375, 2053 | "width": 74, 2054 | "height": 18, 2055 | "angle": 0, 2056 | "strokeColor": "#1e1e1e", 2057 | "backgroundColor": "transparent", 2058 | "fillStyle": "solid", 2059 | "strokeWidth": 1, 2060 | "strokeStyle": "solid", 2061 | "roughness": 1, 2062 | "opacity": 100, 2063 | "groupIds": [], 2064 | "frameId": null, 2065 | "index": "az", 2066 | "roundness": { 2067 | "type": 2 2068 | }, 2069 | "seed": 327123941, 2070 | "version": 33, 2071 | "versionNonce": 1643979941, 2072 | "isDeleted": false, 2073 | "boundElements": null, 2074 | "updated": 1726931232982, 2075 | "link": null, 2076 | "locked": false, 2077 | "points": [ 2078 | [ 2079 | 0, 2080 | 0 2081 | ], 2082 | [ 2083 | 74, 2084 | 18 2085 | ] 2086 | ], 2087 | "lastCommittedPoint": null, 2088 | "startBinding": { 2089 | "elementId": "OUfR-0bVvYgDpWVVB6UvD", 2090 | "focus": -0.3813343768342781, 2091 | "gap": 1, 2092 | "fixedPoint": null 2093 | }, 2094 | "endBinding": { 2095 | "elementId": "j7KAo4MWazXyzWPQCk28R", 2096 | "focus": -0.4578958730250668, 2097 | "gap": 1, 2098 | "fixedPoint": null 2099 | }, 2100 | "startArrowhead": null, 2101 | "endArrowhead": "arrow", 2102 | "elbowed": false 2103 | } 2104 | ], 2105 | "appState": { 2106 | "gridSize": 20, 2107 | "gridStep": 5, 2108 | "gridModeEnabled": false, 2109 | "viewBackgroundColor": "#ffffff" 2110 | }, 2111 | "files": {} 2112 | } -------------------------------------------------------------------------------- /architecture.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 17 | grafanaNetwork: Internalhaproxy internalNPMDockerRegistry<PRIVATE_IP>:::80Network Mainhaproxy publicYour App 1Your App 2certbot<PUBLIC_IP>:::80 <PUBLIC_IP>:::443Your private appsprometheusnode-exportercadvisorpostgres-exporterpostgres -------------------------------------------------------------------------------- /config.sh.example: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # You should not need to modify this file, it's purpose is to 4 | # remember the path to the real configuration, and load it 5 | # from "$DATA_BASE_PATH"/config.sh 6 | # 7 | # This keeps it all under a single directory tree, to make backups 8 | # simple. 9 | # 10 | 11 | export SCRIPT_BASE_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 12 | 13 | export DATA_BASE_PATH= 14 | 15 | source "$DATA_BASE_PATH"/config.sh 16 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | networks: 2 | internal: 3 | external: true 4 | name: internal 5 | main: 6 | external: true 7 | name: main 8 | services: 9 | haproxy_public: 10 | image: haproxy:3.1.5-alpine 11 | hostname: haproxy-public 12 | container_name: proxy_public 13 | networks: 14 | - main 15 | ports: 16 | - "${PUBLIC_IP}:${PUBLIC_PORT_HTTP}:80" 17 | - "${PUBLIC_IP}:${PUBLIC_PORT_HTTPS}:443" 18 | ulimits: 19 | nofile: 10240 20 | volumes: 21 | - type: bind 22 | source: ${DATA_BASE_PATH}/haproxy-public 23 | target: /usr/local/etc/haproxy 24 | read_only: true 25 | - type: bind 26 | source: ${DATA_BASE_PATH}/haproxy-public-ssl 27 | target: /usr/local/etc/ssl/certs 28 | read_only: true 29 | - type: bind 30 | source: ${DATA_BASE_PATH}/haproxy-public-stats 31 | target: /var/lib/haproxy-socket 32 | read_only: false 33 | haproxy_internal: 34 | image: haproxy:3.1.5-alpine 35 | hostname: haproxy-internal 36 | container_name: proxy_internal 37 | networks: 38 | - internal 39 | ports: 40 | - "${PRIVATE_IP}:${PRIVATE_PORT_HTTP}:80" 41 | - "${PRIVATE_IP}:${PRIVATE_PORT_HTTPS}:443" 42 | - "${PRIVATE_IP}:5432:5432" 43 | ulimits: 44 | nofile: 10240 45 | volumes: 46 | - type: bind 47 | source: ${DATA_BASE_PATH}/haproxy-internal 48 | target: /usr/local/etc/haproxy 49 | read_only: true 50 | - type: bind 51 | source: ${DATA_BASE_PATH}/haproxy-internal-ssl 52 | target: /usr/local/etc/ssl/certs 53 | read_only: true 54 | -------------------------------------------------------------------------------- /docs/postgres.md: -------------------------------------------------------------------------------- 1 | # Postgres Application 2 | Scrappy guide to operating postgres using `shoe-string-server` 3 | 4 | ## Running 5 | Create `data` directory structure: 6 | ```shell 7 | mkdir -p ~/data/postgres/16/data 8 | chown -R 999 ~/data/postgres/16 9 | chmod -R 700 ~/data/postgres/16 10 | ``` 11 | 12 | Create `postgres.yaml` application in `applications-internal`: 13 | ```yaml 14 | version: "3.9" 15 | networks: 16 | internal: 17 | external: true 18 | name: internal 19 | postgres: 20 | external: true 21 | name: postgres 22 | services: 23 | application: 24 | image: postgres:16 25 | hostname: postgres 26 | container_name: postgres 27 | networks: 28 | - internal 29 | - postgres 30 | environment: 31 | POSTGRES_PASSWORD: 'change me' 32 | volumes: 33 | - type: bind 34 | source: ${DATA_BASE_PATH}/postgres/16/data 35 | target: /var/lib/postgresql/data 36 | ``` 37 | I don't remember why I have it on both internal and postgres networks... it probably only needs to be on one of these. 38 | 39 | ## Connecting 40 | Just exec `psql` in the container, if using default `pg_hba.conf` then `trust` auth will be used. 41 | ```shell 42 | docker exec -it postgres psql -U postgres 43 | ``` 44 | 45 | ## Backups 46 | Just create a tarball of the data directory. Optionally setup WAL archiving to S3 / whatever. 47 | 48 | ## Upgrades 49 | Take a backup before upgrading. This method requires downtime and uses this project: https://github.com/tianon/docker-postgres-upgrade 50 | 51 | Tested successfully once to upgrade from 13 -> 16, YMMV 52 | ```shell 53 | # Suspend reconciliation Cron if applicable 54 | crontab -e 55 | 56 | # Prepare the new data directory 57 | mkdir -p ~/data/postgres/$NEW_VERSION/data 58 | chown -R 999 ~/data/postgres/$NEW_VERSION 59 | chmod -R 700 ~/data/postgres/$NEW_VERSION 60 | 61 | # Stop the database 62 | docker compose -f ~/data/applications-internal/postgres.yaml -p ~/data/applications-internal/postgres.yaml down --remove-orphans 63 | 64 | # Run the upgrade 65 | docker run -it --name=postgres-upgrade -v $HOME/data/postgres:/var/lib/postgresql tianon/postgres-upgrade:$OLD_VERSION-to-$NEW_VERSION --link 66 | ``` 67 | Then in `postgres.yaml` application: 68 | - Update docker tag 69 | - Update data directory volume 70 | 71 | Now start the new version and check everything is working: 72 | ```shell 73 | ~/cluster/applications/watchup-up.sh 74 | # psql in to check all is ok 75 | ``` 76 | 77 | Finally, we can delete the old data directory and upgrade container: 78 | ```shell 79 | docker rm postgres-upgrade 80 | rm -rf ~/data/postgres/$OLD_VERSION 81 | ``` 82 | -------------------------------------------------------------------------------- /init/create-empty-configuration-structure.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -exo pipefail 4 | 5 | DATA_BASE_PATH=${1:-/var/cluster-config} 6 | 7 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 8 | 9 | if [ -d "${DATA_BASE_PATH}" ]; then 10 | echo "Error: Directory ${DATA_BASE_PATH} already exists." 11 | exit 1 12 | fi 13 | 14 | cp -rv "$__dir"/template/ "$DATA_BASE_PATH"/ 15 | 16 | pushd $DATA_BASE_PATH 17 | mv .gitignore.template .gitignore 18 | git init 19 | git add . 20 | git commit -m "initial commit" 21 | popd 22 | 23 | # haproxy needs to write to this directory, so we change it's ownership 24 | # to the uid:gid that the proxy executes as. 25 | sudo chown 99:99 "$DATA_BASE_PATH"/haproxy-public-stats 26 | 27 | openssl dhparam -out "$DATA_BASE_PATH"/haproxy-public/dhparam 2048 28 | openssl dhparam -out "$DATA_BASE_PATH"/haproxy-internal/dhparam 2048 29 | 30 | 31 | # Save the path to config.sh 32 | sed "s||$DATA_BASE_PATH|g" "$__dir"/../config.sh.example > "$__dir"/../config.sh 33 | -------------------------------------------------------------------------------- /init/install-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # WARNING 4 | # Currently written for the amazon linux 2 distribution 5 | # WARNING 6 | 7 | # 8 | # This is a convenience script to install docker + docker-compose, 9 | # and configure the daemon to start at boot time, plus grant the 10 | # current user access to the docker socket. 11 | # 12 | # You may not wish to use this on a production system, and rather 13 | # install and configure these manually, or via ansible etc, for 14 | # greater control over the particulars. 15 | 16 | # Amazon Linux 2 is not supported - use yum version instead, though 17 | # might be less recent 18 | #bash <(curl -fsSL https://get.docker.com) 19 | sudo yum update -y 20 | 21 | sudo amazon-linux-extras install kernel-ng 22 | sudo amazon-linux-extras install docker 23 | sudo amazon-linux-extras install epel 24 | 25 | sudo yum -y install wireguard-tools 26 | sudo yum -y install docker bash-completion 27 | 28 | sudo groupadd docker 29 | sudo usermod -aG docker $USER 30 | 31 | sudo systemctl enable docker.service 32 | sudo systemctl enable containerd.service 33 | 34 | sudo systemctl start docker.service 35 | sudo systemctl start containerd.service 36 | 37 | OCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker} 38 | mkdir -p $DOCKER_CONFIG/cli-plugins 39 | curl -SL https://github.com/docker/compose/releases/download/v2.29.6/docker-compose-linux-x86_64 -o $DOCKER_CONFIG/cli-plugins/docker-compose 40 | chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose 41 | 42 | docker version 43 | docker compose --version 44 | 45 | sudo docker run --rm hello-world 46 | 47 | # install docker shell completion 48 | sudo curl \ 49 | -L https://raw.githubusercontent.com/docker/docker-ce/master/components/cli/contrib/completion/bash/docker \ 50 | -o /etc/bash_completion.d/docker.sh 51 | 52 | # install docker-compose shell completion 53 | sudo curl \ 54 | -L https://raw.githubusercontent.com/docker/compose/master/contrib/completion/bash/docker-compose \ 55 | -o /etc/bash_completion.d/docker-compose 56 | -------------------------------------------------------------------------------- /init/template/.gitignore.template: -------------------------------------------------------------------------------- 1 | /* 2 | !.gitignore 3 | !config.sh 4 | !/applications-internal 5 | !/applications-internal/* 6 | !/applications-public 7 | !/applications-public/* 8 | !/docker 9 | !/docker/conf 10 | !/docker/conf/* 11 | /docker/storage 12 | !/haproxy-internal 13 | !/haproxy-internal/* 14 | !/haproxy-public 15 | !/haproxy-public/* 16 | !/telegraf 17 | !/telegraf/telegraf.conf 18 | !/verdaccio 19 | !/verdaccio/conf 20 | !/verdaccio/conf/* 21 | /verdaccio/storage 22 | -------------------------------------------------------------------------------- /init/template/applications-internal/monitoring-grafana.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | x-container-port: 3000 3 | x-external-host-names: 4 | - grafana.internal.example.com 5 | networks: 6 | internal: 7 | external: true 8 | name: internal 9 | services: 10 | application: 11 | image: grafana/grafana:11.2.0 12 | hostname: monitoring_grafana 13 | container_name: monitoring_grafana 14 | networks: 15 | - internal 16 | user: "1000:1000" 17 | volumes: 18 | - type: bind 19 | source: ${DATA_BASE_PATH}/grafana 20 | target: /var/lib/grafana 21 | -------------------------------------------------------------------------------- /init/template/applications-internal/postgres.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | networks: 3 | internal: 4 | external: true 5 | name: internal 6 | postgres: 7 | external: true 8 | name: postgres 9 | services: 10 | application: 11 | image: postgres:16 12 | hostname: postgres 13 | container_name: postgres 14 | networks: 15 | - internal 16 | - postgres 17 | environment: 18 | POSTGRES_PASSWORD: '' 19 | volumes: 20 | - type: bind 21 | source: ${DATA_BASE_PATH}/postgres/16/data 22 | target: /var/lib/postgresql/data 23 | -------------------------------------------------------------------------------- /init/template/applications-internal/prometheus.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | x-container-port: 9090 3 | x-external-host-names: 4 | - 5 | networks: 6 | internal: 7 | external: true 8 | name: internal 9 | main: 10 | external: true 11 | name: main 12 | postgres: 13 | external: true 14 | name: postgres 15 | services: 16 | application: 17 | image: prom/prometheus:v3.0.0-beta.0 18 | container_name: monitoring_prometheus 19 | hostname: monitoring_prometheus 20 | networks: 21 | - internal 22 | - main 23 | user: 1000:1000 24 | command: 25 | - --config.file=/etc/prometheus/prometheus.yml 26 | - --storage.tsdb.path=/var/data 27 | - --storage.tsdb.retention.time=30d 28 | - --storage.tsdb.min-block-duration=2h 29 | - --storage.tsdb.max-block-duration=2h 30 | - --query.lookback-delta=1m 31 | - --storage.tsdb.no-lockfile 32 | - --storage.tsdb.wal-compression 33 | volumes: 34 | - type: bind 35 | source: ${DATA_BASE_PATH}/prometheus/conf/prometheus.yml 36 | target: /etc/prometheus/prometheus.yml 37 | read_only: true 38 | - type: bind 39 | source: ${DATA_BASE_PATH}/prometheus/data 40 | target: /var/data 41 | 42 | node_exporter: 43 | image: quay.io/prometheus/node-exporter:latest 44 | container_name: monitoring_node_exporter 45 | hostname: monitoring_node_exporter 46 | command: 47 | - '--path.rootfs=/host' 48 | networks: 49 | - internal 50 | pid: host 51 | volumes: 52 | - '/:/host:ro,rslave' 53 | postgres_exporter: 54 | image: quay.io/prometheuscommunity/postgres-exporter 55 | container_name: monitoring_postgres_exporter 56 | hostname: monitoring_postgres_exporter 57 | networks: 58 | - internal 59 | - postgres 60 | environment: 61 | DATA_SOURCE_URI: "postgres:5432/postgres?sslmode=disable" 62 | DATA_SOURCE_USER: 63 | DATA_SOURCE_PASS: 64 | cadvisor: 65 | image: gcr.io/cadvisor/cadvisor:v0.49.1 66 | container_name: monitoring_cadvisor 67 | hostname: monitoring_cadvisor 68 | privileged: true 69 | networks: 70 | - internal 71 | volumes: 72 | - /:/rootfs:ro 73 | - /var/run:/var/run:ro 74 | - /sys:/sys:ro 75 | - /var/lib/docker/:/var/lib/docker:ro 76 | - /dev/disk/:/dev/disk:ro 77 | - /dev/kmsg:/dev/kmsg 78 | -------------------------------------------------------------------------------- /init/template/applications-internal/registry-docker.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | x-container-port: 5000 3 | x-external-host-names: 4 | - docker.internal.example.com 5 | networks: 6 | internal: 7 | external: true 8 | name: internal 9 | services: 10 | application: 11 | image: registry:2.8 12 | hostname: registry_docker 13 | container_name: registry_docker 14 | networks: 15 | - internal 16 | volumes: 17 | - type: bind 18 | source: ${DATA_BASE_PATH}/docker/storage 19 | target: /var/lib/registry 20 | -------------------------------------------------------------------------------- /init/template/applications-internal/registry-npm.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | x-container-port: 4873 3 | x-external-host-names: 4 | - npm.internal.example.com 5 | networks: 6 | internal: 7 | external: true 8 | name: internal 9 | services: 10 | application: 11 | image: verdaccio/verdaccio:5.32 12 | hostname: registry_npm 13 | container_name: registry_npm 14 | networks: 15 | - internal 16 | volumes: 17 | - type: bind 18 | source: ${DATA_BASE_PATH}/verdaccio/storage 19 | target: /verdaccio/storage 20 | - type: bind 21 | source: ${DATA_BASE_PATH}/verdaccio/conf 22 | target: /verdaccio/conf 23 | -------------------------------------------------------------------------------- /init/template/applications-public/example-application.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | x-container-port: 80 3 | x-external-host-names: 4 | - application.example.com 5 | networks: 6 | main: 7 | external: true 8 | name: main 9 | services: 10 | application: 11 | image: nginxdemos/hello:latest 12 | hostname: example-application 13 | container_name: example-application 14 | networks: 15 | - main 16 | -------------------------------------------------------------------------------- /init/template/config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export INFLUXDB_ADMIN_USER=admin 4 | export INFLUXDB_ADMIN_PASSWORD=admin 5 | 6 | export LETS_ENCRYPT_EMAIL_ADDRESS=support@example.com 7 | 8 | # if using ssl certs for proxy_internal *and* using zonomi as a dns provider 9 | export ZONOMI_API_KEY="" 10 | 11 | # On AWS you can find these addresses by: 12 | # curl http://169.254.169.254/latest/meta-data/public-ipv4 13 | # curl http://169.254.169.254/latest/meta-data/local-ipv4 14 | 15 | export PUBLIC_IP=127.0.0.1 16 | export PUBLIC_PORT_HTTP=80 17 | export PUBLIC_PORT_HTTPS=443 18 | 19 | export PRIVATE_IP=10.12.0.1 20 | export PRIVATE_PORT=80 21 | 22 | # Space separated list of additional docker bridge networks to create 23 | export ADDITIONAL_NETWORKS=() 24 | -------------------------------------------------------------------------------- /init/template/docker/storage/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mnahkies/shoe-string-server/a35caae845b9f05df92478edc9e7eb82968b65c9/init/template/docker/storage/.gitkeep -------------------------------------------------------------------------------- /init/template/grafana/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mnahkies/shoe-string-server/a35caae845b9f05df92478edc9e7eb82968b65c9/init/template/grafana/.gitkeep -------------------------------------------------------------------------------- /init/template/haproxy-internal-ssl/invalid.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFBzCCAu+gAwIBAgIUFuQkg+n7LD3PK3O84LSJEi/z28swDQYJKoZIhvcNAQEL 3 | BQAwEzERMA8GA1UEAwwIaW52YWxpZC4wHhcNMjEwNzExMTUxNTMyWhcNMzEwNzA5 4 | MTUxNTMyWjATMREwDwYDVQQDDAhpbnZhbGlkLjCCAiIwDQYJKoZIhvcNAQEBBQAD 5 | ggIPADCCAgoCggIBAMhk/iHpKTxBRW7KHsNoOEWefefV2GSB5D/x2lw9Sal6E41a 6 | /C4+y35P929PM/jZCVyZ2Js61QaZMAmsOwwkqdJQxp6Tgn11ZmrZqL+iPl05cCeZ 7 | KYuvFUBvoq6+6SVko+Gfw1hIczJeMrTxFKMgXYsaIW7Znw8mOvRmxZwbaOfXD4zj 8 | XDDStiCQOy0SEGrm08oSt94DyP3oV+BEGRd3pDeZ7C5dNfykymjDJ6D+2PxPwsQ1 9 | YNUZoHcuxGr2GnVhKszO3SBbCGWldZ0M0P6ENuu6dhZzf8sEtnAzfBzpPOuwtz8f 10 | mn/R26P2sQF2LsFMBCIDfavolb6SSNASr64ZFOAvJsqdFm+CAuuGofOsjGi5UJNH 11 | BEEX28tf9eQfiHaYeHFXOGV5pt8D1EV/ep2VWYPq9XS9yhKooSyyWprvsPTh1LR7 12 | 6kd0pI+WFsEA0swkHPa5xqS4FO3ZlsaLSL54+3MOM0P+RBEn5sUcDGvNU3uyLLr+ 13 | EvjG7MkJAhVB6KA1DjCLYRWn0ooGaZhtkOaLY99HiVaUqMv1Lz09Bu/PAm0inKOs 14 | UAXGlNDHYro7w3aIfnw788qxbYcVcX0vy3TpObqetRJMyobV0eTB0D9MZWh1wUmN 15 | +yp2GCu5ToYFqcdzSFpa9y2NmuonXXlSu1yAGmrTJMROzVzTwfCOkIjdxsURAgMB 16 | AAGjUzBRMB0GA1UdDgQWBBQjbqwPoMJd7eM7BHs0bACAM5k8RjAfBgNVHSMEGDAW 17 | gBQjbqwPoMJd7eM7BHs0bACAM5k8RjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 18 | DQEBCwUAA4ICAQAaDPhiv7pNHqL/MJlF0ui0woJ04R2SqbuMD3dc8A4sdLZP10sP 19 | SfvE6IOW1w9KO4czDWr0L2+khEH8lZEWIfBuHaNwsl0EvyDrYYZ3+qyp41lhRcnf 20 | G7aGHIvk2DVu7YbwxeJYzSzCj8OdAHIppaL5V02UdID5GmadsufxMDk0yRpCLZ90 21 | Lvh7Dls/UoGw7y/joDYL9YkXlojIgP2z8hyj9dLK3byu9XaZ8JkwroPzXNao4y5f 22 | TAnsIu5mVJSGvBl0gIKn9oYGYGwTe6eU1L1dycqmEoQQbJttBkP95dl7Ib/Fcg2k 23 | ITH85h2YpJ4l04/v2xlStFIM5DgylMSNW4i7NjLwcFsYqhMyXCqqjZuGOYl5lDaZ 24 | b/Kltas4GlX7IUCqIy6mc1i9h6E4GtPtJfWPbOpRAXTnOT0KZCt7bMqSGAIeCtvZ 25 | Np2S/6HtML9/vQYzD0NTa6NlwYfDlRDSS41JGbzFCoUJhOxSx6XCbaLsJZggCe3x 26 | OxmvV7R02yUWhaF5VZq9piiUtx0ezWzLwlZEAWJttBIJS/36ZRnUGHJkgilIsYPW 27 | +bAihjnvVxwZpAOuoVbZeYH89/ayvEGtbeWEfp9Y9f9Dk4mS3gBuuGCDQYV/b+06 28 | 8xdLFekagmUX4VKDf1uRmYcDem3cSmRUqvhDsieX6yB1T4YOYT4dO91hLQ== 29 | -----END CERTIFICATE----- 30 | -----BEGIN PRIVATE KEY----- 31 | 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 80 | amq8gS/kmairHfk5EzANBgRrw1FfeXQ4 81 | -----END PRIVATE KEY----- 82 | -------------------------------------------------------------------------------- /init/template/haproxy-internal/directory.html: -------------------------------------------------------------------------------- 1 | HTTP/1.0 200 OK 2 | Cache-Control: no-cache 3 | Connection: close 4 | Content-Type: text/html 5 | 6 | 7 | 8 | 9 | 10 | server.example.com - directory 11 | 25 | 26 | 44 | 45 | 46 |
47 |
48 | 49 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /init/template/haproxy-internal/haproxy.cfg.template: -------------------------------------------------------------------------------- 1 | global 2 | log 127.0.0.1 local2 3 | maxconn 4000 4 | 5 | ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305 6 | ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 7 | ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 8 | 9 | ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305 10 | ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 11 | ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 12 | 13 | # openssl dhparam -out "$DATA_BASE_PATH"/haproxy-internal/dhparam 2048 14 | ssl-dh-param-file /usr/local/etc/haproxy/dhparam 15 | 16 | resolvers docker_resolver 17 | nameserver dns 127.0.0.11:53 18 | 19 | #--------------------------------------------------------------------- 20 | # common defaults that all the 'listen' and 'backend' sections will 21 | # use if not designated in their block 22 | #--------------------------------------------------------------------- 23 | defaults 24 | mode http 25 | log global 26 | option httplog 27 | option dontlognull 28 | option http-server-close 29 | option forwardfor except 127.0.0.0/8 30 | option redispatch 31 | retries 3 32 | timeout http-request 10s 33 | timeout queue 1m 34 | timeout connect 10s 35 | timeout client 1m 36 | timeout server 1m 37 | timeout http-keep-alive 10s 38 | timeout check 10s 39 | maxconn 3000 40 | 41 | # allows proxy to start immediately when some/all backends are not available 42 | default-server init-addr none 43 | 44 | listen postgres 45 | bind *:5432 46 | mode tcp 47 | balance leastconn 48 | server postgres postgres:5432 resolvers docker_resolver 49 | 50 | #--------------------------------------------------------------------- 51 | # main frontend which proxys to the backends 52 | #--------------------------------------------------------------------- 53 | frontend internal 54 | bind *:80 55 | bind *:443 ssl crt /usr/local/etc/ssl/certs/ alpn h2,http/1.1 56 | 57 | filter compression 58 | compression algo gzip 59 | 60 | # http-request redirect scheme https if !{ ssl_fc } 61 | http-request set-header X-Forwarded-Proto https if { ssl_fc } 62 | 63 | {{USE_BACKENDS}} 64 | 65 | default_backend no-match 66 | 67 | #--------------------------------------------------------------------- 68 | # stats frontend which powers prometheus metrics 69 | #--------------------------------------------------------------------- 70 | frontend stats 71 | bind *:8404 72 | http-request use-service prometheus-exporter if { path /metrics } 73 | stats enable 74 | stats uri /stats 75 | stats refresh 10s 76 | 77 | {{BACKENDS}} 78 | 79 | backend no-match 80 | mode http 81 | http-request set-log-level silent 82 | errorfile 503 /usr/local/etc/haproxy/directory.html 83 | #http-request deny deny_status 400 84 | -------------------------------------------------------------------------------- /init/template/haproxy-public-ssl/invalid.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFBzCCAu+gAwIBAgIUFuQkg+n7LD3PK3O84LSJEi/z28swDQYJKoZIhvcNAQEL 3 | BQAwEzERMA8GA1UEAwwIaW52YWxpZC4wHhcNMjEwNzExMTUxNTMyWhcNMzEwNzA5 4 | MTUxNTMyWjATMREwDwYDVQQDDAhpbnZhbGlkLjCCAiIwDQYJKoZIhvcNAQEBBQAD 5 | ggIPADCCAgoCggIBAMhk/iHpKTxBRW7KHsNoOEWefefV2GSB5D/x2lw9Sal6E41a 6 | /C4+y35P929PM/jZCVyZ2Js61QaZMAmsOwwkqdJQxp6Tgn11ZmrZqL+iPl05cCeZ 7 | KYuvFUBvoq6+6SVko+Gfw1hIczJeMrTxFKMgXYsaIW7Znw8mOvRmxZwbaOfXD4zj 8 | XDDStiCQOy0SEGrm08oSt94DyP3oV+BEGRd3pDeZ7C5dNfykymjDJ6D+2PxPwsQ1 9 | YNUZoHcuxGr2GnVhKszO3SBbCGWldZ0M0P6ENuu6dhZzf8sEtnAzfBzpPOuwtz8f 10 | mn/R26P2sQF2LsFMBCIDfavolb6SSNASr64ZFOAvJsqdFm+CAuuGofOsjGi5UJNH 11 | BEEX28tf9eQfiHaYeHFXOGV5pt8D1EV/ep2VWYPq9XS9yhKooSyyWprvsPTh1LR7 12 | 6kd0pI+WFsEA0swkHPa5xqS4FO3ZlsaLSL54+3MOM0P+RBEn5sUcDGvNU3uyLLr+ 13 | EvjG7MkJAhVB6KA1DjCLYRWn0ooGaZhtkOaLY99HiVaUqMv1Lz09Bu/PAm0inKOs 14 | UAXGlNDHYro7w3aIfnw788qxbYcVcX0vy3TpObqetRJMyobV0eTB0D9MZWh1wUmN 15 | +yp2GCu5ToYFqcdzSFpa9y2NmuonXXlSu1yAGmrTJMROzVzTwfCOkIjdxsURAgMB 16 | AAGjUzBRMB0GA1UdDgQWBBQjbqwPoMJd7eM7BHs0bACAM5k8RjAfBgNVHSMEGDAW 17 | gBQjbqwPoMJd7eM7BHs0bACAM5k8RjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 18 | DQEBCwUAA4ICAQAaDPhiv7pNHqL/MJlF0ui0woJ04R2SqbuMD3dc8A4sdLZP10sP 19 | SfvE6IOW1w9KO4czDWr0L2+khEH8lZEWIfBuHaNwsl0EvyDrYYZ3+qyp41lhRcnf 20 | G7aGHIvk2DVu7YbwxeJYzSzCj8OdAHIppaL5V02UdID5GmadsufxMDk0yRpCLZ90 21 | Lvh7Dls/UoGw7y/joDYL9YkXlojIgP2z8hyj9dLK3byu9XaZ8JkwroPzXNao4y5f 22 | TAnsIu5mVJSGvBl0gIKn9oYGYGwTe6eU1L1dycqmEoQQbJttBkP95dl7Ib/Fcg2k 23 | ITH85h2YpJ4l04/v2xlStFIM5DgylMSNW4i7NjLwcFsYqhMyXCqqjZuGOYl5lDaZ 24 | b/Kltas4GlX7IUCqIy6mc1i9h6E4GtPtJfWPbOpRAXTnOT0KZCt7bMqSGAIeCtvZ 25 | Np2S/6HtML9/vQYzD0NTa6NlwYfDlRDSS41JGbzFCoUJhOxSx6XCbaLsJZggCe3x 26 | OxmvV7R02yUWhaF5VZq9piiUtx0ezWzLwlZEAWJttBIJS/36ZRnUGHJkgilIsYPW 27 | +bAihjnvVxwZpAOuoVbZeYH89/ayvEGtbeWEfp9Y9f9Dk4mS3gBuuGCDQYV/b+06 28 | 8xdLFekagmUX4VKDf1uRmYcDem3cSmRUqvhDsieX6yB1T4YOYT4dO91hLQ== 29 | -----END CERTIFICATE----- 30 | -----BEGIN PRIVATE KEY----- 31 | MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDIZP4h6Sk8QUVu 32 | yh7DaDhFnn3n1dhkgeQ/8dpcPUmpehONWvwuPst+T/dvTzP42QlcmdibOtUGmTAJ 33 | rDsMJKnSUMaek4J9dWZq2ai/oj5dOXAnmSmLrxVAb6KuvuklZKPhn8NYSHMyXjK0 34 | 8RSjIF2LGiFu2Z8PJjr0ZsWcG2jn1w+M41ww0rYgkDstEhBq5tPKErfeA8j96Ffg 35 | RBkXd6Q3mewuXTX8pMpowyeg/tj8T8LENWDVGaB3LsRq9hp1YSrMzt0gWwhlpXWd 36 | DND+hDbrunYWc3/LBLZwM3wc6TzrsLc/H5p/0duj9rEBdi7BTAQiA32r6JW+kkjQ 37 | Eq+uGRTgLybKnRZvggLrhqHzrIxouVCTRwRBF9vLX/XkH4h2mHhxVzhleabfA9RF 38 | f3qdlVmD6vV0vcoSqKEsslqa77D04dS0e+pHdKSPlhbBANLMJBz2ucakuBTt2ZbG 39 | i0i+ePtzDjND/kQRJ+bFHAxrzVN7siy6/hL4xuzJCQIVQeigNQ4wi2EVp9KKBmmY 40 | bZDmi2PfR4lWlKjL9S89PQbvzwJtIpyjrFAFxpTQx2K6O8N2iH58O/PKsW2HFXF9 41 | L8t06Tm6nrUSTMqG1dHkwdA/TGVodcFJjfsqdhgruU6GBanHc0haWvctjZrqJ115 42 | UrtcgBpq0yTETs1c08HwjpCI3cbFEQIDAQABAoICAQCK1U0reTnUQPQ1mVpOzvmN 43 | YgygfYr5tvPHSWua0+sguy6olAx6jY615/jo6Np84QCXYw6qHMxRUffx+5y9APmW 44 | d9fjLRcOjDN0e29ptKG4PH7zMC2UVKxIhA8VObaU7XCMc/8GPstwbcp7iTpe+aFV 45 | KblX7vU/raDSihEF4gd/94MSfMH2IUWEsegNhaJSLbE1Ilq4Oa8aGcon2YX4uC7R 46 | RpZWWMV6T0Db67ic1XLG+wtYnBKGEMcXSxNRd0dFYxgf5IFWVTswEL23HJX+fuL9 47 | 1aTSURqMMKukeEbYUM6gC0IpPV/whhfBNLvU42fpCx0h44FBhFWaPgasAQYte7r1 48 | i9QKfphXCFuba1ErakSmfuzKKdshtVfKuOgPcOHZfzhFJMY8YWJy9YiLqoXJyNw8 49 | zHzqbjII4z40unN9L8NR8eMpw9F350/8t4Tmiu8ufdgZj0eZ+2V6Wr0LWaQYvbvQ 50 | fo6b5+jaAprR4E6SaxRwiJUjnOtoPeNGciPK9uusr9OfX8q7Q9xQejGe0A2RjMs+ 51 | 951eUU+QsY20orh8Agmhn9ef44DPJ54cDl0/rGP7HorF4KPzZs/pM8rD21x/wkss 52 | b8DLQL4s/Q1aYcxEUYGvVzn0BG7rgxIrQ+pkn24dCEim4sijHzar/vZLdhxeyDvk 53 | pSrxn/9/K2rFukhwOmAqQQKCAQEA8C+XpndZQ5rpMCmdPAaSa623RlCq5h0Iv33V 54 | hA+2Odlxo4n6+F7nFPH2aK7NZvbooPe1befX++fqad7tVSM57gXx0xSPmNFI22kA 55 | +T7jNj4Cd/qcANUbj5OhPPeZHTVgFDbxch3IReNvONZJ6sXeJ3HO2BKVoOHpEwx7 56 | w8ozkF1pgFTcdem00scQ2OID035BVmtJhX+1BFlpNSofKT7kBYfi497kk2FgDP1M 57 | oCsB9Atht/MvwZodfOICJspqqgewTwl1ZIxtxoqkWyo8edWtvGu6TxhmHKouN+u7 58 | NcVzAwgSp1qVOt2vrvcQ9XPG0JgyO8VlMq+GGvk1P985N5Gq7QKCAQEA1Za0M0As 59 | opJ6U4Ml0Xpl/cedYWbdE3KqciyjOg7C2CwWvsfqxDmq+20jkR2oFuVh7rWPngcK 60 | fn62FukEe37EmSkcJ+c4kJD4VmYK0QjieSTlA783fx4jy3Ki6VPQ+iNopGyfdjlD 61 | eSLzw5bqMS3+FfxqXc2orCVT5e8e4PoGo3iOpCYTFpPZ1V+tNmL2gbKJG/diVwEG 62 | J6u7WvEY8D9U08bJ4HT+a7Y6PLkcc9vkUPAF0LHwmbMSP+HHXYfvWQwc9/ogM9rg 63 | i8faN77lzyrMsZGT2wbaK2Ormnpm+1vbyd4WsmtIgjnQ6aYPtLgHge+2F9wYy5iC 64 | eDnrS113mPGqNQKCAQEA4Fez0Hsd13W0ZZYZFV4WGZmy3GywlCt+Zk6ExsteK/6c 65 | lGJnFuKd+QRgYjsUBB6P/vogbr3lEZf3blgZWjKDA35t9/j2f4jMApS7ohNJavrU 66 | l3hvip+DFMKSEj46t/uI+JixQSPsUssyseYGONIExNcamtwRAqrAZ1h7qc8OBsQZ 67 | rRKWsVRP36isZcgGRt9g6/VeQOGeFKfnCw+C96WUmk3ocWtaGQcVDkzx65EATBDt 68 | f0IY0z6+WE1KMS1UH+j6l3iAPCCm0JHjHnv+7NXXZ21AImzpw9B5RyZaQMliuewq 69 | cEK9rLQwSr7fCkTP6TqfgRDJi2RDYKJxSG2aJftbqQKCAQEAkPJL6ffkEpOrFh+L 70 | O6R/sICVqjL/VqJbT05BmzXHPqJ08RWEmZO1GBlRqLpiht813aQzCYAnu/5LnjKW 71 | CyTVUEdYxy+f8GOhVCi9sGwCHUpPbIKIq+iNTBTIv+VUjVwYOHVKphVoV73DhXlW 72 | Bvf6VXtNx8i4bdKLJCBpaS3j8W3wBy+bhpfhnW7ngmAYf53kdCknAHo+bg+Z/rZd 73 | QTUJYQqnl35t5jTtQqYwy+NvBwpMN/t5lySx/s4+2JdWc6zHv/VrgyVHezSuP2R6 74 | WmPmM57ioyH8TrO+EgB9AJmvB0iDSKOLumx3/Qfn4rF8zs4ZWgnQfeJKsEJGDogo 75 | kI93/QKCAQAq6PaKFJEe+VaHP1aaft//TE1fzdkoIMYUOVv2uhvxZKt9ap+Tmc5r 76 | tCAfoj+SLQehs/I+mG5pMZmwOlMC6CxMmUsHnLl/Y8C6UXSow3BqR3aOeV6WmrAP 77 | ZR+SYEtYooL1V8g1VzH4yieVAYPlov6JCBaLI9dXIgvkl/+qqI5PKHP1ym6BqCYH 78 | Po9KTzIsV2XVT9pU4pmi+F5YG56xxAvQzm8wqDwbLStaqXQr6Ett5Jg/SEfjBtO9 79 | ebrUpEzsx5R5w7FCeqojxUcXz+QI90fuT4uFbYa0s5R699C1BRH5kSyQulanWQ0z 80 | amq8gS/kmairHfk5EzANBgRrw1FfeXQ4 81 | -----END PRIVATE KEY----- 82 | -------------------------------------------------------------------------------- /init/template/haproxy-public-stats/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mnahkies/shoe-string-server/a35caae845b9f05df92478edc9e7eb82968b65c9/init/template/haproxy-public-stats/.gitkeep -------------------------------------------------------------------------------- /init/template/haproxy-public/haproxy.cfg.template: -------------------------------------------------------------------------------- 1 | global 2 | log 127.0.0.1 local2 3 | maxconn 4000 4 | 5 | # turn on stats unix socket, mostly-ro 6 | stats socket /var/lib/haproxy-socket/stats.sock level operator user haproxy group haproxy 7 | 8 | ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305 9 | ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 10 | ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 11 | 12 | ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305 13 | ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 14 | ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 15 | 16 | # openssl dhparam -out "$DATA_BASE_PATH"/haproxy-public/dhparam 2048 17 | ssl-dh-param-file /usr/local/etc/haproxy/dhparam 18 | 19 | resolvers docker_resolver 20 | nameserver dns 127.0.0.11:53 21 | 22 | #--------------------------------------------------------------------- 23 | # common defaults that all the 'listen' and 'backend' sections will 24 | # use if not designated in their block 25 | #--------------------------------------------------------------------- 26 | defaults 27 | mode http 28 | log global 29 | option httplog 30 | option dontlognull 31 | option http-server-close 32 | option forwardfor except 127.0.0.0/8 33 | option redispatch 34 | retries 3 35 | timeout http-request 10s 36 | timeout queue 1m 37 | timeout connect 10s 38 | timeout client 1m 39 | timeout server 1m 40 | timeout http-keep-alive 10s 41 | timeout check 10s 42 | maxconn 3000 43 | 44 | # allows proxy to start immediately when some/all backends are not available 45 | default-server init-addr none 46 | 47 | #--------------------------------------------------------------------- 48 | # main frontend which proxys to the backends 49 | #--------------------------------------------------------------------- 50 | frontend external 51 | bind *:80 52 | # note: this must be commented for first start, when no certificates exist. 53 | bind *:443 ssl crt /usr/local/etc/ssl/certs/ alpn h2,http/1.1 54 | 55 | filter compression 56 | compression algo gzip 57 | 58 | acl acme path_beg /.well-known/acme-challenge 59 | 60 | http-request redirect scheme https if !acme !{ ssl_fc } 61 | http-request set-header X-Forwarded-Proto https if { ssl_fc } 62 | use_backend letsencrypt if acme 63 | 64 | {{USE_BACKENDS}} 65 | 66 | default_backend no-match 67 | 68 | #--------------------------------------------------------------------- 69 | # stats frontend which powers prometheus metrics 70 | #--------------------------------------------------------------------- 71 | frontend stats 72 | bind *:8404 73 | http-request use-service prometheus-exporter if { path /metrics } 74 | stats enable 75 | stats uri /stats 76 | stats refresh 10s 77 | 78 | {{BACKENDS}} 79 | 80 | backend no-match 81 | http-request deny deny_status 400 82 | 83 | backend letsencrypt 84 | balance roundrobin 85 | server letsencrypt certbot:80 resolvers docker_resolver resolve-prefer ipv4 86 | -------------------------------------------------------------------------------- /init/template/letsencrypt/etc/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mnahkies/shoe-string-server/a35caae845b9f05df92478edc9e7eb82968b65c9/init/template/letsencrypt/etc/.gitkeep -------------------------------------------------------------------------------- /init/template/letsencrypt/var/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mnahkies/shoe-string-server/a35caae845b9f05df92478edc9e7eb82968b65c9/init/template/letsencrypt/var/.gitkeep -------------------------------------------------------------------------------- /init/template/prometheus/conf/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s # Default scrape interval, change as needed 3 | evaluation_interval: 15s # Default evaluation interval, change as needed 4 | 5 | scrape_configs: 6 | - job_name: 'prometheus' 7 | static_configs: 8 | - targets: [ 'localhost:9090' ] 9 | - job_name: node 10 | static_configs: 11 | - targets: [ 'monitoring_node_exporter:9100' ] 12 | - job_name: postgres 13 | static_configs: 14 | - targets: [ "monitoring_postgres_exporter:9187" ] 15 | - job_name: haproxy 16 | static_configs: 17 | - targets: [ 18 | 'proxy_internal:8404', 19 | 'proxy_public:8404' 20 | ] 21 | - job_name: cadvisor 22 | static_configs: 23 | - targets: [ 'monitoring_cadvisor:8080' ] 24 | -------------------------------------------------------------------------------- /init/template/prometheus/data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mnahkies/shoe-string-server/a35caae845b9f05df92478edc9e7eb82968b65c9/init/template/prometheus/data/.gitkeep -------------------------------------------------------------------------------- /init/template/verdaccio/conf/config.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # This is the config file used for the docker images. 3 | # It allows all users to do anything, so don't use it on production systems. 4 | # 5 | # Do not configure host and port under `listen` in this file 6 | # as it will be ignored when using docker. 7 | # see https://github.com/verdaccio/verdaccio/blob/master/wiki/docker.md#docker-and-custom-port-configuration 8 | # 9 | # Look here for more config file examples: 10 | # https://github.com/verdaccio/verdaccio/tree/master/conf 11 | # 12 | 13 | # path to a directory with all packages 14 | storage: /verdaccio/storage 15 | 16 | auth: 17 | htpasswd: 18 | file: /verdaccio/conf/htpasswd 19 | # Maximum amount of users allowed to register, defaults to "+inf". 20 | # You can set this to -1 to disable registration. 21 | #max_users: 1000 22 | security: 23 | api: 24 | jwt: 25 | sign: 26 | expiresIn: 60d 27 | notBefore: 1 28 | web: 29 | sign: 30 | expiresIn: 7d 31 | 32 | # a list of other known repositories we can talk to 33 | uplinks: 34 | npmjs: 35 | url: https://registry.npmjs.org/ 36 | 37 | packages: 38 | '@*/*': 39 | # scoped packages 40 | access: $all 41 | publish: $all 42 | proxy: npmjs 43 | 44 | '**': 45 | # allow all users (including non-authenticated users) to read and 46 | # publish all packages 47 | # 48 | # you can specify usernames/groupnames (depending on your auth plugin) 49 | # and three keywords: "$all", "$anonymous", "$authenticated" 50 | access: $all 51 | 52 | # allow all known users to publish packages 53 | # (anyone can register by default, remember?) 54 | publish: $all 55 | 56 | # if package is not available locally, proxy requests to 'npmjs' registry 57 | proxy: npmjs 58 | 59 | # To use `npm audit` uncomment the following section 60 | middlewares: 61 | audit: 62 | enabled: true 63 | 64 | # log settings 65 | logs: 66 | - { type: stdout, format: pretty, level: trace } 67 | #- {type: file, path: verdaccio.log, level: info} 68 | -------------------------------------------------------------------------------- /init/template/verdaccio/conf/htpasswd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mnahkies/shoe-string-server/a35caae845b9f05df92478edc9e7eb82968b65c9/init/template/verdaccio/conf/htpasswd -------------------------------------------------------------------------------- /init/template/verdaccio/storage/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mnahkies/shoe-string-server/a35caae845b9f05df92478edc9e7eb82968b65c9/init/template/verdaccio/storage/.gitkeep -------------------------------------------------------------------------------- /init/test-example-application.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Example of how to test the proxied applications prior to 4 | # setting up DNS records etc. You can also run this from another 5 | # machine with the appropriate public ip 6 | curl 127.0.0.1:80 --header "Host: application.example.com" 7 | -------------------------------------------------------------------------------- /lib/load-applications.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const yaml = require('./js-yaml') 5 | 6 | /** 7 | * @param directory string 8 | * @returns {{externalHostNames: (*|*[]), containerPort: (*|number), internalHostName: string | (() => string)}[]} 9 | */ 10 | module.exports.loadApplications = function loadApplications(directory) { 11 | const applications = [] 12 | const applicationFilenames = fs.readdirSync(directory) 13 | 14 | for (const applicationFilename of applicationFilenames) { 15 | if (!applicationFilename.endsWith('.yaml') && !applicationFilename.endsWith('.yml')) { 16 | continue 17 | } 18 | 19 | const application = yaml.load(fs.readFileSync(path.join(directory, applicationFilename), 'utf-8')) 20 | applications.push(application) 21 | } 22 | 23 | /* 24 | interface Application { 25 | x-container-port: number; 26 | x-external-host-names: string[]; 27 | services: { 28 | application: { 29 | hostname: string 30 | container_name: string 31 | } 32 | } 33 | } 34 | */ 35 | 36 | return applications.map(app => { 37 | const internalHostName = app?.services?.application?.hostname 38 | const externalHostNames = app['x-external-host-names'] || [] 39 | const containerPort = app['x-container-port'] ?? 80 40 | 41 | const result = { 42 | internalHostName, 43 | externalHostNames, 44 | containerPort, 45 | } 46 | 47 | process.stderr.write(`found application ${JSON.stringify(result)}\n`) 48 | return result 49 | }).filter(it => { 50 | if (!it.internalHostName) { 51 | process.stderr.write(`ERROR: skipping application as could not find internal hostname ${JSON.stringify(it)}\n`) 52 | return false 53 | } 54 | 55 | if (!it.externalHostNames.length) { 56 | process.stderr.write(`INFO: skipping application as had no external hostnames (${it.internalHostName})\n`) 57 | return false 58 | } 59 | 60 | return true 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /proxy/haproxy-generate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | const {loadApplications} = require('../lib/load-applications') 5 | 6 | function generateUseBackends(applications) { 7 | return applications.map(app => { 8 | return app.externalHostNames.map(hostName => { 9 | return ` use_backend ${app.internalHostName} if { hdr_beg(host) -i ${hostName} }` 10 | }).join('\n') 11 | }).join('\n\n') 12 | } 13 | 14 | function generateBackends(applications) { 15 | return applications.map(app => { 16 | return [ 17 | `backend ${app.internalHostName}`, 18 | ` balance roundrobin`, 19 | ` server ${app.internalHostName} ${app.internalHostName}:${app.containerPort} resolvers docker_resolver` 20 | ].join('\n') 21 | }).join('\n\n') 22 | } 23 | 24 | 25 | function main() { 26 | const applicationsDirectory = process.argv[2] 27 | const proxyConfigDirectory = process.argv[3] 28 | 29 | console.info(`generating proxy configuration from directory ${applicationsDirectory} to ${proxyConfigDirectory}`) 30 | 31 | const applications = loadApplications(applicationsDirectory) 32 | const template = fs.readFileSync(path.join(proxyConfigDirectory, 'haproxy.cfg.template'), 'utf-8') 33 | 34 | const warning = `#--------------------------------------------------------------------- 35 | # WARNING: automatically generated, please edit the template file haproxy.cfg.template 36 | #--------------------------------------------------------------------- 37 | 38 | ` 39 | 40 | const output = warning + template 41 | .replace('{{USE_BACKENDS}}', generateUseBackends(applications)) 42 | .replace('{{BACKENDS}}', generateBackends(applications)) 43 | 44 | fs.writeFileSync(path.join(proxyConfigDirectory, 'haproxy.cfg'), output, {encoding: 'utf-8'}) 45 | } 46 | 47 | 48 | main() 49 | -------------------------------------------------------------------------------- /proxy/reload-haproxy-config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | source "${__dir}"/../config.sh 7 | 8 | set -x 9 | 10 | docker run -it --rm --user="1000:1000" \ 11 | -v="${SCRIPT_BASE_PATH}:/code" \ 12 | -v="${DATA_BASE_PATH}:/home/node" node:16-alpine /code/proxy/haproxy-generate.js /home/node/applications-public /home/node/haproxy-public 13 | 14 | if [ "$(docker ps -aq -f status=running -f name=proxy_public)" ]; then 15 | docker kill -s HUP proxy_public 16 | fi 17 | 18 | docker run -it --rm --user="1000:1000" \ 19 | -v="${SCRIPT_BASE_PATH}:/code" \ 20 | -v="${DATA_BASE_PATH}:/home/node" node:16-alpine /code/proxy/haproxy-generate.js /home/node/applications-internal /home/node/haproxy-internal 21 | 22 | if [ "$(docker ps -aq -f status=running -f name=proxy_internal)" ]; then 23 | docker kill -s HUP proxy_internal 24 | fi 25 | -------------------------------------------------------------------------------- /ssl-internal/issue-new-ssl-cert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | source "${__dir}"/../config.sh 7 | 8 | set -x 9 | 10 | LEGO_DIR="${DATA_BASE_PATH}"/lego 11 | DOMAINS=$1 12 | 13 | echo "issuing cert for ${DOMAINS} using ${LETS_ENCRYPT_EMAIL_ADDRESS}" 14 | 15 | docker run -it --rm \ 16 | -v "${LEGO_DIR}:/var/lego" \ 17 | --name lego \ 18 | -e ZONOMI_API_KEY="${ZONOMI_API_KEY}" \ 19 | -e ZONOMI_PROPAGATION_TIMEOUT=600 \ 20 | -e ZONOMI_TTL=60 \ 21 | goacme/lego:v4.22 \ 22 | --path /var/lego \ 23 | --email "${LETS_ENCRYPT_EMAIL_ADDRESS}" \ 24 | --dns.resolvers 8.8.8.8 \ 25 | --dns zonomi --domains "${DOMAINS}" \ 26 | --accept-tos \ 27 | run 28 | -------------------------------------------------------------------------------- /ssl-internal/push-cert-to-haproxy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | source "${__dir}"/../config.sh 7 | 8 | set -x 9 | 10 | LEGO_DIR="${DATA_BASE_PATH}"/lego/certificates 11 | 12 | NAME=$1 13 | DESTINATION="${DATA_BASE_PATH}/haproxy-internal-ssl" 14 | 15 | echo "pushing cert to haproxy ${LEGO_DIR}/${NAME}" 16 | 17 | bash -c "sudo cat ${LEGO_DIR}/${NAME}.crt ${LEGO_DIR}/${NAME}.issuer.crt ${LEGO_DIR}/${NAME}.key > ${DESTINATION}/${NAME}.pem" 18 | -------------------------------------------------------------------------------- /ssl-internal/renew-ssl-cert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | source "${__dir}"/../config.sh 7 | 8 | set -x 9 | 10 | LEGO_DIR="${DATA_BASE_PATH}"/lego 11 | DOMAINS=$1 12 | 13 | echo "issuing cert for ${DOMAINS} using ${LETS_ENCRYPT_EMAIL_ADDRESS}" 14 | 15 | docker run -it --rm --name lego \ 16 | -v "${LEGO_DIR}:/var/lego" \ 17 | --name lego \ 18 | -e ZONOMI_API_KEY="${ZONOMI_API_KEY}" \ 19 | -e ZONOMI_PROPAGATION_TIMEOUT=1200 \ 20 | -e ZONOMI_TTL=60 \ 21 | goacme/lego \ 22 | --path /var/lego \ 23 | --email "${LETS_ENCRYPT_EMAIL_ADDRESS}" \ 24 | --dns.resolvers 8.8.8.8 --dns.resolvers 8.8.4.4 \ 25 | --dns zonomi --domains "${DOMAINS}" \ 26 | --accept-tos \ 27 | renew 28 | -------------------------------------------------------------------------------- /ssl-public/generate-issue-all-ssl-certs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const {loadApplications} = require('../lib/load-applications') 3 | const DIR = process.argv[2] 4 | 5 | loadApplications(DIR) 6 | .map(it => it.externalHostNames.join(',')) 7 | .forEach(it => console.log(`issue-new-ssl-cert.sh ${it}; 8 | push-cert-to-haproxy.sh ${it.split(',')[0]}`)) 9 | -------------------------------------------------------------------------------- /ssl-public/generate-renew-all-ssl-certs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const {loadApplications} = require('../lib/load-applications') 3 | const DIR = process.argv[2] 4 | 5 | const applications = loadApplications(DIR) 6 | 7 | applications.map(it => it.externalHostNames.join(',')) 8 | .forEach(it => console.log(`push-cert-to-haproxy.sh ${it.split(',')[0]}`)) 9 | -------------------------------------------------------------------------------- /ssl-public/issue-all-ssl-certs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | source "${__dir}"/../config.sh 7 | 8 | set -x 9 | 10 | docker run -i --rm --user="1000:1000" \ 11 | -v="${SCRIPT_BASE_PATH}:/code" \ 12 | -v="${DATA_BASE_PATH}:/home/node" node:16-alpine /code/ssl-public/generate-issue-all-ssl-certs.js /home/node/applications-public | while read -r line; do 13 | "${__dir}"/$line 14 | done 15 | 16 | "${__dir}"/../proxy/reload-haproxy-config.sh 17 | -------------------------------------------------------------------------------- /ssl-public/issue-new-ssl-cert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | source "${__dir}"/../config.sh 7 | 8 | set -x 9 | 10 | LETS_ENCRYPT_DIR="${DATA_BASE_PATH}"/letsencrypt 11 | 12 | DOMAINS=$1 13 | 14 | echo "issuing cert for ${DOMAINS}" 15 | 16 | docker run -it --rm --name certbot \ 17 | -v "${LETS_ENCRYPT_DIR}/etc:/etc/letsencrypt" \ 18 | -v "${LETS_ENCRYPT_DIR}/var:/var/lib/letsencrypt" \ 19 | --network=main \ 20 | --name certbot \ 21 | certbot/certbot certonly \ 22 | --standalone \ 23 | -m "${LETS_ENCRYPT_EMAIL_ADDRESS}" \ 24 | --non-interactive \ 25 | --agree-tos \ 26 | -d ${DOMAINS} 27 | -------------------------------------------------------------------------------- /ssl-public/issue-new-ssl-cert.staging.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | source "${__dir}"/../config.sh 7 | 8 | set -x 9 | 10 | LETS_ENCRYPT_DIR="${DATA_BASE_PATH}"/letsencrypt 11 | 12 | DOMAINS=$1 13 | 14 | docker run -it --rm --name certbot \ 15 | -v "${LETS_ENCRYPT_DIR}/etc:/etc/letsencrypt" \ 16 | -v "${LETS_ENCRYPT_DIR}/var:/var/lib/letsencrypt" \ 17 | --network=main \ 18 | --name certbot \ 19 | certbot/certbot certonly \ 20 | --standalone \ 21 | -m "${LETS_ENCRYPT_EMAIL_ADDRESS}" \ 22 | --non-interactive \ 23 | --agree-tos \ 24 | -d ${DOMAINS} \ 25 | --staging 26 | -------------------------------------------------------------------------------- /ssl-public/push-cert-to-haproxy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | source "${__dir}"/../config.sh 7 | 8 | set -x 9 | 10 | LETS_ENCRYPT_DIR="${DATA_BASE_PATH}"/letsencrypt/etc 11 | 12 | NAME=$1 13 | DESTINATION="${DATA_BASE_PATH}/haproxy-public-ssl" 14 | 15 | echo "pushing cert to haproxy ${LETS_ENCRYPT_DIR}/live/${NAME}" 16 | 17 | bash -c "sudo cat ${LETS_ENCRYPT_DIR}/live/${NAME}/fullchain.pem ${LETS_ENCRYPT_DIR}/live/${NAME}/privkey.pem > ${DESTINATION}/${NAME}.pem" 18 | -------------------------------------------------------------------------------- /ssl-public/renew-all-ssl-certs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | source "${__dir}"/../config.sh 7 | 8 | set -x 9 | 10 | LETS_ENCRYPT_DIR="${DATA_BASE_PATH}"/letsencrypt 11 | 12 | docker run -it --rm --name certbot \ 13 | -v "${LETS_ENCRYPT_DIR}/etc:/etc/letsencrypt" \ 14 | -v "${LETS_ENCRYPT_DIR}/var:/var/lib/letsencrypt" \ 15 | --network=main \ 16 | --name certbot \ 17 | certbot/certbot renew \ 18 | --standalone \ 19 | -m "${LETS_ENCRYPT_EMAIL_ADDRESS}" \ 20 | --non-interactive \ 21 | --agree-tos 22 | 23 | docker run -i --rm --user="1000:1000" \ 24 | -v="${SCRIPT_BASE_PATH}:/code" \ 25 | -v="${DATA_BASE_PATH}:/home/node" node:16-alpine /code/ssl-public/generate-renew-all-ssl-certs.js /home/node/applications-public | while read -r line; do 26 | "${__dir}"/$line 27 | done 28 | 29 | "${SCRIPT_BASE_PATH}"/proxy/reload-haproxy-config.sh 30 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | source "${__dir}"/config.sh 7 | 8 | set -x 9 | 10 | "${__dir}"/proxy/reload-haproxy-config.sh 11 | 12 | docker network prune -f 13 | 14 | docker network create --driver=bridge main || true 15 | docker network create --driver=bridge internal || true 16 | 17 | for name in ${ADDITIONAL_NETWORKS:-()}; do 18 | docker network create --driver=bridge "$name" || true 19 | done 20 | 21 | sleep 2 22 | 23 | docker compose --file "${__dir}"/docker-compose.yaml -p cluster-core up -d --force-recreate --remove-orphans 24 | 25 | "${__dir}"/applications/watchdog-up.sh 26 | -------------------------------------------------------------------------------- /stop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | source "${__dir}"/config.sh 7 | 8 | set -x 9 | 10 | docker compose -f "${__dir}"/docker-compose.yaml -p cluster-core down --remove-orphans 11 | 12 | "${__dir}"/applications/watchdog-down.sh 13 | 14 | docker network prune -f 15 | -------------------------------------------------------------------------------- /util/backup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -exo pipefail 4 | 5 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | source "${__dir}"/../config.sh 7 | 8 | OUTPUT_PATH=$1 9 | 10 | sudo tar cvjf "$OUTPUT_PATH"/"$(date -Iseconds)".bz2 "$DATA_BASE_PATH" 11 | --------------------------------------------------------------------------------