feat(canary): add canary.dev + canary.prod Dockerfiles + import infra/

Production Dockerfile (infra/docker/canary.prod):
- Multi-stage: golang:1.25-alpine builder → distroless/static:nonroot final
- CGO_ENABLED=0, -trimpath, -ldflags='-s -w' → minimal static binary
- Embeds config.yaml; runs as nonroot user
- ENTRYPOINT /canary, CMD -config /config.yaml
- EXPOSE 8080

Development Dockerfile (infra/docker/canary.dev):
- golang:1.25-alpine + air-verse/air for hot reload
- Source bind-mounted by dev.compose.yml; .air.toml drives rebuilds
- go mod download cached at image-build time, dep refresh at runtime if changed
- EXPOSE 8080

Also imports the rest of the template's infra/ that was untracked:
- infra/docker/vite.dev + vite.prod (existing, unchanged)
- infra/nginx/{nginx.conf, dev.nginx, prod.nginx, nginx.prod.conf}
  These will be extended in Phase 15 with /api, /c, /k upstream blocks.
This commit is contained in:
CarterPerez-dev 2026-05-10 05:24:42 -04:00
parent 8b70bfbba9
commit a5c8d17134
8 changed files with 380 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# =============================================================================
# ©AngelaMos | 2026
# canary.dev
# =============================================================================
# Development image: golang:1.25-alpine + Air for hot reload.
# Source is bind-mounted by dev.compose.yml; Air rebuilds on file change.
#
# Build context: ./backend
# Reads .air.toml from the bind-mounted source.
# =============================================================================
FROM golang:1.25-alpine
RUN apk add --no-cache git curl wget
RUN go install github.com/air-verse/air@latest
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
EXPOSE 8080
CMD ["air", "-c", ".air.toml"]

View File

@ -0,0 +1,41 @@
# =============================================================================
# ©AngelaMos | 2026
# canary.prod
# =============================================================================
# Multi-stage build:
# Stage 1 (builder) golang:1.25-alpine, CGO_ENABLED=0, trimpath, ldflags
# Stage 2 (final) gcr.io/distroless/static:nonroot — no shell, non-root
#
# Build context: ./backend (set by compose.yml)
# Result: /canary single static binary, runs as nonroot user.
# =============================================================================
FROM golang:1.25-alpine AS builder
RUN apk add --no-cache ca-certificates git
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags="-s -w" \
-o /out/canary \
./cmd/canary
FROM gcr.io/distroless/static:nonroot AS final
WORKDIR /
COPY --from=builder /out/canary /canary
COPY --from=builder /src/config.yaml /config.yaml
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/canary"]
CMD ["-config", "/config.yaml"]

View File

@ -0,0 +1,25 @@
# =============================================================================
# AngelaMos | 2026
# vite.dev
# =============================================================================
# Development Dockerfile for Vite/React frontend
# Features: pnpm, HMR support, polling for Docker file watching
# =============================================================================
# syntax=docker/dockerfile:1
FROM node:22-slim
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
COPY package.json pnpm-lock.yaml* ./
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile
COPY . .
EXPOSE 5173
CMD ["pnpm", "dev", "--host", "0.0.0.0"]

View File

@ -0,0 +1,59 @@
# =============================================================================
# AngelaMos | 2026
# vite.prod
# =============================================================================
# Production Dockerfile: builds Vite app, serves via Nginx
# =============================================================================
# syntax=docker/dockerfile:1
# ============================================================================
# BUILD STAGE
# ============================================================================
FROM node:22-slim AS builder
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
COPY react-scss/package.json react-scss/pnpm-lock.yaml* ./
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile
COPY react-scss/ .
ARG VITE_API_URL=/api
ARG VITE_APP_TITLE="My App"
ENV VITE_API_URL=${VITE_API_URL} \
VITE_APP_TITLE=${VITE_APP_TITLE}
RUN pnpm build
# ============================================================================
# PRODUCTION STAGE
# ============================================================================
FROM nginx:1.27-alpine AS production
RUN rm -rf /usr/share/nginx/html/* && \
rm /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html
COPY --chown=nginx:nginx infra/nginx/nginx.prod.conf /etc/nginx/nginx.conf
COPY --chown=nginx:nginx infra/nginx/prod.nginx /etc/nginx/conf.d/default.conf
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
USER nginx
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:80/health || exit 1
CMD ["nginx", "-g", "daemon off;"]

View File

@ -0,0 +1,35 @@
# =============================================================================
# AngelaMos | 2026
# dev.nginx
# =============================================================================
# Development server block: proxies to Vite dev server with HMR
server {
listen 80;
listen [::]:80;
server_name _;
access_log /var/log/nginx/access.log main_timed;
error_log /var/log/nginx/error.log debug;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
location / {
proxy_pass http://frontend_dev;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 60s;
proxy_buffering off;
}
}

View File

@ -0,0 +1,76 @@
# AngelaMos | 2026
# nginx.conf
# Development nginx configuration
user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 4096;
multi_accept on;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream frontend_dev {
server frontend:5173;
keepalive 8;
}
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
limit_req_status 429;
log_format main_timed '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
client_body_buffer_size 128k;
client_header_buffer_size 16k;
client_max_body_size 10m;
large_client_header_buffers 4 16k;
client_body_timeout 12s;
client_header_timeout 12s;
send_timeout 10s;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
gzip_disable "msie6";
include /etc/nginx/conf.d/*.conf;
}

View File

@ -0,0 +1,65 @@
# AngelaMos | 2026
# nginx.prod.conf
# Production nginx configuration
worker_processes auto;
worker_rlimit_nofile 65535;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 4096;
multi_accept on;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
limit_req_status 429;
log_format main_timed '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
client_body_buffer_size 128k;
client_header_buffer_size 16k;
client_max_body_size 10m;
large_client_header_buffers 4 16k;
client_body_timeout 12s;
client_header_timeout 12s;
send_timeout 10s;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
gzip_disable "msie6";
include /etc/nginx/conf.d/*.conf;
}

View File

@ -0,0 +1,54 @@
# =============================================================================
# AngelaMos | 2026
# prod.nginx
# =============================================================================
# Production server block: serves built static files
# Note: SSL handled by Cloudflare, not here
server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
index index.html;
access_log /var/log/nginx/access.log main_timed buffer=32k flush=5s;
error_log /var/log/nginx/error.log warn;
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;
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
try_files $uri =404;
}
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|avif|woff|woff2|ttf|eot|otf)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location / {
add_header Cache-Control "no-cache, must-revalidate";
try_files $uri $uri/ /index.html;
}
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}