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:
parent
8b70bfbba9
commit
a5c8d17134
|
|
@ -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"]
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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;"]
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue