Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,6 @@ docker-compose*.yaml
*.tsbuildinfo
.eslintcache
.prettierignore

# Dockerfile itself (not needed in build context for static files)
Dockerfile*
143 changes: 70 additions & 73 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,105 +1,102 @@
# Universal Dockerfile for DevOps Daily
# Switch environments with: BUILD_ENV=development or BUILD_ENV=production
# Multi-stage build for optimized development and production images

ARG NODE_VERSION=20.18.1
ARG PNPM_VERSION=10.11.1
ARG BUILD_ENV=production
ARG NODE_VERSION=22.13.1
ARG PNPM_VERSION=10.28.1

FROM node:${NODE_VERSION}-bullseye-slim
# ===========================================================================
# Stage 1: Base image with common dependencies
# ===========================================================================
FROM node:${NODE_VERSION}-bookworm-slim AS base

ARG BUILD_ENV
ARG PNPM_VERSION

# Install system packages based on environment
RUN apt-get update && \
apt-get upgrade -y && \
if [ "$BUILD_ENV" = "production" ]; then \
apt-get install -y --no-install-recommends nginx curl; \
else \
apt-get install -y --no-install-recommends curl; \
fi && \
apt-get install -y --no-install-recommends curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

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

WORKDIR /app

# Copy package files
COPY package.json pnpm-lock.yaml ./
# ===========================================================================
# Stage 2: Development image
# Hot-reload enabled for local development
# ===========================================================================
FROM base AS development

# Install dependencies based on environment
RUN if [ "$BUILD_ENV" = "production" ]; then \
pnpm install --frozen-lockfile --prod; \
else \
pnpm install --frozen-lockfile; \
fi
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

# Copy source code
COPY . .

# Build for production if needed
RUN if [ "$BUILD_ENV" = "production" ]; then \
NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 pnpm run build:cf && \
rm -rf /var/www/html/* && \
mkdir -p /var/www/html && \
cp -r /app/out/* /var/www/html/ && \
echo 'server {' > /etc/nginx/sites-available/default && \
echo ' listen 80;' >> /etc/nginx/sites-available/default && \
echo ' listen [::]:80;' >> /etc/nginx/sites-available/default && \
echo ' server_name localhost;' >> /etc/nginx/sites-available/default && \
echo ' root /var/www/html;' >> /etc/nginx/sites-available/default && \
echo ' index index.html;' >> /etc/nginx/sites-available/default && \
echo '' >> /etc/nginx/sites-available/default && \
echo ' gzip on;' >> /etc/nginx/sites-available/default && \
echo ' gzip_vary on;' >> /etc/nginx/sites-available/default && \
echo ' gzip_min_length 1024;' >> /etc/nginx/sites-available/default && \
echo ' gzip_types text/plain text/css text/xml text/javascript application/javascript application/json;' >> /etc/nginx/sites-available/default && \
echo '' >> /etc/nginx/sites-available/default && \
echo ' add_header X-Frame-Options "SAMEORIGIN" always;' >> /etc/nginx/sites-available/default && \
echo ' add_header X-Content-Type-Options "nosniff" always;' >> /etc/nginx/sites-available/default && \
echo ' add_header X-XSS-Protection "1; mode=block" always;' >> /etc/nginx/sites-available/default && \
echo ' add_header Referrer-Policy "strict-origin-when-cross-origin" always;' >> /etc/nginx/sites-available/default && \
echo ' add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;' >> /etc/nginx/sites-available/default && \
echo '' >> /etc/nginx/sites-available/default && \
echo ' location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {' >> /etc/nginx/sites-available/default && \
echo ' expires 1y;' >> /etc/nginx/sites-available/default && \
echo ' add_header Cache-Control "public, immutable";' >> /etc/nginx/sites-available/default && \
echo ' }' >> /etc/nginx/sites-available/default && \
echo '' >> /etc/nginx/sites-available/default && \
echo ' location / {' >> /etc/nginx/sites-available/default && \
echo ' try_files \$uri \$uri.html /index.html;' >> /etc/nginx/sites-available/default && \
echo ' }' >> /etc/nginx/sites-available/default && \
echo '' >> /etc/nginx/sites-available/default && \
echo ' error_page 404 /404.html;' >> /etc/nginx/sites-available/default && \
echo '}' >> /etc/nginx/sites-available/default; \
fi

# Environment variables
ENV NODE_ENV=${BUILD_ENV:-production}
ENV NODE_ENV=development
ENV NEXT_TELEMETRY_DISABLED=1
ENV WATCHPACK_POLLING=true

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

# Expose ports (both for flexibility)
EXPOSE 3000 80
EXPOSE 3000

# Health check that adapts to environment
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD if [ "$NODE_ENV" = "production" ]; then \
curl -f http://localhost:80/ || exit 1; \
else \
curl -f http://localhost:3000/ || exit 1; \
fi

# Start command based on environment
CMD ["/bin/sh", "-c", "if [ \"$NODE_ENV\" = \"production\" ]; then nginx -g 'daemon off;'; else pnpm run dev; fi"]
CMD curl -f http://localhost:3000/ || exit 1

CMD ["pnpm", "run", "dev"]

# ===========================================================================
# Stage 3: Production builder
# Builds the static site for production
# ===========================================================================
FROM base AS builder

COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

COPY . .

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

RUN pnpm run build:cf

# ===========================================================================
# Stage 4: Production runtime
# Minimal nginx image serving static files
# ===========================================================================
FROM nginx:1.27-alpine AS production

RUN rm /etc/nginx/conf.d/default.conf

COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /app/out /usr/share/nginx/html

RUN chown -R nginx:nginx /usr/share/nginx/html && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx && \
touch /var/run/nginx.pid && \
chown -R nginx:nginx /var/run/nginx.pid

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

LABEL org.opencontainers.image.title="DevOps Daily"
LABEL org.opencontainers.image.description="A modern content platform for DevOps professionals"
LABEL org.opencontainers.image.source="https://github.com/The-DevOps-Daily/devops-daily"
LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.vendor="DevOps Daily"
LABEL org.opencontainers.image.authors="DevOps Daily Team"

EXPOSE 80

HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1

CMD ["nginx", "-g", "daemon off;"]
24 changes: 12 additions & 12 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ services:
build:
context: .
dockerfile: Dockerfile
target: development
args:
NODE_VERSION: 20.18.1
PNPM_VERSION: 10.11.1
BUILD_ENV: development
NODE_VERSION: 22.13.1
PNPM_VERSION: 10.28.1
container_name: devops-daily-dev
image: devops-daily:dev
ports:
Expand Down Expand Up @@ -63,16 +63,16 @@ services:

# ===========================================================================
# Production Service
# Uses the main Dockerfile with nginx for serving static files
# Uses multi-stage build with nginx for serving static files
# ===========================================================================
prod:
build:
context: .
dockerfile: Dockerfile
target: production
args:
NODE_VERSION: 20.18.1
PNPM_VERSION: 10.11.1
BUILD_ENV: production
NODE_VERSION: 22.13.1
PNPM_VERSION: 10.28.1
container_name: devops-daily-prod
image: devops-daily:prod
ports:
Expand All @@ -84,22 +84,22 @@ services:
# Restart policy for production
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/"]
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
start_period: 10s
labels:
- "com.devops-daily.service=prod"
- "com.devops-daily.environment=production"
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
logging:
driver: "json-file"
options:
Expand Down
64 changes: 64 additions & 0 deletions docker/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
worker_connections 1024;
use epoll;
multi_accept on;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;

# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
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;

server {
listen 80;
listen [::]:80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;

# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

# Static file caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}

# Main location
location / {
try_files $uri $uri.html $uri/ /index.html;
}

# Error pages
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
}
}