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