
Ory Hydra is an open-source OAuth 2.0 Authorization Server and OpenID Connect (OIDC) provider. Unlike identity platforms that bundle user management, Hydra delegates authentication to a separate login and consent application that you control. It handles the OAuth 2.0 authorization code flow, client credentials flow, token introspection, and token revocation through an API-first architecture that separates the authorization protocol from identity storage.
This article explains how to deploy Ory Hydra on a Linux server using Docker Compose with PostgreSQL, Nginx, and a Java-based login and consent application.
Before you begin, you need to:
hydra.example.com).Ory Hydra reads its configuration from a YAML file and reads database credentials and secrets from environment variables at runtime. The directory structure separates the Hydra configuration, the Nginx reverse proxy configuration, and the TLS certificate storage into distinct paths.
Create the project directory with all required subdirectories.
The command creates three subdirectories:
config/: Stores the Hydra configuration file and the Nginx server configuration.data/postgres/: Persists PostgreSQL database files across container restarts.data/certbot/conf/: Stores the Let's Encrypt TLS certificate files.Navigate to the project directory.
Clone the Java reference login and consent application into the project directory.
If you have your own application that implements the Hydra login and consent protocol, skip this step and replace ./reference-app in the Docker Compose file with your application's source directory.
Generate a system secret for signing tokens and encrypting database records. Hydra requires a secret of at least 16 characters.
Run this command twice and copy both output strings. You use the first value as YOUR_SYSTEM_SECRET and the second as YOUR_PAIRWISE_SALT (the OIDC pairwise subject identifier salt in the configuration file).
The system secret is written into hydra.yml for reference, but the SECRETS_SYSTEM environment variable defined in .env takes precedence at runtime because Docker Compose passes it directly to the Hydra container. Replace YOUR_SYSTEM_SECRET in both files with the same generated value to keep them consistent.
Create the Hydra configuration file.
Add the following content. Replace hydra.example.com with your domain name, YOUR_SYSTEM_SECRET with the first generated value, and YOUR_PAIRWISE_SALT with the second.
Save and close the file.
serve.public: Sets the public API base URL to the HTTPS path that matches the Nginx reverse proxy. The CORS section restricts cross-origin requests to your domain and specifies the allowed HTTP methods and headers, and permits credentials so the browser includes session cookies with API requests.serve.admin: Declares the admin API base URL, used by Hydra when generating self-referencing links in admin responses. The actual network restriction is enforced by the Docker Compose port binding (127.0.0.1:4445:4445), which limits the admin API to the loopback interface and prevents access from the public network.urls: Tells Hydra where to redirect users during the login and consent steps of the authorization flow. These paths are served by the login and consent app in the Docker Compose stack.secrets.system: A secret used to sign access tokens and encrypt sensitive records in the database. Changing this value invalidates all existing tokens.oidc.subject_identifiers: Enables both public and pairwise subject identifier types. Public identifiers return the same user ID to all clients. Pairwise identifiers generate a unique, unlinkable user ID per client, which prevents client correlation.strategies.access_token: Sets the token format to opaque, meaning tokens are random strings that must be validated through the introspection endpoint. The alternative is jwt, which allows stateless validation but cannot be revoked before expiry.ttl: Configures token lifetimes. Access tokens expire after 1 hour. Refresh tokens expire after 720 hours (30 days). Authorization codes expire after 10 minutes.log: Sets the log level to info and disables sensitive value exposure to prevent tokens and secrets from appearing in log output.The admin API base_url uses http://127.0.0.1:4445/ intentionally. The admin API accepts and rejects login and consent requests without authentication, and must never be exposed through the public reverse proxy.
Create the Nginx configuration file.
Add the following content. Replace all instances of hydra.example.com with your actual domain name.
Save and close the file.
The first server block listens on port 80 and redirects all HTTP traffic to HTTPS. The second server block listens on port 443 with TLS and contains three location blocks:
location = /callback: Serves the static callback HTML page from the Nginx container's filesystem. Nginx handles this directly without proxying to any upstream service.location ~ ^/(login|consent|logout|demo): Routes login, consent, logout, and demo requests to the login and consent app on port 8080. Hydra redirects users to these paths during the authorization flow to perform authentication and scope approval.location /: Routes all other requests to the Hydra public API on port 4444. This covers the OAuth 2.0 authorization endpoint, token endpoint, token revocation endpoint, and the OpenID Connect discovery document at /.well-known/openid-configuration. Because all services run on the same Docker network, Nginx resolves the container hostname hydra through Docker's internal DNS. The resolver 127.0.0.11 valid=30s directive tells Nginx to use Docker's internal DNS resolver, which is required when using variables in proxy_pass directives. No traffic flows through the host network for these connections.
The X-Forwarded-Proto header on all proxy blocks ensures Hydra recognizes that the original client connection uses HTTPS, which is required for secure redirect generation and cookie handling.
Create the callback page that displays the authorization code after a successful OAuth 2.0 flow.
Add the following content.
Save and close the file.
The callback page reads the code and error query parameters from the URL using the JavaScript URLSearchParams API and renders the result using Vue. On a successful flow, it displays the authorization code and a ready-to-run curl command to exchange it for tokens. On a failed flow, it displays the error and reason returned by Hydra.
Create the environment variables file.
Add the following content. Replace EXAMPLE_DB_PASSWORD with a strong, unique database password, and replace YOUR_SYSTEM_SECRET with the same value you placed in config/hydra.yml.
Save and close the file.
Docker Compose manages all services as a single deployment unit. Certbot runs once as a standalone container to obtain the initial TLS certificate before Nginx starts, which requires port 80 to be free at that point.
Create the Docker Compose file.
Add the following content.
Save and close the file.
postgres: A PostgreSQL 16 database that stores OAuth 2.0 clients, authorization codes, tokens, and consent records. The volume mounted at ./data/postgres persists data across container restarts. The healthcheck polls pg_isready every 5 seconds so that dependent services wait for the database to accept connections before starting.hydra-migrate: A one-time initialization service that runs database migrations to create the required schema tables. It waits for the postgres healthcheck to pass before running, then exits with code 0 after migrations complete. The hydra service waits for this container to exit before starting.hydra: The main Ory Hydra server. The serve all command starts both the public API on port 4444 and the admin API on port 4445. The admin port is bound to 127.0.0.1 on the host via the ports binding (127.0.0.1:4445:4445), which restricts access to the loopback interface and prevents exposure on public network interfaces. The public API on port 4444 is accessible only through the Docker network, where Nginx routes external traffic. The SECRETS_SYSTEM environment variable overrides the value in hydra.yml at runtime, so the secret Hydra actually uses comes from the environment rather than the configuration file.hydra-login-consent: The Java-based reference login and consent application. It builds from the ./reference-app source using a multi-stage Gradle and Eclipse Temurin Docker build. It provides the /login, /consent, and /demo endpoints that users interact with during the authorization flow. Because it uses network_mode: "service:hydra", it shares the Hydra container's network namespace and reaches the admin API at 127.0.0.1:4445.nginx: An Nginx reverse proxy that terminates TLS and routes external traffic to the Hydra public API and the login and consent app. It is the only service that exposes ports to the host (80 and 443), serving as the single entry point for all external traffic. All services connect to a shared hydra-network bridge network, which allows inter-container communication using service names as hostnames. Each service includes a deploy.resources.limits block that caps CPU and memory usage, preventing a misbehaving container from starving other services on the host.
The PostgreSQL Data Source Name (DSN) uses sslmode=disable because the database connection travels over the internal Docker bridge network, isolated from external traffic. If you move PostgreSQL to a separate host, change sslmode=disable to sslmode=require and configure TLS certificates on the PostgreSQL server.
Run Certbot as a Docker container in standalone mode to get a TLS certificate from Let's Encrypt. Replace admin@example.com with your email address and hydra.example.com with your domain name.
Certbot starts a temporary web server on port 80, completes the ACME domain verification challenge, and saves the certificate files. After verification, the container exits and removes itself.
Check that the certificate files exist. Replace hydra.example.com with your domain name.
Verify that the output lists fullchain.pem, privkey.pem, chain.pem, cert.pem, and README. These files are mounted into the Nginx container as a read-only volume, which the Nginx configuration references for TLS termination.
Create the certificate renewal script.
Add the following content.
Save and close the file, then make it executable.
Open the crontab editor. If this is the first time you run crontab -e, the system prompts you to select an editor from a numbered list. Enter the number corresponding to /bin/nano or your preferred editor.
Add the following line to run the renewal script at 3:00 a.m. on the 1st and 15th of every month.
Save and close the file. The cron job stops Nginx, runs Certbot in non-interactive renewal mode, and restarts Nginx. Let's Encrypt certificates expire after 90 days, so running the cron job twice a month for renewal keeps the certificate updated. Output and errors are appended to renew-cert.log for troubleshooting.
Certbot only renews certificates that expire within 30 days, so running the script twice a month is safe. Running on two separate dates means a transient failure on the 1st is retried on the 15th, rather than waiting a full month.
Start all services in detached mode.
Wait about 30 seconds for the database to initialize and migrations to complete.
Check the status of all containers.
Verify that postgres, hydra, hydra-login-consent, and nginx all show Up, and that hydra-migrate shows Exited (0). The exit code 0 confirms that database migrations completed successfully.
Check the Hydra logs to confirm that the server started without errors.
Verify that the output contains two lines showing Hydra listening on 0.0.0.0:4444 and 0.0.0.0:4445. If you see database connection errors, wait a few seconds and restart the Hydra container.
For more information on managing Docker Compose stacks, see the How To Use Docker Compose article.
Hydra exposes a public API on port 4444 for OAuth 2.0 and OIDC requests, and an admin API on port 4445 for client management. The admin API is only accessible from the server through the loopback port binding. OAuth 2.0 clients in Hydra represent applications that are authorized to request tokens on behalf of users. The reference login and consent application deployed in the previous section handles the /login and /consent steps during the authorization code flow.
Test the Hydra admin API health endpoint from the server.
A successful response returns {"status":"ok"}, confirming that the admin API is healthy.
Test the public API through the Nginx HTTPS reverse proxy. Replace hydra.example.com with your domain name.
A successful response returns {"status":"ok"}, confirming that Nginx routes requests to the Hydra public API.
Verify that the OpenID Connect discovery document is accessible. Replace hydra.example.com with your domain name.
Verify that the response contains a JSON document with the issuer, authorization_endpoint, token_endpoint, and jwks_uri fields. A valid response confirms that Hydra's OIDC provider is operational and reachable through the reverse proxy.
Register a new OAuth 2.0 client using the Hydra CLI inside the running container. Replace hydra.example.com with your domain name.
The response is a JSON object containing the registered client. Copy the client_id and client_secret values.
The offline_access scope requests a refresh token alongside the access token, allowing the client to get new access tokens without prompting the user again. The openid scope triggers Hydra to issue an ID token identifying the authenticated user.
Open a web browser and navigate to the authorization URL below. Replace both instances of hydra.example.com with your domain name, and replace CLIENT_ID with the client_id from the previous step.
Hydra checks for an active session and, finding none, redirects the browser to the login page.
On the login page, enter the demo credentials below and click Log in. The login app accepts the challenge through the Hydra admin API and redirects the browser to the consent page.
foo@bar.compasswordOn the consent page, review the requested scopes and click Allow access. The consent app accepts the challenge and Hydra redirects the browser to https://hydra.example.com/callback with the authorization code in the code query parameter.
The callback page displays the authorization code and a ready-to-run curl command to exchange it for tokens. Replace YOUR_CLIENT_ID and YOUR_CLIENT_SECRET in the displayed command with the values from the client registration step, then run the command from your server terminal.
A successful response returns a JSON object containing an access_token, refresh_token, and id_token. Copy the access_token value.
Introspect the access token to confirm that it is active. Replace ACCESS_TOKEN with the access token from the previous step.
A valid, active token returns a JSON response with "active": true, the sub field containing the authenticated user's identifier, the scope field listing the granted scopes, and token metadata including iss, iat, and exp. A resource server calls this endpoint to validate an opaque token and retrieve its claims without sharing any secrets or decoding a JWT.
You have deployed Ory Hydra on a Linux server using Docker Compose. The admin API is bound to the loopback interface and never exposed through the public reverse proxy. For configuration covering identity provider integration, custom token claims, and high-availability deployments, refer to the official Ory Hydra documentation.
0 Comments
Be the first to comment and share your perspective with the community.