Skip to content

Latest commit

 

History

History
258 lines (183 loc) · 8.81 KB

File metadata and controls

258 lines (183 loc) · 8.81 KB
title Deploy with Docker
description Deploy Quackback with Docker and Docker Compose.
icon docker

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 -d

Open 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:latest
If 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 quackback user for security
  • Entrypoint (docker-entrypoint.sh) runs database migrations automatically on every startup, then starts the server
  • Optional seeding with SEED_DATABASE=true to 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=latest

Email

Without 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.):

  1. Create a PostgreSQL 18+ database
  2. Enable the pgvector extension (and pg_cron if you want scheduled analytics refreshes)
  3. Point DATABASE_URL at 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.mjs

Reverse 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 -d

Drizzle 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 -d

Health Checks

# Application health
curl http://localhost:3000/api/health

Troubleshooting

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 app

ERR_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

  1. Verify PostgreSQL is healthy:
    docker compose -f docker-compose.prod.yml ps
  2. 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=8080

Security

Follow these practices for production deployments.
  1. Use strong secrets — generate with openssl rand -base64 32
  2. Enable HTTPS — always use TLS in production (via a reverse proxy)
  3. Don't expose datastores — the prod compose keeps Postgres/Dragonfly/MinIO off public ports; keep it that way
  4. Pin the image — set QUACKBACK_TAG to a specific version rather than latest
  5. Back up regularly — automate database (and object-storage) backups
  6. Keep updated — pull latest images regularly

Next Steps