Self-hosted deployment using Docker Compose with nginx reverse proxy, TLS termination, and optional services.
Internet
|
v
[nginx:443/80]
|
+-- /api/* --> server:3000
+-- /health --> server:3000
+-- /dev --> server:3000
+-- /collaboration/ --> server:3000 (WebSocket)
+-- /uploads/ --> server:3000
+-- /* --> web:80
|
server:3000--+--postgres:5432
|
(optional)
minio:9000 | meilisearch:7700
All inter-service communication happens over an internal Docker network. Only nginx exposes ports to the host.
- Docker Engine 24+ with Docker Compose v2
- Domain name pointing to your server's IP address
- TLS certificate (own certificate or Let's Encrypt)
- 2 GB RAM minimum (4 GB recommended with optional services)
- 10 GB disk minimum (more depending on file uploads and backups)
# Clone the repository
git clone https://github.com/Akaal-Creatives/LibreDiary.git
cd LibreDiary
# Create environment file
cp .env.example .env
# Edit .env with your production values (see Section 3)
nano .env
# Place TLS certificates (see Section 4) — or use HTTP-only mode (see Section 4b)
mkdir -p tooling/docker/nginx/ssl
cp /path/to/fullchain.pem tooling/docker/nginx/ssl/fullchain.pem
cp /path/to/privkey.pem tooling/docker/nginx/ssl/privkey.pem
# Build images
docker compose -f tooling/docker/docker-compose.production.yml build
# Start services (migrations and seed run automatically on first boot)
docker compose -f tooling/docker/docker-compose.production.yml up -dEdit .env in the project root. The server container reads this file and applies container-specific overrides (DATABASE_URL, storage paths, etc.) automatically.
| Variable | Description | Example |
|---|---|---|
POSTGRES_PASSWORD |
PostgreSQL password (no default for security) | a-strong-random-password |
APP_URL |
Public URL of your instance | https://diary.example.com |
API_URL |
Public API URL (same domain in production) | https://diary.example.com |
APP_SECRET |
Application secret (min 32 characters) | generate-with-openssl-rand-hex-32 |
SESSION_SECRET |
Session secret (min 32 characters) | generate-with-openssl-rand-hex-32 |
DOMAIN |
Domain name for nginx | diary.example.com |
FRONTEND_URL |
Public frontend URL (for OAuth redirects) | https://diary.example.com |
| Variable | Default | Description |
|---|---|---|
POSTGRES_USER |
librediary |
PostgreSQL username |
POSTGRES_DB |
librediary |
PostgreSQL database name |
HTTP_PORT |
80 |
Host port for HTTP |
HTTPS_PORT |
443 |
Host port for HTTPS |
SSL_CERT_PATH |
./nginx/ssl |
Path to TLS certificates directory |
STORAGE_TYPE |
local |
Storage provider: local, minio, s3 |
BACKUP_ENABLED |
false |
Enable scheduled backups |
BACKUP_SCHEDULE |
0 2 * * * |
Backup cron schedule |
AI_ENABLED |
true |
Enable AI features |
OPENROUTER_API_KEY |
- | OpenRouter API key for AI |
MINIO_CONSOLE_PORT |
9001 |
Host port for MinIO console |
MEILISEARCH_API_KEY |
masterkey |
Meilisearch master key |
# Generate secure random strings
openssl rand -hex 32 # For APP_SECRET
openssl rand -hex 32 # For SESSION_SECRET
openssl rand -hex 24 # For POSTGRES_PASSWORDPlace your certificate files in the SSL directory:
mkdir -p tooling/docker/nginx/ssl
cp /path/to/fullchain.pem tooling/docker/nginx/ssl/fullchain.pem
cp /path/to/privkey.pem tooling/docker/nginx/ssl/privkey.pemOr set a custom path via SSL_CERT_PATH in .env:
SSL_CERT_PATH=/etc/letsencrypt/live/diary.example.com- Initial certificate — temporarily comment out the HTTPS server block or use HTTP-only mode, then:
# Install certbot on the host
sudo apt install certbot
# Obtain certificate (nginx must be running for ACME challenge)
sudo certbot certonly --webroot \
-w /var/lib/docker/volumes/librediary_certbot/_data \
-d diary.example.com
# Copy certificates
sudo cp /etc/letsencrypt/live/diary.example.com/fullchain.pem tooling/docker/nginx/ssl/
sudo cp /etc/letsencrypt/live/diary.example.com/privkey.pem tooling/docker/nginx/ssl/- Auto-renewal — add a cron job:
# Edit crontab
crontab -e
# Add renewal (checks twice daily, restarts nginx on success)
0 0,12 * * * certbot renew --quiet && \
cp /etc/letsencrypt/live/diary.example.com/fullchain.pem /path/to/LibreDiary/tooling/docker/nginx/ssl/ && \
cp /etc/letsencrypt/live/diary.example.com/privkey.pem /path/to/LibreDiary/tooling/docker/nginx/ssl/ && \
docker compose -f /path/to/LibreDiary/tooling/docker/docker-compose.production.yml exec nginx nginx -s reloadFor local testing or environments where TLS is terminated upstream (e.g. Cloudflare, load balancer), use the HTTP-only compose override. No certificates are required:
# Build
docker compose -f tooling/docker/docker-compose.production.yml build
# Start in HTTP-only mode
docker compose \
-f tooling/docker/docker-compose.production.yml \
-f tooling/docker/docker-compose.local.yml \
up -dThis swaps the nginx config to serve everything over port 80 without any TLS directives.
The simplest way to deploy — no source code or build step required. Uses published Docker images from the registry.
# Clone the repository (only needed for config files)
git clone https://github.com/Akaal-Creatives/LibreDiary.git
cd LibreDiary
# Create environment file
cp .env.example .env
nano .env
# Start all services
docker compose up -d
# HTTP-only mode (no TLS certificates needed)
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d
# With optional services
docker compose --profile storage --profile search up -dThe root docker-compose.yml references pre-built images (librediary/server:0.1.0, librediary/web:0.1.0). No docker compose build is needed.
To build and tag images locally (e.g. for custom modifications or publishing):
# Build both server and web images, tagged with version + latest
./scripts/docker-build.sh
# Build and push to a registry
./scripts/docker-build.sh --pushThe script reads the version from package.json and tags each image with both the version number and latest.
For development or when you need to build from source, use the production compose file directly:
# Set the compose file alias (optional, for convenience)
export COMPOSE_FILE=tooling/docker/docker-compose.production.yml
# Build all images
docker compose -f tooling/docker/docker-compose.production.yml build
# Start all services (detached)
docker compose -f tooling/docker/docker-compose.production.yml up -d
# View logs
docker compose -f tooling/docker/docker-compose.production.yml logs -f
# View logs for a specific service
docker compose -f tooling/docker/docker-compose.production.yml logs -f server
# Stop all services
docker compose -f tooling/docker/docker-compose.production.yml down
# Rebuild a single service
docker compose -f tooling/docker/docker-compose.production.yml build server
docker compose -f tooling/docker/docker-compose.production.yml up -d serverMigrations run automatically on every container start via the Docker entrypoint script. This includes:
- Prisma migrations (
prisma migrate deploy) — schema changes - Custom SQL migrations — search vectors and other extensions
- Database seed — creates the super admin account on first boot (idempotent; skips if admin exists)
To run migrations manually (e.g. for debugging):
docker compose -f tooling/docker/docker-compose.production.yml exec server \
npx prisma migrate deploy --schema=apps/server/prisma/schema.prismaEnable MinIO for S3-compatible file storage instead of local disk:
# Start with MinIO
docker compose -f tooling/docker/docker-compose.production.yml --profile storage up -d
# Set in .env
STORAGE_TYPE=minio
STORAGE_S3_ENDPOINT=http://minio:9000
STORAGE_S3_BUCKET=librediary
STORAGE_S3_ACCESS_KEY=minioadmin
STORAGE_S3_SECRET_KEY=minioadminThe MinIO console is accessible at http://your-server:9001 (configurable via MINIO_CONSOLE_PORT).
Enable Meilisearch for enhanced search with typo tolerance:
# Start with Meilisearch
docker compose -f tooling/docker/docker-compose.production.yml --profile search up -d
# Set in .env
MEILISEARCH_HOST=http://meilisearch:7700
MEILISEARCH_API_KEY=your-secure-master-keydocker compose -f tooling/docker/docker-compose.production.yml \
--profile storage --profile search up -d# Pull latest code
git pull origin main
# Rebuild images
docker compose -f tooling/docker/docker-compose.production.yml build
# Restart services (migrations run automatically on server start)
docker compose -f tooling/docker/docker-compose.production.yml up -dLibreDiary has a built-in backup system. Enable it in .env:
BACKUP_ENABLED=true
BACKUP_SCHEDULE=0 2 * * * # Daily at 2 AM
BACKUP_RETENTION_DAYS=30
BACKUP_STORAGE_TYPE=LOCALBackups are stored in the server_backups volume, mounted at /app/backups inside the container.
Back up Docker volumes directly for disaster recovery:
# Backup PostgreSQL data
docker run --rm \
-v librediary_postgres_prod:/data:ro \
-v $(pwd)/backups:/backup \
alpine tar czf /backup/postgres-$(date +%Y%m%d).tar.gz -C /data .
# Backup uploaded files
docker run --rm \
-v librediary_uploads_prod:/data:ro \
-v $(pwd)/backups:/backup \
alpine tar czf /backup/uploads-$(date +%Y%m%d).tar.gz -C /data .docker compose -f tooling/docker/docker-compose.production.yml exec postgres \
pg_dump -U librediary librediary > backup-$(date +%Y%m%d).sqldocker compose -f tooling/docker/docker-compose.production.yml exec -T postgres \
psql -U librediary librediary < backup-20260211.sql# Check service status
docker compose -f tooling/docker/docker-compose.production.yml ps
# Check logs for errors
docker compose -f tooling/docker/docker-compose.production.yml logs server
docker compose -f tooling/docker/docker-compose.production.yml logs nginx- Ensure
POSTGRES_PASSWORDis set in.env - Check that the postgres container is healthy:
docker compose ... ps postgres - The server automatically uses
postgres:5432(internal hostname) — do not changeDATABASE_URLmanually
- The server container may still be starting. Check health:
docker compose ... ps server - Verify the server logs:
docker compose ... logs server
- Ensure nginx is routing
/collaboration/to the server - Check that the WebSocket upgrade headers are being passed through
- Verify there are no intermediate proxies stripping upgrade headers
- Verify certificate files exist:
ls -la tooling/docker/nginx/ssl/ - Ensure
fullchain.pemcontains the full chain (server cert + intermediate) - Check nginx logs:
docker compose ... logs nginx
- Check
client_max_body_sizein nginx config (default: 50m) - Verify the
server_uploadsvolume is mounted correctly - Check disk space:
df -h
- Strong, unique
POSTGRES_PASSWORDset - Strong, unique
APP_SECRETset (min 32 characters) - Strong, unique
SESSION_SECRETset (min 32 characters) -
APP_URLandAPI_URLset to production domain with HTTPS -
FRONTEND_URLset to production frontend URL (for OAuth redirects) -
DOMAINset for nginx - Valid TLS certificates installed
- SMTP configured for email features (password reset, invitations)
- Firewall configured — only ports 80 and 443 open
- Regular backup schedule configured
- MinIO credentials changed from defaults (if using MinIO)
- Meilisearch API key changed from default (if using Meilisearch)
- OAuth credentials configured (if using GitHub/Google login)
- Log rotation configured on host
- Monitoring/alerting set up for service health
Developed by Akaal Creatives