Skip to content

Commit 0b97326

Browse files
authored
refactor: Consolidate to single universal Dockerfile with BUILD_ENV argument (#569)
* refactor: Consolidate Dockerfiles into unified multi-target approach This PR simplifies Docker configuration by replacing two separate Dockerfiles with a single unified Dockerfile using build targets. ## Changes ### Files Added: - `Dockerfile.unified` - New unified Dockerfile with 'development' and 'production' targets ### Files Modified: - `docker-compose.yaml` - Updated to use unified Dockerfile with explicit targets - `.dockerignore` - Keep ignoring Dockerfile.dev specifically - `.github/workflows/docker-validate.yml` - Updated to lint unified Dockerfile - `README.md` - Updated Docker documentation to reflect unified approach ### Files To Remove (in next commit): - `Dockerfile` (old production-only) - `Dockerfile.dev` (old development-only) ## Benefits 1. **Single Source of Truth**: One Dockerfile to maintain instead of two 2. **Shared Base Layers**: Common stages (base, deps, deps-dev) are reused 3. **Better Caching**: BuildKit can optimize shared layers between targets 4. **Simpler Maintenance**: Changes to Node/pnpm versions in one place 5. **Consistent Behavior**: Same base configuration for dev and prod ## How It Works The unified Dockerfile has 5 stages: - `base`: Common setup (Node.js, pnpm installation) - `deps`: Production dependencies only - `deps-dev`: All dependencies (including devDependencies) - `development` (TARGET): Dev server with hot-reload - `builder`: Builds Next.js static export - `production` (TARGET): Nginx serving static export Docker Compose automatically selects the correct target: - `dev` service → `development` target - `prod` service → `production` target ## Migration Path Existing users can continue using docker-compose without changes. Direct docker build commands need to specify `-f Dockerfile.unified` and `--target`. Once validated, we'll remove the old Dockerfiles in a follow-up commit. * refactor: Consolidate to single universal Dockerfile with BUILD_ENV argument - Removed Dockerfile.unified and Dockerfile.dev - Created single Dockerfile that switches between dev/prod via BUILD_ENV - Simplified docker-compose.yaml to use BUILD_ENV instead of targets - Updated health checks to use curl for both environments - Updated .dockerignore to remove specific Dockerfile references - Updated docker validation workflow to lint new Dockerfile - Updated README documentation with simpler build commands - Architecture: One Dockerfile, two modes, controlled by single argument * fix: Resolve hadolint issues in Dockerfile - Add --no-install-recommends to apt-get install (DL3015) - Escape $uri variables in nginx config (SC2016) - Use JSON notation for CMD (DL3025) - Improves security and follows Docker best practices * chore: Add hadolint config to ignore version pinning warnings DL3008 and SC2016 are ignored because: - Pinning apt package versions creates maintenance burden - System packages are upgraded for security (apt-get upgrade) - Echo statements for nginx config are properly escaped - These checks are too strict for our use case
1 parent 830c5b9 commit 0b97326

File tree

7 files changed

+138
-206
lines changed

7 files changed

+138
-206
lines changed

.dockerignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,6 @@ docs
5050
!README.md
5151

5252
# Docker
53-
Dockerfile
54-
Dockerfile.*
5553
docker-compose*.yml
5654
docker-compose*.yaml
5755
.docker

.github/workflows/docker-validate.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,6 @@ jobs:
4040
dockerfile: Dockerfile
4141
failure-threshold: warning
4242

43-
- name: Lint Dockerfile.dev
44-
uses: hadolint/[email protected]
45-
with:
46-
dockerfile: Dockerfile.dev
47-
failure-threshold: warning
48-
4943
- name: Summary
5044
run: |
5145
echo "✅ Docker configuration validation complete"

.hadolint.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
ignored:
3+
- DL3008 # Pin versions in apt-get install
4+
- SC2016 # Shell expansion in echo statements
5+
6+
trustedRegistries:
7+
- docker.io

Dockerfile

Lines changed: 75 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,159 +1,105 @@
1-
# =============================================================================
2-
# Dockerfile for DevOps Daily
3-
# Multi-stage build for optimized image size
4-
# =============================================================================
1+
# Universal Dockerfile for DevOps Daily
2+
# Switch environments with: BUILD_ENV=development or BUILD_ENV=production
53

6-
# Build arguments for version pinning and configurability
74
ARG NODE_VERSION=20.18.1
85
ARG PNPM_VERSION=10.11.1
9-
ARG NGINX_VERSION=1.27-alpine
6+
ARG BUILD_ENV=production
107

11-
# =============================================================================
12-
# Stage 1: Dependencies
13-
# Install production dependencies in a separate stage for better caching
14-
# =============================================================================
15-
FROM node:${NODE_VERSION}-bullseye-slim AS deps
8+
FROM node:${NODE_VERSION}-bullseye-slim
169

10+
ARG BUILD_ENV
1711
ARG PNPM_VERSION
1812

19-
# Install security updates and pnpm with specific version
13+
# Install system packages based on environment
2014
RUN apt-get update && \
2115
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 && \
2221
apt-get clean && \
23-
rm -rf /var/lib/apt/lists/* && \
24-
corepack enable && \
22+
rm -rf /var/lib/apt/lists/*
23+
24+
# Install pnpm
25+
RUN corepack enable && \
2526
corepack prepare pnpm@${PNPM_VERSION} --activate
2627

2728
WORKDIR /app
2829

29-
# Copy package files for dependency installation
30+
# Copy package files
3031
COPY package.json pnpm-lock.yaml ./
3132

32-
# Install dependencies
33-
RUN pnpm install --frozen-lockfile
34-
35-
# =============================================================================
36-
# Stage 2: Builder
37-
# Build the Next.js application
38-
# =============================================================================
39-
FROM node:${NODE_VERSION}-bullseye-slim AS builder
40-
41-
ARG PNPM_VERSION
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
4239

43-
# Install security updates and pnpm with specific version
44-
RUN apt-get update && \
45-
apt-get upgrade -y && \
46-
apt-get clean && \
47-
rm -rf /var/lib/apt/lists/* && \
48-
corepack enable && \
49-
corepack prepare pnpm@${PNPM_VERSION} --activate
50-
51-
WORKDIR /app
52-
53-
# Copy dependencies from deps stage
54-
COPY --from=deps /app/node_modules ./node_modules
55-
56-
# Copy all source files
40+
# Copy source code
5741
COPY . .
5842

59-
# Set environment to production for build
60-
ENV NODE_ENV=production
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}
6182
ENV NEXT_TELEMETRY_DISABLED=1
83+
ENV WATCHPACK_POLLING=true
6284

63-
# Build arguments for optional cache mount
64-
ARG BUILDKIT_CACHE_MOUNT_NS=devops-daily
65-
66-
# Build the application
67-
# Using build:cf for faster builds (skips image generation)
68-
# For full build with image generation, use: RUN pnpm run build
69-
RUN --mount=type=cache,target=/app/.next/cache,id=${BUILDKIT_CACHE_MOUNT_NS} \
70-
pnpm run build:cf
71-
72-
# =============================================================================
73-
# Stage 3: Production Runner
74-
# Minimal image to serve the static export
75-
# =============================================================================
76-
FROM nginx:${NGINX_VERSION} AS runner
77-
78-
# Add labels for better container management
85+
# Metadata labels
7986
LABEL org.opencontainers.image.title="DevOps Daily"
8087
LABEL org.opencontainers.image.description="A modern content platform for DevOps professionals"
8188
LABEL org.opencontainers.image.source="https://github.com/The-DevOps-Daily/devops-daily"
8289
LABEL org.opencontainers.image.licenses="MIT"
8390
LABEL org.opencontainers.image.vendor="DevOps Daily"
8491
LABEL org.opencontainers.image.authors="DevOps Daily Team"
8592

86-
# Install security updates
87-
RUN apk update && \
88-
apk upgrade && \
89-
rm -rf /var/cache/apk/*
90-
91-
# Remove default nginx static assets
92-
RUN rm -rf /usr/share/nginx/html/*
93-
94-
# Copy custom nginx configuration for SPA routing
95-
COPY <<EOF /etc/nginx/conf.d/default.conf
96-
server {
97-
listen 80;
98-
listen [::]:80;
99-
server_name localhost;
100-
root /usr/share/nginx/html;
101-
index index.html;
102-
103-
# Gzip compression
104-
gzip on;
105-
gzip_vary on;
106-
gzip_min_length 1024;
107-
gzip_proxied expired no-cache no-store private auth;
108-
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/javascript application/json;
109-
gzip_comp_level 6;
110-
111-
# Security headers
112-
add_header X-Frame-Options "SAMEORIGIN" always;
113-
add_header X-Content-Type-Options "nosniff" always;
114-
add_header X-XSS-Protection "1; mode=block" always;
115-
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
116-
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
117-
118-
# Cache static assets
119-
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
120-
expires 1y;
121-
add_header Cache-Control "public, immutable";
122-
}
123-
124-
# Main location block
125-
location / {
126-
try_files \$uri \$uri.html /index.html;
127-
}
128-
129-
# Error page handling
130-
error_page 404 /404.html;
131-
location = /404.html {
132-
internal;
133-
}
134-
}
135-
EOF
136-
137-
# Copy the static export from builder stage
138-
COPY --from=builder /app/out /usr/share/nginx/html
139-
140-
# Adjust permissions for non-root user
141-
RUN chown -R nginx:nginx /usr/share/nginx/html && \
142-
chown -R nginx:nginx /var/cache/nginx && \
143-
chown -R nginx:nginx /var/log/nginx && \
144-
chmod -R 755 /usr/share/nginx/html && \
145-
touch /run/nginx.pid && \
146-
chown -R nginx:nginx /run/nginx.pid
147-
148-
# Switch to non-root user
149-
USER nginx
150-
151-
# Expose port 80
152-
EXPOSE 80
93+
# Expose ports (both for flexibility)
94+
EXPOSE 3000 80
15395

154-
# Health check
155-
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
156-
CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1
96+
# Health check that adapts to environment
97+
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
157103

158-
# Start nginx
159-
CMD ["nginx", "-g", "daemon off;"]
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"]

Dockerfile.dev

Lines changed: 0 additions & 46 deletions
This file was deleted.

0 commit comments

Comments
 (0)