Databases AWS

Dgraph 25.5.1 on AWS User Guide

| Product: Dgraph 25.5.1 on Ubuntu 24.04

Overview

This guide covers launching and using the Dgraph 25.5.1 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened Neo4j & Graph and RDF Databases AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. Every product in the listing is described at /guides/graph-databases-aws/.

Dgraph is a distributed, horizontally scalable native graph database. It stores data as nodes and typed edges, answers deep graph traversals, full-text, regular-expression, geo and vector-indexed queries, and speaks two query languages: DQL (Dgraph Query Language) over HTTP and gRPC, and GraphQL, generated from a GraphQL schema you upload. One dgraph binary runs two services: Zero, which coordinates the cluster, and Alpha, which stores the data and serves queries. Dgraph is licensed under the Apache License, Version 2.0; since version 25 its access control lists (ACL), multi-tenancy, backup and restore features are open source too.

What cloudimg adds to the upstream build:

  • No default login. Dgraph ACL is switched on. On first boot the image generates a unique 32-character password for the groot administrator and a unique 256-bit key that signs Dgraph's access tokens. Dgraph's documented default (groot / password) is changed before the API is reachable from the network, and refused from then on. The key is read by Dgraph from a file, never from a command line.
  • Nothing runs without a login. Dgraph runs with anonymous=none: a caller who has not logged in may only call /login and /health. Every query, mutation, schema change, GraphQL request and admin call needs an access token.
  • HTTPS from the first boot. nginx serves the Dgraph HTTP and GraphQL APIs on port 443 with a certificate generated on the instance for its own hostname. Port 80 only redirects to HTTPS, and requests for any other hostname are dropped.
  • Every Dgraph port is loopback only. Zero (5080, 6080) and Alpha (7080, 8080, 9080) listen on 127.0.0.1; only SSH, 80 and 443 are reachable. nginx publishes only the API paths: Dgraph's /debug profiling endpoints and Zero's admin port are never exposed.
  • Sandboxed services. Zero and Alpha run as an unprivileged dgraph user in a systemd sandbox that can write only /var/lib/dgraph, cannot read system secrets such as /etc/shadow, the credentials file or the TLS key, and cannot reach the instance metadata service. Exports and backups therefore can only be written under /var/lib/dgraph.
  • Fully patched Ubuntu 24.04 at build time, with per-instance secrets and the cloudimg security defaults.

What is on the image: Ubuntu 24.04 LTS, Dgraph 25.5.1 (the official release binary, checksum-verified) in /opt/dgraph, all Dgraph data on a dedicated 30 GiB data volume at /var/lib/dgraph, nginx, and the cloudimg first-boot service. First boot also loads a tiny demo graph (three Person nodes joined by friend edges) so your first query returns data.

Connecting to your instance

Version SSH user API
Dgraph 25.5.1 on Ubuntu 24.04 ubuntu https://<public-dns>/

Prerequisites

  1. An AWS account and permission to launch EC2 instances
  2. An EC2 key pair in the Region you launch in
  3. A security group that allows TCP 22 from your own address, and TCP 443 (and optionally 80, which only redirects) from the clients that will use the API. The Dgraph ports 5080, 6080, 7080, 8080 and 9080 are never exposed: they listen on loopback only.

Recommended instance type: m5.large (2 vCPU, 8 GiB). Dgraph Alpha keeps hot data in memory; choose a larger instance for bigger graphs.

Step 1: Launch the instance

Option A: AWS Marketplace console

  1. Open the cloudimg Hardened Neo4j & Graph and RDF Databases AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
  2. Choose Launch your software, then in Version choose Dgraph 25.5.1 on Ubuntu 24.04 - YYYY-MM-DD and your Region.
  3. Instance type: m5.large is recommended.
  4. Key pair: choose your EC2 key pair.
  5. Security group: TCP 22 from your own address; TCP 443 (and 80) from your clients.
  6. Launch. First boot takes under a minute after the instance is reachable, while it generates this instance's certificate, signing key and groot password and loads the demo graph.

Option B: AWS CLI

Run this on your own workstation. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the key pair, security group and subnet with your own:

aws ec2 run-instances \
  --image-id ami-0123456789abcdef0 \
  --instance-type m5.large \
  --key-name my-key-pair \
  --security-group-ids sg-0123456789abcdef0 \
  --subnet-id subnet-0123456789abcdef0 \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=dgraph}]'

The AMI already requires IMDSv2, so you do not need --metadata-options. The data volume is created from the AMI automatically.

Step 2: Connect via SSH

ssh -i my-key-pair.pem ubuntu@<vm-ip>

The login banner shows the API and GraphQL URLs for this instance.

Step 3: Check the services and health

Confirm Zero, Alpha and nginx are running, first boot has finished, the health endpoint answers over HTTPS and the installed version is 25.5.1:

systemctl is-active dgraph-zero.service dgraph-alpha.service nginx.service
test -f /var/lib/cloudimg/dgraph-firstboot.done && echo first-boot-done
curl -sk -o /dev/null -w 'healthz: HTTP %{http_code}\n' https://localhost/healthz
/opt/dgraph/bin/dgraph version 2>/dev/null | grep -E 'Dgraph version|Go version'

All three services report active, the first-boot marker is present, /healthz returns HTTP 200 and the binary reports v25.5.1 built with go1.27.3.

Every Dgraph port is on loopback; nginx owns the public ports:

sudo ss -tlnp | grep -E ':(5080|6080|7080|8080|9080|80|443) '

You see dgraph on 127.0.0.1 ports 5080 and 6080 (Zero) and 7080, 8080 and 9080 (Alpha), and nginx on ports 80 and 443.

Terminal showing dgraph-zero, dgraph-alpha and nginx active, the first-boot marker, healthz HTTP 200, Dgraph v25.5.1 built with go1.27.3, and every Dgraph port listening on 127.0.0.1 with nginx on 80 and 443

Step 4: Read the per-instance credentials

The groot password is unique to this instance and is written to a root-only file at first boot. Print only the field names, never the password itself:

sudo grep -oE '^[A-Z_]+=' /root/dgraph-credentials.txt

This lists DGRAPH_HOST, DGRAPH_URL, DGRAPH_GRAPHQL_URL, DGRAPH_ADMIN_URL, DGRAPH_GROOT_USER (groot) and DGRAPH_GROOT_PASSWORD. Read the password with sudo sed -n 's/^DGRAPH_GROOT_PASSWORD=//p' /root/dgraph-credentials.txt when you need it, and keep it safe: groot is the Dgraph administrator (member of the guardians group). Dgraph stores only a bcrypt hash of it.

Step 5: The authentication gate

Dgraph answers a refused request with HTTP 200 and an errors body, so look at the message. A query without a token is refused, and so are a wrong password and Dgraph's documented default password:

curl -sk -H 'Content-Type: application/dql' -X POST https://localhost/query \
  -d '{ q(func: has(name)) { uid } }' | grep -o 'requires an identified caller'
printf '{"userid":"groot","password":"%s"}' 'wrong-password' | curl -sk -X POST --data-binary @- https://localhost/login | grep -o 'invalid username or password'
printf '{"userid":"groot","password":"%s"}' 'password' | curl -sk -X POST --data-binary @- https://localhost/login | grep -o 'invalid username or password'

Each line prints the refusal. Log in with the per-instance password to get an access token. The password is sent on standard input, so it never appears in the process list or your shell history:

PW=$(sudo sed -n 's/^DGRAPH_GROOT_PASSWORD=//p' /root/dgraph-credentials.txt)
JWT=$(printf '{"userid":"groot","password":"%s"}' "$PW" | curl -sk -X POST --data-binary @- https://localhost/login \
  | sed -n 's/.*"accessJWT":"\([^"]*\)".*/\1/p')
echo "access token received: ${#JWT} characters"

Send the token as the X-Dgraph-AccessToken header on every call. Access tokens last 6 hours; the /login response also carries a refreshJWT you can exchange for a new access token.

Terminal showing an anonymous query refused because it requires an identified caller, a wrong password and the default groot password both refused with invalid username or password, and a login with the per instance password returning an access token

Step 6: Query the demo graph with DQL

PW=$(sudo sed -n 's/^DGRAPH_GROOT_PASSWORD=//p' /root/dgraph-credentials.txt)
JWT=$(printf '{"userid":"groot","password":"%s"}' "$PW" | curl -sk -X POST --data-binary @- https://localhost/login \
  | sed -n 's/.*"accessJWT":"\([^"]*\)".*/\1/p')
curl -sk -H "X-Dgraph-AccessToken: $JWT" -H 'Content-Type: application/dql' -X POST https://localhost/query \
  -d '{ people(func: type(Person), orderasc: name) { name age friend { name } } }' \
  | python3 -c 'import json,sys; print(json.dumps(json.load(sys.stdin)["data"], indent=1))'

The answer lists Alice (31), Bob (27) and Carol (35), each with the friend their friend edge points to.

Step 7: Write your own data

Add to the schema, write nodes with an RDF mutation and query them back. /alter changes the schema, /mutate?commitNow=true writes and commits in one call:

PW=$(sudo sed -n 's/^DGRAPH_GROOT_PASSWORD=//p' /root/dgraph-credentials.txt)
JWT=$(printf '{"userid":"groot","password":"%s"}' "$PW" | curl -sk -X POST --data-binary @- https://localhost/login \
  | sed -n 's/.*"accessJWT":"\([^"]*\)".*/\1/p')
curl -sk -H "X-Dgraph-AccessToken: $JWT" -X POST https://localhost/alter \
  -d 'title: string @index(term) .
      directed_by: [uid] .'; echo
curl -sk -H "X-Dgraph-AccessToken: $JWT" -H 'Content-Type: application/rdf' -X POST 'https://localhost/mutate?commitNow=true' \
  -d '{ set {
        _:film <title> "The Graph Story" .
        _:film <directed_by> _:director .
        _:director <name> "Dana Director" .
      } }' | grep -o '"code":"Success"'
curl -sk -H "X-Dgraph-AccessToken: $JWT" -H 'Content-Type: application/dql' -X POST https://localhost/query \
  -d '{ films(func: anyofterms(title, "graph")) { title directed_by { name } } }' \
  | python3 -c 'import json,sys; print(json.dumps(json.load(sys.stdin)["data"], indent=1))'

The schema change returns "code":"Success", the mutation commits, and the query finds the film by a word in its title and follows the directed_by edge to its director.

Terminal showing a DQL round trip over HTTPS with an access token: the demo graph of Alice, Bob and Carol with their friends, a schema alter returning Success, an RDF mutation committing a film and its director, and a term query traversing the directed_by edge

Step 8: GraphQL

Dgraph generates a complete GraphQL API from a GraphQL schema. Upload one to /admin/schema, then query and mutate at /graphql with the same access token:

PW=$(sudo sed -n 's/^DGRAPH_GROOT_PASSWORD=//p' /root/dgraph-credentials.txt)
JWT=$(printf '{"userid":"groot","password":"%s"}' "$PW" | curl -sk -X POST --data-binary @- https://localhost/login \
  | sed -n 's/.*"accessJWT":"\([^"]*\)".*/\1/p')
curl -sk -H "X-Dgraph-AccessToken: $JWT" -X POST https://localhost/admin/schema \
  --data-binary 'type Task { id: ID! title: String! @search(by: [term]) done: Boolean }' | grep -o '"code":"Success"'
sleep 3
curl -sk -H "X-Dgraph-AccessToken: $JWT" -H 'Content-Type: application/json' -X POST https://localhost/graphql \
  -d '{"query":"mutation { addTask(input: [{title: \"Try Dgraph GraphQL\", done: false}]) { task { title done } } }"}'; echo
curl -sk -H "X-Dgraph-AccessToken: $JWT" -H 'Content-Type: application/json' -X POST https://localhost/graphql \
  -d '{"query":"{ queryTask { title done } }"}'; echo

The schema upload returns "code":"Success"; the mutation and the query both return the Try Dgraph GraphQL task. GraphQL subscriptions work over WebSocket at the same /graphql path for same-origin clients.

Step 9: Where things live

Component Path
Dgraph binary (read-only, root-owned) /opt/dgraph/bin/dgraph
Units dgraph-zero.service, dgraph-alpha.service (/etc/systemd/system/)
ACL signing key (per instance, mode 0640 root:dgraph) /etc/dgraph/acl-secret
Data volume /var/lib/dgraph
Zero write-ahead log /var/lib/dgraph/zw
Alpha postings, write-ahead log, temporary files /var/lib/dgraph/p, /var/lib/dgraph/w, /var/lib/dgraph/t
Exports /var/lib/dgraph/export
Logs journalctl -u dgraph-alpha -u dgraph-zero
Credentials file /root/dgraph-credentials.txt (mode 0600)
Per-instance certificate /etc/ssl/dgraph/
nginx site /etc/nginx/sites-available/cloudimg-dgraph
Licence /usr/share/doc/dgraph/LICENSE.txt

The data volume is mounted by filesystem UUID:

findmnt /var/lib/dgraph
df -h /var/lib/dgraph

To change a Dgraph flag (for example Alpha's cache size), run sudo systemctl edit dgraph-alpha.service, override ExecStart= and restart the service. Keep --bindall=false, --acl "secret-file=/etc/dgraph/acl-secret" and --security "whitelist=127.0.0.1; anonymous=none" so Dgraph stays on loopback with ACL enforced.

Step 10: gRPC clients and Ratel

The gRPC port (9080) used by the official clients (dgo, pydgraph, dgraph-js) stays on loopback. Reach it, and Alpha's HTTP port, through an SSH tunnel from your workstation:

ssh -i my-key-pair.pem -N -L 9080:127.0.0.1:9080 -L 8080:127.0.0.1:8080 ubuntu@<vm-ip>

Point a client at localhost:9080 and log in as groot with the per-instance password (ACL applies on gRPC too). Dgraph's Ratel query UI is a separate project and is not installed; if you run Ratel on your workstation, connect it to http://localhost:8080 through the same tunnel.

Step 11: Create an application user

Use a dedicated user with only the permissions it needs instead of groot. Create a user and a group, and give the group read access to the name predicate (replace the password with a strong one of your own):

PW=$(sudo sed -n 's/^DGRAPH_GROOT_PASSWORD=//p' /root/dgraph-credentials.txt)
JWT=$(printf '{"userid":"groot","password":"%s"}' "$PW" | curl -sk -X POST --data-binary @- https://localhost/login \
  | sed -n 's/.*"accessJWT":"\([^"]*\)".*/\1/p')
curl -sk -H "X-Dgraph-AccessToken: $JWT" -H 'Content-Type: application/json' -X POST https://localhost/admin \
  -d '{"query":"mutation { addGroup(input: [{name: \"readers\", rules: [{predicate: \"name\", permission: 4}]}]) { group { name } } }"}'; echo
curl -sk -H "X-Dgraph-AccessToken: $JWT" -H 'Content-Type: application/json' -X POST https://localhost/admin \
  -d '{"query":"mutation { addUser(input: [{name: \"app\", password: \"your-strong-password\", groups: [{name: \"readers\"}]}]) { user { name } } }"}'; echo

Permission 4 is read, 2 write and 1 modify (schema); add them up for combined rights. Change groot's own password the same way with updateUser(input: {filter: {name: {eq: "groot"}}, set: {password: "..."}}), then update /root/dgraph-credentials.txt.

Step 12: Exports and backups

Export the whole database as RDF (or json) into /var/lib/dgraph/export:

PW=$(sudo sed -n 's/^DGRAPH_GROOT_PASSWORD=//p' /root/dgraph-credentials.txt)
JWT=$(printf '{"userid":"groot","password":"%s"}' "$PW" | curl -sk -X POST --data-binary @- https://localhost/login \
  | sed -n 's/.*"accessJWT":"\([^"]*\)".*/\1/p')
curl -sk -H "X-Dgraph-AccessToken: $JWT" -H 'Content-Type: application/json' -X POST https://localhost/admin \
  -d '{"query":"mutation { export(input: {format: \"rdf\"}) { response { code message } } }"}'; echo
sudo ls /var/lib/dgraph/export

The call returns "code":"Success" and a dgraph.r... directory appears with the RDF and schema files. Copy exports off the instance (for example to Amazon S3), and snapshot the /var/lib/dgraph EBS volume for point-in-time backups. Dgraph's backup mutation (full and incremental binary backups) also writes only under /var/lib/dgraph on this image, or to an S3 or MinIO destination you configure.

Step 13: Serve under your own DNS name

Point a DNS record at the instance, then regenerate the certificate and pin the served hostname:

sudo dgraph-set-hostname dgraph.example.com

This issues a new self-signed certificate for that name (replace it with your own CA- or ACM-issued certificate for production) and makes nginx answer only for it. For a public, browser-trusted certificate, terminate TLS at an Application Load Balancer or CloudFront in front of the instance and keep the security group's 443 open only to that front.

Security notes

Dgraph 25.5.1 is the latest release at the time of writing; it includes the fix for the unauthenticated Zero admin endpoint advisory (GHSA-qj98-gp63-54xj) and every earlier Dgraph security fix. On top of that, this image:

  • switches on Dgraph ACL with a per-instance signing key and groot password, and anonymous=none, so nothing but /login and /health works without a token;
  • keeps every Dgraph port on loopback and publishes only the API paths through nginx; /debug returns 403 and Zero's admin API is never exposed;
  • runs Zero and Alpha as an unprivileged user in a systemd sandbox: they can write only /var/lib/dgraph, and /root, /etc/shadow, /etc/ssh, the credentials file and the TLS key are not visible to them;
  • blocks the instance metadata service (169.254.169.254) for Zero, Alpha and nginx, so a URL in a GraphQL @custom resolver, a lambda URL or a backup destination can never reach the instance's credentials;
  • refuses cross-origin state-changing requests and WebSocket upgrades at nginx, and logs the real client address.

The debug endpoints are not published, and the metadata block is part of each service's unit:

curl -sk -o /dev/null -w 'debug endpoint via nginx: HTTP %{http_code}\n' https://localhost/debug/vars
systemctl show -p IPAddressDeny dgraph-zero.service dgraph-alpha.service nginx.service

nginx answers HTTP 403, and all three units deny 169.254.169.254 and 168.63.129.16 (plus the IPv6 metadata address).

An export into a system directory is refused by the sandbox:

PW=$(sudo sed -n 's/^DGRAPH_GROOT_PASSWORD=//p' /root/dgraph-credentials.txt)
JWT=$(printf '{"userid":"groot","password":"%s"}' "$PW" | curl -sk -X POST --data-binary @- https://localhost/login \
  | sed -n 's/.*"accessJWT":"\([^"]*\)".*/\1/p')
curl -sk -H "X-Dgraph-AccessToken: $JWT" -H 'Content-Type: application/json' -X POST https://localhost/admin \
  -d '{"query":"mutation { export(input: {format: \"rdf\", destination: \"/root/stolen\"}) { response { code } } }"}' \
  | grep -o 'resolving export failed because task failed'
sudo test -e /root/stolen || echo 'nothing was written to /root'

Dgraph reports that the export task failed and nothing is written.

Terminal showing nginx returning 403 for the Dgraph debug endpoint, the instance metadata service unreachable from inside the dgraph-alpha service, and an export into /root refused by the sandbox with nothing written

Keep the instance patched (sudo apt-get update && sudo apt-get upgrade), keep the groot password safe, export or snapshot /var/lib/dgraph regularly, and expose only ports 22 and 443 to the networks that need them.

Support

cloudimg provides 24/7 technical support for every version in this listing by email and live chat, 365 days a year, with a 24-hour response SLA: support@cloudimg.co.uk.

Trademarks

Dgraph is a trademark of its respective owner. cloudimg is not affiliated with or endorsed by the Dgraph project or its maintainers.