From 92c5160f6e6d87624877b723346fe054b5076fd1 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 2 Jan 2026 14:38:52 -0600 Subject: [PATCH] Add Kubernetes deployment manifests for towerops.net --- k8s/Dockerfile | 101 +++++++++++++++++++++++++++++++++++++++++ k8s/certificate.yaml | 14 ++++++ k8s/deployment.yaml | 62 +++++++++++++++++++++++++ k8s/ingressroute.yaml | 48 ++++++++++++++++++++ k8s/kustomization.yaml | 9 ++++ k8s/namespace.yaml | 5 ++ k8s/service.yaml | 14 ++++++ 7 files changed, 253 insertions(+) create mode 100644 k8s/Dockerfile create mode 100644 k8s/certificate.yaml create mode 100644 k8s/deployment.yaml create mode 100644 k8s/ingressroute.yaml create mode 100644 k8s/kustomization.yaml create mode 100644 k8s/namespace.yaml create mode 100644 k8s/service.yaml diff --git a/k8s/Dockerfile b/k8s/Dockerfile new file mode 100644 index 00000000..a5dba55a --- /dev/null +++ b/k8s/Dockerfile @@ -0,0 +1,101 @@ +# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian +# instead of Alpine to avoid DNS resolution issues in production. +# +# https://hub.docker.com/r/hexpm/elixir/tags?name=ubuntu +# https://hub.docker.com/_/ubuntu/tags +# +# This file is based on these images: +# +# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image +# - https://hub.docker.com/_/debian/tags?name=trixie-20251229-slim - for the release image +# - https://pkgs.org/ - resource for finding needed packages +# - Ex: docker.io/hexpm/elixir:1.19.4-erlang-28.3-debian-trixie-20251229-slim +# +ARG ELIXIR_VERSION=1.19.4 +ARG OTP_VERSION=28.3 +ARG DEBIAN_VERSION=trixie-20251229-slim + +ARG BUILDER_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" +ARG RUNNER_IMAGE="docker.io/debian:${DEBIAN_VERSION}" + +FROM ${BUILDER_IMAGE} AS builder + +# install build dependencies +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential git \ + && rm -rf /var/lib/apt/lists/* + +# prepare build dir +WORKDIR /app + +# install hex + rebar +RUN mix local.hex --force \ + && mix local.rebar --force + +# set build ENV +ENV MIX_ENV="prod" + +# install mix dependencies +COPY mix.exs mix.lock ./ +RUN mix deps.get --only $MIX_ENV +RUN mkdir config + +# copy compile-time config files before we compile dependencies +# to ensure any relevant config change will trigger the dependencies +# to be re-compiled. +COPY config/config.exs config/${MIX_ENV}.exs config/ +RUN mix deps.compile + +RUN mix assets.setup + +COPY priv priv + +COPY lib lib + +# Compile the release +RUN mix compile + +COPY assets assets + +# compile assets +RUN mix assets.deploy + +# Changes to config/runtime.exs don't require recompiling the code +COPY config/runtime.exs config/ + +COPY rel rel +RUN mix release + +# start a new build stage so that the final image will only contain +# the compiled release and other runtime necessities +FROM ${RUNNER_IMAGE} AS final + +RUN apt-get update \ + && apt-get install -y --no-install-recommends libstdc++6 openssl libncurses6 locales ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Set the locale +RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen \ + && locale-gen + +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US:en +ENV LC_ALL=en_US.UTF-8 + +WORKDIR "/app" +RUN chown nobody /app + +# set runner ENV +ENV MIX_ENV="prod" + +# Only copy the final release from the build stage +COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/towerops ./ + +USER nobody + +# If using an environment that doesn't automatically reap zombie processes, it is +# advised to add an init process such as tini via `apt-get install` +# above and adding an entrypoint. See https://github.com/krallin/tini for details +# ENTRYPOINT ["/tini", "--"] + +CMD ["/app/bin/server"] diff --git a/k8s/certificate.yaml b/k8s/certificate.yaml new file mode 100644 index 00000000..6846c7e5 --- /dev/null +++ b/k8s/certificate.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: towerops-net-cert + namespace: towerops +spec: + secretName: towerops-net-tls + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + dnsNames: + - towerops.net + - www.towerops.net diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 00000000..b6990b81 --- /dev/null +++ b/k8s/deployment.yaml @@ -0,0 +1,62 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: towerops + namespace: towerops +spec: + replicas: 2 + selector: + matchLabels: + app: towerops + template: + metadata: + labels: + app: towerops + spec: + containers: + - name: towerops + image: registry.gitlab.com/graham/towerops:latest + imagePullPolicy: Always + ports: + - containerPort: 4000 + name: http + env: + - name: PORT + value: "4000" + - name: PHX_HOST + value: "towerops.net" + - name: MIX_ENV + value: "prod" + - name: RELEASE_COOKIE + valueFrom: + secretKeyRef: + name: towerops-secrets + key: RELEASE_COOKIE + - name: SECRET_KEY_BASE + valueFrom: + secretKeyRef: + name: towerops-secrets + key: SECRET_KEY_BASE + envFrom: + - secretRef: + name: towerops-db + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: /health + port: 4000 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: 4000 + initialDelaySeconds: 10 + periodSeconds: 5 diff --git a/k8s/ingressroute.yaml b/k8s/ingressroute.yaml new file mode 100644 index 00000000..483bb719 --- /dev/null +++ b/k8s/ingressroute.yaml @@ -0,0 +1,48 @@ +--- +# HTTPS IngressRoute for TowerOps +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: towerops-https + namespace: towerops +spec: + entryPoints: + - websecure + routes: + - match: Host(`towerops.net`) || Host(`www.towerops.net`) + kind: Rule + services: + - name: towerops + port: 4000 + tls: + secretName: towerops-net-tls +--- +# HTTP to HTTPS redirect +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: towerops-http + namespace: towerops +spec: + entryPoints: + - web + routes: + - match: Host(`towerops.net`) || Host(`www.towerops.net`) + kind: Rule + services: + - name: towerops + port: 4000 + middlewares: + - name: redirect-https + namespace: towerops +--- +# Middleware for HTTPS redirect +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: redirect-https + namespace: towerops +spec: + redirectScheme: + scheme: https + permanent: true diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml new file mode 100644 index 00000000..00a392e4 --- /dev/null +++ b/k8s/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: towerops +resources: + - namespace.yaml + - deployment.yaml + - service.yaml + - certificate.yaml + - ingressroute.yaml diff --git a/k8s/namespace.yaml b/k8s/namespace.yaml new file mode 100644 index 00000000..eebca3cb --- /dev/null +++ b/k8s/namespace.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: towerops diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 00000000..596f5d07 --- /dev/null +++ b/k8s/service.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: towerops + namespace: towerops +spec: + selector: + app: towerops + ports: + - name: http + port: 4000 + targetPort: 4000 + type: ClusterIP