feat: enhance Docker and environment configurations for development and production

This commit is contained in:
2026-03-06 18:56:53 +03:00
parent cdfd5778bf
commit 33c8eaa182
12 changed files with 249 additions and 68 deletions
+8
View File
@@ -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
+6
View File
@@ -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
+122
View File
@@ -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
+2
View File
@@ -3,6 +3,8 @@
/node_modules
/build
generated
# Logs
logs
*.log
+22
View File
@@ -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)
+1 -1
View File
@@ -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',
},
+1 -1
View File
@@ -25,5 +25,5 @@
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
"exclude": ["node_modules", "jest.config.ts", "jest.setup.ts"]
}
+2 -2
View File
@@ -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
View File
@@ -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:
+15 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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": "",