Internal teams spend hours reconciling data across spreadsheets, ticketing systems, and ad-hoc scripts. ToolJet is an open-source low-code platform that helps engineering and operations teams build admin panels, dashboards, approval workflows, and CRUD applications without spinning up a new microservice for every operational need. In this guide, we will deploy ToolJet on Ubuntu with Docker Compose, publish it through Caddy with automatic HTTPS, keep the application container bound to localhost, and add the operational controls that make it safe for production use: secret management, database backups, health checks, and a clear acceptance checklist.
The target audience is a platform team, small business, or internal IT group that wants a maintainable internal-tools layer. The pattern keeps the application stack isolated, exposes only Caddy to the public internet, stores secrets in an environment file with restricted permissions, and verifies each layer before inviting users. You can integrate SSO and Git-sync later, but this baseline gives you a dependable, upgrade-friendly foundation that behaves predictably during maintenance windows and incident response.
Architecture and flow overview
The public request flow is straightforward. Users open https://tools.example.com. Caddy terminates TLS, applies basic security headers, and proxies traffic to 127.0.0.1:8088. Docker maps that host-only port to ToolJet inside the container. ToolJet stores application data in PostgreSQL and uses Redis for caching and background job processing. Uploaded assets and generated files live on a bind-mounted directory. This keeps the database and cache private, makes backups straightforward, and avoids publishing the application container directly to the internet.
- Caddy: public HTTPS entry point, automatic certificates, compression, and security headers.
- ToolJet: low-code application builder, runtime, and admin interface.
- PostgreSQL: relational database for apps, users, queries, workspace state, and audit history.
- Redis: caching layer, session store, and background job queue.
- Persistent volumes: local directories for database state, uploaded files, and backup artifacts.
Keeping the application port on 127.0.0.1 means the service is reachable through Caddy, not directly from the internet. If you later move the service behind an identity-aware proxy, VPN, or Web Application Firewall, the same separation of responsibilities still works.
Prerequisites
- Ubuntu 22.04 or 24.04 server with a non-root sudo user.
- A DNS record such as
tools.example.compointing to the server. - Ports 80 and 443 reachable from the internet for Caddy certificate issuance.
- Docker Engine, Docker Compose plugin, and Caddy installed.
- At least 2 vCPU, 4 GB RAM, and 20 GB disk for comfortable operation.
- A backup target outside the server, such as S3, a NAS, or a managed backup system.
Step-by-step deployment
1) Install Docker, Compose, Caddy, and firewall basics
Start from a clean host and install only the pieces needed for this stack. Enabling the firewall early avoids accidentally exposing services while you iterate.
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg ufw git
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin caddy
sudo systemctl enable --now docker
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enableIf the copy button is unavailable in your browser/editor, manually copy the command block above.
2) Create directories and environment file
Use a fixed layout so every runbook references the same paths. Store secrets in .env.prod with restricted permissions.
sudo mkdir -p /opt/tooljet/{compose,backups}
sudo mkdir -p /var/lib/tooljet-data
sudo chown -R $USER:$USER /opt/tooljet
sudo chown -R 1000:1000 /var/lib/tooljet-data
chmod 700 /opt/tooljet
# Generate strong secrets
LOCKBOX=$(openssl rand -hex 32)
SECRET=$(openssl rand -hex 64)
cat > /opt/tooljet/.env.prod <<EOF
TOOLJET_HOST=https://tools.example.com
LOCKBOX_MASTER_KEY=${LOCKBOX}
SECRET_KEY_BASE=${SECRET}
PG_DB=tooljet
PG_USER=tooljet
PG_HOST=postgres
PG_PASS=$(openssl rand -base64 32)
PG_PORT=5432
REDIS_HOST=redis
REDIS_PORT=6379
CHECK_FOR_UPDATES=true
DISABLE_TOOLJET_TELEMETRY=false
EOF
chmod 600 /opt/tooljet/.env.prodIf the copy button is unavailable in your browser/editor, manually copy the command block above.
3) Define Compose services
Pin image versions, bind the application to localhost, and add health checks so the orchestrator can detect unhealthy states.
cat > /opt/tooljet/compose/docker-compose.yml <<'EOF'
services:
postgres:
image: postgres:16-alpine
container_name: tooljet-postgres
restart: unless-stopped
env_file:
- /opt/tooljet/.env.prod
environment:
POSTGRES_DB: ${PG_DB}
POSTGRES_USER: ${PG_USER}
POSTGRES_PASSWORD: ${PG_PASS}
volumes:
- /var/lib/tooljet-data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${PG_USER} -d ${PG_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- tooljet
redis:
image: redis:7-alpine
container_name: tooljet-redis
restart: unless-stopped
volumes:
- /var/lib/tooljet-data/redis:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- tooljet
tooljet:
image: tooljet/tooljet-ce:v2.67.0
container_name: tooljet-app
restart: unless-stopped
env_file:
- /opt/tooljet/.env.prod
ports:
- "127.0.0.1:8088:80"
volumes:
- /var/lib/tooljet-data/tooljet:/var/lib/tooljet
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:80/api/health"]
interval: 30s
timeout: 5s
retries: 5
start_period: 60s
networks:
- tooljet
networks:
tooljet:
driver: bridge
EOFIf the copy button is unavailable in your browser/editor, manually copy the command block above.
4) Configure Caddy reverse proxy
Create a minimal Caddyfile that handles TLS, compression, and proxy headers. The application container is not exposed directly.
cat > /opt/tooljet/Caddyfile <<'EOF'
tools.example.com {
reverse_proxy 127.0.0.1:8088 {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Proto {scheme}
}
encode gzip
header {
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
EOF
sudo caddy adapt --config /opt/tooljet/Caddyfile > /dev/null
sudo cp /opt/tooljet/Caddyfile /etc/caddy/Caddyfile
sudo systemctl reload caddyIf the copy button is unavailable in your browser/editor, manually copy the command block above.
5) Start services and verify health
Bring the stack up and confirm each container reports healthy before proceeding to setup.
cd /opt/tooljet/compose
docker compose up -d
docker compose ps
docker compose logs -f tooljetIf the copy button is unavailable in your browser/editor, manually copy the command block above.
6) Run first-time setup
Open https://tools.example.com in a browser and create the first workspace and admin user. After setup, confirm that you can build a simple application, connect to a data source, and invite a test user. Document the admin credentials in your password manager, not in the server.
For teams that plan to use version control, configure Git integration early so application definitions can be reviewed in pull requests rather than edited directly in production. This keeps changes visible, auditable, and rollback-friendly.
7) Backup script
Automate database dumps and file archives so recovery is a matter of minutes, not hours.
cat > /opt/tooljet/backups/backup-tooljet.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
TS=$(date +%F-%H%M)
mkdir -p /opt/tooljet/backups/archive
# Database dump via running container
sudo docker exec tooljet-postgres pg_dump -U tooljet -d tooljet | gzip > /opt/tooljet/backups/archive/tooljet-db-${TS}.sql.gz
# Application files
sudo tar -czf /opt/tooljet/backups/archive/tooljet-files-${TS}.tgz /var/lib/tooljet-data/tooljet /opt/tooljet/.env.prod /opt/tooljet/compose
echo "Backup created: tooljet-db-${TS}.sql.gz and tooljet-files-${TS}.tgz"
EOF
chmod +x /opt/tooljet/backups/backup-tooljet.shIf the copy button is unavailable in your browser/editor, manually copy the command block above.
8) Acceptance checklist execution
Before inviting users, verify every layer of the stack. Save the results in a runbook so anyone on-call can confirm baseline health in minutes.
# 1. Service health
docker compose -f /opt/tooljet/compose/docker-compose.yml ps
# 2. HTTPS reachability and health endpoint
curl -s https://tools.example.com/api/health
# 3. Database connectivity
docker exec tooljet-postgres pg_isready -U tooljet
# 4. Backup artifacts exist
ls -lh /opt/tooljet/backups/archive | tail -n 3
# 5. TLS certificate validity
echo | openssl s_client -servername tools.example.com -connect tools.example.com:443 2>/dev/null | openssl x509 -noout -datesIf the copy button is unavailable in your browser/editor, manually copy the command block above.
Configuration and secrets handling
Limit admin access to named maintainers and rotate credentials on a schedule. Document who can create apps, connect data sources, and manage workspace settings. If you are in a regulated environment, keep change approvals linked to pull requests and maintenance tickets.
Secrets should be managed as first-class assets. At minimum, protect environment files with strict permissions and encrypted storage. The .env.prod file should never be checked into version control. In larger environments, migrate to a dedicated secret manager and keep key names stable so deployment templates do not drift between staging and production.
Treat application definitions as code: export queries and component layouts to Git so they can be reviewed and versioned. This reduces unexpected behavior during incidents and helps new team members understand intent quickly. Consistency is more important than cleverness when multiple people maintain internal tools.
Finally, define upgrade policy early. Pin versions in the Compose file, test in staging, and require validation evidence before production rollouts. The internal-tools layer must be dependable under pressure; operational discipline is what makes that true over time. From an operations-management perspective, assign a primary and secondary owner for this service and document decision rights clearly: who can approve upgrades, who can rollback, and who must be paged when acceptance tests fail.
Verification
After deployment and each upgrade, verify service state, health endpoint response, and backup output. Save results in a runbook so anyone on-call can confirm baseline health in minutes.
- Service starts after host reboot.
- All containers report healthy status.
- Health endpoint responds with success.
- TLS path remains valid through Caddy.
- Backup artifacts are generated on schedule.
- Application builds and queries execute without errors.
Verification is a contract with future incident response. If checks are skipped, downtime risk increases. For stronger operational maturity, keep a short acceptance template with timestamp, environment, operator, and outcome stored in your incident-management system.
Common issues and fixes
Container exits on startup
Check environment variable syntax, database connectivity, and filesystem permissions on mounted data paths. Review logs with docker compose logs tooljet for explicit connection errors.
Database connection errors
Ensure PostgreSQL is healthy before ToolJet starts. The Compose file uses depends_on with health conditions, but verify PG_HOST matches the service name postgres.
Wrong public URL or callback errors
Set TOOLJET_HOST to the public HTTPS address and confirm Caddy forwards the correct X-Forwarded-Proto header.
Slow queries or timeouts
Check PostgreSQL connection pool limits, add appropriate indexes for large tables, and verify Redis is reachable for caching. Scale the host before tuning application concurrency.
Missing assets or file uploads after restart
Confirm the bind-mounted volume path exists and is writable by the container user. Reapply ownership if the host user changes.
Service not available after reboot
Enable Docker service startup and confirm that Compose containers use restart: unless-stopped. Validate that Caddy is running before users arrive.
FAQ
Is ToolJet safe for production internal tools?
Yes, when you add the operational layers shown in this guide: TLS, secrets management, backups, health checks, and access controls.
How does ToolJet compare to Retool or Budibase?
ToolJet is open-source and self-hostable, with a similar visual builder. The deployment model and data sovereignty are often the deciding factors for teams with compliance requirements.
Should the database be on the same host?
For small teams, co-locating PostgreSQL and Redis is practical and maintainable. As scale or compliance requirements grow, migrate the database to a managed service.
What is a safe upgrade process?
Pin versions, back up first, test in staging, and verify the health endpoint and a sample application before inviting users back.
Can we start with SQLite and migrate later?
ToolJet requires PostgreSQL for production features. Start with PostgreSQL from day one to avoid migration complexity.
How should we handle secret rotation?
Rotate database and application secrets during planned maintenance windows. Update .env.prod, restart containers, and verify connectivity before closing the change ticket.
What should the handoff include?
Runbook commands, acceptance checks, rollback steps, escalation contacts, and a link to the internal-tools ownership policy.
Internal links
- https://sysbrix.com/blog/guides-3/production-guide-deploy-baserow-with-docker-compose-caddy-postgresql-redis-on-ubuntu-552
- https://sysbrix.com/blog/guides-3/production-guide-deploy-directus-with-docker-compose-caddy-postgresql-redis-on-ubuntu-558
- https://sysbrix.com/blog/guides-3/production-guide-deploy-plane-with-docker-compose-caddy-postgresql-redis-minio-on-ubuntu-569
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.
Header image: Unsplash, no watermark.