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
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
.next
.git
.github
.env*.local
*.md
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ updates:
directory: "/"
schedule:
interval: "weekly"

- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
92 changes: 92 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Deploy

# Builds the web image and deploys it to Datum Cloud Compute on every push to
# main. See DEPLOY.md for the one-time setup this depends on.
on:
push:
branches: [main]
workflow_dispatch: {}

concurrency:
group: deploy-web
cancel-in-progress: false

env:
IMAGE: ghcr.io/altsmanager/web

jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image: ${{ steps.image.outputs.image }}
steps:
- uses: actions/checkout@v4

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set image tag
id: image
run: echo "image=${IMAGE}:${{ github.sha }}" >> "$GITHUB_OUTPUT"

- uses: docker/setup-buildx-action@v3

# NEXT_PUBLIC_API_URL is inlined into the client bundle at build time, so
# it has to be known here, not just at deploy time. It's set once, by
# hand, as the API_PUBLIC_URL repository variable, after the api
# workload's first deploy produces its public hostname (see DEPLOY.md) --
# it shouldn't change afterwards, since that hostname is tied to the
# workload, not to any particular deploy.
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.image.outputs.image }}
build-args: |
NEXT_PUBLIC_API_URL=${{ vars.API_PUBLIC_URL }}
cache-from: type=gha
cache-to: type=gha,mode=max

deploy:
needs: build-and-push
runs-on: ubuntu-latest
env:
DATUM_PROJECT: altsmanager
steps:
- uses: actions/setup-go@v5
with:
go-version: "1.23"

- name: Install datumctl
run: go install go.datum.net/datumctl@latest

# Service-account login per https://www.datum.net/docs/datumctl/auth/logging-in.
# The service account must be scoped to exactly this one project --
# datum-cloud/datumctl#324 is an open bug where --credentials login fails
# non-interactively if the account can see more than one context.
- name: Authenticate to Datum Cloud
env:
DATUM_SERVICE_ACCOUNT_KEY: ${{ secrets.DATUM_SERVICE_ACCOUNT_KEY }}
run: |
umask 077
printf '%s' "$DATUM_SERVICE_ACCOUNT_KEY" > "$RUNNER_TEMP/datum-key.json"
datumctl login --credentials "$RUNNER_TEMP/datum-key.json"
rm -f "$RUNNER_TEMP/datum-key.json"

# NOTE: unverified against a real deployment -- Compute access for this
# project is pending as of when this workflow was written (see DEPLOY.md).
- name: Deploy to Datum Compute
run: |
datumctl compute deploy altsmanager-web \
--image="${{ needs.build-and-push.outputs.image }}" \
--city=DFW \
--min=1 \
--http-port=3000
52 changes: 52 additions & 0 deletions DEPLOY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Deploying to Datum Cloud

This repo deploys to [Datum Cloud](https://datum.net) Compute via
`.github/workflows/deploy.yml`, which runs on every push to `main`. See the
[api repo's DEPLOY.md](https://github.com/AltsManager/api/blob/main/DEPLOY.md)
for the shared infrastructure (Datum project, Compute access, service
account) -- this document only covers what's specific to the frontend.

## Status

- [ ] Everything in the api repo's DEPLOY.md status list, since this depends
on the same project and Compute access
- [ ] `altsmanager-api` deployed at least once, so its public hostname exists
- [ ] `API_PUBLIC_URL` repository variable set to that hostname

## Why the build needs the API's URL upfront

`NEXT_PUBLIC_API_URL` is inlined into the client-side JavaScript bundle at
`next build` time -- it can't be supplied later, at deploy or run time, the
way a server-side env var can. So the api workload has to be deployed first
(at least once) to get its public hostname, and only then can this repo's
image be built with that URL baked in.

In steady state this isn't a problem: the api's public hostname is tied to
its Compute workload, not to any individual deploy, so it stays the same
across api releases. `API_PUBLIC_URL` only needs to be set once, not updated
on every deploy.

## One-time setup

1. Complete the api repo's setup through "First deploy", and note the
`altsmanager-api` workload's public hostname.
2. In this repo's GitHub settings, add a repository **variable** (not secret --
it ends up in client-side JS, so it's already public) named
`API_PUBLIC_URL` set to that hostname, e.g. `https://altsmanager-api.<...>.datum.app`.
3. Add the same `DATUM_SERVICE_ACCOUNT_KEY` secret as the api repo (or a
separate service account scoped the same way -- either works, since both
deploy into the same `altsmanager` project).
4. Push to `main`, or run the `Deploy` workflow manually.

## Verify once Compute access is granted

Same open questions as the api repo's DEPLOY.md: whether repeated
`datumctl compute deploy` calls preserve prior settings, and the exact
non-interactive login behavior.

## Required GitHub secrets/variables

| Name | Type | Purpose |
|---|---|---|
| `DATUM_SERVICE_ACCOUNT_KEY` | secret | Service account credentials JSON for `datumctl login --credentials` |
| `API_PUBLIC_URL` | variable | The deployed api's public hostname, baked into the client bundle at build time |
24 changes: 24 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM node:22-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:22-slim AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
RUN npm run build

FROM node:22-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
USER node

EXPOSE 3000
ENV PORT=3000 HOSTNAME=0.0.0.0
CMD ["node", "server.js"]
1 change: 1 addition & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const securityHeaders = [
];

const nextConfig: NextConfig = {
output: "standalone",
async headers() {
return [
{
Expand Down
Loading