mirror of
https://github.com/kbenestad/mdcms.git
synced 2026-06-18 15:24:32 +00:00
301 lines
6.7 KiB
Markdown
301 lines
6.7 KiB
Markdown
---
|
||
title: Docker
|
||
sort: 120
|
||
section-id: deployment
|
||
keywords: Docker, Dockerfile, docker-compose, container, containerisation, deployment
|
||
description: Containerising a Velox application with Docker and docker-compose
|
||
language: en
|
||
---
|
||
|
||
# Docker
|
||
|
||
Containerising your Velox application with Docker makes it portable across any infrastructure — from a simple VPS to a Kubernetes cluster. This guide covers building optimised Docker images and running multi-service stacks with docker-compose.
|
||
|
||
## Dockerfile
|
||
|
||
A production-grade multi-stage Dockerfile:
|
||
|
||
```dockerfile
|
||
# Stage 1: Install dependencies
|
||
FROM node:22-alpine AS deps
|
||
WORKDIR /app
|
||
|
||
# Copy package manifests only — enables Docker layer caching
|
||
COPY package.json package-lock.json ./
|
||
RUN npm ci --frozen-lockfile
|
||
|
||
# Stage 2: Build the application
|
||
FROM node:22-alpine AS builder
|
||
WORKDIR /app
|
||
|
||
COPY --from=deps /app/node_modules ./node_modules
|
||
COPY . .
|
||
|
||
# Build args — pass at build time
|
||
ARG PUBLIC_BASE_URL
|
||
ARG NODE_ENV=production
|
||
ENV PUBLIC_BASE_URL=$PUBLIC_BASE_URL
|
||
ENV NODE_ENV=$NODE_ENV
|
||
|
||
RUN npm run build
|
||
|
||
# Stage 3: Production runtime
|
||
FROM node:22-alpine AS runner
|
||
WORKDIR /app
|
||
|
||
ENV NODE_ENV=production
|
||
ENV PORT=3700
|
||
|
||
# Create non-root user for security
|
||
RUN addgroup --system --gid 1001 veloxgroup && \
|
||
adduser --system --uid 1001 veloxuser
|
||
|
||
# Copy only production artefacts
|
||
COPY --from=builder --chown=veloxuser:veloxgroup /app/.velox/output ./
|
||
COPY --from=builder --chown=veloxuser:veloxgroup /app/package.json ./
|
||
|
||
# Install production dependencies only
|
||
RUN npm ci --omit=dev --frozen-lockfile
|
||
|
||
USER veloxuser
|
||
|
||
EXPOSE 3700
|
||
|
||
CMD ["node", "server.js"]
|
||
```
|
||
|
||
## Building and Running
|
||
|
||
```bash
|
||
# Build the image
|
||
docker build \
|
||
--build-arg PUBLIC_BASE_URL=https://example.com \
|
||
-t my-velox-app:latest .
|
||
|
||
# Run the container
|
||
docker run \
|
||
--env-file .env.production \
|
||
-p 3700:3700 \
|
||
--name velox-app \
|
||
my-velox-app:latest
|
||
|
||
# Run in background
|
||
docker run -d \
|
||
--env-file .env.production \
|
||
-p 3700:3700 \
|
||
--restart unless-stopped \
|
||
--name velox-app \
|
||
my-velox-app:latest
|
||
```
|
||
|
||
## `.dockerignore`
|
||
|
||
Exclude files from the build context to speed up builds:
|
||
|
||
```
|
||
node_modules
|
||
.velox
|
||
.git
|
||
.gitignore
|
||
*.md
|
||
.env
|
||
.env.*
|
||
!.env.example
|
||
tests
|
||
*.test.*
|
||
*.spec.*
|
||
coverage
|
||
```
|
||
|
||
## docker-compose
|
||
|
||
A complete stack with the app, PostgreSQL, and Redis:
|
||
|
||
```yaml
|
||
# docker-compose.yml
|
||
version: '3.9'
|
||
|
||
services:
|
||
app:
|
||
build:
|
||
context: .
|
||
args:
|
||
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-http://localhost:3700}
|
||
image: my-velox-app:latest
|
||
ports:
|
||
- "3700:3700"
|
||
environment:
|
||
NODE_ENV: production
|
||
DATABASE_URL: postgresql://velox:${DB_PASSWORD}@db:5432/velox
|
||
REDIS_URL: redis://redis:6379
|
||
SESSION_SECRET: ${SESSION_SECRET}
|
||
JWT_SECRET: ${JWT_SECRET}
|
||
depends_on:
|
||
db:
|
||
condition: service_healthy
|
||
redis:
|
||
condition: service_healthy
|
||
restart: unless-stopped
|
||
healthcheck:
|
||
test: ["CMD", "wget", "-qO-", "http://localhost:3700/api/health"]
|
||
interval: 30s
|
||
timeout: 10s
|
||
retries: 3
|
||
start_period: 40s
|
||
|
||
db:
|
||
image: postgres:16-alpine
|
||
environment:
|
||
POSTGRES_USER: velox
|
||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||
POSTGRES_DB: velox
|
||
volumes:
|
||
- postgres_data:/var/lib/postgresql/data
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U velox"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
restart: unless-stopped
|
||
|
||
redis:
|
||
image: redis:7-alpine
|
||
command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes
|
||
volumes:
|
||
- redis_data:/data
|
||
healthcheck:
|
||
test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "${REDIS_PASSWORD}", "ping"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
restart: unless-stopped
|
||
|
||
nginx:
|
||
image: nginx:alpine
|
||
ports:
|
||
- "80:80"
|
||
- "443:443"
|
||
volumes:
|
||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||
- nginx_cache:/var/cache/nginx
|
||
depends_on:
|
||
- app
|
||
restart: unless-stopped
|
||
|
||
volumes:
|
||
postgres_data:
|
||
redis_data:
|
||
nginx_cache:
|
||
```
|
||
|
||
## Nginx Reverse Proxy
|
||
|
||
```nginx
|
||
# nginx/nginx.conf
|
||
events { worker_connections 1024; }
|
||
|
||
http {
|
||
upstream velox_app {
|
||
server app:3700;
|
||
keepalive 64;
|
||
}
|
||
|
||
# Redirect HTTP → HTTPS
|
||
server {
|
||
listen 80;
|
||
server_name example.com www.example.com;
|
||
return 301 https://$host$request_uri;
|
||
}
|
||
|
||
server {
|
||
listen 443 ssl http2;
|
||
server_name example.com www.example.com;
|
||
|
||
ssl_certificate /etc/nginx/ssl/fullchain.pem;
|
||
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
|
||
ssl_protocols TLSv1.2 TLSv1.3;
|
||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||
|
||
# Static assets with long cache
|
||
location /assets/ {
|
||
proxy_pass http://velox_app;
|
||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||
}
|
||
|
||
# Application
|
||
location / {
|
||
proxy_pass http://velox_app;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header 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_set_header X-Forwarded-Proto $scheme;
|
||
proxy_cache_bypass $http_upgrade;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## Health Check Endpoint
|
||
|
||
Add a health check to your application for Docker and load balancers:
|
||
|
||
```typescript
|
||
// routes/api/health+server.ts
|
||
import { defineHandler, json } from 'velox/server';
|
||
import { db } from '$lib/db';
|
||
|
||
export const GET = defineHandler(async () => {
|
||
try {
|
||
await db.$queryRaw`SELECT 1`;
|
||
return json({ status: 'ok', timestamp: new Date().toISOString() });
|
||
} catch (error) {
|
||
return json({ status: 'error', error: String(error) }, { status: 503 });
|
||
}
|
||
});
|
||
```
|
||
|
||
## Running Database Migrations
|
||
|
||
Run migrations as a separate one-shot container before starting the app:
|
||
|
||
```yaml
|
||
# docker-compose.yml — add this service
|
||
migrate:
|
||
image: my-velox-app:latest
|
||
command: npx prisma migrate deploy
|
||
environment:
|
||
DATABASE_URL: postgresql://velox:${DB_PASSWORD}@db:5432/velox
|
||
depends_on:
|
||
db:
|
||
condition: service_healthy
|
||
restart: "no"
|
||
```
|
||
|
||
## Container Best Practices
|
||
|
||
| Practice | Why |
|
||
|----------|-----|
|
||
| Multi-stage builds | Reduces final image size by 60–80% |
|
||
| Non-root user | Limits damage if the container is compromised |
|
||
| Read-only filesystem | Mount only what needs to be writable |
|
||
| `--restart unless-stopped` | Survives host reboots |
|
||
| Resource limits | Prevents a runaway container from affecting neighbours |
|
||
| Health checks | Enables zero-downtime rolling updates |
|
||
|
||
Set resource limits:
|
||
|
||
```yaml
|
||
app:
|
||
deploy:
|
||
resources:
|
||
limits:
|
||
cpus: '1.0'
|
||
memory: 512M
|
||
reservations:
|
||
cpus: '0.25'
|
||
memory: 128M
|
||
```
|