Skip to content

Commit 5bd6a55

Browse files
yoyo930021claude
andcommitted
feat(core): implement production deployment and project deep copy (phase-12)
- B-12.1: Deep project copy with ID mapping (member_tags, task_templates, todo_templates, data_schemas, task_template_tags, memories, tool_configs, permission_settings) - B-12.2: Rate limiting (global 100/s, auth 10/min, AI 5/min), security headers (X-Content-Type-Options, X-Frame-Options, HSTS, CSP), CORS middleware from APP_CORS_ORIGINS env var - F-12.1: Frontend PWA support (vite-plugin-pwa), vendor chunk splitting, service worker with API caching - S-12.1: Multi-stage Dockerfile (Rust + Node.js builders + slim runtime), Dockerfile.dev, .dockerignore - S-12.2: Production docker-compose with Caddy, deploy script, env example - S-12.3: Database backup/restore scripts, file backup, crontab example Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 2d5c12e commit 5bd6a55

29 files changed

+4502
-125
lines changed

.dockerignore

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Build artifacts
2+
target/
3+
frontend/node_modules/
4+
frontend/dist/
5+
node_modules/
6+
7+
# Version control
8+
.git/
9+
.gitignore
10+
11+
# IDE
12+
.idea/
13+
.vscode/
14+
*.swp
15+
*.swo
16+
17+
# Environment files (secrets)
18+
.env
19+
.env.*
20+
!.env.prod.example
21+
22+
# Docker
23+
Dockerfile*
24+
docker-compose*.yml
25+
.dockerignore
26+
27+
# Documentation
28+
docs/plans/
29+
*.md
30+
!README.md
31+
32+
# CI/CD
33+
.github/
34+
35+
# Backups and logs
36+
*.log
37+
backups/
38+
39+
# OS files
40+
.DS_Store
41+
Thumbs.db
42+
43+
# Test artifacts
44+
coverage/
45+
*.profraw
46+
47+
# Batch config
48+
batch-config.json

.env.prod.example

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# ──────────────────────────────────────────────────────────────
2+
# Conf-Ops Production Environment Variables
3+
# Copy this file to .env.prod and fill in the values
4+
# ──────────────────────────────────────────────────────────────
5+
6+
# ── Database ─────────────────────────────────────────────────
7+
DATABASE_URL=postgres://confops:CHANGE_ME@postgres:5432/confops
8+
DATABASE_MAX_CONNECTIONS=20
9+
DATABASE_MIN_CONNECTIONS=5
10+
11+
# ── Auth ─────────────────────────────────────────────────────
12+
AUTH_JWT_SECRET=CHANGE_ME_TO_RANDOM_SECRET
13+
AUTH_JWT_ISSUER=conf-ops
14+
JWT_ACCESS_EXPIRY=900
15+
JWT_REFRESH_EXPIRY=604800
16+
AUTH_WEBAUTHN_RP_ID=confops.dev
17+
AUTH_WEBAUTHN_RP_ORIGIN=https://confops.dev
18+
AUTH_WEBAUTHN_RP_NAME=Conf-Ops
19+
20+
# ── App ──────────────────────────────────────────────────────
21+
APP_HOST=0.0.0.0
22+
APP_PORT=8080
23+
APP_LOG_LEVEL=info,conf_ops=debug
24+
APP_BASE_URL=https://confops.dev
25+
APP_CORS_ORIGINS=https://confops.dev
26+
FRONTEND_URL=https://confops.dev
27+
28+
# ── Storage ──────────────────────────────────────────────────
29+
STORAGE_BASE_PATH=/data/files
30+
STORAGE_MAX_IMAGE_SIZE=10485760
31+
STORAGE_MAX_DOCUMENT_SIZE=52428800
32+
STORAGE_MAX_FILE_SIZE=20971520
33+
34+
# ── Email (SMTP) ────────────────────────────────────────────
35+
SMTP_HOST=smtp.example.com
36+
SMTP_PORT=587
37+
SMTP_USERNAME=noreply@confops.dev
38+
SMTP_PASSWORD=CHANGE_ME
39+
SMTP_FROM=noreply@confops.dev
40+
EMAIL_DOMAIN=confops.dev
41+
EMAIL_INBOUND_API_KEY=CHANGE_ME_INBOUND_KEY
42+
43+
# ── AI (Google Gemini) ──────────────────────────────────────
44+
GEMINI_API_KEY=CHANGE_ME_GEMINI_KEY
45+
GEMINI_MODEL=gemini-2.5-flash
46+
47+
# ── Web Push (VAPID) ────────────────────────────────────────
48+
WEB_PUSH_VAPID_PRIVATE_KEY=CHANGE_ME_VAPID_PRIVATE
49+
WEB_PUSH_VAPID_PUBLIC_KEY=CHANGE_ME_VAPID_PUBLIC
50+
51+
# ── CRDT WebSocket ──────────────────────────────────────────
52+
CRDT_WS_MAX_CONNECTIONS_PER_TASK=50
53+
CRDT_WS_HEARTBEAT_INTERVAL_SECS=30
54+
CRDT_WS_IDLE_TIMEOUT_SECS=300
55+
56+
# ── Authorization ───────────────────────────────────────────
57+
AUTHZ_CACHE_TTL_SECS=300
58+
59+
# ── Caddy Domain (used in Caddyfile) ────────────────────────
60+
DOMAIN=api.confops.dev

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ __pycache__/
1616
.env
1717
.env.*
1818
!.env.example
19+
!.env.prod.example
1920

2021
# OS
2122
.DS_Store

Caddyfile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Caddyfile - Production reverse proxy configuration
2+
# Replace {$DOMAIN} with your actual domain or set it as an environment variable
3+
{$DOMAIN:api.confops.dev} {
4+
# HTTP API and WebSocket proxy
5+
reverse_proxy confops-app:8080 {
6+
# WebSocket support (auto-detects Upgrade header)
7+
flush_interval -1
8+
}
9+
10+
# Access logging
11+
log {
12+
output stdout
13+
format json
14+
}
15+
16+
# Security headers (supplement to application-level headers)
17+
header {
18+
# Remove server identification
19+
-Server
20+
}
21+
}

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ resolver = "2"
66
name = "conf-ops"
77
version = "0.1.0"
88
edition = "2021"
9-
rust-version = "1.75"
9+
rust-version = "1.82"
1010

1111
[lints.clippy]
1212
all = { level = "deny", priority = -1 }
@@ -50,6 +50,7 @@ tokio-util = { version = "0.7", features = ["io"] }
5050
regex = "1.12.3"
5151
aho-corasick = "1"
5252
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] }
53+
tower-http = { version = "0.6", features = ["cors"] }
5354

5455
[dev-dependencies]
5556
http-body-util = "0.1.3"

Dockerfile

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# ── Stage 1: Rust builder ────────────────────────────────────
2+
FROM rust:1.82-bookworm AS rust-builder
3+
4+
WORKDIR /app
5+
6+
# Copy Cargo files first for dependency caching
7+
COPY Cargo.toml Cargo.lock ./
8+
COPY xtask/ xtask/
9+
10+
# Create dummy source to build dependencies
11+
RUN mkdir src && echo "fn main() {}" > src/main.rs && \
12+
echo "pub fn lib() {}" > src/lib.rs
13+
RUN cargo build --release 2>/dev/null || true
14+
RUN rm -rf src
15+
16+
# Copy actual source and build
17+
COPY src/ src/
18+
COPY migrations/ migrations/
19+
COPY .sqlx/ .sqlx/
20+
21+
ENV SQLX_OFFLINE=true
22+
RUN touch src/main.rs src/lib.rs && cargo build --release
23+
24+
# ── Stage 2: Node.js builder (frontend) ────────────────────
25+
FROM node:20-slim AS frontend-builder
26+
27+
WORKDIR /app/frontend
28+
29+
# Install pnpm
30+
RUN corepack enable && corepack prepare pnpm@latest --activate
31+
32+
# Copy package files for dependency caching
33+
COPY frontend/package.json frontend/pnpm-lock.yaml ./
34+
35+
RUN pnpm install --frozen-lockfile
36+
37+
# Copy frontend source and build
38+
COPY frontend/ ./
39+
COPY docs/api/openapi-generated.yaml /app/docs/api/openapi-generated.yaml
40+
41+
RUN pnpm run build-only
42+
43+
# ── Stage 3: Runtime ────────────────────────────────────────
44+
FROM debian:bookworm-slim
45+
46+
RUN apt-get update && \
47+
apt-get install -y --no-install-recommends ca-certificates curl && \
48+
rm -rf /var/lib/apt/lists/*
49+
50+
# Create non-root user
51+
RUN groupadd -r confops && useradd -r -g confops confops
52+
53+
# Copy binary
54+
COPY --from=rust-builder /app/target/release/conf-ops /usr/local/bin/confops
55+
56+
# Copy migrations
57+
COPY --from=rust-builder /app/migrations /app/migrations
58+
59+
# Copy frontend build output
60+
COPY --from=frontend-builder /app/frontend/dist /app/static
61+
62+
# Create file storage directory
63+
RUN mkdir -p /data/files && chown -R confops:confops /data/files
64+
65+
WORKDIR /app
66+
67+
USER confops
68+
69+
EXPOSE 8080
70+
71+
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \
72+
CMD curl -f http://localhost:8080/healthz || exit 1
73+
74+
ENTRYPOINT ["confops"]

Dockerfile.dev

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Development Dockerfile with cargo-watch for hot reload
2+
FROM rust:1.82-bookworm
3+
4+
WORKDIR /app
5+
6+
# Install cargo-watch for development hot reload
7+
RUN cargo install cargo-watch
8+
9+
# Install sqlx-cli for migrations
10+
RUN cargo install sqlx-cli --no-default-features --features postgres,rustls
11+
12+
# Copy source (will be overridden by volume mount in dev)
13+
COPY . .
14+
15+
ENV SQLX_OFFLINE=true
16+
17+
EXPOSE 8080
18+
19+
CMD ["cargo", "watch", "-x", "run"]

docker-compose.prod.yml

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
services:
2+
caddy:
3+
image: caddy:2-alpine
4+
restart: unless-stopped
5+
ports:
6+
- "80:80"
7+
- "443:443"
8+
- "443:443/udp" # HTTP/3
9+
volumes:
10+
- ./Caddyfile:/etc/caddy/Caddyfile:ro
11+
- caddy_data:/data
12+
- caddy_config:/config
13+
depends_on:
14+
confops-app:
15+
condition: service_healthy
16+
deploy:
17+
resources:
18+
limits:
19+
memory: 128M
20+
logging:
21+
driver: json-file
22+
options:
23+
max-size: "50m"
24+
max-file: "5"
25+
26+
confops-app:
27+
image: ghcr.io/org/confops:latest
28+
restart: unless-stopped
29+
env_file: .env.prod
30+
ports:
31+
- "127.0.0.1:8080:8080"
32+
volumes:
33+
- file_storage:/data/files
34+
depends_on:
35+
postgres:
36+
condition: service_healthy
37+
healthcheck:
38+
test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
39+
interval: 30s
40+
timeout: 5s
41+
retries: 3
42+
start_period: 10s
43+
deploy:
44+
resources:
45+
limits:
46+
memory: 512M
47+
cpus: "1.0"
48+
logging:
49+
driver: json-file
50+
options:
51+
max-size: "50m"
52+
max-file: "5"
53+
54+
# IMPORTANT: confops-worker must remain a single replica.
55+
# The Reminder Scheduler depends on single-instance execution to avoid
56+
# duplicate notifications. If multi-replica is needed in the future,
57+
# implement PostgreSQL Advisory Lock or SELECT ... FOR UPDATE SKIP LOCKED
58+
# (see docs/system/10-notification-system.md).
59+
confops-worker:
60+
image: ghcr.io/org/confops:latest
61+
restart: unless-stopped
62+
command: ["confops", "--worker"]
63+
env_file: .env.prod
64+
volumes:
65+
- file_storage:/data/files
66+
depends_on:
67+
postgres:
68+
condition: service_healthy
69+
healthcheck:
70+
test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
71+
interval: 30s
72+
timeout: 5s
73+
retries: 3
74+
start_period: 10s
75+
deploy:
76+
replicas: 1 # Do not increase; see above comment
77+
resources:
78+
limits:
79+
memory: 512M
80+
cpus: "0.5"
81+
logging:
82+
driver: json-file
83+
options:
84+
max-size: "50m"
85+
max-file: "5"
86+
87+
postgres:
88+
image: postgres:16
89+
restart: unless-stopped
90+
environment:
91+
POSTGRES_DB: confops
92+
POSTGRES_USER: confops
93+
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
94+
volumes:
95+
- pgdata:/var/lib/postgresql/data
96+
secrets:
97+
- db_password
98+
healthcheck:
99+
test: ["CMD-SHELL", "pg_isready -U confops"]
100+
interval: 10s
101+
timeout: 5s
102+
retries: 5
103+
deploy:
104+
resources:
105+
limits:
106+
memory: 1G
107+
logging:
108+
driver: json-file
109+
options:
110+
max-size: "50m"
111+
max-file: "5"
112+
113+
volumes:
114+
caddy_data:
115+
caddy_config:
116+
pgdata:
117+
file_storage:
118+
driver: local
119+
driver_opts:
120+
type: none
121+
o: bind
122+
device: /data/confops/files
123+
124+
secrets:
125+
db_password:
126+
file: ./secrets/db_password.txt

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"oxlint": "~1.48.0",
4646
"typescript": "~5.9.3",
4747
"vite": "8.0.0-beta.15",
48+
"vite-plugin-pwa": "^1.2.0",
4849
"vitest": "^4.0.18",
4950
"vue-tsc": "^3.2.4"
5051
},

0 commit comments

Comments
 (0)