Documentation debt quietly slows delivery. Teams lose architecture context, runbooks drift, and onboarding repeatedly interrupts senior engineers. A shared docs platform helps, but many teams postpone deployment because they worry about creating another fragile service.
This guide provides a production-oriented path for running Docmost with Docker Compose and a containerized HAProxy edge. The emphasis is reliability and repeatability: secure defaults, explicit health checks, resilient restart behavior, backup routines, and practical day-two troubleshooting.
You will deploy Docmost with PostgreSQL and Redis on a private Docker network, expose only TLS entrypoints, validate service health end to end, and leave with an operational checklist your team can reuse during upgrades.
Architecture and flow overview
Traffic enters HAProxy on ports 80 and 443. HTTP is redirected to HTTPS, and encrypted requests are forwarded to Docmost on the internal Docker network. PostgreSQL stores persistent data while Redis supports cache and job coordination.
This separation keeps the stack maintainable: you can rotate certificates and proxy rules without touching application containers, and you can tune data services independently from web routing. Private service-to-service networking also reduces accidental exposure.
For operations, we include health checks, restart policies, version pinning guidance, secret boundaries, and backup mechanics that capture both database state and uploaded content.
Prerequisites
- Ubuntu 22.04/24.04 with sudo access
- Domain pointed to your server, e.g.
docs.example.com - Docker Engine + Compose plugin
- Ports 80 and 443 reachable
- TLS certificate + private key bundle
- Basic shell familiarity
Use a dedicated service account where possible and do not commit secrets to Git.
Step-by-step deployment
1) Prepare folders
Create stable directories for compose files, proxy config, certs, scripts, and backups.
mkdir -p /opt/docmost/{compose,haproxy/certs,haproxy/config,scripts,backups}
cd /opt/docmost/composeIf copy does not work, manually select and copy.
2) Create private Docker network
Use an explicit shared network for stack isolation.
docker network create docmost_net || trueIf copy does not work, manually select and copy.
3) Write environment variables
Store sensitive values in a local file with strict permissions.
cat > .env <<'EOF'
DOCMOST_DOMAIN=docs.example.com
POSTGRES_DB=docmost
POSTGRES_USER=docmost
POSTGRES_PASSWORD=replace-with-strong-db-password
REDIS_PASSWORD=replace-with-strong-redis-password
APP_SECRET=replace-with-long-random-app-secret
EOF
chmod 600 .envIf copy does not work, manually select and copy.
4) Define application services
Compose declares Docmost, PostgreSQL, and Redis with health checks and persistent volumes.
cat > docker-compose.yml <<'EOF'
services:
postgres:
image: postgres:16
container_name: docmost_postgres
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- docmost_pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
networks: [docmost_net]
redis:
image: redis:7-alpine
container_name: docmost_redis
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "PING"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
networks: [docmost_net]
docmost:
image: docmost/docmost:latest
container_name: docmost_app
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
APP_URL: https://${DOCMOST_DOMAIN}
APP_SECRET: ${APP_SECRET}
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
volumes:
- docmost_storage:/app/data/storage
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/api/health"]
interval: 15s
timeout: 5s
retries: 15
networks: [docmost_net]
volumes:
docmost_pgdata:
docmost_storage:
networks:
docmost_net:
external: true
EOFIf copy does not work, manually select and copy.
5) Configure containerized HAProxy
Keep proxy files under your project path and run HAProxy as a service container.
cat > /opt/docmost/haproxy/config/haproxy.cfg <<'EOF'
global
daemon
maxconn 4096
defaults
mode http
option httplog
timeout connect 5s
timeout client 60s
timeout server 60s
frontend http_in
bind *:80
redirect scheme https code 301 if !{ ssl_fc }
frontend https_in
bind *:443 ssl crt /certs/docs.example.com.pem alpn h2,http/1.1
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-For %[src]
default_backend app
backend app
option httpchk GET /api/health
http-check expect status 200
server docmost docmost_app:3000 check
EOFIf copy does not work, manually select and copy.
cat > /opt/docmost/haproxy/docker-compose.proxy.yml <<'EOF'
services:
haproxy:
image: haproxy:2.9
container_name: docmost_haproxy
ports:
- "80:80"
- "443:443"
volumes:
- /opt/docmost/haproxy/config/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
- /opt/docmost/haproxy/certs:/certs:ro
restart: unless-stopped
networks: [docmost_net]
networks:
docmost_net:
external: true
EOFIf copy does not work, manually select and copy.
6) Start application and proxy
cd /opt/docmost/compose
docker compose --env-file .env up -d
docker compose -f /opt/docmost/haproxy/docker-compose.proxy.yml up -d
docker ps --format "table {{.Names}}\t{{.Status}}"If copy does not work, manually select and copy.
7) Baseline hardening
Restrict secret files and verify runtime logs before handing the service to users.
chmod 600 /opt/docmost/compose/.env
docker logs --tail=120 docmost_app
docker logs --tail=120 docmost_haproxyIf copy does not work, manually select and copy.
Configuration and secret-handling best practices
Rotate secrets on a schedule and after access changes. Keep deployment code versioned, but keep runtime secrets in a protected local file or an external secret manager. If you later adopt Vault, SOPS, or Doppler, preserve variable names so migration is low-risk.
Certificates should be renewed automatically and validated before reload. Treat proxy config checks as a release gate and keep one known-good certificate bundle for emergency rollback.
Use explicit image tags for predictable upgrades, then promote versions from staging to production only after smoke tests pass.
Backup and recovery workflow
Back up both PostgreSQL data and Docmost storage. A green backup job is not enough unless restore testing also succeeds.
cat > /opt/docmost/scripts/backup_docmost.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
TS=$(date +%F_%H%M%S)
OUT=/opt/docmost/backups/$TS
mkdir -p "$OUT"
cd /opt/docmost/compose
source .env
docker exec docmost_postgres pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > "$OUT/postgres.sql"
tar -czf "$OUT/docmost_storage.tar.gz" -C /var/lib/docker/volumes/docmost_storage/_data .
sha256sum "$OUT/postgres.sql" "$OUT/docmost_storage.tar.gz" > "$OUT/checksums.txt"
EOF
chmod +x /opt/docmost/scripts/backup_docmost.shIf copy does not work, manually select and copy.
(crontab -l 2>/dev/null; echo "20 2 * * * /opt/docmost/scripts/backup_docmost.sh >> /opt/docmost/backups/backup.log 2>&1") | crontab -If copy does not work, manually select and copy.
Verification checklist
- DNS resolves correctly
- HTTPS endpoint responds
- HAProxy health checks pass
- Docmost login and page creation work
- Restart test keeps data intact
curl -I https://docs.example.com
docker ps --filter name=docmost --format "{{.Names}} {{.Status}}"
docker compose -f /opt/docmost/compose/docker-compose.yml restart docmost
docker compose -f /opt/docmost/compose/docker-compose.yml psIf copy does not work, manually select and copy.
Common issues and fixes
HTTPS certificate errors
Check that your PEM bundle contains full chain plus private key and that the file is mounted at the path referenced by HAProxy. Restart only after validating syntax.
502/503 from the proxy
Confirm Docmost is healthy on the internal network and that backend naming matches service/container naming in your compose setup.
Database connection failures
Most incidents come from mismatched credentials between initialized database state and current environment variables. Reconcile values and test readiness from the PostgreSQL container.
Slow UI during peak edits
Inspect CPU saturation, disk IOPS, and PostgreSQL contention. As usage grows, move PostgreSQL to dedicated infrastructure and keep app containers stateless.
FAQ
Can I run Docmost without publishing port 3000?
Yes. Keep the app private and expose only HAProxy on 80/443.
Should I use secrets tooling instead of .env?
Yes when available. The .env approach is a practical baseline, not the end state for mature teams.
How often should backups run?
At least daily; high-change teams may need hourly DB dumps and more frequent storage snapshots.
What is the safest upgrade path?
Back up, test in staging, then roll out during a maintenance window with a rollback image ready.
Can I swap HAProxy for another proxy later?
Yes. This architecture keeps proxy and app concerns separate, so migration is straightforward.
When should PostgreSQL move off-box?
When uptime requirements, write load, or compliance needs exceed single-host risk tolerance.
Related internal guides
- Production Guide: Deploy GlitchTip with Docker Compose + Caddy + PostgreSQL on Ubuntu
- Production Guide: Deploy Sentry with Docker Compose + Caddy + PostgreSQL on Ubuntu
- Production Guide: Deploy Plane with Docker Compose + Nginx + PostgreSQL on Ubuntu
Talk to us
If you want help shipping a secure, production-ready Docmost deployment with observability, backup validation, and upgrade planning, our team can help.