PostGraphile 5.2.1 on AWS User Guide
Overview
This guide covers launching and using the PostGraphile 5.2.1 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg developer and API tools listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
PostGraphile reads a PostgreSQL schema and serves a complete GraphQL API for it: tables, views, relations and functions become queries and mutations, with no resolvers to write. The Ruru IDE (PostGraphile's edition of GraphiQL) lets you explore and run that API in the browser. This image runs PostGraphile 5.2.1 on Node.js 24 LTS against PostgreSQL 16 on the same instance, with a small demo schema (authors, posts, a view and a search function) so that you have something to query straight away.
The cloudimg image runs PostGraphile as a private service:
- HTTPS from the first boot. nginx serves PostGraphile on port 443 with a certificate generated on your instance's first boot. Port 80 only redirects to HTTPS, and a request for any name other than your instance's own gets no answer.
- A password on everything. The Ruru IDE, the
/graphqlendpoint and the IDE's files all require the useradminand a password generated on your instance's first boot. There is no default password. The password is stored as a bcrypt hash; the plain text is only in a root-only file (Step 4). - Cross-site requests are refused. Because a browser re-sends a saved password to any site that asks, nginx refuses any request that changes data unless it is JSON, and refuses any request from a web page on another site (Step 8). A page you visit elsewhere cannot run a mutation with your password.
- A least-privilege database role with no password. PostGraphile connects to PostgreSQL over its local socket as the role
postgraphile_public, which can read and write the demo tables and nothing else: it is not a superuser, cannot create anything, cannot read server files or run programs, and has no password because PostgreSQL identifies it by its Linux user (Step 10). The database itself is created on your instance's first boot: the image ships an empty one. - Production settings, checked on every start. PostGraphile listens on loopback only, with watch mode, SQL
EXPLAINoutput, GraphQL over GET and websockets off, errors masked to an id, a 100 kB request limit, a query depth limit and time limits. A configuration check refuses to start PostGraphile if any of these is turned off by mistake (Step 11). - No outbound connections. PostGraphile and PostgreSQL cannot open a network connection to anything, the EC2 instance metadata service included.
- Rate limits that cannot be dodged. Requests are limited per client address, and nginx always replaces the
X-Forwarded-Forheader with the address it accepted the connection from, so a client cannot pretend to be someone else to escape the limit. - Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: PostGraphile 5.2.1 (installed from npm with a pinned lockfile, every package checked against its published integrity hash), Node.js 24.22.2 LTS (the official nodejs.org build, checked against its published SHA-256), PostgreSQL 16, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. npm itself is not on the image. The build record and the MIT licences of PostGraphile and Node.js are in /usr/share/doc/cloudimg-postgraphile/.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet.
- A security group that allows TCP 443 (PostGraphile and the Ruru IDE), TCP 80 (redirects to HTTPS) and TCP 22 (SSH). Open 443 to the networks your users and applications connect from; keep 22 open to your own networks only.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick PostGraphile 5.2.1 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (a t3.medium is enough to try it out), your key pair, your subnet and the security group above.
With the AWS CLI (replace the values in angle brackets):
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
--instance-type m5.large --key-name <your-key-pair> \
--security-group-ids <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=postgraphile}]'
An Elastic IP keeps the server's address, and so its name and certificate, the same across stops and starts.
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| PostGraphile 5.2.1 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot takes under a minute: it creates the PostgreSQL database and the demo schema, sets the server's name, generates the certificate and the admin password, and only then starts PostGraphile and nginx. Check that everything is running:
systemctl is-active postgraphile-firstboot postgraphile-egress-guard postgresql@16-main postgraphile nginx \
&& sudo test -f /var/lib/cloudimg/postgraphile-firstboot.done && echo FIRSTBOOT_DONE
active
active
active
active
active
FIRSTBOOT_DONE
Only SSH and nginx listen on the network. PostGraphile (a Node.js process, which names its main thread MainThread) listens on the loopback address 127.0.0.1:5000 behind nginx, and PostgreSQL has no network port at all:
sudo ss -lntp | awk 'NR>1 && $4 !~ /^127\.0\.0\.5[34]/ {print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u | tee /dev/stderr \
| awk '$1 !~ /^127\./ {sub(/.*:/, "", $1); print $1}' | sort -un | xargs | grep -x '22 80 443' >/dev/null && echo "network listeners: 22 80 443"
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
127.0.0.1:5000 users:(("MainThread"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))
network listeners: 22 80 443
Step 4: Get your address and password
Run sudo cat /var/lib/cloudimg/postgraphile-credentials.txt to see your addresses and the admin password. The file is readable by root only and holds these settings, all generated for this instance:
sudo sed -n 's/^\(POSTGRAPHILE_[A-Z0-9_]*\)=.*/\1/p' /var/lib/cloudimg/postgraphile-credentials.txt | tee /dev/stderr \
| grep -x POSTGRAPHILE_PASSWORD >/dev/null && echo "credentials present"
POSTGRAPHILE_URL
POSTGRAPHILE_GRAPHQL_URL
POSTGRAPHILE_HOST
POSTGRAPHILE_USERNAME
POSTGRAPHILE_PASSWORD
POSTGRAPHILE_TLS_CERT
POSTGRAPHILE_TLS_SHA256_FINGERPRINT
POSTGRAPHILE_DATABASE
credentials present
POSTGRAPHILE_URL is https://<your instance's public DNS name>/, where the Ruru IDE opens, and POSTGRAPHILE_GRAPHQL_URL is the GraphQL endpoint your applications call. The settings that are not secret can be shown on their own:
sudo grep -E '^POSTGRAPHILE_(URL|GRAPHQL_URL|USERNAME|TLS_SHA256_FINGERPRINT)=' /var/lib/cloudimg/postgraphile-credentials.txt \
| tee /dev/stderr | grep -x 'POSTGRAPHILE_USERNAME=admin' >/dev/null
POSTGRAPHILE_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com/
POSTGRAPHILE_GRAPHQL_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com/graphql
POSTGRAPHILE_USERNAME=admin
POSTGRAPHILE_TLS_SHA256_FINGERPRINT=3A:5F:...:C2
The certificate is self-signed and generated on your instance, so your browser warns about it until you trust it (Step 5). For a certificate everyone trusts already, use your own domain (Step 12).
Step 5: Open the Ruru IDE
Copy the certificate to your computer and check that it is the one first boot generated: the SHA-256 of its DER form must match POSTGRAPHILE_TLS_SHA256_FINGERPRINT (ignoring the colons).
scp -i <your-key.pem> ubuntu@<public-ip>:/etc/ssl/postgraphile/postgraphile.crt postgraphile.crt
openssl x509 -in postgraphile.crt -outform der | shasum -a 256
Add postgraphile.crt to your operating system's or browser's trusted certificates (on macOS, open it in Keychain Access and set it to Always Trust), then browse to POSTGRAPHILE_URL. The browser asks for a user name and password: enter admin and the POSTGRAPHILE_PASSWORD from Step 4. Ruru opens with the schema already loaded. Type a query on the left and press the pink run button (or Ctrl-Enter):

The Docs button (top of the left-hand bar) shows every query and mutation PostGraphile generated from the schema:

Step 6: Query the API
Applications send GraphQL as a JSON POST to /graphql with the same user name and password. On the instance, this asks for the demo authors and their published posts. The password is read from the credentials file into a curl configuration, so it never appears on a command line:
C=/var/lib/cloudimg/postgraphile-credentials.txt
H="$(sudo sed -n 's/^POSTGRAPHILE_HOST=//p' "$C")"
curl -s --cacert /etc/ssl/postgraphile/postgraphile.crt \
-K <(sudo sed -n 's/^POSTGRAPHILE_PASSWORD=\(.*\)/user = "admin:\1"/p' "$C") \
-H 'Content-Type: application/json' \
--data-binary '{"query":"{ allAuthors { nodes { name } } searchPosts(search: \"compiler\") { nodes { title authorByAuthorId { name } } } }"}' \
"https://$H/graphql" | python3 -m json.tool | tee /dev/stderr | grep '"name": "Ada Lovelace"' >/dev/null
{
"data": {
"allAuthors": {
"nodes": [
{
"name": "Ada Lovelace"
},
{
"name": "Alan Turing"
},
{
"name": "Grace Hopper"
}
]
},
"searchPosts": {
"nodes": [
{
"title": "The First Compiler",
"authorByAuthorId": {
"name": "Grace Hopper"
}
}
]
}
}
}
searchPosts is the SQL function app_public.search_posts, which PostGraphile turned into a query field by itself. From your own computer, the same request is (curl -u admin asks for the password instead of taking it on the command line):
curl --cacert postgraphile.crt -u admin -H 'Content-Type: application/json' \
--data-binary '{"query":"{ allAuthors { nodes { name } } }"}' https://<public-dns-name>/graphql
Step 7: Create data with a mutation
Every table also gets create, update and delete mutations. This creates a post for Ada Lovelace (author rowId 1) and reads it back by its new rowId:
C=/var/lib/cloudimg/postgraphile-credentials.txt
H="$(sudo sed -n 's/^POSTGRAPHILE_HOST=//p' "$C")"
gql() { curl -s --cacert /etc/ssl/postgraphile/postgraphile.crt \
-K <(sudo sed -n 's/^POSTGRAPHILE_PASSWORD=\(.*\)/user = "admin:\1"/p' "$C") \
-H 'Content-Type: application/json' --data-binary "$1" "https://$H/graphql"; }
NEW="$(gql '{"query":"mutation { createPost(input: {post: {authorId: 1, title: \"Hello from the guide\", body: \"Written through PostGraphile\", published: true}}) { post { rowId title } } }"}')"
ID="$(python3 -c 'import json, sys; print(json.loads(sys.argv[1])["data"]["createPost"]["post"]["rowId"])' "$NEW")" \
&& echo "created post $ID"
gql "{\"query\":\"{ postByRowId(rowId: $ID) { title published authorByAuthorId { name } } }\"}" | tee /dev/stderr \
| grep '"title":"Hello from the guide","published":true,"authorByAuthorId":{"name":"Ada Lovelace"}' >/dev/null && echo && echo "read back"
created post 6
{"data":{"postByRowId":{"title":"Hello from the guide","published":true,"authorByAuthorId":{"name":"Ada Lovelace"}}}}
read back
The same mutation in Ruru:

Step 8: What the front door refuses
Without the password, or with a wrong one, nginx answers 401 before the request reaches PostGraphile, for the IDE and for the API alike:
C=/var/lib/cloudimg/postgraphile-credentials.txt
H="$(sudo sed -n 's/^POSTGRAPHILE_HOST=//p' "$C")"
Q='{"query":"{ allAuthors { totalCount } }"}'
{ curl -s -o /dev/null -w '%{http_code}\n' --cacert /etc/ssl/postgraphile/postgraphile.crt -H 'Content-Type: application/json' --data-binary "$Q" "https://$H/graphql"
curl -s -o /dev/null -w '%{http_code}\n' --cacert /etc/ssl/postgraphile/postgraphile.crt -K <(printf 'user = "admin:not-the-password"\n') \
-H 'Content-Type: application/json' --data-binary "$Q" "https://$H/graphql"
curl -s -o /dev/null -w '%{http_code}\n' --cacert /etc/ssl/postgraphile/postgraphile.crt "https://$H/"
} | xargs | tee /dev/stderr | grep -x '401 401 401' >/dev/null && echo "no password, wrong password and the IDE without a password: refused"
401 401 401
no password, wrong password and the IDE without a password: refused
A web page on another site cannot use your saved password either. An HTML form can only send application/x-www-form-urlencoded, multipart/form-data or text/plain without the browser asking first, and nginx refuses all three with 415; a JSON request carrying another site's Origin gets 403. Both are refused even with the right password, and the same request from your own origin works:
C=/var/lib/cloudimg/postgraphile-credentials.txt
H="$(sudo sed -n 's/^POSTGRAPHILE_HOST=//p' "$C")"
auth() { sudo sed -n 's/^POSTGRAPHILE_PASSWORD=\(.*\)/user = "admin:\1"/p' "$C"; }
{ curl -s -o /dev/null -w '%{http_code}\n' --cacert /etc/ssl/postgraphile/postgraphile.crt -K <(auth) -H 'Origin: https://attacker.example' \
--data 'query=mutation%20%7B%20deletePostByRowId(input%3A%20%7BrowId%3A%201%7D)%20%7B%20deletedPostId%20%7D%20%7D' "https://$H/graphql"
curl -s -o /dev/null -w '%{http_code}\n' --cacert /etc/ssl/postgraphile/postgraphile.crt -K <(auth) -H 'Origin: https://attacker.example' \
-H 'Content-Type: application/json' --data-binary '{"query":"{ allAuthors { totalCount } }"}' "https://$H/graphql"
curl -s -o /dev/null -w '%{http_code}\n' --cacert /etc/ssl/postgraphile/postgraphile.crt -K <(auth) -H "Origin: https://$H" \
-H 'Content-Type: application/json' --data-binary '{"query":"{ allAuthors { totalCount } }"}' "https://$H/graphql"
} | xargs | tee /dev/stderr | grep -x '415 403 200' >/dev/null && echo "cross-site form 415, cross-site JSON 403, same origin 200"
415 403 200
cross-site form 415, cross-site JSON 403, same origin 200
nginx also answers only for the server's own names, keeps a constant health check outside the password, and redirects port 80:
H="$(sudo sed -n 's/^POSTGRAPHILE_HOST=//p' /var/lib/cloudimg/postgraphile-credentials.txt)"
{ curl -s -w ' %{http_code}\n' --cacert /etc/ssl/postgraphile/postgraphile.crt "https://$H/healthz" | xargs
curl -s -o /dev/null -w '%{http_code}\n' -k -H 'Host: someone-else.example' https://127.0.0.1/
curl -s -o /dev/null -w '%{http_code}\n' "http://$H/"
} | xargs | tee /dev/stderr | grep -x 'ok 200 000 301' >/dev/null && echo "front door OK"
ok 200 000 301
front door OK
- Rate limit. Every request that reaches the password check is limited to 5 a second per client address, with bursts of 100, and at most 32 connections per address; over the limit nginx answers
429. The limit applies to every spelling of every path (//graphql,/GRAPHQL,/%67raphql,/graphql;xand so on), because it is set for the whole server, not per path. The password is stored with bcrypt at a low cost so that each check stays cheap; it is 32 random characters, which is what keeps it safe from guessing. - X-Forwarded-For. nginx replaces any
X-Forwarded-Forheader with the address it accepted the connection from, and blanksX-Real-IP,Forwardedand similar headers, so a client cannot change the address the rate limit and the logs use. If you put a load balancer in front of the instance, every client then shares the load balancer's address for the rate limit. - What reaches PostGraphile. nginx does not pass on the
Authorizationheader or cookies (PostGraphile never sees your password), and never forwards a WebSocket upgrade. - Sizes and types. Request bodies are limited to 128 KiB by nginx and 100 kB by PostGraphile. Compressed request bodies (
Content-Encoding) get415andTRACEgets405. GraphQL operations are accepted only in a JSONPOSTbody, never in a URL. - Headers. Responses carry
Strict-Transport-Security,X-Content-Type-Options: nosniff,X-Frame-Options: DENY, aContent-Security-Policythat lets Ruru load only its own scripts,Referrer-Policy: no-referrerandCache-Control: no-store, and nginx does not advertise its version.
What is logged: nginx's access log (/var/log/nginx/access.log) records the client address, request path and status, without query strings. nginx's error log records only start-up problems, not refused requests, so a password in a rejected request never reaches the disk. PostGraphile logs to the journal (journalctl -u postgraphile); it masks every database error to an id in its reply and logs the detail there.
Step 9: Change the password
To give admin a new random password and write it to the credentials file (the old one stops working at once):
sudo postgraphile-rotate-password
sudo grep '^POSTGRAPHILE_PASSWORD=' /var/lib/cloudimg/postgraphile-credentials.txt
The new password goes to htpasswd on standard input, never on a command line.
Step 10: The database and its least-privilege role
PostgreSQL 16 runs on the same instance and listens on its local socket only. The demo database postgraphile_demo has two schemas: app_public, which PostGraphile turns into the API, and app_private, which the API cannot reach. PostGraphile connects as the role postgraphile_public, and PostgreSQL lets only the Linux user postgraphile sign in as that role (peer authentication with a user map), so no database password exists:
sudo -u postgres psql -X -tA -c "SELECT 'roles with a password: ' || count(*) FROM pg_authid WHERE rolpassword IS NOT NULL" \
-c "SELECT rolname, rolsuper, rolcreatedb, rolcreaterole, rolinherit, rolbypassrls FROM pg_roles WHERE rolname = 'postgraphile_public'" \
| tee /dev/stderr | grep -x 'roles with a password: 0' >/dev/null && echo "no database password"
roles with a password: 0
postgraphile_public|f|f|f|f|f
no database password
postgraphile_public can read and write the demo tables in app_public and nothing else. As that role, reading the private schema, the password table or a server file, and creating a table, are all refused:
for q in "SELECT count(*) FROM app_private.secrets" "SELECT count(*) FROM pg_authid" "SELECT pg_read_file('/etc/hostname')" "CREATE TABLE app_public.x (id int)"; do
OUT="$(sudo setpriv --reuid=postgraphile --regid=postgraphile --clear-groups -- \
psql -X -tA -h /var/run/postgresql -U postgraphile_public -d postgraphile_demo -c "$q" 2>&1 || true)"
case "$OUT" in *"permission denied"*) echo "refused: $q" ;; *) echo "NOT refused: $q" ;; esac
done | tee /dev/stderr | grep -c '^refused' | grep -x 4 >/dev/null && echo "4 of 4 refused"
refused: SELECT count(*) FROM app_private.secrets
refused: SELECT count(*) FROM pg_authid
refused: SELECT pg_read_file('/etc/hostname')
refused: CREATE TABLE app_public.x (id int)
4 of 4 refused
To serve your own schema, create it as the postgres superuser (sudo -u postgres psql -d postgraphile_demo, or a new database), grant postgraphile_public only what the API should do (GRANT USAGE ON SCHEMA ..., GRANT SELECT, INSERT, ... ON ...), add the schema to schemas in /etc/postgraphile/graphile.config.js (and the database to DATABASE_URL in /etc/postgraphile/postgraphile.env if you use a new one), then run sudo systemctl restart postgraphile. PostGraphile only shows what the role is allowed to use.
Step 11: The production settings
PostGraphile's settings are in /etc/postgraphile/graphile.config.js. Before every start, postgraphile-config-check loads that file the way PostGraphile does and refuses to start the service if it listens on anything but 127.0.0.1, turns on watch mode, EXPLAIN output, GraphQL over GET, websockets or CORS, accepts bodies other than JSON, lifts the size, depth or time limits, or connects to the database with a password or as a superuser:
sudo /usr/local/sbin/postgraphile-config-check | tee /dev/stderr | grep '^postgraphile-config-check: OK ' >/dev/null
postgraphile-config-check: OK (127.0.0.1:5000; watch, explain, GET operations, websockets off; JSON bodies only; 100000 bytes; planning depth 50; 1 database service(s), no password)
A deeply nested query is refused rather than planned (the planning depth limit is 50), queries have 5 seconds to plan and 30 seconds to run, and an error from the database reaches the caller only as An error occurred (logged with hash: ..., id: ...), with the detail in journalctl -u postgraphile.
Step 12: Use your own domain
Point a DNS name at the instance (an Elastic IP keeps it stable), then make it the server's address. This generates a new certificate for that name, pins nginx to it and updates the addresses in the credentials file:
sudo postgraphile-set-site graphql.example.com
To use a certificate from a public CA instead, for example one issued by certbot with a DNS challenge, run postgraphile-set-site first, then replace /etc/ssl/postgraphile/postgraphile.crt (full chain) and /etc/ssl/postgraphile/postgraphile.key (mode 0600) and run sudo systemctl reload nginx.
Step 13: Run the self-test
The image ships a self-test that checks the configuration, that no password and a wrong one are refused, that Ruru opens and the demo schema answers over HTTPS with this instance's certificate, and that cross-site requests are refused:
sudo postgraphile-selftest | tail -n 1 | tee /dev/stderr | grep '^POSTGRAPHILE_SELFTEST_OK ' >/dev/null
POSTGRAPHILE_SELFTEST_OK no password and a wrong one refused (401), admin opens Ruru and queries the demo schema over https://ec2-203-0-113-10.compute-1.amazonaws.com/ with this instance's certificate; cross-site form, cross-origin JSON and text/plain refused; no GraphQL over GET
Step 14: Back up and restore
Everything the API stores is in the PostgreSQL database postgraphile_demo (or your own). Back it up with:
sudo -u postgres pg_dump -Fc postgraphile_demo > postgraphile_demo-$(date +%F).dump
Restore it on a new instance of this image with sudo -u postgres pg_restore --clean --if-exists -d postgraphile_demo postgraphile_demo-<date>.dump, then sudo systemctl restart postgraphile.
Step 15: Security notes and the build record
The certificate generated at first boot is a server certificate only: it cannot sign other certificates.
openssl x509 -in /etc/ssl/postgraphile/postgraphile.crt -noout -ext basicConstraints,keyUsage,extendedKeyUsage | tee /dev/stderr | grep 'CA:FALSE' >/dev/null
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication
The build record names every component and its checksum:
sed -n '1,6p' /usr/share/doc/cloudimg-postgraphile/BUILD-INFO.txt | tee /dev/stderr | grep '^postgraphile 5\.2\.1 ' >/dev/null
postgraphile 5.2.1 (npm, github.com/graphile/crystal, 2026-11-02), MIT (postgraphile-LICENSE.md); grafserv
1.0.3, grafast 1.2.0, ruru 2.0.3; installed with 'npm ci --omit=dev --ignore-scripts' from the embedded
package-lock.json (189 packages, sha512-integrity from registry.npmjs.org; lockfile sha256 d8fa0f0283052e9b64562ab960708ae491fa2f16c1436669d385cfb311ee2aef)
node 24.22.2 LTS (nodejs.org node-v24.22.2-linux-x64.tar.xz, sha256 48cc0e21e048790e54c0cf5749c2725005313d652651c40b7a21feb9697aaf75 = nodejs.org SHASUMS256.txt),
MIT (node-LICENSE); binary /opt/cloudimg-node/bin/node sha256 904a6d3484241d466989f9df9ba3aab822b1496191bdb28230eb491a55c32a52; npm/npx/corepack removed
advisories npm audit, OSV and trivy over the installed tree: 0 vulnerabilities at build time
PostGraphile and Node.js are MIT licensed; their licences are in /usr/share/doc/cloudimg-postgraphile/. PostGraphile runs as its own unprivileged user postgraphile with no login shell, no capabilities, a read-only view of the system and no access to the credentials file or the TLS key. PostGraphile and PostgreSQL cannot open any network connection (an nftables rule refuses every new connection from their users), and PostGraphile, nginx and PostgreSQL cannot reach the EC2 instance metadata service. PostgreSQL 16 and nginx come from Ubuntu's main archive; the image adds no third-party package repository.
Step 16: Keep it up to date
Ubuntu security updates, including PostgreSQL and nginx, install automatically. To apply everything now:
sudo apt-get update && sudo apt-get upgrade -y
New PostGraphile and Node.js releases arrive as new versions of the AWS Marketplace listing: launch the new version, restore your database backup into it (Step 14), copy over your changes to /etc/postgraphile/graphile.config.js, and run sudo postgraphile-set-site with your domain if you use one.
Support
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
The other versions on this listing are on the developer and API tools guide.