Skip to content

Commit 34a8ab0

Browse files
authored
chore: upgrade Docker setup with multi-stage build and version bumps (#769)
- Node.js 20.18.1 → 22.13.1 (LTS) - pnpm 10.11.1 → 10.28.1 - Base image bullseye → bookworm - Production now uses nginx:alpine for smaller image - Extract nginx config to docker/nginx.conf
1 parent dcf4c3a commit 34a8ab0

File tree

4 files changed

+149
-85
lines changed

4 files changed

+149
-85
lines changed

.dockerignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,6 @@ docker-compose*.yaml
6666
*.tsbuildinfo
6767
.eslintcache
6868
.prettierignore
69+
70+
# Dockerfile itself (not needed in build context for static files)
71+
Dockerfile*

Dockerfile

Lines changed: 70 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,102 @@
11
# Universal Dockerfile for DevOps Daily
2-
# Switch environments with: BUILD_ENV=development or BUILD_ENV=production
2+
# Multi-stage build for optimized development and production images
33

4-
ARG NODE_VERSION=20.18.1
5-
ARG PNPM_VERSION=10.11.1
6-
ARG BUILD_ENV=production
4+
ARG NODE_VERSION=22.13.1
5+
ARG PNPM_VERSION=10.28.1
76

8-
FROM node:${NODE_VERSION}-bullseye-slim
7+
# ===========================================================================
8+
# Stage 1: Base image with common dependencies
9+
# ===========================================================================
10+
FROM node:${NODE_VERSION}-bookworm-slim AS base
911

10-
ARG BUILD_ENV
1112
ARG PNPM_VERSION
1213

13-
# Install system packages based on environment
1414
RUN apt-get update && \
1515
apt-get upgrade -y && \
16-
if [ "$BUILD_ENV" = "production" ]; then \
17-
apt-get install -y --no-install-recommends nginx curl; \
18-
else \
19-
apt-get install -y --no-install-recommends curl; \
20-
fi && \
16+
apt-get install -y --no-install-recommends curl && \
2117
apt-get clean && \
2218
rm -rf /var/lib/apt/lists/*
2319

24-
# Install pnpm
2520
RUN corepack enable && \
2621
corepack prepare pnpm@${PNPM_VERSION} --activate
2722

2823
WORKDIR /app
2924

30-
# Copy package files
31-
COPY package.json pnpm-lock.yaml ./
25+
# ===========================================================================
26+
# Stage 2: Development image
27+
# Hot-reload enabled for local development
28+
# ===========================================================================
29+
FROM base AS development
3230

33-
# Install dependencies based on environment
34-
RUN if [ "$BUILD_ENV" = "production" ]; then \
35-
pnpm install --frozen-lockfile --prod; \
36-
else \
37-
pnpm install --frozen-lockfile; \
38-
fi
31+
COPY package.json pnpm-lock.yaml ./
32+
RUN pnpm install --frozen-lockfile
3933

40-
# Copy source code
4134
COPY . .
4235

43-
# Build for production if needed
44-
RUN if [ "$BUILD_ENV" = "production" ]; then \
45-
NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 pnpm run build:cf && \
46-
rm -rf /var/www/html/* && \
47-
mkdir -p /var/www/html && \
48-
cp -r /app/out/* /var/www/html/ && \
49-
echo 'server {' > /etc/nginx/sites-available/default && \
50-
echo ' listen 80;' >> /etc/nginx/sites-available/default && \
51-
echo ' listen [::]:80;' >> /etc/nginx/sites-available/default && \
52-
echo ' server_name localhost;' >> /etc/nginx/sites-available/default && \
53-
echo ' root /var/www/html;' >> /etc/nginx/sites-available/default && \
54-
echo ' index index.html;' >> /etc/nginx/sites-available/default && \
55-
echo '' >> /etc/nginx/sites-available/default && \
56-
echo ' gzip on;' >> /etc/nginx/sites-available/default && \
57-
echo ' gzip_vary on;' >> /etc/nginx/sites-available/default && \
58-
echo ' gzip_min_length 1024;' >> /etc/nginx/sites-available/default && \
59-
echo ' gzip_types text/plain text/css text/xml text/javascript application/javascript application/json;' >> /etc/nginx/sites-available/default && \
60-
echo '' >> /etc/nginx/sites-available/default && \
61-
echo ' add_header X-Frame-Options "SAMEORIGIN" always;' >> /etc/nginx/sites-available/default && \
62-
echo ' add_header X-Content-Type-Options "nosniff" always;' >> /etc/nginx/sites-available/default && \
63-
echo ' add_header X-XSS-Protection "1; mode=block" always;' >> /etc/nginx/sites-available/default && \
64-
echo ' add_header Referrer-Policy "strict-origin-when-cross-origin" always;' >> /etc/nginx/sites-available/default && \
65-
echo ' add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;' >> /etc/nginx/sites-available/default && \
66-
echo '' >> /etc/nginx/sites-available/default && \
67-
echo ' location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {' >> /etc/nginx/sites-available/default && \
68-
echo ' expires 1y;' >> /etc/nginx/sites-available/default && \
69-
echo ' add_header Cache-Control "public, immutable";' >> /etc/nginx/sites-available/default && \
70-
echo ' }' >> /etc/nginx/sites-available/default && \
71-
echo '' >> /etc/nginx/sites-available/default && \
72-
echo ' location / {' >> /etc/nginx/sites-available/default && \
73-
echo ' try_files \$uri \$uri.html /index.html;' >> /etc/nginx/sites-available/default && \
74-
echo ' }' >> /etc/nginx/sites-available/default && \
75-
echo '' >> /etc/nginx/sites-available/default && \
76-
echo ' error_page 404 /404.html;' >> /etc/nginx/sites-available/default && \
77-
echo '}' >> /etc/nginx/sites-available/default; \
78-
fi
79-
80-
# Environment variables
81-
ENV NODE_ENV=${BUILD_ENV:-production}
36+
ENV NODE_ENV=development
8237
ENV NEXT_TELEMETRY_DISABLED=1
8338
ENV WATCHPACK_POLLING=true
8439

85-
# Metadata labels
8640
LABEL org.opencontainers.image.title="DevOps Daily"
8741
LABEL org.opencontainers.image.description="A modern content platform for DevOps professionals"
8842
LABEL org.opencontainers.image.source="https://github.com/The-DevOps-Daily/devops-daily"
8943
LABEL org.opencontainers.image.licenses="MIT"
9044
LABEL org.opencontainers.image.vendor="DevOps Daily"
9145
LABEL org.opencontainers.image.authors="DevOps Daily Team"
9246

93-
# Expose ports (both for flexibility)
94-
EXPOSE 3000 80
47+
EXPOSE 3000
9548

96-
# Health check that adapts to environment
9749
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
98-
CMD if [ "$NODE_ENV" = "production" ]; then \
99-
curl -f http://localhost:80/ || exit 1; \
100-
else \
101-
curl -f http://localhost:3000/ || exit 1; \
102-
fi
103-
104-
# Start command based on environment
105-
CMD ["/bin/sh", "-c", "if [ \"$NODE_ENV\" = \"production\" ]; then nginx -g 'daemon off;'; else pnpm run dev; fi"]
50+
CMD curl -f http://localhost:3000/ || exit 1
51+
52+
CMD ["pnpm", "run", "dev"]
53+
54+
# ===========================================================================
55+
# Stage 3: Production builder
56+
# Builds the static site for production
57+
# ===========================================================================
58+
FROM base AS builder
59+
60+
COPY package.json pnpm-lock.yaml ./
61+
RUN pnpm install --frozen-lockfile
62+
63+
COPY . .
64+
65+
ENV NODE_ENV=production
66+
ENV NEXT_TELEMETRY_DISABLED=1
67+
68+
RUN pnpm run build:cf
69+
70+
# ===========================================================================
71+
# Stage 4: Production runtime
72+
# Minimal nginx image serving static files
73+
# ===========================================================================
74+
FROM nginx:1.27-alpine AS production
75+
76+
RUN rm /etc/nginx/conf.d/default.conf
77+
78+
COPY docker/nginx.conf /etc/nginx/nginx.conf
79+
COPY --from=builder /app/out /usr/share/nginx/html
80+
81+
RUN chown -R nginx:nginx /usr/share/nginx/html && \
82+
chown -R nginx:nginx /var/cache/nginx && \
83+
chown -R nginx:nginx /var/log/nginx && \
84+
touch /var/run/nginx.pid && \
85+
chown -R nginx:nginx /var/run/nginx.pid
86+
87+
ENV NODE_ENV=production
88+
ENV NEXT_TELEMETRY_DISABLED=1
89+
90+
LABEL org.opencontainers.image.title="DevOps Daily"
91+
LABEL org.opencontainers.image.description="A modern content platform for DevOps professionals"
92+
LABEL org.opencontainers.image.source="https://github.com/The-DevOps-Daily/devops-daily"
93+
LABEL org.opencontainers.image.licenses="MIT"
94+
LABEL org.opencontainers.image.vendor="DevOps Daily"
95+
LABEL org.opencontainers.image.authors="DevOps Daily Team"
96+
97+
EXPOSE 80
98+
99+
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
100+
CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1
101+
102+
CMD ["nginx", "-g", "daemon off;"]

docker-compose.yaml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ services:
1212
build:
1313
context: .
1414
dockerfile: Dockerfile
15+
target: development
1516
args:
16-
NODE_VERSION: 20.18.1
17-
PNPM_VERSION: 10.11.1
18-
BUILD_ENV: development
17+
NODE_VERSION: 22.13.1
18+
PNPM_VERSION: 10.28.1
1919
container_name: devops-daily-dev
2020
image: devops-daily:dev
2121
ports:
@@ -63,16 +63,16 @@ services:
6363

6464
# ===========================================================================
6565
# Production Service
66-
# Uses the main Dockerfile with nginx for serving static files
66+
# Uses multi-stage build with nginx for serving static files
6767
# ===========================================================================
6868
prod:
6969
build:
7070
context: .
7171
dockerfile: Dockerfile
72+
target: production
7273
args:
73-
NODE_VERSION: 20.18.1
74-
PNPM_VERSION: 10.11.1
75-
BUILD_ENV: production
74+
NODE_VERSION: 22.13.1
75+
PNPM_VERSION: 10.28.1
7676
container_name: devops-daily-prod
7777
image: devops-daily:prod
7878
ports:
@@ -84,22 +84,22 @@ services:
8484
# Restart policy for production
8585
restart: unless-stopped
8686
healthcheck:
87-
test: ["CMD", "curl", "-f", "http://localhost:80/"]
87+
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80/"]
8888
interval: 30s
8989
timeout: 10s
9090
retries: 3
91-
start_period: 40s
91+
start_period: 10s
9292
labels:
9393
- "com.devops-daily.service=prod"
9494
- "com.devops-daily.environment=production"
9595
deploy:
9696
resources:
9797
limits:
9898
cpus: '1.0'
99-
memory: 1G
100-
reservations:
101-
cpus: '0.5'
10299
memory: 512M
100+
reservations:
101+
cpus: '0.25'
102+
memory: 128M
103103
logging:
104104
driver: "json-file"
105105
options:

docker/nginx.conf

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
worker_processes auto;
2+
error_log /var/log/nginx/error.log warn;
3+
pid /var/run/nginx.pid;
4+
5+
events {
6+
worker_connections 1024;
7+
use epoll;
8+
multi_accept on;
9+
}
10+
11+
http {
12+
include /etc/nginx/mime.types;
13+
default_type application/octet-stream;
14+
15+
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
16+
'$status $body_bytes_sent "$http_referer" '
17+
'"$http_user_agent" "$http_x_forwarded_for"';
18+
19+
access_log /var/log/nginx/access.log main;
20+
21+
sendfile on;
22+
tcp_nopush on;
23+
tcp_nodelay on;
24+
keepalive_timeout 65;
25+
types_hash_max_size 2048;
26+
27+
# Gzip compression
28+
gzip on;
29+
gzip_vary on;
30+
gzip_min_length 1024;
31+
gzip_proxied any;
32+
gzip_comp_level 6;
33+
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml application/rss+xml application/atom+xml image/svg+xml;
34+
35+
server {
36+
listen 80;
37+
listen [::]:80;
38+
server_name localhost;
39+
root /usr/share/nginx/html;
40+
index index.html;
41+
42+
# Security headers
43+
add_header X-Frame-Options "SAMEORIGIN" always;
44+
add_header X-Content-Type-Options "nosniff" always;
45+
add_header X-XSS-Protection "1; mode=block" always;
46+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
47+
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
48+
49+
# Static file caching
50+
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
51+
expires 1y;
52+
add_header Cache-Control "public, immutable";
53+
}
54+
55+
# Main location
56+
location / {
57+
try_files $uri $uri.html $uri/ /index.html;
58+
}
59+
60+
# Error pages
61+
error_page 404 /404.html;
62+
error_page 500 502 503 504 /50x.html;
63+
}
64+
}

0 commit comments

Comments
 (0)