← 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