001 - Bootcamp Deployment Reference Guide
001 - Bootcamp Deployment Reference Guide
Infrastructure: DigitalOcean VPS (Ubuntu) + Docker Compose + Caddy + Watchtower
Registry: GitHub Container Registry (GHCR)
Repository: USERNAME/bootcamp (your personal GitHub account)
Domain: USERNAME.lazuar.dev (where USERNAME = your GitHub username)
📌 Throughout this guide, replace
USERNAMEwith your actual GitHub username. Example: if your GitHub username isalidev, your domain isalidev.lazuar.dev.
Deployment Phases Overview
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ PHASE 1: CI/CD Setup (creates your Docker images) │
│ ───────────────────────────────────────────────── │
│ 1. Generate PAT with write:packages scope │
│ 2. Configure repo: Actions permissions + secrets │
│ 3. Trigger first build (GitHub Actions) │
│ 4. Link packages to repository ← CRITICAL, easy to miss! │
│ │
│ PHASE 2: VPS Deployment (manual, one-time) │
│ ────────────────────────────────────────── │
│ 5. DNS record (instructor creates) │
│ 6. VPS: create .env, Caddyfile, docker-compose.yml │
│ 7. VPS: docker login + docker compose pull + docker compose up │
│ 8. Verify live site │
│ │
│ PHASE 3: Ongoing (automated) │
│ ──────────────────────────── │
│ 9. Push code → GitHub Actions builds → Watchtower auto-updates │
│ (zero manual intervention required!) │
│ │
└─────────────────────────────────────────────────────────────────────┘
⚠️ You must complete Phase 1 before Phase 2. Your VPS cannot pull images until GitHub Actions has built and pushed them to GHCR.
Architecture Overview
┌──────────────────────────────────────────────────────────────────┐
│ GITHUB │
│ │
│ USERNAME/bootcamp (your personal repo) │
│ │ │
│ │ GitHub Actions builds images on push to main │
│ ▼ │
│ ghcr.io/USERNAME/bootcamp/api:latest │
│ ghcr.io/USERNAME/bootcamp/web:latest │
│ ghcr.io/USERNAME/bootcamp/admin:latest │
│ │ │
└───────┼────────────────────────────────────────────────────────────┘
│
│ YOUR VPS pulls YOUR images
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ Your VPS │
│ USERNAME.lazuar.dev │
│ pulls & runs YOUR images │
│ own database │
└──────────────────────────────────────────────────────────────────┘
Key concept: You own the entire pipeline. You push code → GitHub Actions builds your Docker images → your VPS pulls and runs them.
1. Generate a GitHub Personal Access Token (PAT)
Your VPS needs a PAT to pull Docker images from GHCR, and GitHub Actions needs it to push images.
- Go directly to: https://github.com/settings/tokens
- Click Generate new token → Generate new token (classic).
- Set the Note (e.g., "Bootcamp GHCR read/write").
- Set Expiration to "90 days" or "No expiration".
- Under Select scopes, check:
write:packages— to push AND pull Docker images to/from GHCRread:packages— (automatically included with write:packages)
- Scroll to the bottom and click Generate token.
- COPY THE TOKEN IMMEDIATELY (it starts with
ghp_...). You will never see it again.
💾 Save this token somewhere safe. You'll use it in THREE places:
- GitHub Actions secrets (to push images during CI/CD)
- VPS Docker login (to pull images)
- Watchtower environment (to poll for new images)
📌 Why
write:packages? You are both the builder and the deployer. Your GitHub Actions workflow pushes images to GHCR, and your VPS pulls them. Thewrite:packagesscope covers both push and pull operations.
2. Configure Your Repository for CI/CD
You need to configure GitHub Actions on your repository so it can build Docker images and push them to GHCR.
2.1 Enable GitHub Actions
- Go to:
https://github.com/USERNAME/bootcamp/settings/actions - Under Actions permissions, select Allow all actions and reusable workflows
- Click Save
2.2 Set Workflow Permissions
- Still on the same page, scroll to Workflow permissions
- Select Read and write permissions
- Check ✅ Allow GitHub Actions to create and approve pull requests (optional but useful)
- Click Save
2.3 Add Repository Secrets
GitHub Actions needs credentials to push images to GHCR.
- Go to:
https://github.com/USERNAME/bootcamp/settings/secrets/actions - Click New repository secret
- Add the following secrets:
| Name | Value |
|---|---|
CR_USER | Your GitHub username (e.g., alidev) |
CR_PAT | The PAT you generated in Section 1 (e.g., ghp_...) |
📌 These secrets are used by the workflow file (
.github/workflows/deploy.yml) to authenticate with GHCR during the build step.
2.4 Verify Workflow File Exists
Ensure your repository has the CI/CD workflow at .github/workflows/deploy.yml. This file builds and pushes Docker images when you push to main. See 002 - GitHub CI/CD Deployment for the full workflow explanation.
💡 First build: After configuring secrets, you need to trigger the workflow. The first build creates your images on GHCR. Your VPS cannot pull images until this first build completes successfully.
2.5 Trigger Your First Build
📌 Recommended: Use the manual trigger (
workflow_dispatch) for your first build. The path-based filters in the workflow may skip jobs if your initial push only contained README changes or if the paths haven't changed since the last commit.
Option A: Manual Trigger (Recommended for First Build)
- Go to:
https://github.com/USERNAME/bootcamp/actions - Select "Build and Push Docker Images" from the left sidebar
- Click "Run workflow" dropdown (right side)
- Select branch:
main - Click the green "Run workflow" button
Option B: Push a Change
# Make any small change to trigger the workflow
cd ~/bootcamp
git add .
git commit -m "ci: trigger first build"
git push origin main
Monitor the build: Go to https://github.com/USERNAME/bootcamp/actions
Wait for all jobs to complete (✅ green checkmarks). This usually takes 3-8 minutes for the first build.
2.6 Verify Images Exist on GHCR
After the first successful build:
- Go to:
https://github.com/USERNAME?tab=packages - You should see three packages:
bootcamp/apibootcamp/webbootcamp/admin
⚠️ If packages don't appear, check the Actions log for errors. Common issue: secrets not configured correctly (Section 2.3).
2.7 Link Packages to Repository
⚠️ CRITICAL — ONE-TIME STEP: After the first successful build creates your packages, you MUST link them back to your repository. Without this step, your next push will fail with
permission_denied: write_package.
This step grants your repository's GitHub Actions workflow permission to overwrite existing packages on subsequent builds.
For each of the three packages, do the following:
- Go to:
https://github.com/USERNAME?tab=packages - Click on
bootcamp/api - Click Package settings (right side or gear icon)
- Scroll to Manage Actions access
- Click Add Repository
- Search for
bootcamp→ select yourUSERNAME/bootcamprepository - Set role to Write
- Click Save
Repeat for bootcamp/web and bootcamp/admin.
Direct links (replace USERNAME with your GitHub username):
https://github.com/users/USERNAME/packages/container/bootcamp%2Fapi/settingshttps://github.com/users/USERNAME/packages/container/bootcamp%2Fweb/settingshttps://github.com/users/USERNAME/packages/container/bootcamp%2Fadmin/settings
📌 Verification: After linking, push a small change to
mainand confirm the Actions build succeeds without permission errors. If your second build fails withdenied: permission_denied: write_package, you missed this step.
3. DNS Configuration (Cloudflare)
The instructor will create your DNS record. You need to provide your Droplet IP address (you'll get this after creating your VPS in Phase 2).
What the instructor creates:
| Type | Name | Content | Proxy Status |
|---|---|---|---|
| A | USERNAME | [YOUR_VPS_IP] | DNS Only (Grey Cloud) |
Example: GitHub username alidev, Droplet IP 167.71.45.123:
| Type | Name | Content | Proxy Status |
|---|---|---|---|
| A | alidev | 167.71.45.123 | DNS Only (Grey) |
Result: https://alidev.lazuar.dev
⚠️ Cloudflare proxy must be disabled (grey cloud) so Caddy can issue SSL certificates via Let's Encrypt.
(Because we use path-based routing — /api, /admin, / — you only need ONE DNS record!)
4. Server Configuration (VPS)
SSH into your DigitalOcean VPS:
ssh root@YOUR_DROPLET_IP
📌 If you haven't created your VPS yet, follow 006 - VPS Setup Guide first, then return here.
4.1 Create Project Directory
mkdir -p ~/kanban-prod
cd ~/kanban-prod
4.2 Login to GitHub Container Registry
Authenticate so Docker can pull images from your personal GHCR:
export GH_USERNAME="your-github-username"
export CR_PAT="ghp_your_token_here"
echo $CR_PAT | docker login ghcr.io -u $GH_USERNAME --password-stdin
Expected output: Login Succeeded
💡 This login persists in
~/.docker/config.json. You only need to do it once unless the token expires.
5. Deployment Files (On VPS)
Create the following three files inside ~/kanban-prod.
📌 Replace
USERNAMEwith your actual GitHub username in ALL THREE files.
File 1: .env
nano ~/kanban-prod/.env
# ─── Database (DigitalOcean Managed PostgreSQL) ─────────────
DATABASE_URL=Host=YOUR_DB_HOST;Port=25060;Database=defaultdb;Username=doadmin;Password=YOUR_DB_PASSWORD;SSL Mode=Require;Trust Server Certificate=true
# ─── Admin Seed ─────────────────────────────────────────────
ADMIN_EMAIL=admin@kanban.local
ADMIN_PASSWORD=Admin123!
ADMIN_DISPLAY_NAME=System Admin
# ─── ASP.NET ────────────────────────────────────────────────
ASPNETCORE_ENVIRONMENT=Production
# ─── CORS & Cookies (replace USERNAME with your GitHub username) ───
CORS_ORIGINS=https://USERNAME.lazuar.dev
COOKIE_DOMAIN=USERNAME.lazuar.dev
# ─── Watchtower (credentials to pull from YOUR personal GHCR) ─────
REPO_USER=USERNAME
REPO_PASS=ghp_your_token_here
⚠️ Replace:
USERNAME→ your GitHub username (in CORS_ORIGINS, COOKIE_DOMAIN, and REPO_USER)YOUR_DB_HOST/YOUR_DB_PASSWORD→ your actual database credentials (see 008 - Managed Database)ghp_your_token_here→ your PAT (same token from Section 1)
File 2: Caddyfile
nano ~/kanban-prod/Caddyfile
USERNAME.lazuar.dev {
# API Traffic → .NET API
handle /api/* {
reverse_proxy api:5010
}
# SignalR Hub (WebSocket support)
handle /api/hubs/* {
reverse_proxy api:5010
}
# Admin Panel → Admin Caddy Container
handle /admin/* {
reverse_proxy admin:80
}
# Everything Else → Next.js Web App
handle /* {
reverse_proxy web:3000
}
}
⚠️ Replace
USERNAMEwith your actual GitHub username. Example:alidev.lazuar.dev { ... }
File 3: docker-compose.yml
nano ~/kanban-prod/docker-compose.yml
services:
# ─── GATEWAY (Caddy — HTTPS + Reverse Proxy) ──────────────
gateway:
image: caddy:2-alpine
container_name: kanban-gateway
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- api
- web
- admin
# ─── API (.NET) ────────────────────────────────────────────
api:
image: ghcr.io/USERNAME/bootcamp/api:latest
container_name: kanban-api
restart: always
environment:
- DATABASE_URL=${DATABASE_URL}
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT}
- ADMIN_EMAIL=${ADMIN_EMAIL}
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
- ADMIN_DISPLAY_NAME=${ADMIN_DISPLAY_NAME}
- CORS_ORIGINS=${CORS_ORIGINS}
- COOKIE_DOMAIN=${COOKIE_DOMAIN}
# ─── WEB (Next.js) ────────────────────────────────────────
web:
image: ghcr.io/USERNAME/bootcamp/web:latest
container_name: kanban-web
restart: always
# ─── ADMIN (Vite + Caddy) ─────────────────────────────────
admin:
image: ghcr.io/USERNAME/bootcamp/admin:latest
container_name: kanban-admin
restart: always
# ─── WATCHTOWER (Auto-Update from GHCR) ───────────────────
watchtower:
image: containrrr/watchtower
container_name: kanban-watchtower
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- REPO_USER=${REPO_USER}
- REPO_PASS=${REPO_PASS}
command: --interval 300 --cleanup
volumes:
caddy_data:
caddy_config:
⚠️ Replace
USERNAMEin ALL THREE image paths with your actual GitHub username (lowercase). Example:ghcr.io/alidev/bootcamp/api:latest
6. Launch & Verification
Run these commands inside ~/kanban-prod:
6.1 Verify First Build Completed
⚠️ Before proceeding: Confirm that your GitHub Actions first build completed successfully (Section 2.5) AND that you've linked packages to your repository (Section 2.7). Without both steps, the images won't exist or won't be pullable.
# Try pulling one image to confirm access
docker pull ghcr.io/USERNAME/bootcamp/api:latest
If this fails:
| Error | Cause | Fix |
|---|---|---|
not found or manifest unknown | First build hasn't completed | Check github.com/USERNAME/bootcamp/actions |
unauthorized | PAT wrong or expired | Re-run docker login ghcr.io (Section 4.2) |
denied | Token lacks scope | Regenerate PAT with write:packages |
6.2 Start All Services
docker compose up -d
6.3 Verify Running Containers
docker compose ps
All 5 containers should show status "Up":
NAME STATUS
kanban-gateway Up 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
kanban-api Up
kanban-web Up
kanban-admin Up
kanban-watchtower Up
6.4 Check SSL Certificate
docker compose logs --tail=30 gateway
Look for: "certificate obtained successfully"
⏱️ It may take 30-60 seconds for Caddy to issue the certificate on first start.
6.5 Verify in Browser
| URL | Expected |
|---|---|
https://USERNAME.lazuar.dev | Web app (login/register page) |
https://USERNAME.lazuar.dev/admin/ | Admin panel |
https://USERNAME.lazuar.dev/api/ | "Kanban API is running!" |
🎉 Congratulations! Your app is live!
7. How Auto-Updates Work
Because Watchtower is running on your VPS, your app updates automatically when you push new code:
- You push code to
USERNAME/bootcamp(main branch) - GitHub Actions builds new Docker images
- New images are pushed to
ghcr.io/USERNAME/bootcamp/...:latest - Watchtower (on your VPS) checks every 5 minutes
- Watchtower detects new images → pulls them → restarts containers
- Your app is updated! Zero SSH required!
You push code → GitHub Actions → YOUR GHCR → Watchtower → Your app updates
↑
Polls every 5 minutes
The Full Loop
┌─────────────────────────────────────────────────────────────────┐
│ │
│ YOU: git push origin main │
│ │ │
│ ▼ │
│ GITHUB ACTIONS: .github/workflows/deploy.yml triggers │
│ │ • Detects which folders changed │
│ │ • Builds only affected images (api / web / admin) │
│ │ • Pushes to ghcr.io/USERNAME/bootcamp/...:latest │
│ │ │
│ ▼ │
│ YOUR GHCR: new images available │
│ │ │
│ ▼ (within ~5 minutes) │
│ WATCHTOWER (on your VPS): │
│ │ • Polls GHCR every 300 seconds │
│ │ • Detects new image SHA digests │
│ │ • Pulls new images │
│ │ • Gracefully restarts updated containers │
│ │ │
│ ▼ │
│ 🎉 YOUR APP IS UPDATED │
│ USERNAME.lazuar.dev ← live with latest changes │
│ │
└─────────────────────────────────────────────────────────────────┘
Check Watchtower Activity
docker logs kanban-watchtower --tail 20
Look for lines like:
Found new ghcr.io/USERNAME/bootcamp/api:latest image (sha256:abc...)
Stopping /kanban-api
Creating /kanban-api
If Build Fails
A failed build means no new images are pushed and your VPS continues running the previous version. Check:
# View build status
# Go to: https://github.com/USERNAME/bootcamp/actions
# Common build failures:
# - Dockerfile syntax error
# - Dependencies not found
# - Secrets not configured (Section 2.3)
# - Packages not linked (Section 2.7) ← most common on second build!
8. Manual Update (If Watchtower Is Slow)
If you don't want to wait 5 minutes:
cd ~/kanban-prod
docker compose pull
docker compose up -d
9. Common Operations
View Logs
# All services
docker compose logs -f
# Specific service
docker compose logs -f api
docker compose logs --tail 30 api
Restart a Service
docker compose restart api
docker compose restart gateway
Stop Everything
docker compose down
Start Again
docker compose up -d
Update .env and Apply
nano ~/kanban-prod/.env
# Make changes, save
docker compose down
docker compose up -d
10. Troubleshooting Quick Reference
| Problem | Command |
|---|---|
| Container not starting | docker compose logs api |
| SSL not working | docker compose logs gateway | grep certificate |
| Can't pull images | docker login ghcr.io (re-authenticate) |
| Images not found | Check GitHub Actions completed at github.com/USERNAME/bootcamp/actions |
| Build failed | Check Actions logs for error details |
| Second build fails with permission error | Link packages to repo (Section 2.7) |
| Check what's running | docker compose ps |
| Resource usage | docker stats --no-stream |
| Full diagnostic | ~/diagnose.sh (see 006) |
Common Issues
| Problem | Cause | Fix |
|---|---|---|
denied: permission_denied: write_package in Actions | Packages not linked to repo | Complete Section 2.7 |
unauthorized when pulling on VPS | PAT expired or wrong scope | Regenerate PAT with write:packages, re-login |
| Watchtower not updating | REPO_USER/REPO_PASS wrong in .env | Verify username and PAT in .env match Section 1 |
| Packages not visible on profile | First build hasn't run | Trigger build (Section 2.5) |
| Actions not triggering | Workflow file missing or paths don't match | Verify .github/workflows/deploy.yml exists; use manual trigger |
For detailed troubleshooting, see 007 - Troubleshooting FAQ.
Quick Checklist
Before launching, verify:
Phase 1 — CI/CD (must complete first):
- Generated PAT with
write:packagesscope - Repository Actions enabled with "Read and write permissions"
- Repository secrets configured (
CR_USER+CR_PAT) - First GitHub Actions build completed successfully (all green ✅)
- Images visible at
github.com/USERNAME?tab=packages - Packages linked to repository (Section 2.7)
Phase 2 — VPS Deployment:
- Instructor confirmed DNS record (
USERNAME.lazuar.dev→ your IP) - VPS: Docker installed and running
- VPS: UFW allows ports 22, 80, 443
- VPS: Logged into GHCR (
docker login ghcr.io) - VPS:
.envhas correctDATABASE_URL,CORS_ORIGINS,COOKIE_DOMAIN,REPO_USER,REPO_PASS - VPS:
Caddyfilehas correct domain (USERNAME.lazuar.dev) - VPS:
docker-compose.ymlusesghcr.io/USERNAME/bootcamp/...(YOUR username) -
docker compose up -d→ all containers "Up" - Browser:
https://USERNAME.lazuar.devloads the app ✅