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 USERNAME with your actual GitHub username. Example: if your GitHub username is alidev, your domain is alidev.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.

  1. Go directly to: https://github.com/settings/tokens
  2. Click Generate new token → Generate new token (classic).
  3. Set the Note (e.g., "Bootcamp GHCR read/write").
  4. Set Expiration to "90 days" or "No expiration".
  5. Under Select scopes, check:
    • write:packages — to push AND pull Docker images to/from GHCR
    • read:packages — (automatically included with write:packages)
  6. Scroll to the bottom and click Generate token.
  7. 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:

  1. GitHub Actions secrets (to push images during CI/CD)
  2. VPS Docker login (to pull images)
  3. 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. The write:packages scope 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

  1. Go to: https://github.com/USERNAME/bootcamp/settings/actions
  2. Under Actions permissions, select Allow all actions and reusable workflows
  3. Click Save

2.2 Set Workflow Permissions

  1. Still on the same page, scroll to Workflow permissions
  2. Select Read and write permissions
  3. Check ✅ Allow GitHub Actions to create and approve pull requests (optional but useful)
  4. Click Save

2.3 Add Repository Secrets

GitHub Actions needs credentials to push images to GHCR.

  1. Go to: https://github.com/USERNAME/bootcamp/settings/secrets/actions
  2. Click New repository secret
  3. Add the following secrets:
NameValue
CR_USERYour GitHub username (e.g., alidev)
CR_PATThe 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)

  1. Go to: https://github.com/USERNAME/bootcamp/actions
  2. Select "Build and Push Docker Images" from the left sidebar
  3. Click "Run workflow" dropdown (right side)
  4. Select branch: main
  5. 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:

  1. Go to: https://github.com/USERNAME?tab=packages
  2. You should see three packages:
    • bootcamp/api
    • bootcamp/web
    • bootcamp/admin

⚠️ If packages don't appear, check the Actions log for errors. Common issue: secrets not configured correctly (Section 2.3).

⚠️ 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:

  1. Go to: https://github.com/USERNAME?tab=packages
  2. Click on bootcamp/api
  3. Click Package settings (right side or gear icon)
  4. Scroll to Manage Actions access
  5. Click Add Repository
  6. Search for bootcamp → select your USERNAME/bootcamp repository
  7. Set role to Write
  8. 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/settings
  • https://github.com/users/USERNAME/packages/container/bootcamp%2Fweb/settings
  • https://github.com/users/USERNAME/packages/container/bootcamp%2Fadmin/settings

📌 Verification: After linking, push a small change to main and confirm the Actions build succeeds without permission errors. If your second build fails with denied: 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:

TypeNameContentProxy Status
AUSERNAME[YOUR_VPS_IP]DNS Only (Grey Cloud)

Example: GitHub username alidev, Droplet IP 167.71.45.123:

TypeNameContentProxy Status
Aalidev167.71.45.123DNS 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 USERNAME with 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 USERNAME with 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 USERNAME in 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:

ErrorCauseFix
not found or manifest unknownFirst build hasn't completedCheck github.com/USERNAME/bootcamp/actions
unauthorizedPAT wrong or expiredRe-run docker login ghcr.io (Section 4.2)
deniedToken lacks scopeRegenerate 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

URLExpected
https://USERNAME.lazuar.devWeb 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:

  1. You push code to USERNAME/bootcamp (main branch)
  2. GitHub Actions builds new Docker images
  3. New images are pushed to ghcr.io/USERNAME/bootcamp/...:latest
  4. Watchtower (on your VPS) checks every 5 minutes
  5. Watchtower detects new images → pulls them → restarts containers
  6. 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

ProblemCommand
Container not startingdocker compose logs api
SSL not workingdocker compose logs gateway | grep certificate
Can't pull imagesdocker login ghcr.io (re-authenticate)
Images not foundCheck GitHub Actions completed at github.com/USERNAME/bootcamp/actions
Build failedCheck Actions logs for error details
Second build fails with permission errorLink packages to repo (Section 2.7)
Check what's runningdocker compose ps
Resource usagedocker stats --no-stream
Full diagnostic~/diagnose.sh (see 006)

Common Issues

ProblemCauseFix
denied: permission_denied: write_package in ActionsPackages not linked to repoComplete Section 2.7
unauthorized when pulling on VPSPAT expired or wrong scopeRegenerate PAT with write:packages, re-login
Watchtower not updatingREPO_USER/REPO_PASS wrong in .envVerify username and PAT in .env match Section 1
Packages not visible on profileFirst build hasn't runTrigger build (Section 2.5)
Actions not triggeringWorkflow file missing or paths don't matchVerify .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:packages scope
  • 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: .env has correct DATABASE_URL, CORS_ORIGINS, COOKIE_DOMAIN, REPO_USER, REPO_PASS
  • VPS: Caddyfile has correct domain (USERNAME.lazuar.dev)
  • VPS: docker-compose.yml uses ghcr.io/USERNAME/bootcamp/... (YOUR username)
  • docker compose up -d → all containers "Up"
  • Browser: https://USERNAME.lazuar.dev loads the app ✅
Built with LogoFlowershow