Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. From versio
- Log schema cache queries timings on `log-level=debug` by @steve-chavez in #4805
- Add GHC runtime metrics to the metrics endpoint by @mkleczek in #4862
- Support running the admin server on a unix socket by @wolfgangwalther in #5003
- Enable starting multiple PostgREST instances using the same ports on platforms supporting it by @mkleczek in #4703 #4694

### Fixed

Expand Down
1 change: 1 addition & 0 deletions docs/postgrest.dict
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ Redux
refactor
reloadable
Reloadable
reuseport
requester's
RESTful
RLS
Expand Down
12 changes: 9 additions & 3 deletions docs/references/admin_server.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@ Two endpoints ``live`` and ``ready`` will then be available. Both these endpoint

.. important::

If you have a machine with multiple network interfaces and multiple PostgREST instances in the same port, you need to specify a unique :ref:`hostname <server-host>`
in the configuration of each PostgREST instance for the health check to work correctly. Don't use the special values(``!4``, ``*``, etc) in this case because the health check
could report a false positive.
Multiple PostgREST instances can share the same public API host and port when
:ref:`server-reuseport` is enabled on operating systems that support
``SO_REUSEPORT``. Admin ports are not shared: give each instance a different
:ref:`admin-server-port`, otherwise the new instance will fail to start.
Comment on lines +19 to +22

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Included this in https://github.com/PostgREST/postgrest/pull/4703/changes#r3470538404. To have everything in one place.


If the machine has multiple network interfaces, configure concrete
:ref:`server-host` and :ref:`admin-server-host` values when you need health
checks to target a specific process. Avoid special values (``!4``, ``*``, etc)
in this case because the health check could report a false positive.
Comment on lines +24 to +27

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This doesn't look related to this feature?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This doesn't look related to this feature?

Not directly but I added it because it is important in this case: we have multiple PostgREST processes running at the same time and it is easy to target the wrong one with health checks.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Right, make sense. But it feels a bit out of place here. I believe it should go inside the server-reuseport section in the config.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So let me see whether I understand the problem this tries to hint at: I turn on server-reuseport. I set server-host at the default of !4. This automatically applies to admin-server-host as well, I think. I now accidentally set the admin-server-port to the same value for both instances. According to the note further up, I would expect this to fail, because the same port for the admin server is used.

But it's not, because it's using a different interface. So I run two admin servers on the same port, but on different interfaces. Now, things start to break.

Is this what you had in mind?

If yes, I feel like it fits right in here. But it should be more framed as an exception to the above rule ("admin servers on the same port will fail to start").

If not.. please elaborate.


Live
----
Expand Down
47 changes: 47 additions & 0 deletions docs/references/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ admin-server-port

Specifies the port for the :ref:`admin_server`. Cannot be equal to :ref:`server-port`.

When running multiple PostgREST instances on the same :ref:`server-port`, use
a different ``admin-server-port`` for each instance. Admin ports are not shared
between instances, so readiness checks always target one specific PostgREST
instance.

Comment on lines +179 to +183

@steve-chavez steve-chavez Jun 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd suggest putting all these paragraphs under the reuseport section, otherwise it's kinda hard to hunt them down.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Added this paragraph on my suggestion: https://github.com/PostgREST/postgrest/pull/4703/changes#r3470538404

Can be deleted from here if you agree

.. _admin-server-unix-socket:

admin-server-unix-socket
Expand Down Expand Up @@ -939,6 +944,48 @@ server-port

The TCP port to bind the web server. Use ``0`` to automatically assign a port.

When :ref:`server-reuseport` is enabled on an operating system that supports
``SO_REUSEPORT``, you can start multiple PostgREST instances on the same
:ref:`server-host` and ``server-port``. For example, two PostgREST processes
can use the same configuration:

.. code:: ini

server-host = "127.0.0.1"
server-port = 3000
server-reuseport = true

New connections are then distributed by the operating system between the
running PostgREST processes. This can be used to start a replacement process
before stopping the old one, or to run several PostgREST processes behind one
port.

If ``server-reuseport`` is disabled, starting another PostgREST process on
the same host and port will fail with the usual address-in-use error.

.. _server-reuseport:

server-reuseport
----------------

=============== =================================
**Type** Bool
**Default** false
**Reloadable** N
**Environment** PGRST_SERVER_REUSEPORT
**In-Database** `n/a`
=============== =================================

Enables ``SO_REUSEPORT`` on the TCP server socket. This allows multiple
PostgREST processes to bind to the same :ref:`server-host` and
:ref:`server-port` when the operating system supports it.

Enabling this setting on an operating system that does not support
``SO_REUSEPORT`` is a configuration error. PostgREST will fail to start
instead of falling back to a normal TCP socket.

This setting does not apply when :ref:`server-unix-socket` is used.

Comment on lines +947 to +988

@steve-chavez steve-chavez Jun 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ditto here, maybe like:

Suggested change
When :ref:`server-reuseport` is enabled on an operating system that supports
``SO_REUSEPORT``, you can start multiple PostgREST instances on the same
:ref:`server-host` and ``server-port``. For example, two PostgREST processes
can use the same configuration:
.. code:: ini
server-host = "127.0.0.1"
server-port = 3000
server-reuseport = true
New connections are then distributed by the operating system between the
running PostgREST processes. This can be used to start a replacement process
before stopping the old one, or to run several PostgREST processes behind one
port.
If ``server-reuseport`` is disabled, starting another PostgREST process on
the same host and port will fail with the usual address-in-use error.
.. _server-reuseport:
server-reuseport
----------------
=============== =================================
**Type** Bool
**Default** false
**Reloadable** N
**Environment** PGRST_SERVER_REUSEPORT
**In-Database** `n/a`
=============== =================================
Enables ``SO_REUSEPORT`` on the TCP server socket. This allows multiple
PostgREST processes to bind to the same :ref:`server-host` and
:ref:`server-port` when the operating system supports it.
Enabling this setting on an operating system that does not support
``SO_REUSEPORT`` is a configuration error. PostgREST will fail to start
instead of falling back to a normal TCP socket.
This setting does not apply when :ref:`server-unix-socket` is used.
.. _server-reuseport:
server-reuseport
----------------
=============== =================================
**Type** Bool
**Default** false
**Reloadable** N
**Environment** PGRST_SERVER_REUSEPORT
**In-Database** `n/a`
=============== =================================
Enables ``SO_REUSEPORT`` on the TCP server socket. This allows multiple
PostgREST processes to bind to the same :ref:`server-host` and
:ref:`server-port` when the operating system supports it.
For example, two PostgREST processes can use the same configuration
.. code:: ini
server-host = "127.0.0.1"
server-port = 3000
server-reuseport = true
New connections are then distributed by the operating system between the
running PostgREST processes. This can be used to start a replacement process
before stopping the old one, or to run several PostgREST processes behind one
port.
Use a different ``admin-server-port`` for each instance. Admin ports are not shared
between instances:
- Readiness checks always target one specific PostgREST
- Give each instance a different :ref:`admin-server-port`, otherwise the new instance will fail to start.
Enabling this setting on an operating system that does not support
``SO_REUSEPORT`` is a configuration error. PostgREST will fail to start
instead of falling back to a normal TCP socket.
This setting does not apply when :ref:`server-unix-socket` is used.

.. _server-trace-header:

server-trace-header
Expand Down
25 changes: 19 additions & 6 deletions src/PostgREST/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ import PostgREST.Version (docsVersion, prettyVersion)
import Control.Monad.Writer
import qualified Data.ByteString.Char8 as BS
import qualified Data.List as L
import Data.Streaming.Network (bindPortTCP)
import Data.Streaming.Network (HostPreference,
bindPortGenEx,
bindPortTCP)
import qualified Data.Text as T
import qualified Network.HTTP.Types as HTTP
import Network.HTTP.Types.Header (hVary)
Expand All @@ -79,7 +81,7 @@ import Protolude hiding (Handler)

run :: AppState -> IO ()
run appState = do
conf <- AppState.getConfig appState
conf@AppConfig{configServerReusePort} <- AppState.getConfig appState

mainSocketRef <- newIORef Nothing
adminSocket <- initAdminServerSocket conf
Expand All @@ -96,7 +98,10 @@ run appState = do
-- Kick off and wait for the initial SchemaCache load before creating the
-- main API socket.
AppState.schemaCacheLoader appState
AppState.waitForSchemaCacheInit appState
if configServerReusePort then
AppState.waitForSchemaCacheLoaded appState
else
AppState.waitForSchemaCacheInit appState

mainSocket <- initServerSocket conf
atomicWriteIORef mainSocketRef $ Just mainSocket
Comment thread
steve-chavez marked this conversation as resolved.
Expand Down Expand Up @@ -271,11 +276,11 @@ addRetryHint delay response = do
isServiceUnavailable :: Wai.Response -> Bool
isServiceUnavailable response = Wai.responseStatus response == HTTP.status503

initSocket :: (Applicative f, Traversable f) => Maybe String -> FileMode -> Text -> f Int -> IO (f NS.Socket)
initSocket unixSocket unixSocketMode tcpHost tcpPort =
initSocket :: (Applicative f, Traversable f) => Maybe String -> FileMode -> Text -> f Int -> (Int -> HostPreference -> IO NS.Socket) -> IO (f NS.Socket)
initSocket unixSocket unixSocketMode tcpHost tcpPort bindTCP =
maybe initTCPSocket initDomainSocket unixSocket
where
initTCPSocket = traverse (`bindPortTCP` (fromString $ T.unpack tcpHost)) tcpPort
initTCPSocket = traverse (`bindTCP` (fromString $ T.unpack tcpHost)) tcpPort
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
-- but we need to have runtime error if we try to use it in Windows, not compile time error
initDomainSocket = fmap pure . (`createAndBindDomainSocket` unixSocketMode)
Expand All @@ -285,9 +290,17 @@ initServerSocket AppConfig{..} =
runIdentity <$> initSocket
configServerUnixSocket configServerUnixSocketMode
configServerHost (pure configServerPort)
(if configServerReusePort then bindPortTCPWithReusePort else bindPortTCP)

initAdminServerSocket :: AppConfig -> IO (Maybe NS.Socket)
initAdminServerSocket AppConfig{..} =
initSocket
configAdminServerUnixSocket configAdminServerUnixSocketMode
configAdminServerHost configAdminServerPort
bindPortTCP

bindPortTCPWithReusePort :: Int -> HostPreference -> IO NS.Socket
bindPortTCPWithReusePort port hostPreference =
bindPortGenEx [(NS.ReusePort, 1)] NS.Stream port hostPreference >>= listenSocket
where
listenSocket sock = NS.listen sock (max 2048 NS.maxListenQueue) $> sock
4 changes: 4 additions & 0 deletions src/PostgREST/AppState.hs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module PostgREST.AppState
, isLoaded
, isPending
, waitForSchemaCacheInit
, waitForSchemaCacheLoaded
) where

import qualified Data.ByteString.Char8 as BS
Expand Down Expand Up @@ -390,6 +391,9 @@ isSchemaCacheLoaded = atomically . (pure . fromMaybe False <=< tryReadTMVar) . g
waitForSchemaCacheInit :: AppState -> IO ()
waitForSchemaCacheInit = atomically . void . readTMVar . getSCStatusTMVar . stateSCacheStatus

waitForSchemaCacheLoaded :: AppState -> IO ()
waitForSchemaCacheLoaded = atomically . (check <=< readTMVar) . getSCStatusTMVar . stateSCacheStatus

-- | Reads the in-db config and reads the config file again
-- | We don't retry reading the in-db config after it fails immediately, because it could have user errors. We just report the error and continue.
readInDbConfig :: Bool -> AppState -> IO ()
Expand Down
4 changes: 4 additions & 0 deletions src/PostgREST/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ data AppConfig = AppConfig
, configServerCorsAllowedOrigins :: [Text]
, configServerHost :: Text
, configServerPort :: Int
, configServerReusePort :: Bool
, configServerTraceHeader :: Maybe (CI.CI BS.ByteString)
, configServerTimingEnabled :: Bool
, configServerUnixSocket :: Maybe FilePath
Expand Down Expand Up @@ -204,6 +205,7 @@ toText conf =
,("server-cors-allowed-origins", q . T.intercalate "," . configServerCorsAllowedOrigins)
,("server-host", q . configServerHost)
,("server-port", show . configServerPort)
,("server-reuseport", T.toLower . show . configServerReusePort)
,("server-trace-header", q . T.decodeUtf8 . maybe mempty CI.original . configServerTraceHeader)
,("server-timing-enabled", T.toLower . show . configServerTimingEnabled)
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
Expand Down Expand Up @@ -322,6 +324,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> parseCORSAllowedOrigins "server-cors-allowed-origins"
<*> (defaultServerHost <$> optString "server-host")
<*> parseServerPort "server-port"
<*> (fromMaybe False <$> optBool "server-reuseport")
<*> (fmap (CI.mk . encodeUtf8) <$> optString "server-trace-header")
<*> (fromMaybe False <$> optBool "server-timing-enabled")
<*> (fmap T.unpack <$> optString "server-unix-socket")
Expand Down Expand Up @@ -784,6 +787,7 @@ exampleConfigFile = S.unlines
, ""
, "server-host = \"!4\""
, "server-port = 3000"
, "server-reuseport = false"
, ""
, "## Allow getting the request-response timing information through the `Server-Timing` header"
, "server-timing-enabled = false"
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/aliases.config
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ openapi-server-proxy-uri = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-reuseport = false
server-timing-enabled = false
server-trace-header = ""
server-unix-socket = ""
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/boolean-numeric.config
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ openapi-server-proxy-uri = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-reuseport = false
server-timing-enabled = false
server-trace-header = ""
server-unix-socket = ""
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/boolean-string.config
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ openapi-server-proxy-uri = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-reuseport = false
server-timing-enabled = false
server-trace-header = ""
server-unix-socket = ""
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/defaults.config
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ openapi-server-proxy-uri = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-reuseport = false
server-timing-enabled = false
server-trace-header = ""
server-unix-socket = ""
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/jspath-str-op-dump1.config
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ openapi-server-proxy-uri = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-reuseport = false
server-timing-enabled = false
server-trace-header = ""
server-unix-socket = ""
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/jspath-str-op-dump2.config
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ openapi-server-proxy-uri = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-reuseport = false
server-timing-enabled = false
server-trace-header = ""
server-unix-socket = ""
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/jspath-str-op-dump3.config
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ openapi-server-proxy-uri = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-reuseport = false
server-timing-enabled = false
server-trace-header = ""
server-unix-socket = ""
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/jspath-str-op-dump4.config
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ openapi-server-proxy-uri = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-reuseport = false
server-timing-enabled = false
server-trace-header = ""
server-unix-socket = ""
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/jspath-str-op-dump5.config
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ openapi-server-proxy-uri = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-reuseport = false
server-timing-enabled = false
server-trace-header = ""
server-unix-socket = ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ openapi-server-proxy-uri = "https://otherexample.org/api"
server-cors-allowed-origins = "http://otherorigin.com"
server-host = "0.0.0.0"
server-port = 80
server-reuseport = true
server-timing-enabled = true
server-trace-header = "traceparent"
server-unix-socket = "/tmp/pgrst_io_test.sock"
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/no-defaults-with-db.config
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ openapi-server-proxy-uri = "https://example.org/api"
server-cors-allowed-origins = "http://origin.com"
server-host = "0.0.0.0"
server-port = 80
server-reuseport = true
server-timing-enabled = false
server-trace-header = "CF-Ray"
server-unix-socket = "/tmp/pgrst_io_test.sock"
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/no-defaults.config
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ openapi-server-proxy-uri = "https://postgrest.org"
server-cors-allowed-origins = "http://example.com"
server-host = "0.0.0.0"
server-port = 80
server-reuseport = true
server-timing-enabled = true
server-trace-header = "X-Request-Id"
server-unix-socket = "/tmp/pgrst_io_test.sock"
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/types.config
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ openapi-server-proxy-uri = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-reuseport = false
server-timing-enabled = false
server-trace-header = ""
server-unix-socket = ""
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/expected/utf-8.config
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ openapi-server-proxy-uri = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-reuseport = false
server-timing-enabled = false
server-trace-header = ""
server-unix-socket = ""
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/no-defaults-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ PGRST_OPENAPI_SERVER_PROXY_URI: 'https://postgrest.org'
PGRST_SERVER_CORS_ALLOWED_ORIGINS: "http://example.com"
PGRST_SERVER_HOST: 0.0.0.0
PGRST_SERVER_PORT: 80
PGRST_SERVER_REUSEPORT: true
PGRST_SERVER_TRACE_HEADER: X-Request-Id
PGRST_SERVER_TIMING_ENABLED: true
PGRST_SERVER_UNIX_SOCKET: /tmp/pgrst_io_test.sock
Expand Down
1 change: 1 addition & 0 deletions test/io/configs/no-defaults.config
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ openapi-server-proxy-uri = "https://postgrest.org"
server-cors-allowed-origins = "http://example.com"
server-host = "0.0.0.0"
server-port = 80
server-reuseport = true
server-trace-header = "X-Request-Id"
server-timing-enabled = true
server-unix-socket = "/tmp/pgrst_io_test.sock"
Expand Down
4 changes: 2 additions & 2 deletions test/io/postgrest.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def run(
admin_port=None,
host=None,
wait_for=Admin.ready,
wait_max_seconds=1,
wait_max_seconds=3,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why did we increase the default? Could this be done for particular tests instead?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It was somewhat flaky on my machine. Can roll it back if needed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes, let's try that

no_pool_connection_available=False,
no_startup_stdout=True,
):
Expand Down Expand Up @@ -266,7 +266,7 @@ def wait_until_status_code(url, max_seconds, status_code):

time.sleep(0.1)

if response:
if response is not None:
raise PostgrestTimedOut(f"{response.status_code}: {response.text}")
else:
raise PostgrestTimedOut()
Expand Down
Loading