mirror of
https://github.com/andwati/blog-nest.git
synced 2026-10-30 02:22:16 +00:00
feat: enhance Docker and environment configurations for development and production
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
# ── Shared ──
|
||||
NODE_ENV=development
|
||||
|
||||
# ── Database (PostgreSQL) ──
|
||||
DATABASE_URL=postgresql://blog:postgres@localhost:5432/blog_dev
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_PORT=5432
|
||||
DATABASE_NAME=blog_dev
|
||||
DATABASE_USER=blog
|
||||
DATABASE_PASSWORD=postgres
|
||||
|
||||
# ── API (NestJS) ──
|
||||
API_PORT=3001
|
||||
|
||||
|
||||
@@ -75,6 +75,8 @@ web_modules/
|
||||
|
||||
.turbo
|
||||
|
||||
.pnpm-store*
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
@@ -85,6 +87,10 @@ web_modules/
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# TLS/SSL certificates (never commit private keys)
|
||||
infra/ssl/*
|
||||
!infra/ssl/.gitkeep
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
SHELL := /bin/bash
|
||||
|
||||
ROOT_DIR := $(shell pwd)
|
||||
COMPOSE_DEV := docker compose -f infra/docker-compose.yml
|
||||
COMPOSE_PROD := docker compose -f infra/docker-compose.prod.yml
|
||||
|
||||
DOMAIN ?=
|
||||
EMAIL ?=
|
||||
|
||||
.PHONY: help \
|
||||
dev-up dev-down dev-logs dev-ps dev-restart \
|
||||
prod-build prod-up prod-down prod-logs prod-ps prod-restart \
|
||||
deploy deploy-rebuild health \
|
||||
ssl-selfsigned ssl-certbot ssl-install ssl-renew-test
|
||||
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " make dev-up # Start development stack"
|
||||
@echo " make dev-down # Stop development stack"
|
||||
@echo " make dev-logs # Follow development logs"
|
||||
@echo " make dev-ps # Show development service status"
|
||||
@echo " make dev-restart # Restart development stack"
|
||||
@echo ""
|
||||
@echo " make prod-build # Build production images"
|
||||
@echo " make prod-up # Start production stack"
|
||||
@echo " make prod-down # Stop production stack"
|
||||
@echo " make prod-logs # Follow production logs"
|
||||
@echo " make prod-ps # Show production service status"
|
||||
@echo " make prod-restart # Restart production stack"
|
||||
@echo ""
|
||||
@echo " make deploy # Build + start production stack"
|
||||
@echo " make deploy-rebuild # Down + build(no-cache) + up"
|
||||
@echo " make health # Check HTTP/HTTPS + compose status"
|
||||
@echo ""
|
||||
@echo " make ssl-selfsigned # Generate local self-signed certs in infra/ssl"
|
||||
@echo " make ssl-certbot DOMAIN=example.com [email protected]"
|
||||
@echo " # Request Let's Encrypt cert (standalone)"
|
||||
@echo " make ssl-install DOMAIN=example.com"
|
||||
@echo " # Copy LE certs into infra/ssl and restart nginx"
|
||||
@echo " make ssl-renew-test # Dry-run certbot renew"
|
||||
|
||||
dev-up:
|
||||
$(COMPOSE_DEV) up
|
||||
|
||||
dev-down:
|
||||
$(COMPOSE_DEV) down
|
||||
|
||||
dev-logs:
|
||||
$(COMPOSE_DEV) logs -f
|
||||
|
||||
dev-ps:
|
||||
$(COMPOSE_DEV) ps
|
||||
|
||||
dev-restart:
|
||||
$(COMPOSE_DEV) down
|
||||
$(COMPOSE_DEV) up -d
|
||||
|
||||
prod-build:
|
||||
$(COMPOSE_PROD) build
|
||||
|
||||
prod-up:
|
||||
$(COMPOSE_PROD) up -d
|
||||
|
||||
prod-down:
|
||||
$(COMPOSE_PROD) down
|
||||
|
||||
prod-logs:
|
||||
$(COMPOSE_PROD) logs -f
|
||||
|
||||
prod-ps:
|
||||
$(COMPOSE_PROD) ps
|
||||
|
||||
prod-restart:
|
||||
$(COMPOSE_PROD) down
|
||||
$(COMPOSE_PROD) up -d
|
||||
|
||||
deploy:
|
||||
$(COMPOSE_PROD) build
|
||||
$(COMPOSE_PROD) up -d
|
||||
|
||||
deploy-rebuild:
|
||||
$(COMPOSE_PROD) down
|
||||
$(COMPOSE_PROD) build --no-cache
|
||||
$(COMPOSE_PROD) up -d
|
||||
|
||||
health:
|
||||
@echo "== Compose status =="
|
||||
$(COMPOSE_PROD) ps
|
||||
@echo ""
|
||||
@echo "== HTTP check (expect 301) =="
|
||||
@curl -sS -I http://localhost:80 | head -1
|
||||
@echo "== HTTPS check (expect 200) =="
|
||||
@curl -k -sS -o /dev/null -w "%{http_code}\n" https://localhost:443
|
||||
|
||||
ssl-selfsigned:
|
||||
mkdir -p infra/ssl
|
||||
docker run --rm -v $(ROOT_DIR)/infra/ssl:/out alpine:3.21 sh -lc "apk add --no-cache openssl >/dev/null && openssl req -x509 -nodes -newkey rsa:2048 -keyout /out/key.pem -out /out/cert.pem -days 365 -subj '/CN=localhost'"
|
||||
chmod 600 infra/ssl/key.pem
|
||||
@echo "Self-signed certs created at infra/ssl/cert.pem and infra/ssl/key.pem"
|
||||
|
||||
ssl-certbot:
|
||||
@if [ -z "$(DOMAIN)" ] || [ -z "$(EMAIL)" ]; then \
|
||||
echo "Usage: make ssl-certbot DOMAIN=example.com [email protected]"; \
|
||||
exit 1; \
|
||||
fi
|
||||
sudo certbot certonly --standalone -d $(DOMAIN) --email $(EMAIL) --agree-tos --no-eff-email
|
||||
|
||||
ssl-install:
|
||||
@if [ -z "$(DOMAIN)" ]; then \
|
||||
echo "Usage: make ssl-install DOMAIN=example.com"; \
|
||||
exit 1; \
|
||||
fi
|
||||
mkdir -p infra/ssl
|
||||
sudo cp /etc/letsencrypt/live/$(DOMAIN)/fullchain.pem infra/ssl/cert.pem
|
||||
sudo cp /etc/letsencrypt/live/$(DOMAIN)/privkey.pem infra/ssl/key.pem
|
||||
sudo chown $(USER):$(USER) infra/ssl/cert.pem infra/ssl/key.pem
|
||||
chmod 600 infra/ssl/key.pem
|
||||
$(COMPOSE_PROD) restart nginx
|
||||
@echo "Installed Let's Encrypt certs for $(DOMAIN) and restarted nginx"
|
||||
|
||||
ssl-renew-test:
|
||||
sudo certbot renew --dry-run
|
||||
@@ -3,6 +3,8 @@
|
||||
/node_modules
|
||||
/build
|
||||
|
||||
generated
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import nextJest from 'next/jest'
|
||||
|
||||
const createJestConfig = nextJest({
|
||||
dir: './',
|
||||
})
|
||||
|
||||
const config = {
|
||||
testEnvironment: 'jsdom',
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
||||
testMatch: ['**/*.spec.ts?(x)'],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
},
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{ts,tsx}',
|
||||
'!src/**/*.spec.{ts,tsx}',
|
||||
'!src/**/layout.tsx',
|
||||
],
|
||||
coverageDirectory: 'coverage',
|
||||
}
|
||||
|
||||
export default createJestConfig(config)
|
||||
@@ -8,7 +8,7 @@ const createJestConfig = nextJest({
|
||||
const config: Config = {
|
||||
testEnvironment: 'jsdom',
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
||||
testRegex: '.*\\.spec\\.tsx?$',
|
||||
testMatch: ['**/*.spec.ts?(x)'],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
},
|
||||
|
||||
@@ -25,5 +25,5 @@
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": ["node_modules", "jest.config.ts", "jest.setup.ts"]
|
||||
}
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ COPY apps/api ./apps/api
|
||||
COPY tsconfig.json ./
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
RUN pnpm --filter @dpep/api run build
|
||||
RUN pnpm --filter api run build
|
||||
|
||||
# Stage 3: Backend Runtime
|
||||
FROM node:24-alpine AS backend-prod
|
||||
@@ -37,7 +37,7 @@ COPY --from=backend-build /app/apps/api/package.json ./apps/api/
|
||||
|
||||
EXPOSE 3000
|
||||
ENV NODE_ENV=production
|
||||
CMD ["node", "apps/api/dist/main.js"]
|
||||
CMD ["node", "apps/api/dist/src/main.js"]
|
||||
|
||||
# Stage 4: Build Frontend
|
||||
FROM node:24-alpine AS frontend-build
|
||||
|
||||
+37
-18
@@ -14,28 +14,47 @@ Multi-stage Docker build configuration for backend (NestJS) and frontend (Next.j
|
||||
docker build -f infra/Dockerfile -t blog-nest:latest .
|
||||
```
|
||||
|
||||
### `docker-compose.prod.yml`
|
||||
Production Docker Compose orchestration with PostgreSQL, Redis, Nginx, and both services.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
cd deploy
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
**Or from root:**
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### `docker-compose.yml`
|
||||
Development Docker Compose setup with hot-reload volumes and exposed ports.
|
||||
**Base configuration** with shared service definitions. Contains common settings for both development and production environments.
|
||||
|
||||
**Usage:**
|
||||
### `docker-compose.override.yml`
|
||||
**Development overrides** - automatically loaded by Docker Compose CLI when running `docker-compose up`. Includes:
|
||||
- Development container names (with `_dev` suffix)
|
||||
- Hot-reload code volumes
|
||||
- Published ports for local access
|
||||
- Debug logging level
|
||||
|
||||
### `docker-compose.prod.yml`
|
||||
**Production overrides** - applied only when explicitly specified. Includes:
|
||||
- Production container names
|
||||
- Health checks with longer intervals
|
||||
- Restricted port access (localhost only)
|
||||
- Logging volumes
|
||||
- SSL/TLS configuration
|
||||
- Default fallback values for credentials
|
||||
|
||||
## Usage
|
||||
|
||||
### Development
|
||||
```bash
|
||||
cd deploy
|
||||
docker compose up -d
|
||||
cd infra
|
||||
docker compose up
|
||||
```
|
||||
Or from root:
|
||||
```bash
|
||||
docker compose -f infra/docker-compose.yml -f infra/docker-compose.override.yml up
|
||||
```
|
||||
|
||||
### Production
|
||||
```bash
|
||||
docker compose -f infra/docker-compose.yml -f infra/docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Structure
|
||||
The three-file approach eliminates duplication:
|
||||
- `docker-compose.yml` = base + shared config
|
||||
- `docker-compose.override.yml` = dev-specific overrides (auto-loaded)
|
||||
- `docker-compose.prod.yml` = prod-specific overrides (explicit)
|
||||
|
||||
### `nginx.conf`
|
||||
Nginx reverse proxy configuration with:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
version: '3.8'
|
||||
# Production Docker Compose - for production deployment
|
||||
# Usage: pnpm docker:prod
|
||||
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
@@ -6,16 +7,17 @@ services:
|
||||
image: postgres:17-alpine
|
||||
container_name: blog_postgres
|
||||
restart: unless-stopped
|
||||
env_file: ../.env
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER:-blog}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
|
||||
POSTGRES_DB: ${DB_NAME:-blog_prod}
|
||||
POSTGRES_USER: ${DATABASE_USER:-blog}
|
||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-changeme}
|
||||
POSTGRES_DB: ${DATABASE_NAME:-blog_prod}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432" # Only expose to localhost
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-blog}"]
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USER:-blog}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -27,6 +29,7 @@ services:
|
||||
image: redis:7-alpine
|
||||
container_name: blog_redis
|
||||
restart: unless-stopped
|
||||
env_file: ../.env
|
||||
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-changeme}
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
@@ -44,10 +47,11 @@ services:
|
||||
api:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/Dockerfile
|
||||
dockerfile: infra/Dockerfile
|
||||
target: backend-prod
|
||||
container_name: blog_api
|
||||
restart: unless-stopped
|
||||
env_file: ../.env
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
@@ -56,7 +60,7 @@ services:
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 3000
|
||||
DATABASE_URL: postgresql://${DB_USER:-blog}:${DB_PASSWORD:-changeme}@postgres:5432/${DB_NAME:-blog_prod}
|
||||
DATABASE_URL: postgresql://${DATABASE_USER:-blog}:${DATABASE_PASSWORD:-changeme}@postgres:${DATABASE_PORT:-5432}/${DATABASE_NAME:-blog_prod}
|
||||
REDIS_URL: redis://:${REDIS_PASSWORD:-changeme}@redis:6379
|
||||
API_PORT: 3000
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
@@ -71,18 +75,20 @@ services:
|
||||
networks:
|
||||
- blog_network
|
||||
volumes:
|
||||
- /var/log/blog:/app/logs # Optional: mount logs to host
|
||||
- /var/log/blog:/app/logs
|
||||
|
||||
# Next.js Frontend
|
||||
frontend:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/Dockerfile
|
||||
dockerfile: infra/Dockerfile
|
||||
target: frontend-prod
|
||||
container_name: blog_frontend
|
||||
restart: unless-stopped
|
||||
env_file: ../.env
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 3001
|
||||
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost/api}
|
||||
expose:
|
||||
- "3001"
|
||||
|
||||
+26
-36
@@ -1,4 +1,5 @@
|
||||
version: '3.8'
|
||||
# Development Docker Compose - for local development with hot reload
|
||||
# Usage: pnpm docker:dev
|
||||
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
@@ -6,16 +7,17 @@ services:
|
||||
image: postgres:17-alpine
|
||||
container_name: blog_postgres_dev
|
||||
restart: unless-stopped
|
||||
env_file: ../.env
|
||||
environment:
|
||||
POSTGRES_USER: blog
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: blog_dev
|
||||
POSTGRES_USER: ${DATABASE_USER:-blog}
|
||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-postgres}
|
||||
POSTGRES_DB: ${DATABASE_NAME:-blog_dev}
|
||||
volumes:
|
||||
- postgres_data_dev:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U blog"]
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USER:-blog} -d ${DATABASE_NAME:-blog_dev}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -27,6 +29,7 @@ services:
|
||||
image: redis:7-alpine
|
||||
container_name: blog_redis_dev
|
||||
restart: unless-stopped
|
||||
env_file: ../.env
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- redis_data_dev:/data
|
||||
@@ -42,61 +45,48 @@ services:
|
||||
|
||||
# NestJS Backend API
|
||||
api:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/Dockerfile
|
||||
target: backend-prod
|
||||
image: node:24-alpine
|
||||
container_name: blog_api_dev
|
||||
restart: unless-stopped
|
||||
working_dir: /workspace
|
||||
command: sh -c "corepack enable && pnpm --filter api run dev"
|
||||
env_file: ../.env
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
CI: "true"
|
||||
NODE_ENV: development
|
||||
PORT: 3000
|
||||
DATABASE_URL: postgresql://blog:postgres@postgres:5432/blog_dev
|
||||
DATABASE_URL: postgresql://${DATABASE_USER:-blog}:${DATABASE_PASSWORD:-postgres}@postgres:${DATABASE_PORT:-5432}/${DATABASE_NAME:-blog_dev}
|
||||
REDIS_URL: redis://redis:6379
|
||||
API_PORT: 3000
|
||||
API_PORT: ${API_PORT:-3001}
|
||||
LOG_LEVEL: debug
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "3001:3001"
|
||||
volumes:
|
||||
- ../apps/api/src:/app/apps/api/src
|
||||
- ..:/workspace
|
||||
networks:
|
||||
- blog_network
|
||||
|
||||
# Next.js Frontend
|
||||
frontend:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/Dockerfile
|
||||
target: frontend-prod
|
||||
image: node:24-alpine
|
||||
container_name: blog_frontend_dev
|
||||
restart: unless-stopped
|
||||
working_dir: /workspace
|
||||
command: sh -c "corepack enable && pnpm --filter frontend exec next dev -p 3000"
|
||||
env_file: ../.env
|
||||
environment:
|
||||
CI: "true"
|
||||
NODE_ENV: development
|
||||
NEXT_PUBLIC_API_URL: http://localhost:3000
|
||||
NEXT_PUBLIC_API_URL: http://localhost:3001
|
||||
PORT: 3000
|
||||
ports:
|
||||
- "3001:3001"
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ../apps/frontend/src:/app/apps/frontend/src
|
||||
networks:
|
||||
- blog_network
|
||||
|
||||
# Nginx Reverse Proxy
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: blog_nginx_dev
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- api
|
||||
- frontend
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ..:/workspace
|
||||
networks:
|
||||
- blog_network
|
||||
|
||||
|
||||
+7
-1
@@ -10,7 +10,13 @@
|
||||
"dev": "turbo run dev --filter='./apps/*'",
|
||||
"format": "turbo run format",
|
||||
"lint": "turbo run lint",
|
||||
"test": "turbo run test"
|
||||
"test": "turbo run test",
|
||||
"docker:dev": "docker compose -f infra/docker-compose.yml up",
|
||||
"docker:dev:build": "docker compose -f infra/docker-compose.yml build",
|
||||
"docker:prod": "docker compose -f infra/docker-compose.prod.yml up -d",
|
||||
"docker:prod:build": "docker compose -f infra/docker-compose.prod.yml build",
|
||||
"docker:down": "docker compose -f infra/docker-compose.yml down",
|
||||
"docker:prod:down": "docker compose -f infra/docker-compose.prod.yml down"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
||||
Reference in New Issue
Block a user