add: support running admin server on unix socket

This is useful when multiple instances run on the same machine, for
example behind a proxy. Unix sockets for web and admin servers can then
be put in the same folder for each instance.

Can be helpful when writing tests as well.
This commit is contained in:
Wolfgang Walther
2026-06-14 17:18:37 +00:00
parent f24bc7092f
commit 2976eb047b
26 changed files with 162 additions and 66 deletions
+1
View File
@@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. From versio
- Add config `db-timezone-enabled` for optional querying of timezones by @taimoorzaeem in #4751
- 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
### Fixed
+1 -1
View File
@@ -3,7 +3,7 @@
Admin Server
############
PostgREST provides an admin server that can be enabled by setting :ref:`admin-server-port`.
PostgREST provides an admin server that can be enabled by setting :ref:`admin-server-port` or `:ref:`admin-server-unix-socket`.
.. _health_check:
+40
View File
@@ -176,6 +176,46 @@ admin-server-port
Specifies the port for the :ref:`admin_server`. Cannot be equal to :ref:`server-port`.
.. _admin-server-unix-socket:
admin-server-unix-socket
------------------------
=============== =================================
**Type** String
**Default** `n/a`
**Reloadable** N
**Environment** PGRST_ADMIN_SERVER_UNIX_SOCKET
**In-Database** `n/a`
=============== =================================
`Unix domain socket <https://en.wikipedia.org/wiki/Unix_domain_socket>`_ where to bind the :ref:`admin_server`.
If specified, this takes precedence over :ref:`admin-server-port`. Example:
.. code:: bash
admin-server-unix-socket = "/tmp/pgrst-admin.sock"
.. _admin-server-unix-socket-mode:
admin-server-unix-socket-mode
-----------------------------
=============== ===================================
**Type** String
**Default** 660
**Reloadable** N
**Environment** PGRST_ADMIN_SERVER_UNIX_SOCKET_MODE
**In-Database** `n/a`
=============== ===================================
`Unix file mode <https://en.wikipedia.org/wiki/File_system_permissions>`_ to be set for the socket specified in :ref:`admin-server-unix-socket`
Needs to be a valid octal between 600 and 777.
.. code:: bash
admin-server-unix-socket-mode = "660"
.. _app.settings.*:
app.settings.*
+8 -4
View File
@@ -265,10 +265,14 @@ initSockets AppConfig{..} = do
Just path -> createAndBindDomainSocket path configServerUnixSocketMode
Nothing -> bindPortTCP configServerPort (fromString $ T.unpack configServerHost)
adminSock <- case configAdminServerPort of
Just adminPort -> do
adminSock <- bindPortTCP adminPort (fromString $ T.unpack configAdminServerHost)
adminSock <- case configAdminServerUnixSocket of
Just path -> do
adminSock <- createAndBindDomainSocket path configAdminServerUnixSocketMode
pure $ Just adminSock
Nothing -> pure Nothing
Nothing -> case configAdminServerPort of
Just adminPort -> do
adminSock <- bindPortTCP adminPort (fromString $ T.unpack configAdminServerHost)
pure $ Just adminSock
Nothing -> pure Nothing
pure (sock, adminSock)
+58 -51
View File
@@ -78,55 +78,57 @@ audMatchesCfg :: AppConfig -> Text -> Bool
audMatchesCfg = maybe (const True) (==) . configJwtAudience
data AppConfig = AppConfig
{ configAppSettings :: [(Text, Text)]
, configClientErrorVerbosity :: Verbosity
, configDbAggregates :: Bool
, configDbAnonRole :: Maybe BS.ByteString
, configDbChannel :: Text
, configDbChannelEnabled :: Bool
, configDbExtraSearchPath :: [Text]
, configDbHoistedTxSettings :: [Text]
, configDbMaxRows :: Maybe Integer
, configDbPlanEnabled :: Bool
, configDbPoolSize :: Int
, configDbPoolAcquisitionTimeout :: Int
, configDbPoolMaxLifetime :: Int
, configDbPoolMaxIdletime :: Int
, configDbPoolAutomaticRecovery :: Bool
, configDbPreRequest :: Maybe QualifiedIdentifier
, configDbPreparedStatements :: Bool
, configDbRootSpec :: Maybe QualifiedIdentifier
, configDbSchemas :: NonEmpty Text
, configDbConfig :: Bool
, configDbPreConfig :: Maybe QualifiedIdentifier
, configDbTimezoneEnabled :: Bool
, configDbTxAllowOverride :: Bool
, configDbTxRollbackAll :: Bool
, configDbUri :: Text
, configFilePath :: Maybe FilePath
, configJWKS :: Maybe JwkSet
, configJwtAudience :: Maybe Text
, configJwtRoleClaimKey :: JSPath
, configJwtSecret :: Maybe BS.ByteString
, configJwtSecretIsBase64 :: Bool
, configJwtCacheMaxEntries :: Int
, configLogLevel :: LogLevel
, configLogQuery :: Bool
, configOpenApiMode :: OpenAPIMode
, configOpenApiSecurityActive :: Bool
, configOpenApiServerProxyUri :: Maybe Text
, configServerCorsAllowedOrigins :: [Text]
, configServerHost :: Text
, configServerPort :: Int
, configServerTraceHeader :: Maybe (CI.CI BS.ByteString)
, configServerTimingEnabled :: Bool
, configServerUnixSocket :: Maybe FilePath
, configServerUnixSocketMode :: FileMode
, configAdminServerHost :: Text
, configAdminServerPort :: Maybe Int
, configRoleSettings :: RoleSettings
, configRoleIsoLvl :: RoleIsolationLvl
, configInternalSCQuerySleep :: Maybe Int32
{ configAppSettings :: [(Text, Text)]
, configClientErrorVerbosity :: Verbosity
, configDbAggregates :: Bool
, configDbAnonRole :: Maybe BS.ByteString
, configDbChannel :: Text
, configDbChannelEnabled :: Bool
, configDbExtraSearchPath :: [Text]
, configDbHoistedTxSettings :: [Text]
, configDbMaxRows :: Maybe Integer
, configDbPlanEnabled :: Bool
, configDbPoolSize :: Int
, configDbPoolAcquisitionTimeout :: Int
, configDbPoolMaxLifetime :: Int
, configDbPoolMaxIdletime :: Int
, configDbPoolAutomaticRecovery :: Bool
, configDbPreRequest :: Maybe QualifiedIdentifier
, configDbPreparedStatements :: Bool
, configDbRootSpec :: Maybe QualifiedIdentifier
, configDbSchemas :: NonEmpty Text
, configDbConfig :: Bool
, configDbPreConfig :: Maybe QualifiedIdentifier
, configDbTimezoneEnabled :: Bool
, configDbTxAllowOverride :: Bool
, configDbTxRollbackAll :: Bool
, configDbUri :: Text
, configFilePath :: Maybe FilePath
, configJWKS :: Maybe JwkSet
, configJwtAudience :: Maybe Text
, configJwtRoleClaimKey :: JSPath
, configJwtSecret :: Maybe BS.ByteString
, configJwtSecretIsBase64 :: Bool
, configJwtCacheMaxEntries :: Int
, configLogLevel :: LogLevel
, configLogQuery :: Bool
, configOpenApiMode :: OpenAPIMode
, configOpenApiSecurityActive :: Bool
, configOpenApiServerProxyUri :: Maybe Text
, configServerCorsAllowedOrigins :: [Text]
, configServerHost :: Text
, configServerPort :: Int
, configServerTraceHeader :: Maybe (CI.CI BS.ByteString)
, configServerTimingEnabled :: Bool
, configServerUnixSocket :: Maybe FilePath
, configServerUnixSocketMode :: FileMode
, configAdminServerHost :: Text
, configAdminServerPort :: Maybe Int
, configAdminServerUnixSocket :: Maybe FilePath
, configAdminServerUnixSocketMode :: FileMode
, configRoleSettings :: RoleSettings
, configRoleIsoLvl :: RoleIsolationLvl
, configInternalSCQuerySleep :: Maybe Int32
}
data LogLevel = LogCrit | LogError | LogWarn | LogInfo | LogDebug
@@ -207,6 +209,8 @@ toText conf =
,("server-unix-socket-mode", q . T.pack . showSocketMode)
,("admin-server-host", q . configAdminServerHost)
,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
,("admin-server-unix-socket", q . maybe mempty T.pack . configAdminServerUnixSocket)
,("admin-server-unix-socket-mode", q . T.pack . showAdminSocketMode)
]
-- quote all app.settings
@@ -230,6 +234,7 @@ toText conf =
where
secret = fromMaybe mempty $ configJwtSecret c
showSocketMode c = showOct (configServerUnixSocketMode c) mempty
showAdminSocketMode c = showOct (configAdminServerUnixSocketMode c) mempty
-- This class is needed for the polymorphism of overrideFromDbOrEnvironment
-- because C.required and C.optional have different signatures
@@ -323,6 +328,8 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> (defaultServerHost <$> optWithAlias (optString "admin-server-host")
(optString "server-host"))
<*> parseAdminServerPort "admin-server-port"
<*> (fmap T.unpack <$> optString "admin-server-unix-socket")
<*> parseSocketFileMode "admin-server-unix-socket-mode"
<*> pure roleSettings
<*> pure roleIsolationLvl
<*> optInt "internal-schema-cache-query-sleep"
@@ -372,10 +379,10 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
Just fileModeText ->
case readOct $ T.unpack fileModeText of
[] ->
fail "Invalid server-unix-socket-mode: not an octal"
fail $ "Invalid " <> T.unpack k <> ": not an octal"
(fileMode, _):_ ->
if fileMode < 384 || fileMode > 511
then fail "Invalid server-unix-socket-mode: needs to be between 600 and 777"
then fail $ "Invalid " <> T.unpack k <> ": needs to be between 600 and 777"
else pure fileMode
parseOpenAPIMode :: C.Key -> C.Parser C.Config OpenAPIMode
+2
View File
@@ -1,5 +1,7 @@
admin-server-host = "!4"
admin-server-port = ""
admin-server-unix-socket = ""
admin-server-unix-socket-mode = "660"
client-error-verbosity = "verbose"
db-aggregates-enabled = false
db-anon-role = ""
@@ -1,5 +1,7 @@
admin-server-host = "!4"
admin-server-port = ""
admin-server-unix-socket = ""
admin-server-unix-socket-mode = "660"
client-error-verbosity = "verbose"
db-aggregates-enabled = false
db-anon-role = ""
@@ -1,5 +1,7 @@
admin-server-host = "!4"
admin-server-port = ""
admin-server-unix-socket = ""
admin-server-unix-socket-mode = "660"
client-error-verbosity = "verbose"
db-aggregates-enabled = false
db-anon-role = ""
+2
View File
@@ -1,5 +1,7 @@
admin-server-host = "!4"
admin-server-port = ""
admin-server-unix-socket = ""
admin-server-unix-socket-mode = "660"
client-error-verbosity = "verbose"
db-aggregates-enabled = false
db-anon-role = ""
@@ -1,5 +1,7 @@
admin-server-host = "!4"
admin-server-port = ""
admin-server-unix-socket = ""
admin-server-unix-socket-mode = "660"
client-error-verbosity = "verbose"
db-aggregates-enabled = false
db-anon-role = ""
@@ -1,5 +1,7 @@
admin-server-host = "!4"
admin-server-port = ""
admin-server-unix-socket = ""
admin-server-unix-socket-mode = "660"
client-error-verbosity = "verbose"
db-aggregates-enabled = false
db-anon-role = ""
@@ -1,5 +1,7 @@
admin-server-host = "!4"
admin-server-port = ""
admin-server-unix-socket = ""
admin-server-unix-socket-mode = "660"
client-error-verbosity = "verbose"
db-aggregates-enabled = false
db-anon-role = ""
@@ -1,5 +1,7 @@
admin-server-host = "!4"
admin-server-port = ""
admin-server-unix-socket = ""
admin-server-unix-socket-mode = "660"
client-error-verbosity = "verbose"
db-aggregates-enabled = false
db-anon-role = ""
@@ -1,5 +1,7 @@
admin-server-host = "!4"
admin-server-port = ""
admin-server-unix-socket = ""
admin-server-unix-socket-mode = "660"
client-error-verbosity = "verbose"
db-aggregates-enabled = false
db-anon-role = ""
@@ -1,5 +1,7 @@
admin-server-host = "127.0.0.1"
admin-server-port = 3001
admin-server-unix-socket = "/tmp/admin_io_test.sock"
admin-server-unix-socket-mode = "666"
app.settings.test = "test"
app.settings.test2 = "test"
client-error-verbosity = "minimal"
@@ -1,5 +1,7 @@
admin-server-host = "127.0.0.1"
admin-server-port = 3001
admin-server-unix-socket = "/tmp/admin_io_test.sock"
admin-server-unix-socket-mode = "666"
app.settings.test = "test"
app.settings.test2 = "test"
client-error-verbosity = "minimal"
@@ -1,5 +1,7 @@
admin-server-host = "127.0.0.1"
admin-server-port = 3001
admin-server-unix-socket = "/tmp/admin_io_test.sock"
admin-server-unix-socket-mode = "666"
app.settings.test = "test"
app.settings.test2 = "test"
client-error-verbosity = "minimal"
+2
View File
@@ -1,5 +1,7 @@
admin-server-host = "!4"
admin-server-port = ""
admin-server-unix-socket = ""
admin-server-unix-socket-mode = "660"
app.settings.test = "Bool False"
client-error-verbosity = "verbose"
db-aggregates-enabled = false
+2
View File
@@ -1,5 +1,7 @@
admin-server-host = "!4"
admin-server-port = ""
admin-server-unix-socket = ""
admin-server-unix-socket-mode = "660"
client-error-verbosity = "verbose"
db-aggregates-enabled = false
db-anon-role = ""
+2
View File
@@ -43,3 +43,5 @@ PGRST_SERVER_UNIX_SOCKET: /tmp/pgrst_io_test.sock
PGRST_SERVER_UNIX_SOCKET_MODE: 777
PGRST_ADMIN_SERVER_HOST: 127.0.0.1
PGRST_ADMIN_SERVER_PORT: 3001
PGRST_ADMIN_SERVER_UNIX_SOCKET: /tmp/admin_io_test.sock
PGRST_ADMIN_SERVER_UNIX_SOCKET_MODE: 666
+2
View File
@@ -40,5 +40,7 @@ server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777"
admin-server-port = 3001
admin-server-host = "127.0.0.1"
admin-server-unix-socket = "/tmp/admin_io_test.sock"
admin-server-unix-socket-mode = "666"
app.settings.test = "test"
app.settings.test2 = "test"
+2
View File
@@ -39,6 +39,8 @@ ALTER ROLE db_config_authenticator IN DATABASE other SET pgrst.db_max_rows = '11
-- non-reloadable configs
ALTER ROLE db_config_authenticator SET pgrst.admin_server_host = 'ignored';
ALTER ROLE db_config_authenticator SET pgrst.admin_server_port = 'ignored';
ALTER ROLE db_config_authenticator SET pgrst.admin_server_unix_socket = 'ignored';
ALTER ROLE db_config_authenticator SET pgrst.admin_server_unix_socket_mode = 'ignored';
ALTER ROLE db_config_authenticator SET pgrst.db_channel = 'ignored';
ALTER ROLE db_config_authenticator SET pgrst.db_channel_enabled = 'ignored';
ALTER ROLE db_config_authenticator SET pgrst.db_config = 'true';
+8 -4
View File
@@ -124,10 +124,14 @@ def run(
env["PGRST_SERVER_UNIX_SOCKET"] = str(socketfile)
baseurl = "http+unix://" + urllib.parse.quote_plus(str(socketfile))
adminport = freeport(used_ports=[port]) if admin_port is None else admin_port
env["PGRST_ADMIN_SERVER_PORT"] = str(adminport)
adminhost = f"[{host}]" if host and is_ipv6(host) else localhost
adminurl = f"http://{adminhost}:{adminport}"
if admin_port:
env["PGRST_ADMIN_SERVER_PORT"] = str(admin_port)
adminhost = f"[{host}]" if host and is_ipv6(host) else localhost
adminurl = f"http://{adminhost}:{admin_port}"
else:
socketfile = pathlib.Path(tmpdir) / "admin.sock"
env["PGRST_ADMIN_SERVER_UNIX_SOCKET"] = str(socketfile)
adminurl = "http+unix://" + urllib.parse.quote_plus(str(socketfile))
command = [POSTGREST_BIN]
env["HPCTIXFILE"] = hpctixfile()
+8 -6
View File
@@ -305,8 +305,9 @@ def test_cli_ready_flag_success(host, defaultenv):
"test PostgREST ready flag succeeds when ready"
port = freeport()
admin_port = freeport(used_ports=[port])
with run(env=defaultenv, host=host, port=port) as postgrest:
with run(env=defaultenv, host=host, port=port, admin_port=admin_port) as postgrest:
output = cli(["--ready"], env=postgrest.config)
(admin_host, admin_port) = get_admin_host_and_port_from_config(postgrest.config)
@@ -330,8 +331,9 @@ def test_cli_ready_flag_fail_when_schema_cache_not_loaded(defaultenv, metapostgr
}
port = freeport()
admin_port = freeport(used_ports=[port])
with run(env=env, port=port) as postgrest:
with run(env=env, port=port, admin_port=admin_port) as postgrest:
# The schema cache query takes at least 500ms, due to PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP above.
# Make it impossible to load the schema cache, by setting statement timeout to 400ms.
set_statement_timeout(metapostgrest, role, 400)
@@ -351,11 +353,11 @@ def test_cli_ready_flag_fail_with_http_exception(defaultenv):
"test PostgREST ready flag fail when http exception occurs"
port = freeport()
admin_port = freeport(used_ports=[port])
# when healthcheck process sends the request to a wrong endpoint
with run(env=defaultenv, port=port) as postgrest:
with run(env=defaultenv, port=port, admin_port=admin_port) as postgrest:
# we set it to some freeport where server and admin server is not running
admin_port = int(postgrest.config["PGRST_ADMIN_SERVER_PORT"])
used_ports = [port, admin_port]
postgrest.config["PGRST_ADMIN_SERVER_PORT"] = str(freeport(used_ports))
@@ -368,7 +370,7 @@ def test_cli_ready_flag_fail_with_http_exception(defaultenv):
)
# When client sends the request to invalid URL
with run(env=defaultenv, port=port) as postgrest:
with run(env=defaultenv, port=port, admin_port=admin_port) as postgrest:
postgrest.config["PGRST_ADMIN_SERVER_PORT"] = str(-1)
output = cli(["--ready"], env=postgrest.config, expect_error=True)
(admin_host, admin_port) = get_admin_host_and_port_from_config(postgrest.config)
@@ -382,7 +384,7 @@ def test_cli_ready_flag_fail_with_special_hostname(defaultenv):
port = freeport()
host = "*4"
with run(env=defaultenv, host=host, port=port) as postgrest:
with run(env=defaultenv, host=host, port=port, admin_port=freeport()) as postgrest:
output = cli(["--ready"], env=postgrest.config, expect_error=True)
assert (
+2
View File
@@ -115,6 +115,8 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configDbTxRollbackAll = True
, configAdminServerHost = "localhost"
, configAdminServerPort = Nothing
, configAdminServerUnixSocket = Nothing
, configAdminServerUnixSocketMode = 432
, configRoleSettings = mempty
, configRoleIsoLvl = mempty
, configInternalSCQuerySleep = Nothing
+2
View File
@@ -156,6 +156,8 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configDbTxRollbackAll = True
, configAdminServerHost = "localhost"
, configAdminServerPort = Nothing
, configAdminServerUnixSocket = Nothing
, configAdminServerUnixSocketMode = 432
, configRoleSettings = mempty
, configRoleIsoLvl = mempty
, configInternalSCQuerySleep = Nothing