← Back to overview

Deployment

One docker compose up and you have a private, self-hosted VaultTeam server.

Backend

Node.js 20 + Express · JWT auth · bcrypt · multer streaming uploads · pg pool

Database

PostgreSQL 16 · RLS policies · tsvector full-text search · init migration in /docker-entrypoint-initdb.d

Storage

Local volume mounted at ./data/vault · files stored as content-addressable objects by SHA-256

TLS

Caddy reverse proxy in front of the app · automatic Let's Encrypt or self-signed for LAN

1. Folder layout

vaultteam/ · tree
vaultteam/
├── docker-compose.yml
├── .env
├── caddy/
│   └── Caddyfile
├── backend/
│   ├── Dockerfile
│   ├── package.json
│   └── src/
│       ├── index.js
│       ├── db/        (migrations, seeders)
│       ├── routes/    (auth, teams, files, notes, audit, admin)
│       ├── middleware/ (auth, rbac, upload)
│       └── lib/       (hash, storage, audit)
├── frontend/
│   ├── Dockerfile
│   └── (this Next.js app)
└── data/
    ├── postgres/      (DB volume)
    └── vault/         (uploaded files, by team & hash)

2. docker-compose.yml

docker-compose.yml · yaml
version: "3.9"

services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: vault
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: vaultteam
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
      - ./backend/src/db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U vault"]
      interval: 5s

  backend:
    build: ./backend
    restart: unless-stopped
    depends_on:
      db: { condition: service_healthy }
    environment:
      DATABASE_URL: postgres://vault:${DB_PASSWORD}@db:5432/vaultteam
      JWT_SECRET: ${JWT_SECRET}
      ADMIN_EMAIL: ${ADMIN_EMAIL}
      ADMIN_PASSWORD: ${ADMIN_PASSWORD}
      UPLOAD_DIR: /vault
      MAX_UPLOAD_MB: 500
    volumes:
      - ./data/vault:/vault

  frontend:
    build: ./frontend
    restart: unless-stopped
    depends_on: [backend]
    environment:
      NEXT_PUBLIC_API_URL: http://backend:4000

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:

3. Environment

.env · dotenv
# Strong secrets — generate with: openssl rand -hex 32
DB_PASSWORD=change-me-strong-db-password
JWT_SECRET=change-me-strong-jwt-secret-min-32-chars
ADMIN_EMAIL=admin@company.local
ADMIN_PASSWORD=change-me-on-first-login

4. Boot

terminal · bash
# Clone
git clone https://git.company.local/infra/vaultteam.git
cd vaultteam

# Generate secrets
echo "DB_PASSWORD=$(openssl rand -hex 24)" >> .env
echo "JWT_SECRET=$(openssl rand -hex 32)" >> .env
echo "ADMIN_EMAIL=admin@company.local" >> .env
echo "ADMIN_PASSWORD=$(openssl rand -hex 16)" >> .env

# Start
docker compose up -d --build

# Tail logs
docker compose logs -f backend

# Visit
open https://vault.company.local

5. Caddy (HTTPS)

caddy/Caddyfile · caddy
vault.company.local {
  reverse_proxy frontend:3000
  encode gzip zstd
}

# LAN / self-signed alternative
:443 {
  tls internal
  reverse_proxy frontend:3000
}

6. Backups

Both volumes are plain directories on the host — back up with rsync, Borg, or whatever your infra uses.

backup.sh · bash
#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%Y%m%d-%H%M%S)
docker exec vaultteam-db-1 pg_dump -U vault vaultteam \
  | gzip > ./backups/db-$STAMP.sql.gz
tar czf ./backups/vault-$STAMP.tar.gz ./data/vault

7. Security checklist

  • ● ✅ Change ADMIN_PASSWORD on first login
  • ● ✅ Rotate JWT_SECRET and DB_PASSWORD every 90 days
  • ● ✅ Restrict the postgres port to localhost (do not expose 5432)
  • ● ✅ Run regular encrypted backups off-site
  • ● ✅ Keep docker images patched — docker compose pull && up -d
  • ● ✅ Enable fail2ban on the host SSH
  • ● ✅ Audit log review is part of quarterly compliance