004 — Docker Cheatsheet

🐳 Docker packages your application and all its dependencies into a container that runs consistently on any machine. This cheatsheet covers everything you'll use during the bootcamp.


Table of Contents

  1. Core Concepts
  2. Docker Images
  3. Docker Containers
  4. Docker Compose
  5. Debugging Containers
  6. Docker Networks & Volumes
  7. Docker Registry (GHCR)
  8. Cleaning Up
  9. Dockerfile Reference
  10. Quick Reference Card

1. Core Concepts

┌──────────────────────────────────────────────────────────┐
│                    DOCKER CONCEPTS                         │
├──────────────────────────────────────────────────────────┤
│                                                           │
│  Dockerfile  ──build──▶  Image  ──run──▶  Container      │
│  (recipe)               (template)        (running app)   │
│                                                           │
│  Think of it like:                                        │
│  Recipe       ──cook──▶  Dish    ──serve──▶  Plate       │
│                                                           │
├──────────────────────────────────────────────────────────┤
│                                                           │
│  Image    = Read-only snapshot (like a VM template)       │
│  Container = Running instance of an image                 │
│  Volume   = Persistent storage (survives container death) │
│  Network  = Communication channel between containers      │
│  Registry = Image storage (Docker Hub, GHCR, etc.)        │
│                                                           │
└──────────────────────────────────────────────────────────┘

Lifecycle

Dockerfile → (docker build) → Image → (docker run) → Container
                                ↕                        ↕
                          (docker push/pull)      (start/stop/rm)
                                ↕
                           Registry (GHCR)

2. Docker Images

2.1 Building Images

CommandDescription
docker build -t myapp .Build image from Dockerfile in current dir
docker build -t myapp:v1 .Build with specific tag
docker build -f Dockerfile.prod .Build with specific Dockerfile
docker build --no-cache -t myapp .Build without using cache
docker build --platform linux/amd64 -t myapp .Build for specific platform

Example — Building the bootcamp API:

docker build -t kanban-api -f services/api-dotnet/Dockerfile .

Example — Build and push in one command (buildx):

docker buildx build --platform linux/amd64 --push \
  -t ghcr.io/devopsmalaysia/bootcamp/api:latest \
  -f services/api-dotnet/Dockerfile .

2.2 Listing & Managing Images

CommandDescription
docker imagesList all local images
docker images -aList all images (including intermediate)
docker image lsSame as docker images
docker image rm myappRemove an image
docker rmi myappShorthand for remove
docker rmi $(docker images -q)Remove ALL images ⚠️
docker image inspect myappShow image details (layers, env, etc.)
docker image history myappShow image layer history

2.3 Pulling & Pushing Images

CommandDescription
docker pull nginx:latestDownload image from registry
docker pull ghcr.io/devopsmalaysia/bootcamp/api:latestPull from GHCR
docker push myapp:latestPush image to registry
docker tag myapp:latest ghcr.io/user/myapp:latestTag image for registry

3. Docker Containers

3.1 Running Containers

CommandDescription
docker run nginxRun container (foreground)
docker run -d nginxRun in background (detached)
docker run -d --name web nginxRun with custom name
docker run -d -p 8080:80 nginxMap host port 8080 → container port 80
docker run -d -p 3000:3000 myappExpose app on port 3000
docker run --rm nginxAuto-remove container when it exits
docker run -it ubuntu bashRun interactive terminal
docker run -d --restart always nginxAuto-restart on failure/reboot
docker run -e "DB_HOST=localhost" myappPass environment variable
docker run --env-file .env myappPass env file
docker run -v ./data:/app/data myappMount host directory
docker run --network mynet myappConnect to specific network

Port Mapping Explained:

-p HOST_PORT:CONTAINER_PORT

-p 8080:80     → Access container's port 80 via localhost:8080
-p 3000:3000   → Access container's port 3000 via localhost:3000
-p 443:443     → Access container's port 443 via localhost:443

3.2 Listing Containers

CommandDescription
docker psList running containers
docker ps -aList ALL containers (including stopped)
docker ps -qList only container IDs
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"Custom format

Example output:

CONTAINER ID   IMAGE          COMMAND       STATUS         PORTS                  NAMES
a1b2c3d4e5f6   nginx:latest   "nginx"       Up 2 hours     0.0.0.0:80->80/tcp     web
f6e5d4c3b2a1   postgres:17    "postgres"    Up 2 hours     0.0.0.0:5432->5432/tcp db

3.3 Stopping & Removing Containers

CommandDescription
docker stop CONTAINERGracefully stop (sends SIGTERM)
docker stop $(docker ps -q)Stop ALL running containers
docker kill CONTAINERForce stop (sends SIGKILL)
docker rm CONTAINERRemove stopped container
docker rm -f CONTAINERForce remove (even if running)
docker rm $(docker ps -aq)Remove ALL containers ⚠️
docker start CONTAINERStart a stopped container
docker restart CONTAINERRestart a container

3.4 Container Logs

CommandDescription
docker logs CONTAINERShow all logs
docker logs -f CONTAINERFollow logs (live stream)
docker logs --tail 50 CONTAINERShow last 50 lines
docker logs --tail 50 -f CONTAINERLast 50 + follow
docker logs --since 5m CONTAINERLogs from last 5 minutes
docker logs --since 1h CONTAINERLogs from last 1 hour
docker logs --timestamps CONTAINERShow timestamps

3.5 Executing Commands Inside Containers

CommandDescription
docker exec -it CONTAINER bashOpen bash shell inside container
docker exec -it CONTAINER shOpen sh shell (if no bash)
docker exec CONTAINER ls /appRun single command
docker exec CONTAINER cat /etc/hostsView file inside container
docker exec -it CONTAINER envView environment variables
docker exec -u root CONTAINER commandRun as root user

Example — Debug the API container:

# Open a shell in the running API
docker exec -it kanban-api sh

# Check if database is reachable from inside the container
docker exec -it kanban-api ping postgres

# View the app's environment variables
docker exec kanban-api env | grep DATABASE

3.6 Container Inspection

CommandDescription
docker inspect CONTAINERFull JSON details
docker inspect --format '{{.State.Status}}' CONTAINERGet specific field
docker inspect --format '{{.NetworkSettings.IPAddress}}' CONTAINERGet IP address
docker statsLive resource usage (CPU, RAM, NET)
docker stats --no-streamOne-time snapshot of stats
docker top CONTAINERShow running processes
docker diff CONTAINERShow filesystem changes

4. Docker Compose

Docker Compose manages multi-container applications defined in a docker-compose.yml file. It's how we run the entire Kanban app (API + Web + Admin + DB + Gateway) with one command.

4.1 Essential Commands

CommandDescription
docker compose upCreate & start all services (foreground)
docker compose up -dCreate & start in background (detached)
docker compose downStop & remove containers + networks
docker compose down -vStop & remove including volumes ⚠️
docker compose stopStop without removing
docker compose startStart previously stopped services
docker compose restartRestart all services
docker compose psList running services
docker compose logsView logs of all services
docker compose logs -fFollow all logs
docker compose logs -f apiFollow logs of specific service
docker compose logs --tail 20 apiLast 20 lines of api service

4.2 Build & Update

CommandDescription
docker compose buildBuild all services
docker compose build apiBuild specific service
docker compose build --no-cacheBuild without cache
docker compose up -d --buildRebuild & restart
docker compose pullPull latest images
docker compose up -d --pull alwaysPull & restart

4.3 Using Specific Compose Files

CommandDescription
docker compose -f docker-compose.yml up -dUse specific file
docker compose -f docker-compose.prod.yml up -dUse prod file
docker compose -f docker-compose.yml -f docker-compose.override.yml up -dMerge files

Bootcamp examples:

# Local development (includes PostgreSQL + Adminer)
docker compose up -d

# Production-like (includes Caddy gateway)
docker compose -f docker-compose.prod.yml up -d

4.4 Service Management

CommandDescription
docker compose up -d apiStart only the api service
docker compose restart webRestart only the web service
docker compose stop adminStop only the admin service
docker compose rm apiRemove stopped api container
docker compose exec api shShell into api container
docker compose exec postgres psql -U kanbanOpen PostgreSQL CLI
docker compose run --rm api dotnet testRun one-off command

4.5 Scaling & Status

CommandDescription
docker compose psShow status of all services
docker compose topShow processes in all services
docker compose configValidate & show resolved config
docker compose imagesList images used by services
docker compose port web 3000Show public port mapping

4.6 Docker Compose File Structure (Reference)

# docker-compose.yml
services:
  # Service name (used as hostname in Docker network)
  api:
    # Build from Dockerfile
    build:
      context: .
      dockerfile: services/api-dotnet/Dockerfile
    # OR use pre-built image
    image: ghcr.io/devopsmalaysia/bootcamp/api:latest
    
    container_name: kanban-api        # Custom container name
    restart: always                    # Restart policy
    
    ports:
      - "5010:5010"                   # HOST:CONTAINER
    
    environment:                       # Environment variables
      - DATABASE_URL=Host=postgres;Port=5432;Database=kanban;Username=kanban;Password=secret
      - ASPNETCORE_ENVIRONMENT=Production
    
    env_file:                          # Load from file
      - .env
    
    volumes:
      - ./data:/app/data              # Bind mount
      - app_data:/app/storage         # Named volume
    
    depends_on:                        # Start order
      postgres:
        condition: service_healthy
    
    networks:
      - app_net                        # Connect to network
    
    healthcheck:                       # Health monitoring
      test: ["CMD", "curl", "-f", "http://localhost:5010/health"]
      interval: 30s
      timeout: 10s
      retries: 3

volumes:
  app_data:                            # Named volume definition

networks:
  app_net:
    driver: bridge                     # Network driver

5. Debugging Containers

5.1 Container Won't Start

# Step 1: Check exit code and status
docker compose ps -a

# Step 2: Read the logs
docker compose logs api

# Step 3: Check if it's a build issue
docker compose build api 2>&1 | tail -20

# Step 4: Try running interactively to see the error
docker compose run --rm api sh

5.2 Container Keeps Restarting

# Check restart count and last exit code
docker inspect kanban-api --format '{{.RestartCount}} - Exit: {{.State.ExitCode}}'

# Check recent logs
docker logs --tail 50 kanban-api

# Common exit codes:
#   0   = Exited normally
#   1   = Application error
#   137 = Killed (OOM or docker stop)
#   139 = Segfault
#   143 = Graceful termination (SIGTERM)

5.3 Container Running But App Not Working

# Check if the app is listening inside the container
docker exec kanban-api sh -c "curl -s localhost:5010/health || echo 'NOT RESPONDING'"

# Check environment variables are correct
docker exec kanban-api env | sort

# Check if the app can reach the database
docker exec kanban-api sh -c "nc -zv postgres 5432 2>&1"

# Check DNS resolution between containers
docker exec kanban-api sh -c "nslookup postgres"

# View running processes
docker top kanban-api

5.4 Port Issues

# Check what's using a port on host
# macOS:
lsof -i :3000

# Linux/WSL:
ss -tlnp | grep 3000

# Check container port mappings
docker port kanban-api

# Verify port is exposed in container
docker inspect kanban-api --format '{{range $p, $conf := .NetworkSettings.Ports}}{{$p}} -> {{$conf}}{{"\n"}}{{end}}'

5.5 Network Issues Between Containers

# List networks
docker network ls

# Inspect network (see connected containers)
docker network inspect kanban-prod_default

# Test connectivity between containers
docker exec kanban-api ping kanban-postgres

# Check DNS resolution
docker exec kanban-api nslookup postgres

# Note: Containers in the same docker-compose file share a network
# They can reach each other by SERVICE NAME (not container_name)

5.6 Volume / File Issues

# List volumes
docker volume ls

# Inspect volume (see mount path on host)
docker volume inspect kanban-prod_postgres_data

# Check mounted files inside container
docker exec kanban-gateway cat /etc/caddy/Caddyfile

# Copy file from container to host
docker cp kanban-api:/app/appsettings.json ./debug-settings.json

# Copy file from host to container
docker cp ./Caddyfile kanban-gateway:/etc/caddy/Caddyfile

5.7 Resource Issues

# Real-time resource monitoring
docker stats

# Output:
# CONTAINER    CPU %   MEM USAGE / LIMIT     MEM %   NET I/O
# kanban-api   2.5%    145MiB / 1GiB         14%     1.2kB / 500B
# kanban-web   0.3%    80MiB / 1GiB          8%      800B / 200B

# Check if container was killed by OOM
docker inspect kanban-api --format '{{.State.OOMKilled}}'

# View container resource limits
docker inspect kanban-api --format '{{.HostConfig.Memory}}'

5.8 Complete Debug Session Example

echo "=== SERVICE STATUS ==="
docker compose ps

echo ""
echo "=== RECENT API LOGS ==="
docker compose logs --tail 30 api

echo ""
echo "=== API ENVIRONMENT ==="
docker exec kanban-api env | grep -E "(DATABASE|ASPNET|CORS|COOKIE)" | sort

echo ""
echo "=== NETWORK CHECK ==="
docker exec kanban-api sh -c "nc -zv postgres 5432 2>&1 || echo 'DB UNREACHABLE'"

echo ""
echo "=== HEALTH CHECK ==="
docker exec kanban-api sh -c "curl -sf http://localhost:5010/health && echo ' OK' || echo 'UNHEALTHY'"

echo ""
echo "=== RESOURCE USAGE ==="
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"

6. Docker Networks & Volumes

6.1 Networks

# List networks
docker network ls

# Create network
docker network create mynet

# Connect running container to network
docker network connect mynet kanban-api

# Disconnect container from network
docker network disconnect mynet kanban-api

# Remove network
docker network rm mynet

# Inspect (see which containers are connected)
docker network inspect mynet

How containers communicate in Docker Compose:

┌─── docker-compose network (auto-created) ────────────────┐
│                                                            │
│  ┌─────────┐    ┌─────────┐    ┌──────────┐             │
│  │  api    │◄──▶│  web    │◄──▶│ postgres │             │
│  │ :5010   │    │ :3000   │    │ :5432    │             │
│  └─────────┘    └─────────┘    └──────────┘             │
│                                                            │
│  Containers reach each other by SERVICE NAME:              │
│  api → "postgres:5432" (NOT localhost:5432)                │
│  gateway → "api:5010", "web:3000", "admin:80"             │
│                                                            │
└────────────────────────────────────────────────────────────┘

6.2 Volumes

# List volumes
docker volume ls

# Create volume
docker volume create mydata

# Inspect volume
docker volume inspect mydata

# Remove volume
docker volume rm mydata

# Remove unused volumes
docker volume prune

# Remove ALL volumes (DANGEROUS)
docker volume prune -a

Volume types:

services:
  api:
    volumes:
      # Named volume (managed by Docker, persists)
      - postgres_data:/var/lib/postgresql/data
      
      # Bind mount (maps host directory into container)
      - ./Caddyfile:/etc/caddy/Caddyfile
      
      # Anonymous volume (auto-named, less manageable)
      - /app/node_modules

volumes:
  postgres_data:    # Named volume declaration

7. Docker Registry (GHCR)

GitHub Container Registry (GHCR) stores our Docker images. URL format: ghcr.io/OWNER/REPO/IMAGE:TAG

7.1 Login to GHCR

# Using Personal Access Token (PAT)
echo "ghp_your_token_here" | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin

7.2 Push Images to GHCR

# Tag your local image for GHCR
docker tag kanban-api:latest ghcr.io/devopsmalaysia/bootcamp/api:latest

# Push
docker push ghcr.io/devopsmalaysia/bootcamp/api:latest

7.3 Pull Images from GHCR

# Pull specific image
docker pull ghcr.io/devopsmalaysia/bootcamp/api:latest

# Pull all images defined in docker-compose
docker compose pull

7.4 Build & Push (One Step with Buildx)

docker buildx build --platform linux/amd64 --push \
  -t ghcr.io/devopsmalaysia/bootcamp/api:latest \
  -f services/api-dotnet/Dockerfile .

8. Cleaning Up

⚠️ Docker can consume significant disk space. Clean up regularly.

8.1 Check Disk Usage

docker system df

Example output:

TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          15        3         4.2GB     3.1GB (73%)
Containers      5         3         250MB     200MB (80%)
Local Volumes   8         2         1.5GB     900MB (60%)
Build Cache     --        --        2.1GB     2.1GB

8.2 Remove Unused Resources

CommandWhat it removes
docker system pruneStopped containers + unused networks + dangling images
docker system prune -aAbove + ALL unused images (not just dangling)
docker system prune -a --volumesAbove + unused volumes ⚠️ DATA LOSS
docker image pruneDangling images only
docker image prune -aAll unused images
docker container pruneAll stopped containers
docker volume pruneAll unused volumes ⚠️
docker network pruneAll unused networks
docker builder pruneBuild cache
docker builder prune -aALL build cache

8.3 Selective Cleanup

# Remove specific containers
docker rm container1 container2

# Remove all containers matching a pattern
docker rm $(docker ps -a --filter "name=kanban" -q)

# Remove images by pattern
docker rmi $(docker images "ghcr.io/devopsmalaysia/*" -q)

# Remove old images (dangling = untagged intermediate layers)
docker image prune

# Remove stopped containers older than 24h
docker container prune --filter "until=24h"

8.4 Nuclear Option (Start Fresh)

# ⚠️ REMOVES EVERYTHING — containers, images, volumes, networks, cache
docker system prune -a --volumes -f
docker builder prune -a -f

# Verify clean slate
docker system df

8.5 Docker Compose Cleanup

# Stop and remove containers + default network
docker compose down

# Stop and remove containers + networks + volumes
docker compose down -v

# Stop and remove containers + networks + volumes + images
docker compose down -v --rmi all

# Remove specific service's container and image
docker compose rm -sf api
docker rmi $(docker compose images api -q)

9. Dockerfile Reference

9.1 Common Instructions

InstructionPurposeExample
FROMBase imageFROM node:20-slim
WORKDIRSet working directoryWORKDIR /app
COPYCopy files from hostCOPY package.json .
RUNExecute command during buildRUN npm install
ENVSet environment variableENV NODE_ENV=production
EXPOSEDocument port (doesn't publish)EXPOSE 3000
CMDDefault command when container startsCMD ["node", "server.js"]
ENTRYPOINTFixed command (CMD becomes args)ENTRYPOINT ["dotnet", "app.dll"]
ARGBuild-time variableARG VERSION=latest
VOLUMEDeclare mount pointVOLUME /data

9.2 Multi-Stage Build Pattern

Used in this bootcamp for smaller production images:

# Stage 1: BUILD (large image with dev tools)
FROM node:20-slim AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install
COPY . .
RUN pnpm build

# Stage 2: RUN (small image, only production files)
FROM node:20-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]

Benefits:

  • Build tools don't end up in production image
  • Final image is much smaller (100MB vs 1GB+)
  • Fewer security vulnerabilities

9.3 Bootcamp Dockerfiles Explained

.NET API (services/api-dotnet/Dockerfile):

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS builder    # Big SDK for building
WORKDIR /src
COPY services/api-dotnet/ services/api-dotnet/
COPY contracts/ contracts/
WORKDIR /src/services/api-dotnet
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runner  # Small runtime only
WORKDIR /app
COPY --from=builder /app/publish .
EXPOSE 5010
ENTRYPOINT ["dotnet", "api-dotnet.dll"]

Next.js Web (apps/web/Dockerfile):

FROM node:20-slim AS builder
RUN corepack enable
WORKDIR /app
COPY . .
RUN pnpm install --force
RUN pnpm turbo run build --filter=web

FROM node:20-slim AS runner
ENV NODE_ENV=production
COPY --from=builder /app/apps/web/.next/standalone ./
COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static
EXPOSE 3000
CMD ["node", "apps/web/server.js"]

Vite Admin (apps/admin/Dockerfile):

FROM node:20-slim AS builder
RUN corepack enable
WORKDIR /app
COPY . .
RUN pnpm install --force
RUN pnpm turbo run build --filter=admin

FROM caddy:2-alpine AS runner          # Caddy serves static files
COPY --from=builder /app/apps/admin/dist ./admin
COPY apps/admin/Caddyfile /etc/caddy/Caddyfile
EXPOSE 80

10. Quick Reference Card

Most Used Commands

# ─── BUILD ──────────────────────────────────────────
docker build -t myapp .
docker compose build

# ─── RUN ────────────────────────────────────────────
docker compose up -d              # Start all services
docker compose up -d --build      # Rebuild & start
docker run -d -p 3000:3000 myapp  # Run single container

# ─── STATUS ─────────────────────────────────────────
docker compose ps                 # Service status
docker ps                         # Running containers
docker stats                      # Live resource usage

# ─── LOGS ───────────────────────────────────────────
docker compose logs -f            # Follow all logs
docker compose logs -f api        # Follow one service
docker logs --tail 50 -f NAME     # Last 50 + follow

# ─── DEBUG ──────────────────────────────────────────
docker exec -it CONTAINER sh      # Shell into container
docker exec CONTAINER env         # View env vars
docker inspect CONTAINER          # Full details

# ─── STOP ───────────────────────────────────────────
docker compose down               # Stop & remove
docker compose down -v            # Stop & remove + volumes
docker stop CONTAINER             # Stop one container

# ─── CLEAN ──────────────────────────────────────────
docker system prune -a            # Remove all unused
docker builder prune -a           # Clear build cache

# ─── REGISTRY ───────────────────────────────────────
echo $CR_PAT | docker login ghcr.io -u USER --password-stdin
docker compose pull               # Pull latest images
docker push ghcr.io/org/repo/img  # Push image

Bootcamp-Specific Commands

# ─── LOCAL DEVELOPMENT ──────────────────────────────

# Start database only
docker compose up -d

# Start full production stack locally
docker compose -f docker-compose.prod.yml up -d

# View the app
# Web:   http://localhost/
# Admin: http://localhost/admin/
# API:   http://localhost/api/

# ─── VPS DEPLOYMENT ─────────────────────────────────

# SSH into VPS
ssh vps

# Navigate to project
cd ~/kanban-prod

# Login to GHCR
echo $CR_PAT | docker login ghcr.io -u $GH_USERNAME --password-stdin

# Start services
docker compose up -d

# Check everything is running
docker compose ps

# View logs if something is wrong
docker compose logs -f

# Update after new images are pushed
docker compose pull && docker compose up -d

# ─── COMMON FIXES ───────────────────────────────────

# Restart a misbehaving service
docker compose restart api

# Rebuild if code changed
docker compose up -d --build api

# Reset database (WARNING: deletes all data)
docker compose down -v
docker compose up -d

# Check SSL certificate generation
docker compose logs gateway | grep "certificate"

Environment Variable Patterns

# Pass single variable
docker run -e DATABASE_URL="..." myapp

# Pass from host environment
export DATABASE_URL="..."
docker run -e DATABASE_URL myapp

# Use .env file
docker run --env-file .env myapp

# In docker-compose.yml:
services:
  api:
    environment:
      - DATABASE_URL=${DATABASE_URL}     # From .env or host
      - ASPNETCORE_ENVIRONMENT=Production # Hardcoded value
    env_file:
      - .env                              # Load entire file

Useful Aliases (Add to ~/.bashrc or ~/.zshrc)

# Docker shortcuts
alias d='docker'
alias dc='docker compose'
alias dps='docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'
alias dlogs='docker compose logs -f'
alias dexec='docker exec -it'
alias dclean='docker system prune -af && docker builder prune -af'

# Bootcamp-specific
alias deploy-up='cd ~/kanban-prod && docker compose pull && docker compose up -d'
alias deploy-logs='cd ~/kanban-prod && docker compose logs -f'
alias deploy-status='cd ~/kanban-prod && docker compose ps'

Diagram: Bootcamp Container Architecture

┌─────────────────────────────────────────────────────────────────┐
│                    DOCKER HOST (VPS)                              │
│                                                                   │
│  ┌──────────────────── docker compose network ─────────────────┐ │
│  │                                                              │ │
│  │   ┌─────────────┐     ┌────────────┐    ┌──────────────┐   │ │
│  │   │   gateway    │     │    api     │    │     web      │   │ │
│  │   │   (Caddy)    │────▶│  (.NET)    │    │  (Next.js)   │   │ │
│  │   │   :80/:443   │     │   :5010    │    │    :3000     │   │ │
│  │   └──────┬───────┘     └────────────┘    └──────────────┘   │ │
│  │          │              ┌────────────┐    ┌──────────────┐   │ │
│  │          │              │   admin    │    │  watchtower  │   │ │
│  │          └─────────────▶│   (Vite)   │    │ (auto-update)│   │ │
│  │                         │    :80     │    │              │   │ │
│  │                         └────────────┘    └──────────────┘   │ │
│  │                                                              │ │
│  └──────────────────────────────────────────────────────────────┘ │
│                                                                   │
│  Ports exposed to internet: 80, 443 (via gateway only)            │
│                                                                   │
└───────────────────────────────────────────────────────────────┬──┘
                                                                │
                                                                │ DATABASE_URL
                                                                ▼
                                                  ┌───────────────────────┐
                                                  │  DigitalOcean Managed  │
                                                  │  PostgreSQL :25060     │
                                                  └───────────────────────┘
Built with LogoFlowershow