Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules
.next
.git
.env*
*.md
tests/
.storybook/
storybook-static/
44 changes: 44 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Build-time args for NEXT_PUBLIC_ vars and Sentry source maps
ARG NEXT_PUBLIC_IDP_BASE_URL
ARG SENTRY_ORG
ARG SENTRY_PROJECT
ARG SENTRY_AUTH_TOKEN

ENV NEXT_PUBLIC_IDP_BASE_URL=$NEXT_PUBLIC_IDP_BASE_URL
ENV SENTRY_ORG=$SENTRY_ORG
ENV SENTRY_PROJECT=$SENTRY_PROJECT
ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN

# Dummy DATABASE_URL for build — Next.js compiles server components which
# import lib/database.ts, but it doesn't actually connect during build.
ENV DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy"

RUN npm run build

FROM node:22-alpine AS runner
WORKDIR /app

RUN adduser -D -u 1000 appuser

ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0

COPY --from=builder /app/public ./public
COPY --from=builder --chown=appuser:appuser /app/.next/standalone ./
COPY --from=builder --chown=appuser:appuser /app/.next/static ./.next/static

USER appuser
EXPOSE 3000

CMD ["node", "server.js"]
4 changes: 2 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "./globals.css";
import { NavBar } from "@/components/navbar";
import { ThemeProvider } from "next-themes";
import { Toaster } from "@/components/ui/toaster";
import { Analytics } from "@vercel/analytics/react";

import { StatusIndicatorStack } from "@/components/status-indicators";

const inter = Inter({ subsets: ["latin"] });
Expand All @@ -28,7 +28,7 @@ export default function RootLayout({
<div className="w-full">{children}</div>
<Toaster />
</ThemeProvider>
<Analytics />

</body>
</html>
);
Expand Down
56 changes: 56 additions & 0 deletions cloudbuild.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
options:
logging: CLOUD_LOGGING_ONLY

substitutions:
_NEXT_PUBLIC_IDP_BASE_URL_PROD: 'https://identity.ethanswan.com'
_NEXT_PUBLIC_IDP_BASE_URL_STAGING: 'https://identity-staging.tailc06f30.ts.net'
_SENTRY_ORG: 'forecasting'
_SENTRY_PROJECT: 'forecasting-app'

steps:
# Build prod image
- name: 'gcr.io/cloud-builders/docker'
args:
- 'build'
- '--build-arg'
- 'NEXT_PUBLIC_IDP_BASE_URL=${_NEXT_PUBLIC_IDP_BASE_URL_PROD}'
- '--build-arg'
- 'SENTRY_ORG=${_SENTRY_ORG}'
- '--build-arg'
- 'SENTRY_PROJECT=${_SENTRY_PROJECT}'
- '--build-arg'
- 'SENTRY_AUTH_TOKEN=${_SENTRY_AUTH_TOKEN}'
- '-t'
- 'us-central1-docker.pkg.dev/ethans-services/containers/forecasting:${SHORT_SHA}-prod'
- '-t'
- 'us-central1-docker.pkg.dev/ethans-services/containers/forecasting:prod'
- '.'
# Build staging image
- name: 'gcr.io/cloud-builders/docker'
args:
- 'build'
- '--build-arg'
- 'NEXT_PUBLIC_IDP_BASE_URL=${_NEXT_PUBLIC_IDP_BASE_URL_STAGING}'
- '--build-arg'
- 'SENTRY_ORG=${_SENTRY_ORG}'
- '--build-arg'
- 'SENTRY_PROJECT=${_SENTRY_PROJECT}'
- '--build-arg'
- 'SENTRY_AUTH_TOKEN=${_SENTRY_AUTH_TOKEN}'
- '-t'
- 'us-central1-docker.pkg.dev/ethans-services/containers/forecasting:${SHORT_SHA}-staging'
- '-t'
- 'us-central1-docker.pkg.dev/ethans-services/containers/forecasting:staging'
- '.'
# Push all tags
- name: 'gcr.io/cloud-builders/docker'
args:
- 'push'
- '--all-tags'
- 'us-central1-docker.pkg.dev/ethans-services/containers/forecasting'

images:
- 'us-central1-docker.pkg.dev/ethans-services/containers/forecasting:${SHORT_SHA}-prod'
- 'us-central1-docker.pkg.dev/ethans-services/containers/forecasting:prod'
- 'us-central1-docker.pkg.dev/ethans-services/containers/forecasting:${SHORT_SHA}-staging'
- 'us-central1-docker.pkg.dev/ethans-services/containers/forecasting:staging'
4 changes: 2 additions & 2 deletions instrumentation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as Sentry from "@sentry/nextjs";

export async function register() {
// Load environment configuration first, but only in Node.js runtime and outside of Vercel (the deployment environment)
if (process.env.NEXT_RUNTIME === "nodejs" && !(process.env.VERCEL === "1")) {
// Load environment configuration first, but only in Node.js runtime and outside of K8s (where env vars come from configmaps/secrets)
if (process.env.NEXT_RUNTIME === "nodejs" && !(process.env.K8S === "1")) {
const { loadEnvironment } = await import("./lib/environment");
loadEnvironment();
await import("./sentry.server.config");
Expand Down
41 changes: 41 additions & 0 deletions k8s/argocd/applications.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: forecasting-staging
namespace: argocd
annotations:
argocd-image-updater.argoproj.io/image-list: forecasting=us-central1-docker.pkg.dev/ethans-services/containers/forecasting
argocd-image-updater.argoproj.io/forecasting.update-strategy: newest-build
argocd-image-updater.argoproj.io/forecasting.allow-tags: "regexp:^[a-f0-9]+-staging$"
spec:
project: default
source:
repoURL: https://github.com/eswan18/forecasting
targetRevision: main
path: k8s/staging
destination:
server: https://kubernetes.default.svc
namespace: forecasting-staging
syncPolicy:
automated:
prune: true
selfHeal: true
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: forecasting-prod
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/eswan18/forecasting
targetRevision: main
path: k8s/prod
destination:
server: https://kubernetes.default.svc
namespace: forecasting-prod
syncPolicy:
automated:
prune: true
selfHeal: true
16 changes: 16 additions & 0 deletions k8s/argocd/image-updater.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: argocd-image-updater.argoproj.io/v1alpha1
kind: ImageUpdater
metadata:
name: forecasting-staging
namespace: argocd
spec:
namespace: argocd
writeBackConfig:
method: argocd
applicationRefs:
- namePattern: forecasting-staging
images:
- alias: forecasting
imageName: us-central1-docker.pkg.dev/ethans-services/containers/forecasting
commonUpdateSettings:
updateStrategy: newest-build
9 changes: 9 additions & 0 deletions k8s/base/configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: forecasting-config
data:
K8S: "1"
NODE_ENV: "production"
SENTRY_ORG: "forecasting"
SENTRY_PROJECT: "forecasting-app"
63 changes: 63 additions & 0 deletions k8s/base/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: forecasting
spec:
replicas: 2
selector:
matchLabels:
app: forecasting
template:
metadata:
labels:
app: forecasting
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: forecasting
tolerations:
- key: cloud.google.com/gke-spot
operator: Equal
value: "true"
effect: NoSchedule
containers:
- name: forecasting
image: us-central1-docker.pkg.dev/ethans-services/containers/forecasting:latest
ports:
- containerPort: 3000
readinessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
resources:
requests:
memory: "128Mi"
cpu: "5m"
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
volumeMounts:
- name: secrets-store
mountPath: "/mnt/secrets"
readOnly: true
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
6 changes: 6 additions & 0 deletions k8s/base/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- configmap.yaml
11 changes: 11 additions & 0 deletions k8s/base/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: forecasting
spec:
selector:
app: forecasting
ports:
- port: 80
targetPort: 3000
type: ClusterIP
8 changes: 8 additions & 0 deletions k8s/prod/configmap-env.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: forecasting-prod-env-config
data:
ENV: "prod"
IDP_BASE_URL: "http://identity.identity-prod.svc.cluster.local"
SENTRY_DSN: "https://42fb7fde7d5842831f2324ed33c7f50f@o4509062063587328.ingest.us.sentry.io/4509062066012160"
64 changes: 64 additions & 0 deletions k8s/prod/deployment-patch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: forecasting
spec:
replicas: 2
template:
spec:
serviceAccountName: forecasting-prod-ksa
containers:
- name: forecasting
envFrom:
- configMapRef:
name: forecasting-config
- configMapRef:
name: forecasting-prod-env-config
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: forecasting-prod-secrets
key: DATABASE_URL
- name: ADMIN_DATABASE_URL
valueFrom:
secretKeyRef:
name: forecasting-prod-secrets
key: ADMIN_DATABASE_URL
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: forecasting-prod-secrets
key: JWT_SECRET
- name: ARGON2_SALT
valueFrom:
secretKeyRef:
name: forecasting-prod-secrets
key: ARGON2_SALT
- name: IDP_CLIENT_ID
valueFrom:
secretKeyRef:
name: forecasting-prod-secrets
key: IDP_CLIENT_ID
- name: IDP_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: forecasting-prod-secrets
key: IDP_CLIENT_SECRET
- name: IDP_ADMIN_CLIENT_ID
valueFrom:
secretKeyRef:
name: forecasting-prod-secrets
key: IDP_ADMIN_CLIENT_ID
- name: IDP_ADMIN_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: forecasting-prod-secrets
key: IDP_ADMIN_CLIENT_SECRET
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: forecasting-prod-secrets
11 changes: 11 additions & 0 deletions k8s/prod/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: forecasting-prod
resources:
- ../base
- namespace.yaml
- service-account.yaml
- configmap-env.yaml
- secret-provider-class.yaml
patches:
- path: deployment-patch.yaml
4 changes: 4 additions & 0 deletions k8s/prod/namespace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: forecasting-prod
Loading
Loading