From 2452b3dec4299587868924a6988d6e1ddfc48d47 Mon Sep 17 00:00:00 2001 From: mifi Date: Fri, 13 Feb 2026 15:33:02 -0300 Subject: [PATCH] Update readme, fix docker-compose typo, add pipelines --- .woodpecker/build.yml | 159 +++++++++++++++++++++++++++++++++++++++++ .woodpecker/ci.yml | 119 ++++++++++++++++++++++++++++++ .woodpecker/deploy.yml | 70 ++++++++++++++++++ README.md | 69 +++++++++++------- docker-compose.yml | 2 +- 5 files changed, 393 insertions(+), 26 deletions(-) create mode 100644 .woodpecker/build.yml create mode 100644 .woodpecker/ci.yml create mode 100644 .woodpecker/deploy.yml diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml new file mode 100644 index 0000000..4ef6440 --- /dev/null +++ b/.woodpecker/build.yml @@ -0,0 +1,159 @@ +# Deploy: build image, push to registry, trigger Portainer stack redeploy. +# Runs on push/tag/manual to main only, after ci workflow succeeds. +when: + - branch: main + event: [push, tag, manual] + - event: deployment + evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"' + +depends_on: + - ci + +steps: + - name: Site build + image: node:22-alpine + commands: + - corepack enable + - corepack prepare pnpm@10.29.2 --activate + - pnpm install --frozen-lockfile + - pnpm build + + - name: Docker image build + image: docker:latest + depends_on: + - Site build + environment: + REGISTRY_REPO: git.mifi.dev/mifi-holdings/landing + DOCKER_API_VERSION: '1.43' + DOCKER_BUILDKIT: '1' + volumes: + - /var/run/docker.sock:/var/run/docker.sock + commands: + - set -e + - echo "=== Building Docker image (BuildKit) ===" + - 'echo "Commit SHA: ${CI_COMMIT_SHA:0:8}"' + - 'echo "Registry repo: $REGISTRY_REPO"' + - | + build() { + docker build \ + --progress=plain \ + --platform=linux/amd64 \ + --tag $REGISTRY_REPO:${CI_COMMIT_SHA} \ + --tag $REGISTRY_REPO:latest \ + --label "git.commit=${CI_COMMIT_SHA}" \ + --label "git.branch=${CI_COMMIT_BRANCH}" \ + . + } + for attempt in 1 2 3; do + echo "Build attempt $attempt/3" + if build; then + echo "βœ“ Docker image built successfully" + exit 0 + fi + echo "Build attempt $attempt failed, retrying in 30s..." + sleep 30 + done + echo "All build attempts failed" + exit 1 + + - name: Send Docker Image Build Status Notification (success) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_pushes_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Docker image build success πŸŽ‰"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL" + depends_on: + - Site build + - Docker image build + when: + - status: [success] + + - name: Send Docker Image Build Status Notification (failure) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_pushes_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Docker image build failure πŸ’©"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL" + depends_on: + - Site build + - Docker image build + when: + - status: [failure] + + - name: Push to registry + image: docker:latest + environment: + DOCKER_API_VERSION: '1.43' + REGISTRY_URL: git.mifi.dev + REGISTRY_REPO: git.mifi.dev/mifi-holdings/landing + REGISTRY_USERNAME: + from_secret: gitea_registry_username + REGISTRY_PASSWORD: + from_secret: gitea_package_token + volumes: + - /var/run/docker.sock:/var/run/docker.sock + commands: + - set -e + - echo "=== Pushing to registry ===" + - 'echo "Registry: $REGISTRY_URL"' + - 'echo "Repository: $REGISTRY_REPO"' + - | + echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" \ + -u "$REGISTRY_USERNAME" \ + --password-stdin + - docker push $REGISTRY_REPO:${CI_COMMIT_SHA} + - docker push $REGISTRY_REPO:latest + - echo "βœ“ Images pushed successfully" + depends_on: + - Site build + - Docker image build + + - name: Send Push to Registry Status Notification (success) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_pushes_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Push to registry success πŸŽ‰"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL" + depends_on: + - Push to registry + when: + - status: [success] + + - name: Send Push to Registry Status Notification (failure) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_pushes_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Push to registry failure πŸ’©"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL" + depends_on: + - Push to registry + when: + - status: [failure] diff --git a/.woodpecker/ci.yml b/.woodpecker/ci.yml new file mode 100644 index 0000000..1ecd750 --- /dev/null +++ b/.woodpecker/ci.yml @@ -0,0 +1,119 @@ +# CI: lint and format check. Runs on every PR and every push to main. +when: + - event: pull_request + - branch: main + event: push + +steps: + - name: install + image: node:22-alpine + commands: + - corepack enable + - corepack prepare pnpm@10.29.2 --activate + - pnpm install --frozen-lockfile + + - name: Prettier Format Check + image: node:22-alpine + commands: + - corepack enable + - corepack prepare pnpm@10.29.2 --activate + - pnpm install --frozen-lockfile + - pnpm format:check + depends_on: + - install + + - name: Send Prettier Format Check Status Notification (failure) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_tests_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Prettier Format Check failure πŸ’©"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL + depends_on: + - Prettier Format Check + when: + - status: [failure] + + - name: Lint Check + image: node:22-alpine + commands: + - corepack enable + - corepack prepare pnpm@10.29.2 --activate + - pnpm install --frozen-lockfile + - pnpm lint + depends_on: + - install + + - name: Send Lint Status Notification (failure) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_tests_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Lint failure πŸ’©"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL + depends_on: + - Lint Check + when: + - status: [failure] + + + - name: Build + image: node:22-alpine + commands: + - corepack enable + - corepack prepare pnpm@10.29.2 --activate + - pnpm install --frozen-lockfile + - pnpm build + depends_on: + - install + + - name: Send Build Status Notification (failure) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_tests_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Build failure πŸ’©"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL + depends_on: + - Build + when: + - status: [failure] + + - name: Send CI Pipeline Status Notification (success) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_tests_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] CI pipeline success πŸŽ‰"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL + depends_on: + - install + - Prettier Format Check + - Lint Check + - Build + when: + - status: [success] diff --git a/.woodpecker/deploy.yml b/.woodpecker/deploy.yml new file mode 100644 index 0000000..0ff9939 --- /dev/null +++ b/.woodpecker/deploy.yml @@ -0,0 +1,70 @@ +# Deploy: build image, push to registry, trigger Portainer stack redeploy. +# Runs on push/tag/manual to main only, after ci workflow succeeds. +skip_clone: true +# Use writable workspace when clone is skipped (no root clone step to create /woodpecker/src) +workspace: + base: /tmp + path: deploy +when: + - branch: main + event: [push, tag, manual] + - event: deployment + evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"' + +depends_on: + - ci + +steps: + - name: Trigger Portainer stack redeploy + image: curlimages/curl:latest + environment: + PORTAINER_WEBHOOK_URL: + from_secret: portainer_webhook_url + commands: + - set -e + - echo "=== Triggering Portainer stack redeploy ===" + - | + resp=$(curl -s -w "\n%{http_code}" -X POST "$PORTAINER_WEBHOOK_URL") + body=$(echo "$resp" | head -n -1) + code=$(echo "$resp" | tail -n 1) + if [ "$code" != "200" ] && [ "$code" != "204" ]; then + echo "Webhook failed (HTTP $code): $body" + exit 1 + fi + echo "βœ“ Portainer redeploy triggered (HTTP $code)" + + - name: Send Deploy Status Notification (success) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_pushes_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Production Deploy success πŸŽ‰"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL" + depends_on: + - Trigger Portainer stack redeploy + when: + - status: [success] + + - name: Send Deploy Status Notification (failure) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_pushes_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Production Deploy failure πŸ’©"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL" + depends_on: + - Trigger Portainer stack redeploy + when: + - status: [failure] diff --git a/README.md b/README.md index ed408ed..83fb4f8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # mifi.holdings β€” Landing Page -Static landing site for **mifi.holdings** (and www). Plain HTML/CSS, no framework; served by **nginx** in Docker behind **Traefik**, with CI/CD via **Woodpecker** and deployment via **Portainer**. +Static landing site for **mifi.holdings** (and www). Plain HTML/CSS/JS source; a **build step** produces minified assets and inlines critical CSS. Served by **nginx** in Docker behind **Traefik**, with CI/CD via **Woodpecker** and deployment via **Portainer**. --- @@ -14,6 +14,7 @@ Static landing site for **mifi.holdings** (and www). Plain HTML/CSS, no framewor | **CI/CD** | Woodpecker (ci β†’ build β†’ deploy) | | **Registry** | `git.mifi.dev/mifi-holdings/landing` | | **Package manager** | pnpm | +| **Build output** | `dist/` (gitignored) | --- @@ -28,11 +29,11 @@ Static landing site for **mifi.holdings** (and www). Plain HTML/CSS, no framewor β”‚ Docker container β”‚ mifi-holdings-landing β”‚ nginx:alpine β”‚ port 80 β”‚ /usr/share/ β”‚ - β”‚ nginx/html ← β”‚ static files from image + β”‚ nginx/html ← β”‚ built output from dist/ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` -- **Build**: Docker image = `nginx:alpine` + project `nginx/conf.d/` + `src/` copied into `/usr/share/nginx/html`. +- **Build**: `pnpm build` copies `src/` β†’ `dist/`, minifies JS/CSS, inlines critical CSS (Critters). Docker image = `nginx:alpine` + `nginx/conf.d/` + **`dist/`** into `/usr/share/nginx/html`. - **Run**: Single service on external network `marina-net`; Traefik routes `mifi.holdings` and `www.mifi.holdings` to this container (HTTPS, `security-prison@file` middleware). --- @@ -41,52 +42,70 @@ Static landing site for **mifi.holdings** (and www). Plain HTML/CSS, no framewor ``` . -β”œβ”€β”€ src/ # Static site (served as-is) +β”œβ”€β”€ src/ # Source (HTML, CSS, JS) β”‚ β”œβ”€β”€ index.html -β”‚ └── css/ -β”‚ └── style.css +β”‚ └── assets/ +β”‚ β”œβ”€β”€ css/style.css +β”‚ β”œβ”€β”€ js/ga-init.js +β”‚ └── images/ # favicon, etc. +β”œβ”€β”€ scripts/ +β”‚ └── build.js # Build: copy, minify, inline critical CSS +β”œβ”€β”€ dist/ # Build output (gitignored) β”œβ”€β”€ nginx/ β”‚ └── conf.d/ β”‚ └── default.conf # nginx server config (cache rules, SPA fallback) β”œβ”€β”€ .woodpecker/ -β”‚ β”œβ”€β”€ ci.yml # Lint + format check (PR + push to main) -β”‚ β”œβ”€β”€ build.yml # Build image, push to registry (main) +β”‚ β”œβ”€β”€ ci.yml # Lint, format, build check (PR + push to main) +β”‚ β”œβ”€β”€ build.yml # Site build β†’ Docker image β†’ push (main) β”‚ └── deploy.yml # Trigger Portainer redeploy (main) β”œβ”€β”€ docker-compose.yml # Service + Traefik labels for production -β”œβ”€β”€ Dockerfile # nginx + config + src -β”œβ”€β”€ package.json # pnpm scripts: format, lint, docker build/push +β”œβ”€β”€ Dockerfile # nginx + config + dist (not src) +β”œβ”€β”€ package.json # pnpm scripts: build, format, lint, docker └── README.md ``` --- +## Build (pnpm build) + +- **Output**: `dist/` (gitignored). Do not edit `dist/`; it is generated. +- **Steps** (see `scripts/build.js`): + 1. Clean and copy `src/` β†’ `dist/`. + 2. Minify all `.js` (terser) and `.css` (clean-css). + 3. Inline critical CSS with **Critters** (lazy-loads the rest; no browser required, so it works in CI). +- **When**: Run before `docker build`. CI and the build pipeline both run `pnpm build` before packaging. + +--- + ## Tech stack -- **Frontend**: Static HTML + CSS only (no JS build step). +- **Frontend**: Static HTML + CSS + JS in `src/`; production build minifies and inlines critical CSS. - **Server**: nginx (Alpine) in Docker. -- **Tooling**: **pnpm** (format with Prettier, lint with yamllint for `.woodpecker/*.yml` and `docker-compose.yml`). -- **Deployment**: Docker image β†’ Gitea registry β†’ Portainer stack redeploy. +- **Tooling**: **pnpm**; Prettier (format); ESLint (JS), Stylelint (CSS), yamllint (YAML); **terser**, **clean-css**, **critters** (build). +- **Deployment**: Docker image (from `dist/`) β†’ Gitea registry β†’ Portainer stack redeploy. --- ## Local development -- **Dependencies**: `pnpm install` (only devDependencies: Prettier, yaml-lint). +- **Dependencies**: `pnpm install`. - **Format**: `pnpm format` / `pnpm format:check`. -- **Lint**: `pnpm lint` (yamllint on Woodpecker and docker-compose YAML). +- **Lint**: `pnpm lint` (ESLint for `src/**/*.js`, Stylelint for `src/**/*.css`, yamllint for Woodpecker + docker-compose). Use `pnpm lint:fix` to auto-fix where supported. +- **Build**: `pnpm build` β†’ produces `dist/`. Required before building the Docker image. - **Run locally (Docker)**: - - Build: `pnpm docker:build` (or `docker build --platform linux/amd64 -t git.mifi.dev/mifi-holdings/landing:latest .`). + - Build site: `pnpm build`. + - Image: `pnpm docker:build` (or `docker build --platform linux/amd64 -t git.mifi.dev/mifi-holdings/landing:latest .`). - Run: use `docker-compose up` **only** on a host that has `marina-net` and Traefik; otherwise run the image with a port map and open `http://localhost:`. -There is no dev server in this repo; edit `src/` and refresh the browser or rebuild the image to test. +No dev server; edit `src/` and run `pnpm build` (and optionally `docker run` or a local static server on `dist/`) to test. --- ## Docker -- **Dockerfile**: Copies `nginx/conf.d/` and `src/` into an `nginx:alpine` image. No multi-stage build; the image is the final runtime. +- **Dockerfile**: Copies `nginx/conf.d/` and **`dist/`** (not `src/`) into an `nginx:alpine` image. Run `pnpm build` first so `dist/` exists. - **Image**: Tagged as `git.mifi.dev/mifi-holdings/landing:latest` (and `:` in CI). -- **Local build/push**: `pnpm docker:build`, `pnpm docker:push` (requires login to `git.mifi.dev`). +- **Local build/push**: `pnpm build` β†’ `pnpm docker:build` β†’ `pnpm docker:push` (requires login to `git.mifi.dev`). --- @@ -96,11 +115,11 @@ Three pipelines: 1. **ci** (`.woodpecker/ci.yml`) - **When**: Every PR and every push to `main`. - - **Steps**: Install deps β†’ Prettier format check β†’ yamllint (Woodpecker + docker-compose). Mattermost notifications on success/failure. + - **Steps**: Install deps β†’ Prettier format check β†’ lint (ESLint, Stylelint, yamllint) β†’ **Build** (`pnpm build`). Mattermost notifications on success/failure. 2. **build** (`.woodpecker/build.yml`) - **When**: Push/tag/manual on `main` (and deployment to production); **depends_on: ci**. - - **Steps**: Build Docker image (linux/amd64, up to 3 retries) β†’ push `:latest` and `:` to `git.mifi.dev/mifi-holdings/landing`. Mattermost notifications. + - **Steps**: **Site build** (`pnpm install`, `pnpm build`) β†’ Docker image build (linux/amd64, up to 3 retries) β†’ push `:latest` and `:` to `git.mifi.dev/mifi-holdings/landing`. Mattermost notifications. 3. **deploy** (`.woodpecker/deploy.yml`) - **When**: Same as build (main / production deploy); **depends_on: ci**. @@ -128,7 +147,7 @@ To deploy manually: pull the latest image and redeploy the stack in Portainer, o ## nginx behavior -- **Root**: `/usr/share/nginx/html` (contents of `src/`). +- **Root**: `/usr/share/nginx/html` (contents of **`dist/`** after build). - **HTML**: `Cache-Control: public, no-cache` so updates are visible quickly. - **JS/CSS**: Long cache, `immutable` for hashed/versioned assets. - **Images/fonts**: Cached (30d / 1y). @@ -138,6 +157,6 @@ To deploy manually: pull the latest image and redeploy the stack in Portainer, o ## Summary -- **What it is**: Static landing for mifi.holdings. -- **How it runs**: nginx in Docker, fronted by Traefik on `marina-net`. -- **How it’s updated**: Push to `main` β†’ Woodpecker runs ci, build (image + push), deploy (Portainer webhook); Mattermost reports status. +- **What it is**: Static landing for mifi.holdings; source in `src/`, built output in `dist/`. +- **How it runs**: nginx in Docker serving `dist/`, fronted by Traefik on `marina-net`. +- **How it’s updated**: Push to `main` β†’ Woodpecker runs ci (lint, format, **build**), then build pipeline (**site build** β†’ Docker image β†’ push), then deploy (Portainer webhook); Mattermost reports status. diff --git a/docker-compose.yml b/docker-compose.yml index 3d0d9c3..a66504b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: - 'traefik.docker.network=marina-net' - 'traefik.http.routers.holdings-landing.rule=Host(`mifi.holdings`) || Host(`www.mifi.holdings`)' - 'traefik.http.routers.holdings-landing.entrypoints=websecure' - - 'traefik.http.routers.holdings-landing.middlewares=ecurity-supermax-with-analytics@@file,redirect-www-to-non-www@file' + - 'traefik.http.routers.holdings-landing.middlewares=security-supermax-with-analytics@@file,redirect-www-to-non-www@file' - 'traefik.http.routers.holdings-landing.tls=true' - 'traefik.http.routers.holdings-landing.tls.certresolver=letsencrypt' - 'traefik.http.services.holdings-landing.loadbalancer.server.port=80'