Skip to content

Add Docker Compose production deployment with auto-generated secrets#2750

Open
crueber wants to merge 2 commits intobasecamp:mainfrom
crueber:feature/docker-compose-deployment
Open

Add Docker Compose production deployment with auto-generated secrets#2750
crueber wants to merge 2 commits intobasecamp:mainfrom
crueber:feature/docker-compose-deployment

Conversation

@crueber
Copy link
Copy Markdown

@crueber crueber commented Mar 24, 2026

What this adds

A self-contained Docker Compose production deployment for self-hosters. Three files:

  • bin/docker-entrypoint — extended to auto-generate and persist secrets on first boot
  • docker-compose.yml — wires the image, named volumes, ports, and environment
  • .env.example — documents every operator-facing configuration variable

Why the existing entrypoint wasn't enough

The upstream entrypoint only ran db:prepare. It had no mechanism to supply SECRET_KEY_BASE or VAPID keys, so deploying with docker compose up would immediately crash:

ArgumentError: Missing `secret_key_base` for 'production' environment

Rails requires SECRET_KEY_BASE to be present and stable — it's used to sign all session cookies and encrypted values. If it changes between restarts, every logged-in user is silently signed out. If it's absent entirely, the app refuses to boot.

VAPID keys (VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY) have the same stability requirement: they're the cryptographic identity used to authenticate web push subscriptions. If the keys change, all existing push subscriptions break silently and users stop receiving notifications.

Telling operators to generate these manually (e.g. with rails secret) and paste them into an .env file puts the burden in the wrong place and is easy to get wrong — especially the VAPID key pair, which requires the web-push gem and a specific format.

How the new entrypoint solves it

On first boot (when /rails/secrets/secrets.env does not exist):

  1. Generates SECRET_KEY_BASE via bundle exec rails secret
  2. Generates a VAPID key pair via bundle exec ruby -e "require 'web-push'; ..."
  3. Writes both to /rails/secrets/secrets.env atomically (temp file + mv) to prevent partial writes
  4. Emits a clear log message reminding operators to back up the volume

On every subsequent boot: sources the existing file so Rails receives the same stable values.

The secrets file lives on a dedicated fizzy_secrets named volume (mounted at /rails/secrets) — separate from /rails/storage (databases + uploads) so the two backup concerns are cleanly separated.

docker-compose.yml highlights

  • fizzy_storage/rails/storage — all SQLite databases and Active Storage uploads
  • fizzy_secrets/rails/secrets — the generated secrets.env
  • DISABLE_SSL=true — suppresses Rails' assume_ssl / force_ssl so the app plays nicely behind an external reverse proxy (Traefik, Caddy, Nginx, etc.) without redirect loops
  • SOLID_QUEUE_IN_PUMA=true — no separate worker container needed
  • ACTIVE_STORAGE_SERVICE=local — uploads go into the storage volume
  • DATABASE_ADAPTER=sqlite — no external database required
  • Health check on /up with a 60-second start period to accommodate first-boot secret generation and db:prepare

Quick start

cp .env.example .env      # set BASE_URL and optionally SMTP_* vars
docker compose up -d

That's it. No rails secret, no manual VAPID generation, no credentials file.

crueber added 2 commits March 24, 2026 08:39
- Extend bin/docker-entrypoint to auto-generate and persist SECRET_KEY_BASE
  and VAPID keys on first boot into a dedicated fizzy_secrets volume. On every
  subsequent boot the secrets file is sourced so Rails receives the same stable
  values — no manual key generation step required.
- Add docker-compose.yml with named volumes (fizzy_storage for SQLite databases
  and Active Storage uploads, fizzy_secrets for the generated secrets file),
  a health check, and all required environment variables pre-configured.
- Add .env.example documenting every operator-facing variable.
- Add !/.env.example exception to .gitignore so the template is tracked.

The upstream entrypoint only ran db:prepare; it had no mechanism to supply
SECRET_KEY_BASE or VAPID keys, so the app would crash on first boot with a
missing secret_key_base error and web push would be non-functional. The new
entrypoint generates these values once, writes them atomically to a mounted
volume, and sources them on every subsequent start.
Copilot AI review requested due to automatic review settings March 24, 2026 19:11
Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a self-contained, Docker Compose–based production deployment path for Fizzy, including first-boot secret generation/persistence so the app can start reliably without manual operator key creation.

Changes:

  • Add docker-compose.yml for a single-container production deployment (SQLite + Solid Queue in Puma) with persistent storage and healthcheck.
  • Extend bin/docker-entrypoint to generate and persist SECRET_KEY_BASE and VAPID keys on first boot, then load them on every boot.
  • Add .env.example and update .gitignore so operators have a complete configuration template.

Reviewed changes

Copilot reviewed 2 out of 4 changed files in this pull request and generated 5 comments.

File Description
docker-compose.yml Defines the production Compose service, volumes, environment wiring, and healthcheck.
bin/docker-entrypoint Implements first-boot secret generation + persistent secrets sourcing on startup.
.env.example Documents operator-facing configuration variables for Compose deployments.
.gitignore Ensures .env.example is committed while keeping real .env* files ignored; adds worktree ignore.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- fizzy_secrets:/rails/secrets
environment:
# .env.example has some examples for what needs to be in place for this.
- RAILS_ENV=$RAILS_ENV
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RAILS_ENV=$RAILS_ENV will override the image’s RAILS_ENV=production with an empty string when RAILS_ENV isn’t set in the host/.env, which can cause Rails to boot in the wrong environment. Consider removing this line (let the image default stand) or using a default like production in the compose interpolation.

Suggested change
- RAILS_ENV=$RAILS_ENV
- RAILS_ENV=${RAILS_ENV:-production}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

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.

environment:
# .env.example has some examples for what needs to be in place for this.
- RAILS_ENV=$RAILS_ENV
- FIZZY_HOST=$FIZZY_HOST
Copy link

Copilot AI Mar 24, 2026

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.

Suggested change
- FIZZY_HOST=$FIZZY_HOST

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

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.

- SMTP_USERNAME=$SMTP_USERNAME
- SMTP_PASSWORD=$SMTP_PASSWORD
- SMTP_AUTHENTICATION=$SMTP_AUTHENTICATION
- SMTP_TLS=$SMTP_TLS
Copy link

Copilot AI Mar 24, 2026

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

Suggested change
- SMTP_TLS=$SMTP_TLS
- SMTP_TLS=$SMTP_TLS
- SMTP_SSL_VERIFY_MODE=$SMTP_SSL_VERIFY_MODE

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

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.

Comment on lines +36 to +38
- WEB_CONCURRENCY=2
- JOB_CONCURRENCY=1
- MULTI_TENANT=true
Copy link

Copilot AI Mar 24, 2026

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.

Suggested change
- WEB_CONCURRENCY=2
- JOB_CONCURRENCY=1
- MULTI_TENANT=true
- WEB_CONCURRENCY=${WEB_CONCURRENCY:-2}
- JOB_CONCURRENCY=${JOB_CONCURRENCY:-1}
- MULTI_TENANT=${MULTI_TENANT:-false}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

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.

Copy link
Copy Markdown

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)

Comment on lines +18 to +22
# 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:
Copy link

Copilot AI Mar 24, 2026

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/secrets can be unwritable for the non-root rails user if the mountpoint directory doesn’t exist in the image at build time (Docker will create it as root-owned). Ensure /rails/secrets exists and is owned by uid 1000 in the image so the first-boot secret generation can write secrets.env.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

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.

@david-uhlig
Copy link
Copy Markdown

Good idea, just a heads-up: The recommended name for the Compose file is now compose.yaml:

The default path for a Compose file is compose.yaml (preferred) or compose.yml that is placed in the working directory. Compose also supports docker-compose.yaml and docker-compose.yml for backwards compatibility of earlier versions. If both files exist, Compose prefers the canonical compose.yaml.

https://docs.docker.com/compose/intro/compose-application-model/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants