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.
Comments
No comments yet. Be the first.