Monorepo Guide

Deploy from a monorepo

Each app in your monorepo gets its own Dokku app. Use git subtree push to deploy only that app's subdirectory. This guide explains the concepts — use the Monorepo Tool to generate ready-to-run commands for your repo.

Repo structure

In a pnpm monorepo the default workspace path is packages/. Each deployed app lives under packages/ alongside shared libraries and has its own Dockerfile — Dokku uses it when that subtree is pushed.

my-monorepo/
└── packages/
    ├── web/          ← SvelteKit front-end    (deployed as: my-app-web)
    │   └── Dockerfile
    ├── api/          ← Node.js back-end        (deployed as: my-app-api)
    │   └── Dockerfile
    ├── admin/        ← Admin panel             (deployed as: my-app-admin)
    │   └── Dockerfile
    ├── ui/           ← shared components       (not deployed)
    └── utils/        ← shared utilities        (not deployed)

In this example the three apps and their subfolder paths are:

web packages/web
api packages/api
admin packages/admin
Non-deployed packages (e.g. packages/ui, packages/utils) are never pushed to Dokku directly — they must be bundled or copied by each app's own build step (e.g. via the Dockerfile COPY instruction or a workspace-aware build tool like turborepo).

VPS — one-time setup per app

Run this once on the server for each app. It creates the Dokku app, assigns a subdomain, enables HTTPS, and mounts persistent storage. Repeat for my-app-api, my-app-admin, etc.

VPS — example for "web"
# Replace my-app-web / web.example.com / you@example.com with your values
dokku apps:create my-app-web
dokku domains:set my-app-web web.fastfastapps.com

for app in $(dokku apps:list); do
  dokku letsencrypt:set $app email you@example.com
  dokku letsencrypt:enable $app
done

dokku letsencrypt:cron-job --add
dokku cleanup
docker system prune -af

dokku storage:create my-app-web
chown -R 32767:32767 /var/lib/dokku/data/storage/my-app-web
dokku storage:mount my-app-web /var/lib/dokku/data/storage/my-app-web:/app/storage

dokku ports:set my-app-web http:80:3000 https:443:3000
dokku ports:set maps the public http/https ports to the container's internal port (3000) so nginx knows where to forward traffic. Set DATABASE_URL=/app/storage/db.sqlite via the Env Tool after setup.

Local — add a git remote per app

Each app in the monorepo needs its own Dokku git remote. Give each remote a unique name with a prefix so they don't conflict with each other (or with any plain dokku remote).

Add remotes for all three apps
git remote add dokku-my-app-web   dokku@fastfastapps.com:my-app-web
git remote add dokku-my-app-api   dokku@fastfastapps.com:my-app-api
git remote add dokku-my-app-admin dokku@fastfastapps.com:my-app-admin
Use a prefixed remote name like dokku-my-app-web (not just dokku) so each app in the repo has its own distinct remote and pushes never go to the wrong target.

Local — deploy a subtree

Push only the app's subdirectory to its Dokku remote. Dokku receives it as if it were the full repo root and runs the Dockerfile found there.

Push subtree — web
git subtree push --prefix=packages/web dokku-my-app-web master
Push subtree — api
git subtree push --prefix=packages/api dokku-my-app-api master
Full deploy (commit + push) — web
git add . && git commit -m "deploy web" && git subtree push --prefix=packages/web dokku-my-app-web master
git subtree push rewrites history so only the commits that touched packages/web are pushed. Dokku sees a clean, single-app repo and builds the Dockerfile at its root. You can wrap this into a shell alias or npm run deploy:web script.

Shared packages in the Dockerfile

Because git subtree push sends only the app's subdirectory, shared packages are not available unless the app bundles them. Two common patterns:

Option A — bundle shared code at build time (recommended)

Use a build tool such as turborepo or tsup to compile shared packages into the app's own dist/ folder before committing. The Dockerfile then only needs the app directory and has no dependency on the rest of the repo.

packages/web/Dockerfile
FROM node:24-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build     # shared packages already compiled into dist/

FROM node:24-alpine AS runner
WORKDIR /app
COPY --from=builder /app/build ./build
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev
EXPOSE 3000
CMD ["node", "build/index.js"]

Option B — push full repo, set Dockerfile path

Keep a Dockerfile at the monorepo root and push the entire repo to Dokku. Use DOCKER_BUILD_CONTEXT_PATH on the VPS to point Dokku at the right Dockerfile for each app.

VPS — set Dockerfile path (for web)
dokku docker-options:add my-app-web build "--file packages/web/Dockerfile"
Local — push full repo
git remote add dokku-my-app-web dokku@fastfastapps.com:my-app-web
git add . && git commit -m "deploy web" && git push dokku-my-app-web master
Option B is simpler when shared packages change often, but Dokku rebuilds every app whenever any file in the repo changes. Option A keeps deploys fast and independent per app.