-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add Docker Compose production deployment with auto-generated secrets #2750
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| # Fizzy Docker Compose Configuration | ||
| # Copy this file to .env and fill in the values for your deployment. | ||
| # | ||
| # NOTE: SECRET_KEY_BASE, VAPID_PUBLIC_KEY, and VAPID_PRIVATE_KEY are | ||
| # automatically generated on first boot and stored in the fizzy_secrets volume. | ||
| # Do NOT set them here — the entrypoint sources the secrets file on every boot, | ||
| # which will overwrite any values set in this file. | ||
|
|
||
| # ============================================================================= | ||
| # REQUIRED | ||
| # ============================================================================= | ||
|
|
||
| # Public URL of your Fizzy instance (used in email links, webhooks, etc.) | ||
| # Include the scheme and host; no trailing slash. | ||
| BASE_URL=https://fizzy.example.com | ||
|
|
||
| # ============================================================================= | ||
| # DOCKER IMAGE | ||
| # ============================================================================= | ||
|
|
||
| # Override the GHCR image repo (default: basecamp/fizzy) | ||
| # FIZZY_IMAGE_REPO=basecamp/fizzy | ||
|
|
||
| # Image tag to deploy (default: latest) | ||
| # FIZZY_IMAGE_TAG=latest | ||
|
|
||
| # Host port to expose Fizzy on (default: 3006) | ||
| # In production, this port should be firewalled; only your reverse proxy should reach it. | ||
| # FIZZY_PORT=3006 | ||
|
|
||
| # ============================================================================= | ||
| # MULTI-TENANCY | ||
| # ============================================================================= | ||
|
|
||
| # Set to true to allow anyone to create a new account (sign-up enabled). | ||
| # Set to false (default) for a single-account / invite-only deployment. | ||
| MULTI_TENANT=false | ||
|
|
||
| # ============================================================================= | ||
| # EMAIL (SMTP) | ||
| # ============================================================================= | ||
|
|
||
| # If SMTP_ADDRESS is not set, email delivery is disabled entirely. | ||
| # Required for magic-link login and notification emails. | ||
|
|
||
| # SMTP_ADDRESS=smtp.example.com | ||
| # SMTP_PORT=587 | ||
| # SMTP_DOMAIN=example.com | ||
| # SMTP_USERNAME=user@example.com | ||
| # SMTP_PASSWORD=secret | ||
| # SMTP_AUTHENTICATION=plain | ||
| # SMTP_TLS=false | ||
| # SMTP_SSL_VERIFY_MODE= | ||
|
|
||
| # From address for outgoing emails | ||
| # MAILER_FROM_ADDRESS=fizzy@example.com | ||
|
|
||
| # ============================================================================= | ||
| # LOGGING | ||
| # ============================================================================= | ||
|
|
||
| # Log level: debug, info, warn, error, fatal (default: info) | ||
| # RAILS_LOG_LEVEL=info | ||
|
|
||
| # ============================================================================= | ||
| # PERFORMANCE TUNING | ||
| # ============================================================================= | ||
|
|
||
| # Number of Puma web worker processes (default: physical CPU count) | ||
| # WEB_CONCURRENCY=2 | ||
|
|
||
| # Number of Solid Queue job worker processes (default: physical CPU count) | ||
| # JOB_CONCURRENCY=1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,34 @@ | ||
| #!/bin/bash -e | ||
|
|
||
| SECRETS_FILE="/rails/secrets/secrets.env" | ||
| mkdir -p "$(dirname "$SECRETS_FILE")" | ||
|
|
||
| # On first boot: generate SECRET_KEY_BASE and VAPID keys, persist to secrets file | ||
| if [ ! -f "$SECRETS_FILE" ]; then | ||
| echo "==> First boot detected: generating SECRET_KEY_BASE and VAPID keys..." | ||
|
|
||
| SECRET_KEY_BASE=$(bundle exec rails secret) || { echo "ERROR: Failed to generate SECRET_KEY_BASE"; exit 1; } | ||
|
|
||
| VAPID_KEYS=$(bundle exec ruby -e "require 'web-push'; vapid = WebPush.generate_key; puts 'VAPID_PUBLIC_KEY=' + vapid.public_key; puts 'VAPID_PRIVATE_KEY=' + vapid.private_key") || { echo "ERROR: Failed to generate VAPID keys"; exit 1; } | ||
|
|
||
| TMPFILE=$(mktemp) | ||
| { | ||
| echo "SECRET_KEY_BASE=${SECRET_KEY_BASE}" | ||
| echo "${VAPID_KEYS}" | ||
| } > "$TMPFILE" && mv "$TMPFILE" "$SECRETS_FILE" || { echo "ERROR: Failed to write secrets file"; rm -f "$TMPFILE"; exit 1; } | ||
|
|
||
| echo "==> Secrets written to ${SECRETS_FILE}. Back up the fizzy_secrets volume to preserve these." | ||
| fi | ||
|
|
||
| # On every boot: load the secrets file so all vars are exported into the environment | ||
| set -a | ||
| # shellcheck source=/dev/null | ||
| source "$SECRETS_FILE" | ||
| set +a | ||
|
|
||
| # If running the rails server then create or migrate existing database | ||
| if [ "${1}" == "./bin/thrust" ] && [ "${2}" == "./bin/rails" ] && [ "${3}" == "server" ]; then | ||
| MIGRATE=1 ./bin/rails db:prepare | ||
| fi | ||
|
|
||
| exec "${@}" | ||
| exec "$@" |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,58 @@ | ||||||||||||||
| # docker-compose.yml | ||||||||||||||
| # Production deployment for Fizzy (SQLite + Solid Queue embedded in Puma) | ||||||||||||||
| # | ||||||||||||||
| # The first boot auto-generates SECRET_KEY_BASE and VAPID keys and writes them | ||||||||||||||
| # to the fizzy_secrets volume. Keep that volume backed up. | ||||||||||||||
|
|
||||||||||||||
| services: | ||||||||||||||
| fizzy: | ||||||||||||||
| image: ghcr.io/${FIZZY_IMAGE_REPO:-basecamp/fizzy}:${FIZZY_IMAGE_TAG:-latest} | ||||||||||||||
| # To use a locally built image instead, comment out 'image:' above and uncomment: | ||||||||||||||
| # build: . | ||||||||||||||
| restart: unless-stopped | ||||||||||||||
| ports: | ||||||||||||||
| - "${FIZZY_PORT:-3006}:80" | ||||||||||||||
| volumes: | ||||||||||||||
| # SQLite databases and Active Storage file uploads | ||||||||||||||
| - fizzy_storage:/rails/storage | ||||||||||||||
| # Generated secrets.env (SECRET_KEY_BASE, VAPID keys) live here | ||||||||||||||
| # Uses a dedicated /rails/secrets path to avoid overwriting baked-in config files | ||||||||||||||
| # IMPORTANT: back up this volume — losing it invalidates all sessions and web push subscriptions | ||||||||||||||
| - fizzy_secrets:/rails/secrets | ||||||||||||||
| environment: | ||||||||||||||
| # .env.example has some examples for what needs to be in place for this. | ||||||||||||||
| - RAILS_ENV=$RAILS_ENV | ||||||||||||||
|
||||||||||||||
| - RAILS_ENV=$RAILS_ENV | |
| - RAILS_ENV=${RAILS_ENV:-production} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could, but that should be part of your environment. So I don't see a need to update it. Up to ya'll if you want me to add this.
Copilot
AI
Mar 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FIZZY_HOST is passed into the container here, but it doesn’t appear to be referenced anywhere in the codebase. Keeping unused operator-facing config increases confusion; consider removing it from compose (and .env.example if added later) or wiring it to an actual setting.
| - FIZZY_HOST=$FIZZY_HOST |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fair. I left it because others had it. I am happy to remove it for whoever reviews this, as needed.
Copilot
AI
Mar 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.env.example documents SMTP_SSL_VERIFY_MODE, and production.rb reads it, but docker-compose.yml never passes it into the container. As a result operators can’t configure SSL verification mode when using compose; add it to the environment: list (or switch to env_file:).
| - SMTP_TLS=$SMTP_TLS | |
| - SMTP_TLS=$SMTP_TLS | |
| - SMTP_SSL_VERIFY_MODE=$SMTP_SSL_VERIFY_MODE |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair. I didn't add it to mine, because I didn't need it, and I don't think most people will need it. I left it in as part of the examples though.
Copilot
AI
Mar 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WEB_CONCURRENCY, JOB_CONCURRENCY, and MULTI_TENANT are hard-coded here, which prevents operators from changing them via .env (and MULTI_TENANT=true conflicts with .env.example/config/deploy.yml defaulting it to false). Prefer wiring these from env with sensible defaults so compose behaves as documented.
| - WEB_CONCURRENCY=2 | |
| - JOB_CONCURRENCY=1 | |
| - MULTI_TENANT=true | |
| - WEB_CONCURRENCY=${WEB_CONCURRENCY:-2} | |
| - JOB_CONCURRENCY=${JOB_CONCURRENCY:-1} | |
| - MULTI_TENANT=${MULTI_TENANT:-false} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, they could be moved to the .env.example file, but I didn't see any good reason to do so. So I hardcoded them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is an interesting discussion why one might want to change them: #2350 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mounting a named volume at
/rails/secretscan be unwritable for the non-rootrailsuser if the mountpoint directory doesn’t exist in the image at build time (Docker will create it as root-owned). Ensure/rails/secretsexists and is owned by uid 1000 in the image so the first-boot secret generation can writesecrets.env.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the only got'ja that is left in the build. You have to chown the mount volume once they're created. But I'm used to that with self hosting. Could just switch it to use root, instead. Personally, I'd just leave it. YMMV.