Deploy with Docker
The fastest way to get Quackback into production. Docker handles dependencies, database setup, and migrations automatically.
Prerequisites
- Docker Engine 20.10+
- Docker Compose v2+
- 1 GB RAM minimum (2 GB recommended)
- PostgreSQL 18+ (included in the compose stack, or bring your own)
The published image is multi-arch (linux/amd64 + linux/arm64), so it runs natively on Apple Silicon and ARM servers (AWS Graviton, Ampere, Raspberry Pi).
Quick Deploy
Use Docker Compose (recommended)
docker-compose.prod.yml is a self-contained, single-host stack: the Quackback app plus PostgreSQL, Dragonfly (Redis), and MinIO (S3-compatible storage). The app is the only service with a published port; the datastores stay on the internal network and the upload bucket is private.
# Clone the repository (the Postgres image is built locally for pg_cron + pgvector)
git clone https://github.com/QuackbackIO/quackback.git
cd quackback
# Create your environment file and fill in every value
cp .env.prod.example .env
# Generate secrets with: openssl rand -base64 32
# Start the stack
docker compose -f docker-compose.prod.yml up -dOpen http://localhost:3000 (or your BASE_URL behind a reverse proxy).
The repository's root docker-compose.yml is for local development only — it starts the datastores (with insecure defaults and a world-readable bucket) but no app service. Use docker-compose.prod.yml for self-hosting.
Use Docker Run (bring your own datastores)
If you already run managed PostgreSQL, Redis, and S3, run the app container on its own:
docker run -d \
--name quackback \
-p 3000:3000 \
-e DATABASE_URL="postgresql://user:pass@host:5432/quackback" \
-e REDIS_URL="redis://your-redis-host:6379" \
-e SECRET_KEY="your-32-char-secret" \
-e BASE_URL="https://feedback.yourcompany.com" \
ghcr.io/quackbackio/quackback:latestIf you pass configuration with --env-file instead of -e, do not wrap values in quotes and do not add inline # comments on a value line. Docker's --env-file reads everything after = literally, so DATABASE_URL="postgres://..." is parsed with the quotes attached and fails with ERR_INVALID_URL.
How the Docker Image Works
The Quackback Docker image uses a multi-stage build:
- Builder stage compiles the application and installs dependencies
- Production stage runs as a non-root
quackbackuser for security - Entrypoint (
docker-entrypoint.sh) runs database migrations automatically on every startup, then starts the server - Optional seeding with
SEED_DATABASE=trueto populate demo data - Health endpoint at
/api/health
Because migrations run on every startup, upgrading is as simple as pulling the latest image and restarting. No manual migration step needed. (Set SKIP_MIGRATIONS=true if you run migrations out-of-band, e.g. a Kubernetes pre-upgrade hook.)
Configuration
With the compose stack you set everything in .env (copied from .env.prod.example). The compose file derives the internal connection strings (DATABASE_URL, REDIS_URL, S3_*) from your secrets and wires them to the bundled services automatically — so you only provide the secrets, not the URLs.
Required
# Public URL (must match your domain)
BASE_URL=https://feedback.yourcompany.com
# Auth/encryption key — generate with: openssl rand -base64 32
SECRET_KEY=
# Bundled PostgreSQL
POSTGRES_USER=quackback
POSTGRES_PASSWORD=
POSTGRES_DB=quackback
# Bundled MinIO (also used as the S3 access key / secret)
MINIO_ROOT_USER=quackback
MINIO_ROOT_PASSWORD=
S3_BUCKET=quackback
S3_REGION=us-east-1
# Image tag to run (pin to a release in production)
QUACKBACK_TAG=latestWithout email configuration, OTP login codes are printed to the container logs. For production, configure one of the providers below.
SMTP (recommended)
EMAIL_SMTP_HOST=smtp.example.com
EMAIL_SMTP_PORT=587
EMAIL_SMTP_USER=your-username
EMAIL_SMTP_PASS=your-password
EMAIL_FROM=Quackback <feedback@yourcompany.com>Resend
EMAIL_RESEND_API_KEY=re_xxxxxxxxxxxx
EMAIL_FROM=Quackback <feedback@yourcompany.com>OAuth providers & integrations (optional)
OAuth providers (GitHub, Google, Discord, etc.) and integrations (Slack, etc.) are configured in the admin UI under Settings → Authentication / Integrations. See the Environment Variables Reference for the full list.
Object Storage
The bundled MinIO has no public port, so uploads are served through the app's /api/storage proxy (S3_PROXY=true is set for you in the compose file). To use an external S3 provider (AWS S3, Cloudflare R2, Backblaze B2) instead, point S3_* at your provider and remove the minio and minio-init services from the compose file.
Database
Use the bundled PostgreSQL
The compose stack builds a PostgreSQL 18 image with the required extensions (pgvector, pg_cron) and runs migrations automatically on startup.
Use an external database
For managed databases (AWS RDS, Supabase, Neon, etc.):
- Create a PostgreSQL 18+ database
- Enable the
pgvectorextension (andpg_cronif you want scheduled analytics refreshes) - Point
DATABASE_URLat it (via the Docker Run path, or by editing the compose file)
Migrations run automatically when the container starts. To run them manually:
docker run --rm \
-e DATABASE_URL="your-external-db-url" \
ghcr.io/quackbackio/quackback:latest \
bun /app/migrate.mjsReverse Proxy
In production, place a reverse proxy in front of Quackback for HTTPS. See the Reverse Proxy Configuration guide for Caddy, Nginx, and Traefik examples.
Upgrade
Always back up your database before upgrading.
# 1. Back up your database
docker compose -f docker-compose.prod.yml exec postgres \
pg_dump -Fc -U "$POSTGRES_USER" "$POSTGRES_DB" > backup-$(date +%Y%m%d).dump
# 2. Pull the latest source + image (bump QUACKBACK_TAG in .env to pin a version)
git pull
docker compose -f docker-compose.prod.yml pull
# 3. Restart (migrations run automatically)
docker compose -f docker-compose.prod.yml up -dDrizzle migrations are sequential and safe to re-run, so the container handles schema updates on startup without any manual steps.
Rollback
# Stop containers
docker compose -f docker-compose.prod.yml down
# Restore the backup
docker compose -f docker-compose.prod.yml up -d postgres
docker compose -f docker-compose.prod.yml exec -T postgres \
pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean < backup-YYYYMMDD.dump
# Start the previous image (set QUACKBACK_TAG to the old version in .env first)
docker compose -f docker-compose.prod.yml up -dHealth Checks
# Application health
curl http://localhost:3000/api/healthTroubleshooting
Containers start but the app isn't reachable
Make sure you used docker-compose.prod.yml. A bare docker compose up -d uses the root docker-compose.yml, which only starts the dev datastores and no app:
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs appERR_INVALID_URL on startup
A quoted value in an --env-file .env (e.g. DATABASE_URL="postgres://..."). Remove the quotes — Docker keeps them literally. See the Tip under Use Docker Run.
Database connection failed
- Verify PostgreSQL is healthy:
docker compose -f docker-compose.prod.yml ps - Test the connection:
docker compose -f docker-compose.prod.yml exec postgres psql -U "$POSTGRES_USER" -c "SELECT 1"
Port already in use
Change the published port via APP_PORT in your .env (the container always listens on 3000 internally):
APP_PORT=8080Security
Follow these practices for production deployments.
- Use strong secrets — generate with
openssl rand -base64 32 - Enable HTTPS — always use TLS in production (via a reverse proxy)
- Don't expose datastores — the prod compose keeps Postgres/Dragonfly/MinIO off public ports; keep it that way
- Pin the image — set
QUACKBACK_TAGto a specific version rather thanlatest - Back up regularly — automate database (and object-storage) backups
- Keep updated — pull latest images regularly
Next Steps
- Configuration Reference - All environment variables
- Reverse Proxy Setup - SSL/TLS and proxy configurations
- Troubleshooting - Common issues and solutions