--- title: "Cómo desplegar un servidor GraphQL Apollo en Ubuntu 24.04" description: "Despliega Apollo Server 5 con Express en Ubuntu 24.04: suscripciones WebSocket, límite de profundidad, servicio systemd y Nginx con HTTPS y rate limiting." category: "API y Microservicios" language: es source: https://cubepath.com/es/docs/api-y-microservicios/despliegue-de-servidor-graphql-en-linux updated: 2026-09-25 --- # Cómo desplegar un servidor GraphQL Apollo en Ubuntu 24.04 GraphQL permite que el cliente pida exactamente los campos que necesita en una sola petición, pero esa flexibilidad obliga a proteger el servidor frente a consultas abusivas. En este tutorial desplegarás en Ubuntu 24.04 un servidor GraphQL con Apollo Server 5 sobre Express, con consultas, mutaciones y suscripciones en tiempo real por WebSocket, un límite de profundidad de consultas y la introspección desactivada en producción. Lo ejecutarás como servicio systemd y lo publicarás detrás de Nginx con HTTPS y límite de peticiones. ## Requisitos previos - Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 1 GB de RAM. - Un usuario no root con privilegios `sudo`. - Node.js 24 LTS instalado desde el repositorio de NodeSource. Apollo Server 5 requiere Node.js 20 o superior, y el paquete `nodejs` de Ubuntu 24.04 es la versión 18. - Un dominio con un registro A apuntando al servidor. En esta guía se usa `your_domain`. - Los puertos 80 y 443 accesibles desde Internet. Si todavía no tienes Node.js 24, instálalo así: ```bash sudo apt update sudo apt install ca-certificates curl gnupg sudo install -d -m 0755 /etc/apt/keyrings curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list sudo apt update sudo apt install nodejs node --version ``` ## Paso 1: Crear el usuario y el proyecto El servicio se ejecutará con un usuario de sistema propio. Créalo con `/srv/graphql` como directorio personal y abre una shell con él: ```bash sudo adduser --system --group --home /srv/graphql --shell /bin/bash graphql sudo -iu graphql ``` Como `graphql`, crea el proyecto e instala las dependencias: ```bash mkdir -p ~/app/src && cd ~/app npm init -y npm pkg set type=commonjs npm install @apollo/server @as-integrations/express5 express graphql \ @graphql-tools/schema graphql-ws ws graphql-subscriptions graphql-depth-limit ``` Qué hace cada paquete: | Paquete | Función | | --- | --- | | `@apollo/server` | El servidor GraphQL | | `@as-integrations/express5` | Integración con Express 5 (en Apollo Server 5 ya no viene incluida) | | `@graphql-tools/schema` | Construye un esquema ejecutable, compartido por HTTP y WebSocket | | `graphql-ws` y `ws` | Servidor WebSocket con el protocolo `graphql-transport-ws` para suscripciones | | `graphql-subscriptions` | `PubSub` en memoria para publicar eventos | | `graphql-depth-limit` | Regla de validación que rechaza consultas demasiado anidadas | ## Paso 2: Definir el esquema y los resolvers Crea el esquema con libros y autores. La relación es circular a propósito (un libro tiene autor y un autor tiene libros), porque es justo lo que permite construir consultas anidadas sin fin y lo que protegerás en el paso siguiente: ```bash nano ~/app/src/schema.js ``` ```javascript const { PubSub } = require('graphql-subscriptions'); const pubsub = new PubSub(); // Datos en memoria para el ejemplo; en tu aplicación vendrán de la base de datos const authors = [ { id: '1', name: 'Gabriel García Márquez' }, { id: '2', name: 'Julio Cortázar' }, ]; const books = [ { id: '1', title: 'Cien años de soledad', authorId: '1' }, { id: '2', title: 'Rayuela', authorId: '2' }, ]; const typeDefs = `#graphql type Author { id: ID! name: String! books: [Book!]! } type Book { id: ID! title: String! author: Author! } type Query { books: [Book!]! book(id: ID!): Book } type Mutation { addBook(title: String!, authorId: ID!): Book! } type Subscription { bookAdded: Book! } `; const resolvers = { Query: { books: () => books, book: (_, { id }) => books.find((b) => b.id === id) ?? null, }, Mutation: { addBook: (_, { title, authorId }) => { const book = { id: String(books.length + 1), title, authorId }; books.push(book); pubsub.publish('BOOK_ADDED', { bookAdded: book }); return book; }, }, Subscription: { bookAdded: { subscribe: () => pubsub.asyncIterableIterator(['BOOK_ADDED']), }, }, Book: { author: (book) => authors.find((a) => a.id === book.authorId), }, Author: { books: (author) => books.filter((b) => b.authorId === author.id), }, }; module.exports = { typeDefs, resolvers }; ``` > Nota: `PubSub` de `graphql-subscriptions` guarda los eventos en la memoria del proceso. Sirve con una sola instancia; si ejecutas varias, usa un bus compartido como Redis (paquete `graphql-redis-subscriptions`) para que todas reciban los eventos. ## Paso 3: Crear el servidor HTTP y WebSocket El servidor comparte un único puerto para las peticiones HTTP (consultas y mutaciones) y las conexiones WebSocket (suscripciones), ambas en la ruta `/graphql`: ```bash nano ~/app/src/server.js ``` ```javascript const http = require('node:http'); const express = require('express'); const { ApolloServer } = require('@apollo/server'); const { expressMiddleware } = require('@as-integrations/express5'); const { ApolloServerPluginDrainHttpServer } = require('@apollo/server/plugin/drainHttpServer'); const { makeExecutableSchema } = require('@graphql-tools/schema'); const { WebSocketServer } = require('ws'); const { useServer } = require('graphql-ws/use/ws'); const depthLimit = require('graphql-depth-limit'); const { typeDefs, resolvers } = require('./schema'); const PORT = Number(process.env.PORT) || 4000; const HOST = process.env.HOST || '127.0.0.1'; async function main() { const schema = makeExecutableSchema({ typeDefs, resolvers }); const app = express(); app.set('trust proxy', 'loopback'); const httpServer = http.createServer(app); // Suscripciones: servidor WebSocket en la misma ruta /graphql const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' }); const wsCleanup = useServer({ schema }, wsServer); const server = new ApolloServer({ schema, validationRules: [depthLimit(5)], plugins: [ ApolloServerPluginDrainHttpServer({ httpServer }), { async serverWillStart() { return { async drainServer() { await wsCleanup.dispose(); }, }; }, }, ], }); await server.start(); app.get('/health', (req, res) => res.json({ status: 'ok' })); app.use( '/graphql', express.json({ limit: '100kb' }), expressMiddleware(server, { context: async ({ req }) => ({ ip: req.ip }), }), ); await new Promise((resolve) => httpServer.listen(PORT, HOST, resolve)); console.log(`GraphQL listo en http://${HOST}:${PORT}/graphql`); const shutdown = async (signal) => { console.log(`${signal} recibido, cerrando`); await server.stop(); process.exit(0); }; process.on('SIGTERM', shutdown); process.on('SIGINT', shutdown); } main().catch((err) => { console.error(err); process.exit(1); }); ``` Las decisiones de producción de este archivo: - `depthLimit(5)` rechaza en la fase de validación, antes de ejecutar ningún resolver, cualquier consulta con más de 5 niveles de anidamiento. Ajusta el valor a la consulta legítima más profunda de tus clientes. - `express.json({ limit: '100kb' })` limita el tamaño del cuerpo, lo que también acota el tamaño de las consultas. - Con `NODE_ENV=production`, Apollo Server desactiva la introspección por defecto, así que nadie puede descargar tu esquema completo. Además, su protección CSRF, activa por defecto, rechaza peticiones sin `Content-Type: application/json` ni cabecera de preflight. - `server.stop()` ejecuta los plugins de drenaje: deja de aceptar conexiones, espera a las peticiones en curso y cierra las suscripciones WebSocket de forma limpia. Prueba el servidor antes de convertirlo en servicio: ```bash NODE_ENV=production node src/server.js ``` ```output GraphQL listo en http://127.0.0.1:4000/graphql ``` Déjalo en marcha, abre otra sesión SSH y lanza una consulta: ```bash curl -s http://127.0.0.1:4000/graphql \ -H 'Content-Type: application/json' \ -d '{"query":"{ books { title author { name } } }"}' ``` ```output {"data":{"books":[{"title":"Cien años de soledad","author":{"name":"Gabriel García Márquez"}},{"title":"Rayuela","author":{"name":"Julio Cortázar"}}]}} ``` Ahora envía una consulta que abuse de la relación circular. La regla de profundidad la rechaza: ```bash curl -s http://127.0.0.1:4000/graphql \ -H 'Content-Type: application/json' \ -d '{"query":"{ books { author { books { author { books { author { name } } } } } } }"}' ``` ```output {"errors":[{"message":"'' exceeds maximum operation depth of 5","locations":[{"line":1,"column":54}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]} ``` Detén el servidor con `Ctrl+C` y sal de la shell de `graphql` con `exit`. ## Paso 4: Ejecutar el servidor con systemd Crea el archivo de entorno con la configuración y, más adelante, los secretos de tu base de datos: ```bash sudo install -d -m 0750 -o root -g graphql /etc/graphql sudo nano /etc/graphql/graphql.env ``` ```ini NODE_ENV=production HOST=127.0.0.1 PORT=4000 ``` ```bash sudo chmod 0640 /etc/graphql/graphql.env sudo chown root:graphql /etc/graphql/graphql.env ``` Crea la unidad de systemd: ```bash sudo nano /etc/systemd/system/graphql.service ``` ```ini [Unit] Description=Servidor GraphQL (Apollo Server) After=network-online.target Wants=network-online.target [Service] Type=simple User=graphql Group=graphql WorkingDirectory=/srv/graphql/app EnvironmentFile=/etc/graphql/graphql.env ExecStart=/usr/bin/node src/server.js Restart=on-failure RestartSec=2 TimeoutStopSec=30 NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true [Install] WantedBy=multi-user.target ``` Actívala y comprueba el estado: ```bash sudo systemctl daemon-reload sudo systemctl enable --now graphql systemctl status graphql --no-pager curl -s http://127.0.0.1:4000/health ``` ```output ● graphql.service - Servidor GraphQL (Apollo Server) Loaded: loaded (/etc/systemd/system/graphql.service; enabled; preset: enabled) Active: active (running) since ... {"status":"ok"} ``` Los logs del servidor están en el journal: ```bash journalctl -u graphql -f ``` ## Paso 5: Configurar Nginx con WebSocket y límite de peticiones Nginx termina el TLS, limita la tasa de peticiones por IP y actualiza a WebSocket las conexiones de suscripción. Instálalo y abre el firewall: ```bash sudo apt install nginx sudo ufw allow OpenSSH sudo ufw allow 'Nginx Full' sudo ufw enable ``` Crea el sitio: ```bash sudo nano /etc/nginx/sites-available/graphql ``` ```nginx map $http_upgrade $connection_upgrade { default upgrade; '' close; } limit_req_zone $binary_remote_addr zone=graphql_limit:10m rate=10r/s; upstream graphql_backend { server 127.0.0.1:4000; } server { listen 80; listen [::]:80; server_name your_domain; location /graphql { limit_req zone=graphql_limit burst=20 nodelay; limit_req_status 429; 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_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Las suscripciones mantienen la conexión abierta proxy_read_timeout 1h; proxy_send_timeout 1h; } location = /health { proxy_pass http://graphql_backend; access_log off; } } ``` El `map` envía `Connection: upgrade` solo cuando el cliente pide un WebSocket, así que la misma `location` sirve para peticiones normales y suscripciones. `limit_req` admite 10 peticiones por segundo por IP con ráfagas de hasta 20; el exceso recibe un `429`. Como una sola consulta GraphQL puede ser muy costosa, este límite complementa, no sustituye, al límite de profundidad. Activa el sitio y recarga Nginx: ```bash sudo ln -s /etc/nginx/sites-available/graphql /etc/nginx/sites-enabled/ sudo rm /etc/nginx/sites-enabled/default sudo nginx -t sudo systemctl reload nginx ``` ## Paso 6: Activar HTTPS Obtén el certificado con Certbot, que añade el bloque de 443 y la redirección desde HTTP: ```bash sudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d your_domain --redirect sudo certbot renew --dry-run ``` Comprueba una consulta desde tu equipo: ```bash curl -s https://your_domain/graphql \ -H 'Content-Type: application/json' \ -d '{"query":"{ book(id: \"1\") { title } }"}' ``` ```output {"data":{"book":{"title":"Cien años de soledad"}}} ``` ## Paso 7: Probar las suscripciones Para probar el WebSocket a mano usa `wscat` con el subprotocolo `graphql-transport-ws` desde tu equipo: ```bash npx wscat -c wss://your_domain/graphql -s graphql-transport-ws ``` Cuando aparezca el indicador `>`, inicia la conexión y suscríbete, pegando cada mensaje por separado: ```json {"type":"connection_init"} ``` ```json {"id":"1","type":"subscribe","payload":{"query":"subscription { bookAdded { id title author { name } } }"}} ``` El servidor responde a la primera línea con `{"type":"connection_ack"}`. En otra terminal, lanza una mutación: ```bash curl -s https://your_domain/graphql \ -H 'Content-Type: application/json' \ -d '{"query":"mutation { addBook(title: \"Pedro Páramo\", authorId: \"1\") { id } }"}' ``` En la sesión de `wscat` recibirás el evento: ```output < {"id":"1","type":"next","payload":{"data":{"bookAdded":{"id":"3","title":"Pedro Páramo","author":{"name":"Gabriel García Márquez"}}}}} ``` ## Paso 8: Actualizar el servidor Para desplegar una versión nueva, actualiza el código como `graphql`, instala las dependencias exactas del `package-lock.json` y reinicia el servicio: ```bash sudo -iu graphql bash -c 'cd ~/app && git pull && npm ci --omit=dev' sudo systemctl restart graphql journalctl -u graphql -n 5 --no-pager ``` ```output ... node[6210]: SIGTERM recibido, cerrando ... systemd[1]: Stopped graphql.service - Servidor GraphQL (Apollo Server). ... systemd[1]: Started graphql.service - Servidor GraphQL (Apollo Server). ... node[6288]: GraphQL listo en http://127.0.0.1:4000/graphql ``` Los clientes de suscripción recibirán el cierre del WebSocket; los clientes de `graphql-ws` se reconectan automáticamente con su opción `retryAttempts`. ## Solución de problemas **Las suscripciones fallan con 400 o se cierran al instante.** Nginx no está reenviando las cabeceras `Upgrade` y `Connection`, o la ruta del `WebSocketServer` no coincide con la `location`. Revisa el bloque del paso 5 y `/var/log/nginx/error.log`. **`This operation has been blocked as a potential Cross-Site Request Forgery`.** La petición no lleva `Content-Type: application/json`. Es la protección CSRF de Apollo Server; los clientes GraphQL la envían siempre, así que suele pasar solo con `curl` o formularios. **`GraphQL introspection is not allowed`.** Es lo esperado en producción. Si tu equipo necesita explorar el esquema, hazlo contra un entorno de desarrollo, o pasa `introspection: true` al constructor de `ApolloServer` solo si el esquema no es sensible. **Las suscripciones dejan de llegar con varias instancias.** `PubSub` en memoria no se comparte entre procesos. Usa `graphql-redis-subscriptions` con un Redis común. **`429 Too Many Requests` para clientes legítimos.** Sube `burst` o `rate` en `limit_req_zone`. Si hay otro proxy delante de Nginx, configura el módulo `realip` para que `$binary_remote_addr` sea la IP del cliente y no la del proxy. ## Conclusión Tienes un servidor Apollo Server 5 en producción con consultas, mutaciones y suscripciones por WebSocket, protegido con límite de profundidad, límite de peticiones e introspección desactivada, gestionado por systemd y publicado con HTTPS. Como siguientes pasos, conecta los resolvers a tu base de datos usando `dataloader` para agrupar consultas y evitar el problema N+1, añade autenticación en la función `context` y valida también las operaciones de suscripción con la opción `onSubscribe` de `useServer`, que no pasan por las `validationRules` de Apollo.