
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 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 the following subdirectories:
config/: Stores the Hydra and Nginx configuration files.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 as a standalone container to obtain the initial TLS certificate before Nginx starts. Port 80 must be available during certificate issuance.
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 complete successfully 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, wh
0 Comments
Be the first to comment and share your perspective with the community.