Dgraph 25.5.1 on AWS User Guide
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
grootadministrator 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/loginand/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/debugprofiling endpoints and Zero's admin port are never exposed. - Sandboxed services. Zero and Alpha run as an unprivileged
dgraphuser 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
- An AWS account and permission to launch EC2 instances
- An EC2 key pair in the Region you launch in
- 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
- 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.
- Choose Launch your software, then in Version choose Dgraph 25.5.1 on Ubuntu 24.04 - YYYY-MM-DD and your Region.
- Instance type:
m5.largeis recommended. - Key pair: choose your EC2 key pair.
- Security group: TCP 22 from your own address; TCP 443 (and 80) from your clients.
- 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.

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.

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.

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/loginand/healthworks without a token; - keeps every Dgraph port on loopback and publishes only the API paths through nginx;
/debugreturns 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
@customresolver, 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.

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.