This repository provides a Docker-containerized setup for running Handle Server software for development and testing purposes on your local system or in CI.
The Handle Server software itself is from http://www.handle.net/. For official documentation, production deployment guidance, and support, please refer to the Handle.Net documentation and support channels.
This Docker setup includes:
- Handle Server environment with PostgreSQL backend
- Automated configuration scripts that load settings from environment variables
- Development credentials and examples
Documentation included here:
- Markdown version of HANDLE.NET (Ver. 9) Technical Manual
- Markdown version of Release Notes Handle.Net Version 9.3.2 Software
Other sources of information:
- Documentation for the PyHandle library and its GitHub repo
The Handle Server requires PKCS#8-formatted RSA keys for encryption as documented in the Handle.Net Technical Manual, Section 1.4.1 "Types of Authentication". Generate them before configuring the environment.
Note for Windows users: Use Git Bash for OpenSSL commands, as OpenSSL is not available in PowerShell by default.
# Generate private key in default PKCS8 format
openssl genpkey -algorithm RSA -out mykey.pem
# Extract public key
openssl pkey -in mykey.pem -pubout -out mykey.pem.pubBefore starting the containers, configure any environment settings and provide the keys you generated in Step 1.
Add each on a single line to the docker-compose.yml file.
To convert the keys into the required format, run in Git Bash:
# Private key
openssl pkcs8 -topk8 -in mykey.pem -nocrypt | sed ':a;N;$!ba;s/\n/\\r\\n/g'
# Public key
openssl pkcs8 -topk8 -in mykey.pem -nocrypt | openssl pkey -pubout | sed ':a;N;$!ba;s/\n/\\r\\n/g'For more on overriding env variables see Environment variables in Compose.
| Config | Default | Required | Description |
|---|---|---|---|
| SERVER_PRIVATE_KEY_PEM | None | Yes | PEM PKCS8 format private key from Step 1 |
| SERVER_PUBLIC_KEY_PEM | None | Yes | PEM PKCS8 format public key from Step 1 |
All configuration values have sensible defaults. You only need to modify these if you want to customize the setup:
Interface Configuration
| Config | Default | Required | Description |
|---|---|---|---|
| BIND_ADDRESS | 0.0.0.0 | No | IP address to bind server interfaces to (HTTP, TCP, UDP) |
| NUM_THREADS | 15 | No | Number of threads for TCP/UDP interfaces |
| BIND_PORT | 2641 | No | Port for TCP/UDP Handle protocol |
| LOG_ACCESSES | yes | No | Log access requests to interfaces |
| HTTP_BIND_PORT | 8000 | No | Port for HTTP interface |
| HTTP_NUM_THREADS | 15 | No | Number of threads for HTTP interface |
| BACKLOG | None | No | Network backlog queue size |
| BIND_ADDRESS_V6 | None | No | IPv6 bind address |
| LISTEN_ADDRESS | None | No | Address to listen on (different from bind) |
| ALLOW_RECURSION | None | No | Allow recursive handle resolution |
Server Configuration
| Config | Default | Required | Description |
|---|---|---|---|
| SERVER_ADMINS | None | No | Space-separated list of handle admins e.g. "ADMIN1 ADMIN2 ADMIN3" |
| REPLICATION_ADMINS | None | No | Space-separated list of handle admins for replication e.g. "ADMIN1 ADMIN2 ADMIN3" |
| MAX_SESSION_TIME | 86400000 | No | Max authenticated client session time in milliseconds |
| THIS_SERVER_ID | 1 | No | An identifier for this handle server |
| MAX_AUTH_TIME | 60000 | No | Max time to wait for client to respond to authentication challenge in milliseconds |
| SERVER_ADMIN_FULL_ACCESS | yes | No | Admins listed in SERVER_ADMINS will have full permissions over all handles on the server |
| ALLOW_NA_ADMINS | no | No | Allow global handle server admins access to this handle server |
| TEMPLATE_NS_OVERRIDE | yes | No | Allow template namespace override |
| CASE_SENSITIVE | no | No | Are handles case-sensitive |
| AUTO_HOMED_PREFIXES | TEST | No | Space-separated list of prefixes that are auto-homed e.g., "0.NA/TEST PREFIX2" |
| MAX_HANDLES | None | No | Maximum number of handles the server will store |
| MAX_VALUES | None | No | Maximum number of values per handle |
| TRACE_RESOLUTION | None | No | Enable resolution tracing for debugging |
| ALLOW_LIST_HDLS | None | No | Allow listing handles under a prefix |
Protocol Settings
| Config | Default | Required | Description |
|---|---|---|---|
| NO_UDP_RESOLUTION | yes | No | Set to "no" to enable UDP protocol support |
Logging Configuration
| Config | Default | Required | Description |
|---|---|---|---|
| LOG_SAVE_INTERVAL | Never | No | How often to save logs (Never, Weekly, Monthly, etc.) |
| LOG_SAVE_DIRECTORY | logs | No | Directory to save log files |
Site Information
| Config | Default | Required | Description |
|---|---|---|---|
| HANDLE_HOST_IP | 0.0.0.0 | No | Public handle host IP used for siteinfo.json |
| SITE_DESCRIPTION | Handle Server | No | Description text for the server in siteinfo.json |
Storage Configuration
| Config | Default | Required | Description |
|---|---|---|---|
| STORAGE_TYPE | sql | No | Storage backend type - set to "sql" for PostgreSQL storage |
| SQL_URL | None | No | JDBC URL used to connect to the PostgreSQL database |
| SQL_DRIVER | org.postgresql.Driver | No | Java class that contains the driver for the JDBC connection |
| SQL_LOGIN | postgres | No | Username for database connection |
| SQL_PASSWD | None | No | Password for database connection |
| SQL_READ_ONLY | no | No | Boolean setting for allowing writes to database or not |
HTTP Configuration
| Config | Default | Required | Description |
|---|---|---|---|
| ALLOW_CORS | None | No | Enable CORS for HTTP interface |
| CORS_ORIGINS | None | No | Allowed CORS origins |
| HTTPS_ENABLED | None | No | Enable HTTPS support |
| HTTPS_PORT | None | No | Port for HTTPS interface |
| HTTP_LOG_FORMAT | None | No | HTTP access log format |
| MAX_REQUEST_SIZE | None | No | Maximum HTTP request size |
| SESSION_TIMEOUT | None | No | HTTP session timeout |
| ADMIN_PATH | None | No | Custom path for admin interface |
Start the containers from the project root:
docker compose up --buildNote: The handle_server container build process can take several minutes as it downloads and compiles Java components. To speed up subsequent startups, you can pre-build the handle_server image:
# Pre-build the handle_server image (recommended for faster startup)
docker compose build handle_server
# Then start normally
docker compose upWait for the containers to start up completely. You should see log messages indicating the Handle Server is ready and listening on ports 2641 (TCP/UDP) and 8000 (HTTP/HTTPS).
Test read access to existing handles:
# Read admin handle
curl -k "https://127.0.0.1:8000/api/handles/TEST/ADMIN?pretty=yes"
# Read any handle
curl -k "https://127.0.0.1:8000/api/handles/TEST/EXAMPLE001?pretty=yes"-k: Tell curl to accept self-signed SSL certificates?pretty=yes: Query parameter to get JSON-formatted response
More options are documented in the Handle.Net Technical Manual Section 14.1 Resources.
Inspect handles directly in the PostgreSQL database (for development only):
# Connect to PostgreSQL database
docker exec -it pid4cat-handle-dev-server-postgres-1 psql -U handleuser -d handledb
# View handles in readable format
SELECT encode(handle, 'escape') as handle_name, idx, type, encode(data, 'escape') as data_value
FROM handles WHERE handle LIKE decode('544553542f', 'hex') || '%' ORDER BY handle;For creating handles, authentication is required. The simplest and most reliable method is Basic Authentication as documented in the Handle.Net Technical Manual section 14.6.3 Basic Access Authentication.
This method uses standard HTTP Basic Authentication with percent-encoded username:
-
Create handle data file (
handle_data.json):{ "values": [ { "index": 1, "type": "URL", "data": { "format": "string", "value": "https://example.org/mydata" } }, { "index": 100, "type": "HS_ADMIN", "data": { "format": "admin", "value": { "handle": "TEST/ADMIN", "index": 300, "permissions": "011111110011" } } } ] } -
Create handle with curl:
curl --insecure -v -X PUT "https://127.0.0.1:8000/api/handles/TEST/MY_NEW_HANDLE" \ -H "Content-Type: application/json" \ -u "300%3ATEST%2FADMIN:ASECRETKEY" \ --data-binary "@handle_data.json"
Key points:
- Username
300:TEST/ADMINis percent-encoded as300%3ATEST%2FADMIN - Colon (
:) becomes%3A - Forward slash (
/) becomes%2F - Password is the secret key:
ASECRETKEY
- Username
-
Success response:
{"responseCode":1,"handle":"TEST/MY_NEW_HANDLE"}
This method is reliable, simple, and works consistently across different platforms and curl versions.
The PyHandle library provides a Python interface that implements the challenge-response framework with digital signatures (using HS_PUBKEY). This complex authentication mechanism cannot be used with curl alone. It is documented in the Handle.Net Technical Manual, Section 14.6.4 and the following sections.
# Activate virtual environment (if using one)
.\.venv\Scripts\Activate.ps1 # Windows PowerShell
# or
source .venv/bin/activate # Unix/Linux
# Run the example script
python examples/create_handle_examples.pyThe dockerized Handle Server provides a web-based administration interface for managing handles through a graphical user interface.
Access URL: https://127.0.0.1:8000/admin/
Login Credentials:
- Username:
300:TEST/ADMIN - Password:
ASECRETKEY
The web interface allows you to:
-
View existing handles
-
Create new handles
-
Modify handle values
-
Home/Unhome prefixes
Note: The web admin interface uses username/password authentication (HS_SECKEY) and does not require certificate files.
The dockerized Handle Server consists of three main components:
graph LR
A[Client<br/>curl/pyhandle/<br/>web browser] --> B[Handle Server<br/>HTTP API: 8000<br/>TCP/UDP: 2641]
B --> C[PostgreSQL<br/>Database<br/>Port 5432]
Component Details:
- PostgreSQL: Stores handles in
handlesandnastables with proper indexing - Handle Server: Java application that processes Handle protocol requests
- Configuration System: Jinja2 templates generate server config from environment variables
- Web Interface: Built-in admin GUI accessible via HTTPS
pid4cat-handle-dev-server/
├── docker-compose.yml # Container orchestration
├── handle-server/ # Handle server container
│ ├── Dockerfile # Container build instructions
│ ├── config/ # Configuration templates
│ │ ├── config.dct.jinja # Main server config Jinja2 template
│ │ └── siteinfo.json.jinja # Server metadata Jinja2 template
│ ├── scripts/ # Build and startup scripts
│ │ ├── create_config.py # Jinja2 config generation from env vars
│ │ └── handle.sh # Container startup script
│ └── storage/
│ └── init_db.sql # Database initialization
└── examples/ # Usage examples
├── create_handle_examples.py # PyHandle library demo
└── credentials.json # PyHandle credentials
The Handle Server configuration is generated dynamically from environment variables using Jinja2 templates:
- Environment Variables → Set in
docker-compose.yml - create_config.py Script → Reads env vars and processes Jinja2 templates
- Template Files →
.jinjafiles with{{ variable }}placeholders - Final Config → Generated
.dctand.jsonfiles used by server
flowchart TD
A[Environment Variables<br/>docker-compose.yml] --> C[create_config.py<br/>Script]
B[Jinja2 Templates<br/>config.dct.jinja<br/>siteinfo.json.jinja] --> C
C --> D[Generated Config<br/>config.dct<br/>siteinfo.json]
D --> E[Handle Server<br/>Startup]
| File | Purpose | Variables Used |
|---|---|---|
config.dct.jinja |
Main server settings | {{ BIND_ADDRESS }}, {{ SERVER_ADMINS }}, {{ SQL_URL }}, etc. |
siteinfo.json.jinja |
Server metadata for Handle protocol | {{ HANDLE_HOST_IP }}, {{ SERVER_PUBLIC_KEY_DSA_BASE64 }} |
- Container starts →
handle.shscript runs create_config.pyexecuted:python3 create_config.py /opt/handle/bin /var/handle/svr- Jinja2 templates processed → Final config files generated
- Keys converted from PEM to DSA format using
hdl-convert-key - Handle Server starts with generated configuration
The create_config.py script includes unit tests that can be run locally:
cd handle-server/scripts
python -m unittest test_create_config.pyTests cover configuration building, template rendering, and error handling using Python's standard library with minimal dependencies (requires jinja2).
Database Errors: relation "nas" does not exist
# Solution: Reset database and restart
docker compose down
docker volume rm pid4cat-handle-dev-server_postgres_data
docker compose up --buildFile Path Errors: No such file or directory
Check that file paths in docker-compose.yml match actual file locations after recent moves.
Configuration Not Applied
Environment variables are only read during container build. After changing docker-compose.yml, run:
docker compose up --buildSSL Certificate Warnings
Normal for development. Use the -k flag with curl: curl -k https://127.0.0.1:8000/...
# Essential operations
docker compose up --build # Start with rebuild
docker compose down # Stop all services
docker compose logs -f handle_server # View server logs
docker volume ls # List volumes
docker exec -it <container> bash # Access container shell
# Testing
curl -k "https://127.0.0.1:8000/api/handles/TEST/ADMIN?pretty=yes"
python examples/create_handle_examples.py- Docker and Docker Compose installed
- Remove old volume:
docker volume rm pid4cat-handle-dev-server_postgres_data(if restarting) - Start services:
docker compose up --build - Wait for PostgreSQL "ready to accept connections" message
- Test API:
curl -k "https://127.0.0.1:8000/api/handles/TEST/ADMIN?pretty=yes" - Access web admin: https://127.0.0.1:8000/admin/ (300:TEST/ADMIN / ASECRETKEY)
- TEST Prefix: Configured for local development and testing only. Handles created with TEST prefix are not globally resolvable. They are restricted to your local setup.
- Database Storage: All handles are stored in PostgreSQL with proper indexing and transaction support.
- Ports: Handle Server listens on 2641 (TCP/UDP native protocol) and 8000 (HTTP/HTTPS REST API).
- Security: Uses hardcoded development keys and credentials. Generate your own keys for production use.
datacite/docker-handle was very helpful to figure out details. Here we use more flexible Jinja2 templating, support only PostgreSQL for storage, and use the latest Handle.Net software (v9.3.2).
Created with assistance from Claude Sonnet 4 (Anthropic, accessed August 2025) and Gemini 2.5 Pro (Google, accessed August 2025) via the Claude Code command-line interface, GitHub Copilot in Visual Studio Code, and Warp. All AI-generated code was reviewed, tested, and validated by the authors.
