GraphQL Server Deployment on Linux
GraphQL provides a flexible query language for APIs, and deploying a GraphQL server in production on Linux requires attention to schema design, resolver optimization, subscription handling, caching, and security. This guide covers deploying Apollo Server, configuring Nginx for GraphQL, handling subscriptions over WebSockets, implementing caching strategies, and securing GraphQL endpoints.
Prerequisites
- Ubuntu 20.04+ or CentOS/Rocky 8+ with root access
- Node.js 18+ (or Python with Strawberry/Ariadne for Python GraphQL)
- PM2 installed globally:
sudo npm install -g pm2 - Redis (for subscriptions and caching)
Setting Up Apollo Server
# Create application directory
sudo useradd -r -m -d /opt/graphql -s /bin/bash gqlapp
sudo mkdir -p /opt/graphql/app
sudo chown -R gqlapp:gqlapp /opt/graphql
# Install Node.js dependencies
sudo -u gqlapp bash -c "
cd /opt/graphql/app
npm init -y
npm install @apollo/server graphql graphql-ws ws express cors
npm install --save-dev nodemon
"
Create the server entry point:
sudo tee /opt/graphql/app/src/index.js << 'EOF'
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const express = require('express');
const cors = require('cors');
const { typeDefs } = require('./schema');
const { resolvers } = require('./resolvers');
const { createContext } = require('./context');
const app = express();
const PORT = process.env.PORT || 4000;
async function startServer() {
const server = new ApolloServer({
typeDefs,
resolvers,
// Disable introspection in production
introspection: process.env.NODE_ENV !== 'production',
formatError: (formattedError, error) => {
// Don't expose internal errors to clients
if (process.env.NODE_ENV === 'production') {
return { message: formattedError.message };
}
return formattedError;
},
});
await server.start();
app.use(
'/graphql',
cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') || '*' }),
express.json({ limit: '10mb' }),
expressMiddleware(server, {
context: createContext,
})
);
app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
app.listen(PORT, '127.0.0.1', () => {
console.log(`GraphQL server ready at http://127.0.0.1:${PORT}/graphql`);
if (process.send) process.send('ready');
});
}
startServer().catch(console.error);
EOF
Schema Design and Resolvers
sudo tee /opt/graphql/app/src/schema.js << 'EOF'
const { gql } = require('graphql-tag');
const typeDefs = gql`
type Query {
users(limit: Int, offset: Int): [User!]!
user(id: ID!): User
posts(userId: ID): [Post!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
type Subscription {
userCreated: User!
postPublished(userId: ID): Post!
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
publishedAt: String
}
input CreateUserInput {
name: String!
email: String!
password: String!
}
input UpdateUserInput {
name: String
email: String
}
`;
module.exports = { typeDefs };
EOF
sudo tee /opt/graphql/app/src/resolvers.js << 'EOF'
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();
const resolvers = {
Query: {
users: async (_, { limit = 20, offset = 0 }, { db }) => {
return db.users.findAll({ limit, offset });
},
user: async (_, { id }, { db }) => {
return db.users.findById(id);
},
},
Mutation: {
createUser: async (_, { input }, { db }) => {
const user = await db.users.create(input);
// Publish subscription event
pubsub.publish('USER_CREATED', { userCreated: user });
return user;
},
},
Subscription: {
userCreated: {
subscribe: () => pubsub.asyncIterator(['USER_CREATED']),
},
},
// Field resolvers for nested data (N+1 query prevention via DataLoader)
User: {
posts: async (user, _, { loaders }) => {
return loaders.postsByUser.load(user.id);
},
},
Post: {
author: async (post, _, { loaders }) => {
return loaders.usersById.load(post.authorId);
},
},
};
module.exports = { resolvers, pubsub };
EOF
Subscriptions with WebSockets
sudo tee /opt/graphql/app/src/subscription-server.js << 'EOF'
const { createServer } = require('http');
const { WebSocketServer } = require('ws');
const { useServer } = require('graphql-ws/lib/use/ws');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { typeDefs } = require('./schema');
const { resolvers } = require('./resolvers');
const schema = makeExecutableSchema({ typeDefs, resolvers });
function setupSubscriptions(httpServer) {
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql/subscriptions',
});
return useServer(
{
schema,
context: async (ctx) => {
// Authenticate WebSocket connections
const token = ctx.connectionParams?.authorization;
if (token) {
// Verify token and return context
return { user: await verifyToken(token) };
}
return {};
},
onConnect: (ctx) => {
console.log(`WebSocket connected: ${ctx.extra.request.socket.remoteAddress}`);
},
onDisconnect: () => {
console.log('WebSocket disconnected');
},
},
wsServer
);
}
module.exports = { setupSubscriptions };
EOF
Caching Strategies
Install Redis for caching:
sudo apt install redis-server # Ubuntu
sudo dnf install redis # CentOS/Rocky
sudo systemctl enable --now redis
sudo tee /opt/graphql/app/src/cache.js << 'EOF'
const { createClient } = require('redis');
const redis = createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379',
});
redis.connect().catch(console.error);
// Cache resolver results
async function cachedResolver(cacheKey, ttlSeconds, resolver) {
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const result = await resolver();
await redis.setEx(cacheKey, ttlSeconds, JSON.stringify(result));
return result;
}
// Cache invalidation
async function invalidateCache(pattern) {
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(keys);
}
}
module.exports = { redis, cachedResolver, invalidateCache };
EOF
Add cache directives to your schema:
# Cache user data for 5 minutes
type Query {
user(id: ID!): User @cacheControl(maxAge: 300)
users: [User!]! @cacheControl(maxAge: 60)
}
Nginx Configuration for GraphQL
sudo tee /etc/nginx/sites-available/graphql << 'EOF'
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream graphql_backend {
server 127.0.0.1:4000;
keepalive 32;
}
server {
listen 80;
server_name api.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# Rate limiting for GraphQL endpoint
limit_req_zone $binary_remote_addr zone=graphql:10m rate=100r/m;
limit_req_zone $binary_remote_addr zone=graphql_subscriptions:10m rate=5r/m;
# Main GraphQL endpoint
location /graphql {
limit_req zone=graphql burst=30 nodelay;
proxy_pass http://graphql_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
# WebSocket subscriptions endpoint
location /graphql/subscriptions {
limit_req zone=graphql_subscriptions burst=5 nodelay;
proxy_pass http://graphql_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 3600s; # Long timeout for WebSockets
proxy_send_timeout 3600s;
}
location /health {
proxy_pass http://graphql_backend;
access_log off;
}
}
EOF
sudo ln -s /etc/nginx/sites-available/graphql /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Security Best Practices
Limit query complexity to prevent abuse:
npm install graphql-query-complexity
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
// Reject queries with complexity > 1000
createComplexityLimitRule(1000, {
onCost: (cost) => console.log('Query complexity:', cost),
}),
],
});
Disable introspection in production:
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
// Also block schema introspection query
plugins: [
{
requestDidStart: async () => ({
didResolveOperation: async ({ request }) => {
if (process.env.NODE_ENV === 'production' &&
request.query.includes('__schema')) {
throw new Error('Introspection is disabled');
}
},
}),
},
],
});
Monitoring and Performance
Start with PM2:
sudo tee /opt/graphql/ecosystem.config.js << 'EOF'
module.exports = {
apps: [{
name: 'graphql-api',
script: './src/index.js',
instances: 'max',
exec_mode: 'cluster',
env_production: {
NODE_ENV: 'production',
PORT: 4000,
},
env_file: '/etc/graphql/.env',
log_file: '/var/log/graphql/combined.log',
error_file: '/var/log/graphql/error.log',
max_memory_restart: '512M',
wait_ready: true,
}]
};
EOF
sudo -u gqlapp bash -c "cd /opt/graphql && pm2 start ecosystem.config.js --env production"
sudo -u gqlapp pm2 save
Add Prometheus metrics:
npm install @promster/express prom-client
const { createMiddleware } = require('@promster/express');
app.use(createMiddleware({ app }));
app.get('/metrics', (req, res) => {
res.set('Content-Type', 'text/plain');
res.send(require('prom-client').register.metrics());
});
Troubleshooting
WebSocket connections dropping:
# Check Nginx timeout settings
grep timeout /etc/nginx/sites-available/graphql
# Increase proxy_read_timeout for subscriptions
# Check if Redis pub/sub is working
redis-cli PING
redis-cli SUBSCRIBE test &
redis-cli PUBLISH test "hello"
N+1 query problem causing slow responses:
# Install DataLoader
npm install dataloader
# Implement batch loading for related resources
# Each loader.load() call is batched into a single DB query
GraphQL queries too slow:
# Add query timing logging
# Check resolver execution with Apollo Studio or:
curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ users { id name } }"}' \
-w "\nTime: %{time_total}s\n"
"Introspection is disabled" error during development:
# Check NODE_ENV setting
echo $NODE_ENV
# In development, set: NODE_ENV=development
# Never set introspection based on env in the ecosystem config
Conclusion
Deploying a GraphQL server in production requires balancing flexibility with security—disable introspection in production, implement query complexity limits to prevent abuse, and use DataLoader to solve N+1 query problems. Run multiple instances with PM2 cluster mode for horizontal scaling, use Redis for subscriptions pub/sub and response caching, and configure Nginx to handle both HTTP GraphQL queries and WebSocket subscription connections on the same domain.


