Skip to main content

Docker and Deployment Explained


Table of Contents

  1. What is Docker?
  2. Dockerfile Explained
  3. Multi-Stage Build
  4. Cloud Build Deployment Workflow
  5. Cloud Run Runtime Environment
  6. Deployment Checklist

What is Docker?

Docker is a containerization technology that can package an application and all its dependencies into a "container." Containers are like lightweight, portable "virtual environments" that can run on any machine that supports Docker without worrying about environment differences.

Simple Analogy: Imagine you're moving. The traditional way is to list everything you need to bring and then reassemble it in the new home. Docker is like packing the entire room (including furniture, appliances, decorations) into a "shipping container." After moving to the new home, you can open it directly and use it without reassembly.

Why Use Docker?

  1. Environment Consistency: Development, testing, and production environments are completely consistent, avoiding "works on my machine" problems
  2. Fast Deployment: Containers can start in seconds, much faster than traditional virtual machines
  3. Resource Efficient: Containers share the operating system kernel, lighter than virtual machines
  4. Easy Scaling: Can automatically create or destroy containers based on load

In this project, Docker is used for:

  • Building Application Images: Package frontend and backend code into container images
  • Deploying to Cloud Run: Run containerized applications on Google Cloud Run

Dockerfile Explained

What is a Dockerfile?

A Dockerfile is a text file containing a series of instructions telling Docker how to build a container image. It's like a "recipe," and Docker builds the final "dish" (container image) step by step according to this "recipe."

Project Dockerfile Structure

This project's Dockerfile uses a multi-stage build (Multi-stage Build) approach, divided into three stages:

Stage 1: Frontend Build

FROM node:20-slim AS frontend-build

WORKDIR /app

# Copy package.json and install dependencies
COPY package.json package-lock.json ./
RUN npm install -g npm@11
RUN npm install

# Copy frontend source code and build
COPY . .
RUN npm run build && test -f dist/index.html

Notes:

  • Base Image: node:20-slim (Node.js 20 slim version, smaller size)
  • Working Directory: /app (working directory inside container)
  • Dependency Installation: Copy package.json first, install dependencies (utilizing Docker cache layers)
  • Code Build: Copy source code, run build command, generate dist/ directory

Why Copy package.json First? Docker caches each layer. If package.json hasn't changed, there's no need to reinstall dependencies, which can greatly speed up builds.

Stage 2: Backend Build

FROM node:20-slim AS backend-build

WORKDIR /app

# Copy backend package.json and config files
COPY server/package.json server/package-lock.json server/tsconfig.json ./server/
COPY tsconfig.json /app/tsconfig.json

# Install backend dependencies
WORKDIR /app/server
RUN npm install -g npm@11
RUN npm install

# Copy backend source code and build
COPY server/ ./
RUN npm run build && test -f dist/index.js

Notes:

  • Independent Build: Backend builds in an independent stage, not dependent on frontend
  • TypeScript Compilation: Run npm run build to compile TypeScript to JavaScript
  • Build Verification: Use test -f dist/index.js to verify build artifacts exist

Stage 3: Production Runtime Environment

FROM node:20-slim

WORKDIR /app

# Install production dependencies (only backend dependencies)
WORKDIR /app/server
COPY --from=backend-build /app/server/package.json ./
COPY --from=backend-build /app/server/package-lock.json ./
RUN npm install -g npm@11 \
&& npm ci --omit=dev

# Copy backend build artifacts
COPY --from=backend-build /app/server/dist ./dist

# Copy frontend build artifacts to public directory
WORKDIR /app
COPY --from=frontend-build /app/dist ./public

# Copy startup script
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

# Set environment variables
ENV NODE_ENV=production
ENV STATIC_DIR=/app/public

# Expose port
EXPOSE 8080

# Use startup script
ENTRYPOINT ["/entrypoint.sh"]

Notes:

  • Minimize Image: Only copy build artifacts, not source code and development dependencies
  • Frontend Static Files: Frontend build artifacts copied to public/ directory, served by backend server
  • Startup Script: Use entrypoint.sh as container startup entry point
  • Port Exposure: Expose port 8080 (Cloud Run automatically injects PORT environment variable)

Key Instruction Notes

InstructionDescriptionExample
FROMSpecify base imageFROM node:20-slim
WORKDIRSet working directoryWORKDIR /app
COPYCopy files to containerCOPY package.json ./
RUNExecute commandRUN npm install
ENVSet environment variableENV NODE_ENV=production
EXPOSEDeclare portEXPOSE 8080
ENTRYPOINTSet startup commandENTRYPOINT ["/entrypoint.sh"]

Multi-Stage Build

Why Use Multi-Stage Build?

Multi-stage Build allows using multiple FROM instructions in one Dockerfile. Each stage can have different base images and build steps.

Advantages:

  1. Reduce Image Size: Final image only contains files needed for runtime, not build tools and source code
  2. Improve Security: Source code and build tools won't appear in the final image
  3. Optimize Build Speed: Can build frontend and backend in parallel

Build Workflow

graph TD
A[Start Build] --> B[Stage 1: Frontend Build]
A --> C[Stage 2: Backend Build]
B --> D[Generate dist/ directory]
C --> E[Generate dist/index.js]
D --> F[Stage 3: Production Environment]
E --> F
F --> G[Copy frontend artifacts to public/]
F --> H[Copy backend artifacts to dist/]
F --> I[Install production dependencies]
G --> J[Final Image]
H --> J
I --> J

Build Artifact Size Comparison

Build MethodImage SizeContains
Single-Stage Build~800 MBSource code + node_modules + build tools + build artifacts
Multi-Stage Build~200 MBOnly build artifacts + production dependencies

Space Savings: Multi-stage build can reduce image size by approximately 75%.


Cloud Build Deployment Workflow

What is Cloud Build?

Google Cloud Build is a fully managed CI/CD (Continuous Integration/Continuous Deployment) service that can automatically build, test, and deploy applications. When code is pushed to a Git repository, Cloud Build automatically triggers the build process.

Deployment Workflow

sequenceDiagram
participant Dev as Developer
participant Git as Git Repository
participant CB as Cloud Build
participant AR as Artifact Registry
participant CR as Cloud Run

Dev->>Git: 1. Push code
Git->>CB: 2. Trigger build
CB->>CB: 3. Execute cloudbuild.yaml
CB->>CB: 4. Build Docker image
CB->>AR: 5. Push image to Artifact Registry
CB->>CR: 6. Deploy to Cloud Run
CR-->>Dev: 7. Service available

cloudbuild.yaml Configuration

The cloudbuild.yaml file defines Cloud Build's build and deployment steps:

steps:
# Step 1: Build Docker image
- name: 'gcr.io/cloud-builders/docker'
id: 'build-image'
args:
- 'build'
- '-t'
- '${_AR_HOSTNAME}/${_AR_PROJECT_ID}/${_AR_REPOSITORY}/${_SERVICE_NAME}:latest'
- '-t'
- '${_AR_HOSTNAME}/${_AR_PROJECT_ID}/${_AR_REPOSITORY}/${_SERVICE_NAME}:$SHORT_SHA'
- '.'

# Step 2: Push image (latest tag)
- name: 'gcr.io/cloud-builders/docker'
id: 'push-image-latest'
args: ['push', '${_AR_HOSTNAME}/${_AR_PROJECT_ID}/${_AR_REPOSITORY}/${_SERVICE_NAME}:latest']

# Step 3: Push image (SHA tag)
- name: 'gcr.io/cloud-builders/docker'
id: 'push-image-sha'
args: ['push', '${_AR_HOSTNAME}/${_AR_PROJECT_ID}/${_AR_REPOSITORY}/${_SERVICE_NAME}:$SHORT_SHA']

# Step 4: Deploy to Cloud Run
- name: 'gcr.io/cloud-builders/gcloud'
id: 'deploy-to-cloud-run'
args:
- 'run'
- 'deploy'
- '${_SERVICE_NAME}'
- '--image'
- '${_AR_HOSTNAME}/${_AR_PROJECT_ID}/${_AR_REPOSITORY}/${_SERVICE_NAME}:$SHORT_SHA'
- '--region'
- '${_DEPLOY_REGION}'
- '--platform'
- 'managed'
- '--allow-unauthenticated'
- '--set-env-vars'
- >-
PORT=8080,
VITE_GEMINI_API_KEY=${_VITE_GEMINI_API_KEY},
...

Substitution Variables

Cloud Build uses substitution variables to inject configuration information:

Variable NameDescriptionExample Value
_AR_HOSTNAMEArtifact Registry hostnameasia-northeast1-docker.pkg.dev
_AR_PROJECT_IDGoogle Cloud project IDgrcn-sca-bigquery
_AR_REPOSITORYArtifact Registry repository namecloudrun-source-deploy
_SERVICE_NAMECloud Run service namehelen-new-insighthub
_DEPLOY_REGIONDeployment regionasia-northeast1
$SHORT_SHAGit commit SHA (first 7 characters)a1b2c3d

Configuration Method:

  1. Configure substitution variables in Cloud Build trigger settings
  2. Or define default values in the substitutions section of cloudbuild.yaml

Image Tag Strategy

The system uses two tag strategies:

  1. latest Tag: Always points to the latest build
  2. $SHORT_SHA Tag: Points to a specific Git commit

Advantages:

  • Quick Rollback: If the new version has issues, can quickly roll back to a previous $SHORT_SHA version
  • Version Tracking: Each deployment has a unique tag, facilitating tracking and debugging

Cloud Run Runtime Environment

What is Cloud Run?

Google Cloud Run is a fully managed serverless container runtime environment that can automatically scale, handle traffic load, and bill based on actual usage.

Features:

  • Auto Scaling: Automatically create or destroy container instances based on request volume
  • Pay-as-You-Go: Only charged when containers are running, no charge when idle
  • Zero Operations: No need to manage servers; Google automatically handles infrastructure

Startup Workflow

When the container starts, it executes the entrypoint.sh script:

#!/bin/bash
set -e

# Wait for Cloud Run to inject PORT environment variable
PORT=${PORT:-8080}

# Start backend server
cd /app/server
exec node dist/index.js

Notes:

  • Port Configuration: Cloud Run automatically injects the PORT environment variable, defaulting to 8080
  • Static File Service: Backend server serves frontend static files (from /app/public directory)
  • Process Management: Use exec to ensure the Node.js process becomes the container's main process

Environment Variable Injection

Cloud Run injects environment variables during deployment (through the --set-env-vars parameter):

- '--set-env-vars'
- >-
PORT=8080,
VITE_GEMINI_API_KEY=${_VITE_GEMINI_API_KEY},
VITE_GOOGLE_OAUTH_CLIENT_ID=${_VITE_GOOGLE_OAUTH_CLIENT_ID},
...

How It Works:

  1. Cloud Build reads substitution variables from trigger configuration
  2. When deploying Cloud Run, injects these variables as environment variables
  3. When container starts, can access these variables through process.env

Auto Scaling

Cloud Run automatically adjusts container instance count based on request volume:

  • Minimum Instances: 0 (no instances running when idle, no cost)
  • Maximum Instances: Based on configuration (default 100)
  • Scaling Speed: Usually completes within seconds

Configuration Example (in Cloud Build):

- '--min-instances'
- '0'
- '--max-instances'
- '10'
- '--concurrency'
- '80'

Deployment Checklist

Pre-Deployment Checks

  • Environment Variable Configuration: All required environment variables are configured in Cloud Build trigger
  • Service Account Permissions: Service account has necessary Google Cloud permissions
  • Apps Script Configuration: Apps Script projects are created and configured
  • Artifact Registry: Image repository is created
  • Cloud Run Service: Service is created (first deployment) or exists (update deployment)

Build Checks

  • Dockerfile Syntax: Dockerfile syntax is correct, no errors
  • Dependency Installation: Dependency versions in package.json are correct
  • Build Commands: Frontend and backend build commands are correct
  • Build Artifacts: Build artifacts (dist/ directory) exist

Deployment Checks

  • Image Push: Image successfully pushed to Artifact Registry
  • Service Deployment: Cloud Run service deployment successful
  • Environment Variables: Environment variables correctly injected
  • Service Health: Service health check passes

Post-Deployment Verification

  • Service Accessible: Can access service through Cloud Run URL
  • Frontend Loading: Frontend page loads normally
  • API Calls: Backend API responds normally
  • Feature Testing: Core features (login, file upload, analysis) work normally

Log Checks

  • Build Logs: Cloud Build logs have no errors
  • Runtime Logs: Cloud Run logs have no errors
  • Application Logs: Application logs output normally

Frequently Asked Questions

Q1: Build fails with "npm install" error?

Possible Reasons:

  1. Dependency versions in package.json are incompatible
  2. Network issues causing dependency download failure
  3. Platform-specific optional dependency installation failure

Solutions:

  1. Check dependency versions in package.json
  2. Run npm install locally to verify dependency installation
  3. Upgrade npm to v11 (already handled in Dockerfile)

Q2: Service cannot be accessed after deployment?

Possible Reasons:

  1. Environment variables not correctly injected
  2. Port configuration error
  3. Service not started correctly

Solutions:

  1. Check Cloud Run service's environment variable configuration
  2. View Cloud Run logs to check startup errors
  3. Verify entrypoint.sh script executes correctly

Q3: How to roll back to a previous version?

Method 1: Through Cloud Console

  1. In Cloud Run service page, click "Revisions"
  2. Select the version to roll back to
  3. Click "Manage Traffic," route 100% traffic to that version

Method 2: Through gcloud Command

gcloud run services update-traffic SERVICE_NAME \
--to-revisions REVISION_NAME=100

Q4: How to view build and deployment logs?

Cloud Build Logs:

  1. Open Cloud Build in Google Cloud Console
  2. View build history
  3. Click build record to view detailed logs

Cloud Run Logs:

  1. Open Cloud Run in Google Cloud Console
  2. Select service
  3. Click "Logs" tab to view runtime logs

Summary

This section detailed Docker and deployment-related knowledge, including:

  1. Docker Basics: What is Docker, why use Docker
  2. Dockerfile Explained: Multi-stage build implementation details
  3. Cloud Build Deployment: Automated build and deployment workflow
  4. Cloud Run Runtime: Serverless container runtime mechanism
  5. Deployment Checklist: Checklist items to ensure successful deployment

Correctly understanding and configuring Docker and deployment workflows is key to stable system operation.


Related Documentation: