All case studies
2 min read

Two sites, one repo: monorepo deploys to Dokku

Running tishandavid.com (Angular) and tishandavid.dev (Next.js) from a single repository — two Dokku apps, path-filtered CI, and no accidental cross-deploys.

DokkuDockerGitHub ActionsNext.jsAngular

The problem

I had one repository with two completely different sites in it: an Angular SSR app in website/ and a Next.js app in tishandaviddev/. Both needed to deploy to the same Dokku box, on different domains, without one site's commits triggering a rebuild of the other.

The naive setup — one Dockerfile at the repo root — only builds one app. I needed two.

The architecture

The trick is that Dokku lets each app build from a different subdirectory of the same pushed repo. So:

  • tdavid (the .com app) builds the root Dockerfile, which targets website/.
  • tddev (the .dev app) is configured with build-dir tishandaviddev, so it builds tishandaviddev/Dockerfile.

Then CI pushes the repo to whichever app's files actually changed.

The code that mattered

Each app gets a self-contained Dockerfile. For the Next.js app I lean on output: "standalone" so the runtime image is tiny:

FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
 
FROM node:22-slim AS run
WORKDIR /app
ENV NODE_ENV=production PORT=3000
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

Telling Dokku where to build from is a single command:

dokku builder:set tddev build-dir tishandaviddev

And the CI is two path-filtered workflows, so a change under tishandaviddev/** only ever deploys tddev:

on:
  push:
    branches: [main]
    paths:
      - "tishandaviddev/**"
      - ".github/workflows/deploy-dev.yml"

The .com workflow has the mirror-image filter on website/**. They can't step on each other.

Results

  • Two independent apps from one git push, each on its own domain with its own Let's Encrypt cert.
  • A commit that only touches .dev never rebuilds .com — CI time roughly halved versus deploying both every push.
  • Total hosting is one ~A$9/month VPS running both apps plus a few others.

What I'd do differently

output: "standalone" in a monorepo needs outputFileTracingRoot pinned to the app directory, or Next traces files from the repo root and bloats the bundle. I learned that from a 400MB image before pinning it — set it from day one.

I'd also add a tiny smoke-test step after each deploy (curl the homepage, assert a 200 and a known string) so a green build that boots into a broken page can't pass silently.