Deployment
This guide covers deploying a Viseed CMS application to a production server using Docker.
Prerequisites
- Docker and Docker Compose
- A container registry (Docker Hub, GHCR, or similar)
- A PostgreSQL database accessible from the server
- A domain name (optional, for Nginx setup)
Dockerfile
Add a Dockerfile to the root of your project. A recommended two-stage build using Bun Alpine:
# ── Stage 1: Install dependencies ─────────────────────────────────────────────
FROM oven/bun:1-alpine AS deps
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
# ── Stage 2: Runner ────────────────────────────────────────────────────────────
FROM oven/bun:1-alpine AS runner
RUN apk add --no-cache tzdata
# Non-root user — explicit UID/GID so host volume ownership is predictable
RUN addgroup -S -g 1001 appgroup && adduser -S -u 1001 -G appgroup appuser
WORKDIR /app
RUN chown appuser:appgroup /app
COPY --chown=appuser:appgroup --from=deps /app/node_modules ./node_modules
COPY --chown=appuser:appgroup . .
USER appuser
ENV PORT=3000
EXPOSE 3000
CMD ["bun", "run", "src/index.ts"]Also create a .dockerignore to keep the image lean:
node_modules
.env*
uploads
*.mdEnvironment Variables
Create an .env file on your server with the following variables:
# Required
DATABASE_URL=postgresql://user:password@host:5432/db
# Optional — HTTP port (default 3000)
PORT=3000For managed databases with SSL certificates (e.g. DigitalOcean, AWS RDS), add:
DATABASE_URL=postgresql://user:pass@host:5432/db?sslmode=verify-full&sslrootcert=./ca-certificate.crt
NODE_EXTRA_CA_CERTS=/app/ca-certificate.crtPlace the CA certificate file at the root of your deployment directory (next to docker-compose.yml).
Build & Push to Registry
Build for linux/amd64 and push to your container registry:
# Tag with a version and latest
docker build --platform linux/amd64 \
-t your-registry/your-image:1.0.0 \
-t your-registry/your-image:latest \
.
docker push your-registry/your-image:1.0.0
docker push your-registry/your-image:latestReplace your-registry/your-image with your actual registry path, e.g. ghcr.io/your-org/cms or yourname/cms on Docker Hub.
Docker Compose
Create a docker-compose.yml on the server. The server pulls the pre-built image — no build step required.
name: my-cms
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "5"
services:
app:
image: your-registry/your-image:${IMAGE_TAG:-latest}
env_file: .env
environment:
- NODE_EXTRA_CA_CERTS=/app/ca-certificate.crt # remove if not using SSL cert
volumes:
- ./ca-certificate.crt:/app/ca-certificate.crt:ro # remove if not using SSL cert
- ./uploads:/app/uploads:rw
restart: unless-stopped
logging: *default-logging
networks:
- cms-network
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
depends_on:
app:
condition: service_healthy
restart: unless-stopped
logging: *default-logging
networks:
- cms-network
networks:
cms-network:
driver: bridgeApply migrations before first start
docker compose run --rm app bun run db:migrateOr from your local machine with DATABASE_URL pointing to production:
DATABASE_URL="postgresql://user:pass@prod-host:5432/db" bun run db:migrateStart services
docker compose up -dDeploy a new version
After pushing a new image to the registry:
IMAGE_TAG=1.0.0 docker compose pull && docker compose up -dView logs
docker compose logs -f appNginx Configuration
Create nginx/conf.d/default.conf in your deployment directory:
proxy_http_version 1.1;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
client_max_body_size 50m; # must be >= media.maxFileSizeMb
upstream app {
server app:3000;
}
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://app;
}
}HTTPS
For TLS termination, use Certbot to provision Let's Encrypt certificates and update the Nginx config to listen on port 443.
Volumes to Persist
| Path | Description |
|---|---|
/app/uploads | Uploaded media files |
Graceful Shutdown
Viseed CMS handles SIGTERM and SIGINT signals — it stops accepting new connections, waits for in-flight requests to complete, then closes the database connection cleanly. Docker's default stop_grace_period of 10 seconds is sufficient for most workloads.
Health Check
The server exposes GET /health which returns 200 OK when the CMS is running. This endpoint is used by Docker Compose and load balancers to determine container readiness.