Monorepo containing the backend API (Express/Bun) and frontend (Next.js/Bun) for the EzAttend admin panel.
- Architecture
- Repository Structure
- Local Development
- Docker Images
- CI/CD Pipeline
- Dokploy Deployment
- GitHub Secrets Reference
- Troubleshooting
git push (PEP branch)
|
v
+----------------------------+
| GitHub Actions CI/CD |
| .github/workflows/deploy |
+----------------------------+
| |
v v
+----------------+ +----------------+
| Build Backend | | Build Frontend |
| (multi-arch) | | (multi-arch) |
| amd64 + arm64 | | amd64 + arm64 |
+----------------+ +----------------+
| |
v v
+----------------+ +----------------+
| Push to GHCR | | Push to GHCR |
| ghcr.io/ | | ghcr.io/ |
| ezattend/ | | ezattend/ |
| .../backend | | .../frontend |
+----------------+ +----------------+
| |
v v
+----------------+ +----------------+
| Dokploy | | Dokploy |
| Webhook POST | | Webhook POST |
| (auto-redeploy)| | (auto-redeploy)|
+----------------+ +----------------+
| |
v v
+----------------+ +----------------+
| Backend | | Frontend |
| Container | | Container |
| :5000 | | :3000 |
+----------------+ +----------------+
| |
+----------+----------+ |
| | |
v v v
+-----------+ +-----------+-----------+
| MongoDB | | RabbitMQ | Traefik |
| (external)| | (external) | (SSL) |
+-----------+ +-----------+-----------+
Request flow in production:
Browser --> admin.ezattend.xyz (Traefik :443)
|
v
Frontend (:3000)
|
|--> /api/auth/* (Next.js rewrite proxy)
| |
| v
| api.ezattend.xyz (Traefik :443)
| |
| v
| Backend (:5000) /api/auth/*
|
|--> /api/* (client-side fetch)
|
v
api.ezattend.xyz (Traefik :443)
|
v
Backend (:5000)
|
+----+----+
| |
v v
MongoDB RabbitMQ
admin-dashboard/
.github/workflows/
deploy.yml # CI/CD pipeline definition
backend/
Dockerfile # Multi-stage Bun build
.dockerignore
.env # Local/production env (gitignored)
.env.example # Template for env vars
src/ # Express API source
data/ # SQLite auth.db (gitignored)
frontend/
Dockerfile # Multi-stage Next.js standalone build
.dockerignore
.env # Local env (gitignored)
src/ # Next.js app source
middleware.ts # Auth middleware
docker-compose.yml # Local dev compose (no infra services)
- Bun >= 1.0
- Self-hosted MongoDB (replica set) and RabbitMQ accessible from local machine
cd backend
cp .env.example .env # fill in your credentials
bun install
bun dev # starts on http://localhost:5000cd frontend
echo 'NEXT_PUBLIC_API_URL=http://localhost:5000/api' > .env
echo 'NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:5000' >> .env
bun install
bun dev # starts on http://localhost:3000Runs both services in containers. MongoDB and RabbitMQ must be reachable from the host network (credentials from backend/.env).
docker compose up --build- Backend: http://localhost:5000/api/health
- Frontend: http://localhost:3000
Three-stage build:
- deps -- Installs all dependencies including native modules (
better-sqlite3requires python3, make, g++). - builder -- Runs
bun build src/app.ts --outdir ./dist --target bunto produce a bundled output. - runner --
oven/bun:1-slimwith onlynode_modules,dist/, and a/app/datavolume for the SQLite auth database.
Exposes port 5000. Entry point: bun dist/app.js.
Three-stage build:
- deps -- Installs all dependencies.
- builder -- Runs
bun --bun next buildwithNEXT_PUBLIC_*build args baked in. Requiresoutput: "standalone"innext.config.ts. - runner --
oven/bun:1-slimwith standalone output, static assets, and public directory. Runs as non-root usernextjs.
Exposes port 3000. Entry point: bun server.js.
# Backend
docker build -t ezattend-backend ./backend
# Frontend (build args required -- these get baked into the JS bundle)
docker build \
--build-arg NEXT_PUBLIC_API_URL=https://api.ezattend.xyz/api \
--build-arg NEXT_PUBLIC_BETTER_AUTH_URL=https://api.ezattend.xyz \
-t ezattend-frontend ./frontendDefined in .github/workflows/deploy.yml.
Pushes to the PEP branch that modify files in:
backend/**frontend/**.github/workflows/deploy.yml
push to PEP
|
v
[changes] -- dorny/paths-filter detects which directories changed
|
+-- backend/** changed? --> [build-backend]
| |
| +--> Checkout
| +--> Login to GHCR (GITHUB_TOKEN)
| +--> Setup QEMU (ARM64 emulation)
| +--> Setup Buildx
| +--> Build multi-arch image (amd64 + arm64)
| +--> Push to ghcr.io/ezattend/admin_dashboard/backend
| +--> POST to DOKPLOY_BACKEND_WEBHOOK
|
+-- frontend/** changed? --> [build-frontend]
|
+--> Checkout
+--> Login to GHCR (GITHUB_TOKEN)
+--> Setup QEMU (ARM64 emulation)
+--> Setup Buildx
+--> Build multi-arch image with build-args
| NEXT_PUBLIC_API_URL (from secret)
| NEXT_PUBLIC_BETTER_AUTH_URL (from secret)
+--> Push to ghcr.io/ezattend/admin_dashboard/frontend
+--> POST to DOKPLOY_FRONTEND_WEBHOOK
Only changed services are rebuilt. If you only modify frontend/src/app/page.tsx, only the frontend image is rebuilt and redeployed. The backend remains untouched.
Each push produces two tags per image:
:latest-- always points to the most recent build:<commit-sha>-- immutable reference for rollbacks
GitHub Actions cache (type=gha) is used per service scope. Subsequent builds reuse cached layers, significantly reducing build time.
- Dokploy instance running on your server
- DNS records pointing to the server:
api.ezattend.xyz-> server IPadmin.ezattend.xyz-> server IP
- Self-hosted MongoDB and RabbitMQ accessible from the server
Required for Dokploy to pull private GHCR images.
- Navigate to https://github.com/settings/tokens/new (Classic token).
- Set the note to
dokploy-ghcr-read. - Select the
read:packagesscope only. - Generate the token and copy it.
-
In Dokploy, create a project (or use an existing one).
-
Create Service --> Application. Name:
Admin-Backend. -
General tab --> Provider section:
Field Value Provider Docker Docker Image ghcr.io/ezattend/admin_dashboard/backend:latestRegistry URL https://ghcr.ioUsername Your GitHub username Password The PAT from Step 1 Click Save.
-
Environment tab -- add all variables:
PORT=5000 NODE_ENV=production FRONTEND_URL=https://admin.ezattend.xyz MONGODB_URI=<your-mongodb-connection-string> RABBITMQ_URI=<your-rabbitmq-connection-string> BETTER_AUTH_SECRET=<your-auth-secret> BETTER_AUTH_URL=https://api.ezattend.xyz AUTH_DB_PATH=/app/data/auth.db -
Advanced tab --> Volumes --> Add Volume:
Field Value Mount Type Volume Volume Name ezattend-backend-dataMount Path /app/dataThis persists the SQLite auth database across container restarts and redeploys.
-
Domains tab --> Add domain:
Field Value Host api.ezattend.xyzContainer Port 5000HTTPS Enabled -
Click Deploy.
-
Create Service --> Application. Name:
Admin-Frontend. -
General tab --> Provider section:
Field Value Provider Docker Docker Image ghcr.io/ezattend/admin_dashboard/frontend:latestRegistry URL https://ghcr.ioUsername Your GitHub username Password The PAT from Step 1 Click Save.
-
Environment tab -- add:
NEXT_PUBLIC_BETTER_AUTH_URL=https://api.ezattend.xyzNote:
NEXT_PUBLIC_API_URLis baked into the image at build time via GitHub Actions build args. The runtime env varNEXT_PUBLIC_BETTER_AUTH_URLis used bynext.config.tsfor the auth rewrite proxy during SSR. -
Domains tab --> Add domain:
Field Value Host admin.ezattend.xyzContainer Port 3000HTTPS Enabled -
Click Deploy.
After both services are deployed:
- Go to each service's Deployments tab in Dokploy.
- Copy the Webhook URL.
- Add them as GitHub repository secrets (see next section).
Dokploy will automatically redeploy the service when it receives a POST to the webhook URL.
Navigate to: GitHub repo --> Settings --> Secrets and variables --> Actions --> New repository secret.
| Secret Name | Purpose | Example Value |
|---|---|---|
NEXT_PUBLIC_API_URL |
Frontend build arg: backend API base URL | https://api.ezattend.xyz/api |
NEXT_PUBLIC_BETTER_AUTH_URL |
Frontend build arg: auth proxy target | https://api.ezattend.xyz |
DOKPLOY_BACKEND_WEBHOOK |
Webhook URL to trigger backend redeploy | https://dokploy.example.com/api/... |
DOKPLOY_FRONTEND_WEBHOOK |
Webhook URL to trigger frontend redeploy | https://dokploy.example.com/api/... |
GITHUB_TOKEN is automatically provided by GitHub Actions and does not need to be created manually. It is used to authenticate with GHCR for pushing images.
The workflow uses dorny/paths-filter to detect changes. If only .github/workflows/deploy.yml changed, neither build-backend nor build-frontend will run. To force both builds:
echo "" >> backend/package.json
echo "" >> frontend/package.json
git add -A && git commit -m "ci: trigger full build" && git push origin PEPThe images must be built with platforms: linux/amd64,linux/arm64. This is already configured in the workflow using QEMU emulation. If you see this error, the images were built before the multi-platform fix was added. Trigger a new build.
GHCR packages default to private. Either:
- Option A: Make packages public at
https://github.com/orgs/EzAttend/packages--> Package settings --> Change visibility --> Public. - Option B: Add GHCR credentials (GitHub username + PAT with
read:packages) in each Dokploy service's General tab.
The deps stage installs python3, make, and g++ which are required for better-sqlite3 native compilation. If the build fails, ensure these are present in the Dockerfile's deps stage.
NEXT_PUBLIC_* variables are inlined into the JavaScript bundle at build time by Next.js. They cannot be changed at runtime. If you change these values:
- Update the corresponding GitHub secret.
- Push a change to
frontend/to trigger a rebuild. - The new image will have the updated values baked in.
Each image is tagged with the commit SHA. To rollback in Dokploy, change the image tag from :latest to the specific commit SHA:
ghcr.io/ezattend/admin_dashboard/backend:<commit-sha>
Then redeploy. RabbitMQ (existing) Traefik/domain routing