Database schema changes are one of the highest-risk operations in production engineering. A missed index, an accidental drop, or an incompatible migration can degrade performance or cause outages that take hours to recover. Bytebase is an open-source database DevOps platform that brings version control, review workflows, and automated safety checks to schema changes across PostgreSQL, MySQL, and other engines. It acts as a control plane between developers and production databases, enforcing policies like required approvals, SQL linting, and rollback plans before any change is applied.
This guide deploys Bytebase on Ubuntu with Docker Compose, Caddy, and PostgreSQL. Caddy handles automatic HTTPS and serves the web UI. PostgreSQL stores Bytebase metadata, users, projects, and migration history. Docker Compose keeps the stack reproducible, and a systemd unit ensures it starts on boot. The result is a production-ready database change management platform that reduces deployment risk and gives platform teams full audit visibility into who changed what, when, and why.
Architecture and flow overview
Public users and database administrators connect to https://bytebase.example.com. Caddy terminates TLS, enforces security headers, and proxies traffic to the Bytebase container running on 127.0.0.1:8080. Bytebase itself is a Go binary running inside a container that connects to its own PostgreSQL database for metadata and to external database instances for schema inspection and change execution.
The architecture is intentionally minimal. Bytebase is stateless except for its PostgreSQL backend, so upgrades are simple: back up the metadata database, pull the new image, and recreate the container. Caddy binds to public ports 80 and 443, while Bytebase and PostgreSQL bind only to loopback or internal Docker networks. UFW drops everything except SSH, HTTP, and HTTPS. Backups target the PostgreSQL data directory and can be scheduled with a small shell script.
- Edge: Caddy with automatic TLS, rate limiting, and security headers
- App: Bytebase container with health checks and environment-driven configuration
- State: PostgreSQL with persistent volume, daily logical backups, and connection limits
- Ops: Docker Compose project managed by a systemd unit with restart policy
- Safety: Secrets in
.envwith mode600, dedicated Linux user, and firewall rules
Prerequisites
- Ubuntu 22.04 or 24.04 LTS server with root or sudo access
- A DNS A record such as
bytebase.example.compointing to the server IP - At least 2 vCPU, 4 GB RAM, and 30 GB disk for OS, containers, and backups
- Docker Engine 24.x+ and Docker Compose plugin installed
- Ports 22, 80, and 443 reachable; port 443 must be open for ACME challenges
Step-by-step deployment
1) Create project directories and a dedicated user
Isolate the Bytebase deployment under /opt/bytebase and run it as a non-root user so file permissions are predictable and container escapes are less impactful.
sudo mkdir -p /opt/bytebase/{data,backups}
sudo useradd -r -s /usr/sbin/nologin -d /opt/bytebase bytebase || true
sudo chown -R bytebase:bytebase /opt/bytebase
sudo chmod 750 /opt/bytebase
sudo chmod 700 /opt/bytebase/backups
2) Write the environment file
Bytebase accepts several environment variables. The most important are the external URL, the metadata database connection string, and the initial admin credentials. Generate strong random values for secrets and never commit the file to version control.
cat > /opt/bytebase/.env << 'EOF'
# Bytebase core
BB_EXTERNAL_URL=https://bytebase.example.com
BB_PORT=8080
# Metadata database
PG_HOST=postgres
PG_PORT=5432
PG_DATABASE=bytebase
PG_USER=bytebase
PG_PASSWORD=$(openssl rand -base64 24)
# Initial admin
[email protected]
BB_ADMIN_PASSWORD=$(openssl rand -base64 24)
EOF
After creation, restrict the file so only the owner can read it.
chmod 600 /opt/bytebase/.env
chown bytebase:bytebase /opt/bytebase/.env
3) Create the Docker Compose file
The Compose file defines PostgreSQL and Bytebase services. PostgreSQL uses a named volume mapped to /opt/bytebase/data so host backups can snapshot it directly. Bytebase depends on the database being healthy before it starts.
cat > /opt/bytebase/docker-compose.yml << 'EOF'
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: bytebase
POSTGRES_USER: bytebase
POSTGRES_PASSWORD: ${PG_PASSWORD}
volumes:
- /opt/bytebase/data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U bytebase -d bytebase"]
interval: 10s
timeout: 5s
retries: 5
bytebase:
image: bytebase/bytebase:latest
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
environment:
- EXTERNAL_URL=${BB_EXTERNAL_URL}
- PG_URL=postgresql://${PG_USER}:${PG_PASSWORD}@postgres:${PG_PORT}/${PG_DATABASE}?sslmode=disable
env_file:
- .env
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
interval: 30s
timeout: 10s
retries: 3
volumes:
postgres:
EOF
Notice that Bytebase binds only to 127.0.0.1:8080. Caddy will proxy to this loopback port, so the application is never exposed directly to the public internet.
4) Configure Caddy as the reverse proxy
Caddy handles HTTPS automatically through ACME and proxies API and admin traffic to the Bytebase container. Create a Caddyfile in the project directory and keep the email directive for certificate expiry notices.
cat > /opt/bytebase/Caddyfile << 'EOF'
{
email [email protected]
}
bytebase.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:8080 {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-Proto {scheme}
}
header {
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
Referrer-Policy strict-origin-when-cross-origin
}
}
EOF
5) Create a systemd unit for Docker Compose
A systemd unit ensures the stack starts on boot and restarts after crashes or host reboots. The unit changes into the project directory and runs docker compose up with the --wait flag so systemd knows when all services are healthy.
cat > /etc/systemd/system/bytebase-compose.service << 'EOF'
[Unit]
Description=Bytebase Docker Compose Stack
Requires=docker.service
After=docker.service network.target
[Service]
Type=simple
WorkingDirectory=/opt/bytebase
ExecStart=/usr/bin/docker compose up --wait
ExecStop=/usr/bin/docker compose down
Restart=on-failure
RestartSec=10
User=bytebase
Group=bytebase
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable bytebase-compose.service
6) Launch the stack and verify health
Pull images, start services, and watch the healthchecks pass. The first boot may take a minute while Bytebase initializes the metadata schema and creates the first admin user from the environment file.
cd /opt/bytebase
sudo docker compose pull
sudo systemctl start bytebase-compose.service
sleep 15
sudo docker compose ps
sudo docker compose logs --tail=60 bytebase
7) Open the firewall
Allow SSH, HTTP, and HTTPS. Because Bytebase listens only on localhost, you do not need to expose port 8080 externally.
sudo ufw default deny incoming
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
Configuration and secrets handling best practices
After the web interface loads, log in with the BB_ADMIN_EMAIL and BB_ADMIN_PASSWORD values from .env. Immediately create a dedicated role for database administrators and enable SSO if your organization supports it. Avoid sharing the admin account across the team.
Rotate secrets by updating .env, running docker compose up -d to recreate the Bytebase container, and updating any connected database instances in the admin panel. For higher assurance, consider an external vault that injects secrets through a sidecar. Never store PG_PASSWORD or the admin password in Git repositories or shared documentation wikis.
Backups should include the PostgreSQL database that stores Bytebase metadata. Test restores quarterly on a separate host. Untested backups are optimistic log entries, not recovery guarantees.
cat > /opt/bytebase/backup.sh << 'EOF'
#!/usr/bin/env bash
set -euo pipefail
stamp=$(date -u +%Y%m%dT%H%M%SZ)
mkdir -p /opt/bytebase/backups/$stamp
cd /opt/bytebase
docker compose exec -T postgres pg_dump -U bytebase bytebase > backups/$stamp/bytebase.sql
find backups -mindepth 1 -maxdepth 1 -type d -mtime +14 -exec rm -rf {} +
EOF
chmod +x /opt/bytebase/backup.sh
chown bytebase:bytebase /opt/bytebase/backup.sh
Verification checklist
- Open
https://bytebase.example.comand confirm the certificate is valid - Log in to the admin panel with the credentials from
.env - Create a test project and add a PostgreSQL instance using a read-only or limited-privilege user
- Run a schema sync and confirm tables and indexes are visible in the UI
- Create a test issue requesting a schema change and verify the review workflow triggers
- Check
docker compose psshows all services healthy - Run
systemctl status bytebase-composeand confirm active with no rapid restarts - Execute the backup script and verify the resulting SQL file is non-empty
curl -I https://bytebase.example.com
sudo docker compose ps
sudo systemctl status bytebase-compose.service
sudo ./backup.sh
ls -lah /opt/bytebase/backups/*/
Common issues and fixes
Caddy cannot obtain a certificate
Verify the DNS A record resolves to the server and that ports 80 and 443 are reachable from the internet. Cloud firewalls and stale AAAA records are common causes of ACME failure.
Bytebase fails to start with a database connection error
Ensure the PG_PASSWORD in .env matches the POSTGRES_PASSWORD passed to the Postgres container. Recreate both services with docker compose up -d --force-recreate after fixing the mismatch.
Schema sync shows no tables for an external database
The database user needs permission to read information_schema and pg_catalog. Grant CONNECT and USAGE on the target database and schema, and SELECT on relevant tables, then retry the sync.
Large SQL review diffs time out
Increase the statement timeout in the Bytebase environment settings or break large migrations into smaller batches. For very large tables, consider online schema change tools that Bytebase supports, such as gh-ost or pg-online-schema-change.
Backups grow too large
Archive old backup directories to cold storage and reduce the retention window. For very large metadata databases, switch from pg_dump to incremental physical backups with pg_basebackup or a tool like pgBackRest.
FAQ
Can Bytebase manage multiple database engines at once?
Yes. Bytebase supports PostgreSQL, MySQL, TiDB, Spanner, Snowflake, Oracle, SQL Server, and MongoDB. You can add multiple instances across engines into a single project and enforce consistent review policies.
Should I give Bytebase admin privileges on production databases?
No. Create a dedicated user with the minimum privileges required for schema inspection and change execution. Many teams use a read-only user for initial sync and a separate limited-privilege user for applying approved migrations.
How do I upgrade Bytebase safely?
Back up the metadata database, read the release notes for breaking changes, update the image tag in the Compose file, and run docker compose pull && docker compose up -d. Verify the healthcheck passes and test a schema sync before declaring the upgrade complete.
Can I use an existing PostgreSQL server instead of the container?
Yes. Update PG_URL to point to your external server, remove the Postgres service from Compose, and restart. Ensure network latency between Bytebase and the database stays below 10 milliseconds for acceptable UI performance.
What is the difference between an issue and a migration in Bytebase?
An issue is a request for change that includes SQL statements, context, and approvals. A migration is the execution of an approved issue against a target database. Issues are tracked in the project history, while migrations are recorded in the database change log for audit and rollback purposes.
How should I monitor the stack?
Watch container restart counts, disk usage on /opt/bytebase/data, PostgreSQL connection pool saturation, and API response times. Alert on backup age and certificate expiry so operational issues are caught before they impact users.
Internal links
- Production Guide: Deploy n8n with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu
- Production Guide: Deploy Grist with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu
- Production Guide: Deploy Infisical with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.
Header image: Original SysBrix generated header, no watermark.