Docker and Deployment Explained
Table of Contents
- What is Docker?
- Dockerfile Explained
- Multi-Stage Build
- Cloud Build Deployment Workflow
- Cloud Run Runtime Environment
- 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?
- Environment Consistency: Development, testing, and production environments are completely consistent, avoiding "works on my machine" problems
- Fast Deployment: Containers can start in seconds, much faster than traditional virtual machines
- Resource Efficient: Containers share the operating system kernel, lighter than virtual machines
- 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.jsonfirst, 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 buildto compile TypeScript to JavaScript - Build Verification: Use
test -f dist/index.jsto 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.shas container startup entry point - Port Exposure: Expose port 8080 (Cloud Run automatically injects
PORTenvironment variable)
Key Instruction Notes
| Instruction | Description | Example |
|---|---|---|
FROM | Specify base image | FROM node:20-slim |
WORKDIR | Set working directory | WORKDIR /app |
COPY | Copy files to container | COPY package.json ./ |
RUN | Execute command | RUN npm install |
ENV | Set environment variable | ENV NODE_ENV=production |
EXPOSE | Declare port | EXPOSE 8080 |
ENTRYPOINT | Set startup command | ENTRYPOINT ["/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:
- Reduce Image Size: Final image only contains files needed for runtime, not build tools and source code
- Improve Security: Source code and build tools won't appear in the final image
- 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 Method | Image Size | Contains |
|---|---|---|
| Single-Stage Build | ~800 MB | Source code + node_modules + build tools + build artifacts |
| Multi-Stage Build | ~200 MB | Only 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 Name | Description | Example Value |
|---|---|---|
_AR_HOSTNAME | Artifact Registry hostname | asia-northeast1-docker.pkg.dev |
_AR_PROJECT_ID | Google Cloud project ID | grcn-sca-bigquery |
_AR_REPOSITORY | Artifact Registry repository name | cloudrun-source-deploy |
_SERVICE_NAME | Cloud Run service name | helen-new-insighthub |
_DEPLOY_REGION | Deployment region | asia-northeast1 |
$SHORT_SHA | Git commit SHA (first 7 characters) | a1b2c3d |
Configuration Method:
- Configure substitution variables in Cloud Build trigger settings
- Or define default values in the
substitutionssection ofcloudbuild.yaml
Image Tag Strategy
The system uses two tag strategies:
latestTag: Always points to the latest build$SHORT_SHATag: Points to a specific Git commit
Advantages:
- Quick Rollback: If the new version has issues, can quickly roll back to a previous
$SHORT_SHAversion - 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
PORTenvironment variable, defaulting to 8080 - Static File Service: Backend server serves frontend static files (from
/app/publicdirectory) - Process Management: Use
execto 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:
- Cloud Build reads substitution variables from trigger configuration
- When deploying Cloud Run, injects these variables as environment variables
- 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.jsonare 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:
- Dependency versions in
package.jsonare incompatible - Network issues causing dependency download failure
- Platform-specific optional dependency installation failure
Solutions:
- Check dependency versions in
package.json - Run
npm installlocally to verify dependency installation - Upgrade npm to v11 (already handled in Dockerfile)
Q2: Service cannot be accessed after deployment?
Possible Reasons:
- Environment variables not correctly injected
- Port configuration error
- Service not started correctly
Solutions:
- Check Cloud Run service's environment variable configuration
- View Cloud Run logs to check startup errors
- Verify
entrypoint.shscript executes correctly
Q3: How to roll back to a previous version?
Method 1: Through Cloud Console
- In Cloud Run service page, click "Revisions"
- Select the version to roll back to
- 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:
- Open Cloud Build in Google Cloud Console
- View build history
- Click build record to view detailed logs
Cloud Run Logs:
- Open Cloud Run in Google Cloud Console
- Select service
- Click "Logs" tab to view runtime logs
Summary
This section detailed Docker and deployment-related knowledge, including:
- Docker Basics: What is Docker, why use Docker
- Dockerfile Explained: Multi-stage build implementation details
- Cloud Build Deployment: Automated build and deployment workflow
- Cloud Run Runtime: Serverless container runtime mechanism
- Deployment Checklist: Checklist items to ensure successful deployment
Correctly understanding and configuring Docker and deployment workflows is key to stable system operation.
Related Documentation: