LaunchKit
← All posts
· 22 min read · by The LaunchKit team · 1 views

Rails and Postgres in Docker Compose

Every Rails 8 application already contains a Dockerfile, and it is the wrong one. Point a compose.yaml at it, add a postgres service, run docker compose up, and the web container exits before it has opened a socket. The file is not broken; it was written for Kamal, and it says so in a comment most people scroll past.

Everything below ran on a scratch application generated with rails _8.1.3.1_ new blogdc --database=postgresql, against postgres:17.7-alpine, on Docker Desktop 4.37.2 (engine 27.4.0, compose v2.31.0-desktop.2) on an Apple M2 Max with 12 cores, macOS 26.5.1, with the Docker VM configured for 12 CPUs and 8092 MiB. Ruby is 4.0.5 on both sides. The host ports below (3411, 15433) are arbitrary; 3000 and 5432 were taken. Timing numbers were taken on a laptop under real load, and the load average is quoted where it matters.

What rails new generates is a production image

The generated Dockerfile opens with its own warning, on lines 4 and 8:

# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
# docker build -t blogdc .
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name blogdc blogdc

# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html

It means it. The base stage sets four environment variables that between them make the image useless for development: RAILS_ENV="production", BUNDLE_DEPLOYMENT="1", BUNDLE_WITHOUT="development" and BUNDLE_PATH="/usr/local/bundle". The build stage then runs COPY . ., so your source code is baked into a layer, and bin/rails assets:precompile with SECRET_KEY_BASE_DUMMY=1. What that image is good at, how its 565 MB break down and what it ships that you did not put there is the Rails Docker image; none of it is this.

Here is the whole naive attempt, two services and eleven lines:

services:
  db:
    image: postgres:17.7-alpine
    environment:
      POSTGRES_PASSWORD: secret
  web:
    build: .
    ports:
      - "3411:80"
    depends_on:
      - db

docker compose up --build -d builds cleanly, starts both containers, and the web one is gone ten seconds later:

NAME             IMAGE                  COMMAND                  SERVICE   CREATED          STATUS                      PORTS
dcpnaive-db-1    postgres:17.7-alpine   "docker-entrypoint.s…"   db        11 seconds ago   Up 11 seconds               5432/tcp
dcpnaive-web-1   dcpnaive-web           "/rails/bin/docker-e…"   web       11 seconds ago   Exited (1) 10 seconds ago   
web-1  | bin/rails aborted!
web-1  | ArgumentError: Missing `secret_key_base` for 'production' environment, set this string with `bin/rails credentials:edit` (ArgumentError)
web-1  | Tasks: TOP => db:prepare => db:load_config => environment

Setting RAILS_ENV=development on the service gets past that, and I expected it to fail on the missing development gems. It did not: BUNDLE_WITHOUT=development is baked into the image, so bundler excludes the group rather than looking for it, and ./bin/rails runner 'puts 1' printed 1. The image runs. It just holds a frozen copy of your code with no way to edit it, which is the one thing a development environment has to do.

The three files that work

Development needs its own Dockerfile, and it is shorter than the generated one because it does no asset compilation, no bootsnap precompile and no multi-stage copy:

# syntax=docker/dockerfile:1
ARG RUBY_VERSION=4.0.5
FROM docker.io/library/ruby:$RUBY_VERSION-slim

RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y \
      build-essential curl git libpq-dev libvips libyaml-dev pkg-config postgresql-client && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

WORKDIR /rails

ENV RAILS_ENV="development" \
    BUNDLE_PATH="/usr/local/bundle"

COPY Gemfile Gemfile.lock ./
RUN bundle install

ENTRYPOINT ["./bin/docker-entrypoint-dev"]
EXPOSE 3000
CMD ["./bin/rails", "server", "-b", "0.0.0.0"]

postgresql-client is there so psql works inside the web container, not for the adapter. Name it Dockerfile.dev and note that the generated .dockerignore ends with /Dockerfile*, so the file is excluded from the build context; that does not matter, because Docker reads the Dockerfile from the host rather than from the context.

The compose file, as it stands after everything further down this page:

name: blogdc

services:
  db:
    image: postgres:17.7-alpine
    environment:
      POSTGRES_USER: blogdc
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: blogdc_development
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "15433:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U blogdc -d blogdc_development -h 127.0.0.1"]
      interval: 1s
      timeout: 3s
      retries: 30

  web:
    build:
      context: .
      dockerfile: Dockerfile.dev
    command: ./bin/rails server -b 0.0.0.0
    environment:
      DATABASE_URL: postgres://blogdc:secret@db:5432/blogdc_development
    volumes:
      - .:/rails
      - bundle:/usr/local/bundle
      - tmp:/rails/tmp
    ports:
      - "3411:3000"
    depends_on:
      db:
        condition: service_healthy

volumes:
  pgdata:
  bundle:
  tmp:

And the entrypoint, which exists for two reasons that each get their own section below:

#!/bin/bash -e

# tmp/ outlives the container, whether it is a bind mount or a named volume,
# so a server.pid from a killed container is still there. It holds "1", PID 1
# exists in every container, and rackup's Process.kill(0, pid) reads that as
# "a server is already running".
rm -f ./tmp/pids/server.pid

# Prepare the database whenever this container's job is to serve requests,
# whatever flags follow "server". The generated bin/docker-entrypoint tests
# argument positions and stops matching the moment you add -b 0.0.0.0.
case " $* " in
  *" server "*|*" server") ./bin/rails db:prepare ;;
esac

exec "${@}"

From docker compose down -v to a 200 on http://localhost:3411/posts, with the images already built, took 6.5 seconds.

The bundle volume deserves one sentence of explanation, because it looks like a cache and is not. Mounting an empty named volume over a directory that exists in the image seeds the volume from the image; mounting a bind mount over it hides the image content entirely. So bundle:/usr/local/bundle starts out holding exactly the gems bundle install put there at build time, and survives docker compose down. That is also why adding a gem needs docker compose build web followed by docker compose down -v, or the stale volume keeps shadowing the new layer.

The flag that quietly turns off db:prepare

The generated bin/docker-entrypoint is five lines and one of them is a trap:

#!/bin/bash -e

# If running the rails server then create or migrate existing database
if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then
  ./bin/rails db:prepare
fi

exec "${@}"

The test is positional. It asks whether the second-to-last argument is the literal string ./bin/rails and the last one is server, which is true for exactly the generated CMD ["./bin/thrust", "./bin/rails", "server"] and false for almost everything a compose file puts there. Running that condition with four different argument lists, with db:prepare replaced by an echo:

WOULD RUN: ./bin/rails db:prepare
argv: ./bin/thrust ./bin/rails server
SKIPPED db:prepare
argv: ./bin/rails server -b 0.0.0.0
SKIPPED db:prepare
argv: ./bin/rails server -p 3000
SKIPPED db:prepare
argv: bin/rails server

Dropping the ./ is enough. So is -p 3000. And -b 0.0.0.0, which the next section shows you cannot leave out, is enough on its own.

That is not a shell curiosity, it is a silent behaviour change in a real container. Same image, same Postgres, two databases that do not yet exist, and the only difference is four characters at the end of the command:

--- A: command ends in -b 0.0.0.0 ---
=> Booting Puma
=> Rails 8.1.3.1 application starting in development 
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Listening on http://0.0.0.0:3000

--- B: command ends in server ---
Created database 'probe_b'
== 20260927080506 CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.0028s
== 20260927080506 CreatePosts: migrated (0.0028s) =============================

=> Booting Puma

Container A boots, publishes its port and answers the first request with ActiveRecord::PendingMigrationError (Migrations are pending...). Nothing in the startup log says anything is wrong. The fix in the entrypoint above is to match on server as a whole word wherever it appears rather than on its position. That has a cost and it is worth knowing which: the same four argument lists run against the new condition matched ./bin/rails server and ./bin/rails runner 'start server now', and did not match ./bin/rails server_probe or ./bin/rails db:seed. A runner script with the word server in its source prepares the database before it runs.

-b 0.0.0.0 is not decoration

Rails binds its development server to localhost, and inside a container localhost is the container. One line of railties-8.1.3.1, lib/rails/commands/server/server_command.rb:221:

default_host = environment == "development" ? "localhost" : "0.0.0.0"

There is no Docker detection in that method and no /.dockerenv check anywhere near it. Run the container with a plain ./bin/rails server and publish port 3000, and the log tells you exactly what happened while the port looks correct from the outside:

* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
$ curl -sS -m 5 -o /dev/null -w "http_code=%{http_code}\n" http://localhost:3412/posts
curl: (52) Empty reply from server
http_code=000

Exit code 52 rather than 7 is worth recognising, because it sends people looking in the wrong place. The connection was accepted: Docker's port forwarder is listening on the host and connects into the container's network namespace, where nothing is bound on 0.0.0.0:3000. The TCP handshake succeeds and the reply never comes. BINDING=0.0.0.0 in the environment does the same job as the flag, per the ENV.fetch("BINDING", default_host) on the line below, and has the advantage of not breaking the generated entrypoint's argument test: the same image run with -e BINDING=0.0.0.0 and the bare command ./bin/rails server logged * Listening on http://0.0.0.0:3000 and answered 200.

depends_on waits for the container, not for Postgres

The common advice is that depends_on: [db] starts the containers in order and waits for nothing, so your Rails container races Postgres and loses. On this machine it won, three cold starts out of three, which is why the failure is confusing when it finally arrives.

The numbers, from docker inspect and timestamped logs on a destroyed-and-recreated volume:

db-1  | 2026-09-27T08:15:46.428672887Z 2026-09-27 08:15:46.428 UTC [1] LOG:  database system is ready to accept connections
web-1 | 2026-09-27T08:15:45.768208095Z ENTRYPOINT: calling db:prepare
/blogdcrace-db-1 2026-09-27T08:15:45.470558387Z
/blogdcrace-web-1 2026-09-27T08:15:45.65435547Z

Postgres went from container start to accepting connections in 958 ms, including initdb on an empty volume. The entrypoint reached db:prepare 660 ms before that, and still connected, because bin/rails db:prepare spends longer than 660 ms loading the Rails environment before it dials. The margin is Ruby's boot time. It is not a guarantee of anything.

To see what happens when the margin goes the other way, put a sleep 6 in /docker-entrypoint-initdb.d, which is where the postgres image runs user scripts during initialisation:

db:running
web:exited
web-1  | ActiveRecord::ConnectionNotEstablished: connection to server at "172.22.0.2", port 5432 failed: Connection refused (ActiveRecord::ConnectionNotEstablished)
web-1  |    Is the server running on that host and accepting TCP/IP connections?
web-1  | 
web-1  | Caused by:
web-1  | PG::ConnectionBad: connection to server at "172.22.0.2", port 5432 failed: Connection refused (PG::ConnectionBad)
web-1  | Tasks: TOP => db:prepare

docker compose up -d exits 0 while that happens. Swap depends_on: [db] for condition: service_healthy, keep the same sleep 6, and the web container waits 6 seconds and comes up serving:

db:running
web:running
db-1  | 2026-09-27T08:17:08.829915175Z 2026-09-27 08:17:08.829 UTC [1] LOG:  database system is ready to accept connections
web-1 | 2026-09-27T08:17:09.449255967Z ENTRYPOINT: calling db:prepare
web-1 | 2026-09-27T08:17:15.377030137Z * Listening on http://0.0.0.0:3000
curl http://localhost:3414/posts -> 200

A slow initdb is contrived. WAL replay after an unclean shutdown on a real development database is not, and it is the same shape.

The healthcheck has to speak TCP

The pg_isready healthcheck everyone copies is pg_isready -U postgres, with no host, which checks the Unix socket. The postgres image starts a temporary server to run initdb, the user creation and anything in /docker-entrypoint-initdb.d, stops it, and only then starts the real one. Line 292 of /usr/local/bin/docker-entrypoint.sh inside the image is how that temporary server is started: set -- "$@" -c listen_addresses='' -p "${PGPORT:-5432}". An empty listen_addresses means socket only, so while it is up a socket check is green and nothing is listening on 5432.

The window, from the container log of a fresh volume:

33:2026-09-27 08:11:46.595 UTC [82] LOG:  database system is ready to accept connections
48:2026-09-27 08:11:46.756 UTC [82] LOG:  database system is shut down
52:PostgreSQL init process complete; ready for start up.
59:2026-09-27 08:11:46.849 UTC [1] LOG:  database system is ready to accept connections

161 milliseconds, between process 82 announcing itself and process 82 shutting down. A healthcheck with interval: 1s will usually miss it. Polling both forms from outside, the socket check went green at 0.85 s and the TCP check at 1.07 s after container start, and the 0.22 s gap is mostly docker exec overhead rather than the real window.

It costs one flag to be correct instead of usually correct, so the healthcheck above is pg_isready -U blogdc -d blogdc_development -h 127.0.0.1. The -d in there is theatre and I left it in the compose file before I checked: pg_isready -U blogdc -d no_such_database -h 127.0.0.1 answered 127.0.0.1:5432 - accepting connections and exit 0. pg_isready sends a startup packet and reads the server's response to it; it never opens the database. A check that does is psql -U blogdc -d blogdc_development -h 127.0.0.1 -tAc 'select 1', which on a missing database exits 2 with FATAL: database "no_such_database" does not exist. Whether that is worth a connection every second is your call; on a stack where POSTGRES_DB creates the database during init, it is not.

The connection error that names a socket path

config/database.yml as generated has no host: under development. The line is there, commented out, with a note saying the client uses a domain socket that does not need configuration. Inside the web container there is no PostgreSQL and no socket, so Active Record fails with an error that describes a machine you were not thinking about:

connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: No such file or directory (ActiveRecord::ConnectionNotEstablished)
    Is the server running locally and accepting connections on that socket?
connection to server on socket "/run/postgresql/.s.PGSQL.5432" failed: No such file or directory
connection to server on socket "/tmp/.s.PGSQL.5432" failed: No such file or directory

Three socket paths, because libpq tries them in turn. Nothing in that message mentions Docker, compose, or the fact that the host you want is called db.

DATABASE_URL in the service's environment: is the fix, and it is the better one because it keeps config/database.yml describing a machine where Postgres is local, which is what it does for anyone on the team not using Compose. What that merge actually does to the rest of the file, including the fact that it reaches the primary entry and no other, is Rails database.yml, resolved.

Two spellings of nothing, and compose treats them differently. A probe service echoing both, with and without the variable set in the calling shell:

EMPTY=[] NULLISH=[UNSET]
--- with NULLISH set on the host ---
EMPTY=[] NULLISH=[from_host]

DATABASE_URL: "" sets the variable to the empty string, and Active Record answers RuntimeError: Database URL cannot be empty from connection_url_resolver.rb:26 before it looks at the file. DATABASE_URL: with nothing after it is worse, because it does not fail: compose passes through whatever the shell that ran docker compose up had, so on the laptop of the one developer with a DATABASE_URL exported the container quietly connects somewhere else.

What the bind mount costs

.:/rails is what makes the setup a development environment, and it is where the time goes. Docker Desktop 4.37.2 on this machine has UseVirtualizationFrameworkVirtioFS = True, which is the fast option, and it is still two orders of magnitude off the image layer for small reads.

The same Ruby script in two containers built from the same base, one with the code bind-mounted and one with COPY . . baked in, reading 70 files from app/, config/, db/, lib/ and bin/ twenty times each:

--- BIND MOUNT ---
70 files read 20x: 452 ms
70 files read 20x: 292 ms
70 files read 20x: 279 ms
--- BAKED INTO THE IMAGE ---
69 files read 20x: 5 ms
69 files read 20x: 5 ms
69 files read 20x: 7 ms
--- HOST, APFS ---
70 files read 20x: 22 ms
70 files read 20x: 21 ms
70 files read 20x: 21 ms

1400 reads at roughly 200 microseconds each against roughly 4. The third block is the same script run natively on the directory the bind mount is mounting, so the comparison that matters is 279 ms against 21 ms: crossing the VM boundary costs about 13 times what reading the identical files on macOS costs, and about 56 times what reading them from the image layer costs.

That shows up in every Rails operation that stats or reads the tree, which in development is every request: ActiveSupport::FileUpdateChecker walks the autoload paths before the router gets the request. Three interleaved ab -n 100 -c 1 runs against /posts, one process per line, load average 7.68 at the time:

port3005=3.375ms  port3411=11.280ms  port3415=12.951ms  
port3005=4.064ms  port3411=11.262ms  port3415=10.914ms  
port3005=3.256ms  port3411=11.995ms  port3415=14.345ms  
labels: 3005=native host, 3411=container tmp on bind mount, 3415=container tmp on named volume

Three to four times the per-request latency, in development mode, on the same application against the same Postgres container. Part of that is the port forwarder: the same 50-request loop run from inside the container against 127.0.0.1:3000 had a median of 9.9 ms against 12.53 ms from the host through the published port, so roughly 2.6 ms of it is the hop and the rest is the mount.

The dead end: tmp/ on a volume did not make requests faster

Putting tmp/ on a named volume is the standard advice and it works, for one thing. Bootsnap writes its cache to tmp/cache/bootsnap, and taking those writes off the shared mount is worth about a third of boot time. Ten paired bin/rails runner 'Post.count' runs, alternating between two containers that differ only in where /rails/tmp comes from:

volume=2859ms  bindmount=4093ms
volume=1022ms  bindmount=4125ms
volume=1310ms  bindmount=1136ms
volume=601ms  bindmount=925ms
volume=464ms  bindmount=743ms
volume=535ms  bindmount=1058ms
volume=744ms  bindmount=814ms
volume=440ms  bindmount=838ms
volume=448ms  bindmount=798ms
volume=489ms  bindmount=782ms

Median 568 ms against 826 ms. The first two pairs are the machine being busy, which is what a laptop running other work looks like and why the median is the number quoted.

What I then expected, and measured, and did not get, was a matching improvement in request latency. The three interleaved ab runs in the section above include a container with tmp/ on a volume, on port 3415, and it was not faster: 13.0, 10.9 and 14.3 ms against 11.3, 11.3 and 12.0 ms for the container with tmp/ on the bind mount. An earlier pair of ab runs suggested 79 rps against 43, which is the number I would have published if I had stopped there; it was a cold cache on one side. Once both servers are warm, the file watcher is reading the code tree, not tmp/, and moving tmp/ changes nothing about that.

The volume stays in the compose file anyway. Faster boot is worth one line, and it keeps the pid file off the host, which is the next section.

One pid file, two machines

Two Rails containers sharing one host directory share one tmp/pids/server.pid, and the file contains 1, because Puma is PID 1 in its container. Starting a second container against the same mount:

web-1  | => Booting Puma
web-1  | => Rails 8.1.3.1 application starting in development 
web-1  | => Run `bin/rails server --help` for more startup options
web-1  | A server is already running (pid: 1, file: /rails/tmp/pids/server.pid).
web-1  | Exiting

rackup-2.3.1 lib/rackup/server.rb, check_pid!, is doing exactly what it should:

def check_pid!
  return unless ::File.exist?(options[:pid])

  pid = ::File.read(options[:pid]).to_i
  raise Errno::ESRCH if pid == 0

  Process.kill(0, pid)
  exit_with_pid(pid)
rescue Errno::ESRCH
  ::File.delete(options[:pid])
rescue Errno::EPERM
  exit_with_pid(pid)
end

Process.kill(0, 1) inside a container hits the container's own init and succeeds, so the stale file is never cleaned up. The Errno::ESRCH branch that deletes a dead pid file can never fire for PID 1.

The same file breaks the host, in the other direction and through the other rescue clause. After a container had written it, bin/rails server on macOS printed this, with the middle of the scratch app's path cut out:

A server is already running (pid: 1, file: /private/tmp/.../blogdc/tmp/pids/server.pid).
Exiting
$ cat tmp/pids/server.pid
1
$ ps -p 1 -o pid=,comm=
    1 /sbin/launchd
$ ruby -e 'begin; Process.kill(0,1); puts "ok"; rescue => e; puts "#{e.class}: #{e.message}"; end'
Errno::EPERM: Operation not permitted

PID 1 on macOS is launchd, an unprivileged process cannot signal it, and rescue Errno::EPERM routes straight to exit_with_pid. Two fixes, and the compose file above uses both: rm -f ./tmp/pids/server.pid at the top of the entrypoint, and tmp:/rails/tmp so the file never lands on the host at all. With tmp/ on a volume the pid file still survives a docker compose kill -s SIGKILL web, which is why the rm -f stays.

docker compose down keeps the database, and the disk

docker compose down removes the containers and the network and keeps every named volume, which is the behaviour you want and the one that fills a disk. A row written before the stack came down was still there after it came back:

$ docker compose exec -T web ./bin/rails runner 'Post.create!(title: "persisted", body: "x"); puts Post.count'
1
$ docker compose down && docker compose up -d
$ docker compose exec -T web ./bin/rails runner 'puts Post.count; puts Post.last.title'
1
persisted

docker compose down -v is the one that drops pgdata, and there is no undo. Halfway through writing this page the Docker VM ran out of room and Postgres refused to initialise at all, which is a failure mode worth recognising because it names a path rather than a disk:

db-1  | initdb: error: could not create directory "/var/lib/postgresql/data/pg_wal": No space left on device
db-1  | initdb: removing contents of data directory "/var/lib/postgresql/data"

docker system df at that moment reported 30.83 GB in local volumes, 98% of it reclaimable, and 5.3 GB of build cache. docker builder prune -f is the safe one to reach for first; volumes with no container attached are somebody's database until proven otherwise.

The suite behind this page

The argument matching, the railties default, the rackup pid check and the compose network are all assertions rather than anecdotes. test/docker_compose_test.rb in the scratch app runs inside the web container:

$ docker compose exec -T web bundle exec ruby -Itest test/docker_compose_test.rb
Run options: --seed 62836

# Running:

............

Finished in 0.689859s, 17.3949 runs/s, 28.9914 assertions/s.

12 runs, 20 assertions, 0 failures, 0 errors, 0 skips

Three of those examples read the installed gem source, so they fail loudly on a railties or rackup upgrade that changes the behaviour this page describes, which is the only way a claim about a dependency stays true.

The position: Postgres in Compose, Rails on the host

Compose is the right way to get Postgres, and usually the wrong way to get Rails. Delete the web service, keep db with its volume, its healthcheck and its published port, and run DATABASE_URL=postgres://blogdc:secret@localhost:15433/blogdc_development bin/rails server from your own terminal. Requests get faster by a factor of three, the bind mount problem stops existing, the pid file collision stops existing, and bin/rails console is a command rather than a command wrapped in a command.

What that costs you is the reason people containerised the app in the first place. You need a Ruby and a libpq on the laptop, so onboarding is no longer one command. The development environment stops resembling production, which means a native extension that fails to build on someone's machine is now their problem instead of the image's.

Three things would change the answer. A team on mixed operating systems, where the image is the only way to get one environment. A dependency that genuinely will not build on the host, which on macOS is a shorter list than it used to be but is not empty. And a stack with a third service in it, a specific Postgres extension or a search engine, where the moment you are running Compose anyway the marginal cost of the web service is much lower than the numbers above suggest.

What this page does not cover

Production deployment of the same image is a different problem with different answers, and the Kamal side of it is deploying Rails with Kamal 2: the generated Dockerfile is correct there, and so is the entrypoint whose argument test this page spends a section on. Nothing here measures that image; its size, its layers and its build times are in the Rails Docker image. Dev Containers, which the generated Dockerfile points at in its own comment, are a third approach and are not measured here.

Every measurement above is macOS on Apple silicon. On a Linux host there is no VM and no VirtioFS, bind mounts are ordinary bind mounts, and the entire performance section is moot; on Windows with WSL2 the numbers depend on which filesystem the repository lives on, which I did not test. Nothing here covers building for linux/amd64 from an arm64 machine, docker compose watch, a worker service running Solid Queue, Redis or Sidekiq, or how to get bin/dev and its Procfile to cooperate with a container. The secret_key_base failure is shown, not solved: running the production image locally needs RAILS_MASTER_KEY or SECRET_KEY_BASE, and how to get those to a container without putting them in the compose file is its own page.

Dockerfile.dev, compose.yaml, bin/docker-entrypoint-dev and test/docker_compose_test.rb are 20, 40, 16 and 97 lines, and reproduce against any Docker Desktop you point them at.

#rails #infrastructure #postgresql

Comments

No comments yet. Be the first.

Only used to confirm and publish your comment. Never shown publicly, never shared.

Markdown: **bold**, `code`, ```fenced blocks```, > quotes, [links](url). HTML and images are not rendered.