요금블로그
언어

TCP Mode di Helipod: Deploy WebSocket, gRPC, dan Non-HTTP Services

Tim Helipod
게시일 7분 소요

이 글은 인도네시아어로 작성되었습니다.

Helipod by default menggunakan HTTP/HTTPS untuk routing traffic ke container. Ini cocok untuk 90% use case: web app, REST API, GraphQL. Tapi ada skenario di mana kamu butuh protokol TCP murni, WebSocket server yang persistent, gRPC service, game server, atau custom protocol.

Untuk kasus ini, Helipod menyediakan TCP mode: cara menjalankan service yang komunikasinya bukan via HTTP.

Apa itu TCP Mode?#

Dalam mode normal (HTTP), Helipod menggunakan Traefik sebagai reverse proxy yang melakukan:

  • SSL termination (HTTPS → HTTP ke container)
  • Health check via HTTP GET
  • Routing berdasarkan hostname

Dalam TCP mode, Traefik meneruskan koneksi TCP secara langsung ke container tanpa memproses HTTP. Ini berarti:

  • Koneksi TCP persistent (tidak di-terminate per request)
  • Tidak ada SSL termination otomatis dari Traefik
  • Health check HTTP dinonaktifkan
  • Cocok untuk protokol apapun yang berjalan di atas TCP

Cara Aktifkan TCP Mode#

Di helipack.json:

{
  "run": {
    "port": 9000,
    "isTcp": true,
    "command": "node dist/server.js"
  }
}

Atau jika menggunakan Dockerfile sendiri, pastikan EXPOSE menunjuk port yang benar, dan set isTcp: true di helipack.json.

Penting: Saat isTcp: true, health check HTTP otomatis dinonaktifkan. Helipod menggunakan TCP connection check sebagai gantinya, jika port bisa di-connect, pod dianggap healthy.

Use Case 1: WebSocket Server (Node.js)#

WebSocket membutuhkan koneksi persistent antara client dan server. HTTP mode tidak cocok karena Traefik tidak dirancang untuk koneksi HTTP Upgrade yang long-lived.

Setup WebSocket dengan ws library#

// src/server.js
const WebSocket = require('ws');

const port = parseInt(process.env.PORT || '8080');
const wss = new WebSocket.Server({ port });

wss.on('connection', (ws, req) => {
  console.log(`Client connected from ${req.socket.remoteAddress}`);
  
  ws.on('message', (message) => {
    console.log('Received:', message.toString());
    
    // Broadcast ke semua client
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(message.toString());
      }
    });
  });
  
  ws.on('close', () => {
    console.log('Client disconnected');
  });
  
  ws.send(JSON.stringify({ type: 'connected', message: 'Welcome!' }));
});

console.log(`WebSocket server running on port ${port}`);

helipack.json#

{
  "run": {
    "port": 8080,
    "isTcp": true,
    "command": "node src/server.js"
  }
}

Koneksi dari Client#

// Client-side JavaScript
const ws = new WebSocket('wss://nama-project.helipod.app');

ws.onopen = () => {
  console.log('Connected!');
  ws.send(JSON.stringify({ type: 'hello', data: 'World' }));
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Received:', data);
};

ws.onclose = () => {
  console.log('Disconnected');
};

Use Case 2: WebSocket dengan Socket.IO#

Socket.IO lebih popular dari raw WebSocket karena punya fallback, rooms, dan namespace:

// src/index.js
const { createServer } = require('http');
const { Server } = require('socket.io');

const port = parseInt(process.env.PORT || '3000');
const httpServer = createServer();
const io = new Server(httpServer, {
  cors: {
    origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
    methods: ['GET', 'POST'],
  },
  transports: ['websocket', 'polling'],
});

io.on('connection', (socket) => {
  console.log('User connected:', socket.id);
  
  socket.on('join-room', (roomId) => {
    socket.join(roomId);
    socket.to(roomId).emit('user-joined', socket.id);
  });
  
  socket.on('message', ({ roomId, content }) => {
    io.to(roomId).emit('message', {
      sender: socket.id,
      content,
      timestamp: new Date().toISOString(),
    });
  });
  
  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.id);
  });
});

httpServer.listen(port, () => {
  console.log(`Socket.IO server running on port ${port}`);
});

Catatan Socket.IO: Socket.IO awalnya mencoba HTTP polling sebelum upgrade ke WebSocket. Ini bisa bekerja di HTTP mode juga untuk beberapa kasus. Coba HTTP mode dulu, jika ada masalah dengan koneksi persistent, switch ke TCP mode.

helipack.json untuk Socket.IO#

{
  "run": {
    "port": 3000,
    "isTcp": true,
    "command": "node src/index.js"
  }
}

Use Case 3: gRPC Service (Node.js)#

gRPC menggunakan HTTP/2 dengan binary protocol (Protocol Buffers). Meski secara teknis adalah HTTP/2, gRPC membutuhkan TCP mode di Helipod karena Traefik perlu dikonfigurasi khusus untuk HTTP/2 passthrough.

Define Proto File#

// proto/user.proto
syntax = "proto3";

package user;

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc CreateUser (CreateUserRequest) returns (User);
  rpc ListUsers (ListUsersRequest) returns (stream User);
}

message User {
  string id = 1;
  string name = 2;
  string email = 3;
}

message GetUserRequest {
  string id = 1;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
}

message ListUsersRequest {
  int32 limit = 1;
}

gRPC Server#

// src/server.js
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const { v4: uuidv4 } = require('uuid');

const packageDef = protoLoader.loadSync('./proto/user.proto');
const userProto = grpc.loadPackageDefinition(packageDef).user;

const users = new Map();

const server = new grpc.Server();

server.addService(userProto.UserService.service, {
  getUser: (call, callback) => {
    const user = users.get(call.request.id);
    if (user) {
      callback(null, user);
    } else {
      callback({
        code: grpc.status.NOT_FOUND,
        message: 'User not found',
      });
    }
  },
  
  createUser: (call, callback) => {
    const user = {
      id: uuidv4(),
      name: call.request.name,
      email: call.request.email,
    };
    users.set(user.id, user);
    callback(null, user);
  },
  
  listUsers: (call) => {
    for (const user of users.values()) {
      call.write(user);
    }
    call.end();
  },
});

const port = process.env.PORT || '50051';
server.bindAsync(
  `0.0.0.0:${port}`,
  grpc.ServerCredentials.createInsecure(),
  (err, port) => {
    if (err) throw err;
    console.log(`gRPC server running on port ${port}`);
    server.start();
  }
);

helipack.json untuk gRPC#

{
  "run": {
    "port": 50051,
    "isTcp": true,
    "command": "node src/server.js"
  }
}

Akses dari Service Lain (Internal)#

Dari service lain dalam project yang sama, akses gRPC via internal network:

// Di service lain
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const packageDef = protoLoader.loadSync('./proto/user.proto');
const userProto = grpc.loadPackageDefinition(packageDef).user;

// Gunakan nama service sebagai hostname
const client = new userProto.UserService(
  'user-service:50051',  // nama-service:port
  grpc.credentials.createInsecure()
);

client.getUser({ id: '123' }, (err, response) => {
  if (err) console.error(err);
  else console.log(response);
});

Use Case 4: Game Server (UDP-over-TCP)#

Untuk game server yang membutuhkan low-latency:

// src/gameserver.js
const net = require('net');

const players = new Map();
const port = parseInt(process.env.PORT || '7777');

const server = net.createServer((socket) => {
  const playerId = `player_${Date.now()}`;
  players.set(playerId, socket);
  
  console.log(`Player ${playerId} joined`);
  
  // Send welcome packet
  const welcomePacket = Buffer.from(JSON.stringify({
    type: 'welcome',
    playerId,
    playerCount: players.size,
  }) + '\n');
  socket.write(welcomePacket);
  
  // Broadcast to all players
  for (const [id, playerSocket] of players) {
    if (id !== playerId) {
      playerSocket.write(Buffer.from(JSON.stringify({
        type: 'player_joined',
        playerId,
      }) + '\n'));
    }
  }
  
  socket.on('data', (data) => {
    // Parse and handle game packets
    try {
      const packet = JSON.parse(data.toString().trim());
      
      // Broadcast position update to all other players
      if (packet.type === 'position') {
        for (const [id, playerSocket] of players) {
          if (id !== playerId) {
            playerSocket.write(Buffer.from(JSON.stringify({
              type: 'position',
              playerId,
              x: packet.x,
              y: packet.y,
            }) + '\n'));
          }
        }
      }
    } catch (e) {
      console.error('Invalid packet:', e.message);
    }
  });
  
  socket.on('close', () => {
    players.delete(playerId);
    console.log(`Player ${playerId} left`);
  });
});

server.listen(port, '0.0.0.0', () => {
  console.log(`Game server running on port ${port}`);
});
{
  "run": {
    "port": 7777,
    "isTcp": true,
    "command": "node src/gameserver.js"
  },
  "spec": {
    "cpu": 1,
    "memory": 512
  }
}

Use Case 5: Background Worker Tanpa HTTP Port#

Background worker seperti Celery (Python) atau queue worker tidak butuh HTTP port sama sekali, mereka hanya listen ke message queue (Redis, RabbitMQ) dan proses jobs.

{
  "run": {
    "isTcp": true,
    "port": 0,
    "command": "celery -A myproject worker --loglevel=info --concurrency=4"
  }
}
{
  "run": {
    "isTcp": true,
    "port": 0,
    "command": "php artisan queue:work redis --sleep=3 --tries=3 --max-time=3600"
  }
}

Tips: Set port: 0 untuk worker yang tidak butuh port sama sekali. Helipod tidak akan mencoba expose port atau melakukan health check.

Keterbatasan TCP Mode#

Tidak ada SSL otomatis: Di TCP mode, Traefik tidak melakukan SSL termination. Jika aplikasimu butuh enkripsi (untuk WebSocket wss:// misalnya), kamu perlu handle SSL di dalam aplikasi, atau gunakan service terpisah sebagai SSL terminator.

Tidak ada HTTP health check: Helipod menggunakan TCP connection check. Pastikan service kamu bisa menerima koneksi TCP di port yang ditentukan.

Tidak ada URL publik dengan custom path: Di HTTP mode, kamu bisa punya beberapa service di satu domain dengan path berbeda (/api/, /ws/, dll). Di TCP mode, setiap service butuh port berbeda atau domain berbeda.

Single port: Setiap service hanya bisa expose satu port di TCP mode.

Internal vs External Access#

Internal access (dari service lain dalam project):

  • Gunakan nama service dan port langsung: websocket-service:8080
  • Tidak butuh SSL

External access (dari internet/browser):

  • Gunakan magic domain atau custom domain yang diberikan Helipod
  • Browser modern mendukung WebSocket ke domain yang sama dengan HTTPS

Kesimpulan#

TCP mode di Helipod membuka kemungkinan untuk deploy jenis service yang lebih beragam, dari WebSocket real-time, gRPC microservice, game server, sampai background worker yang tidak butuh HTTP sama sekali.

Konfigurasinya sederhana: tambahkan "isTcp": true dan set port yang tepat di helipack.json. Helipod mengurus routing TCP-nya.

Ingin tahu lebih tentang konfigurasi helipack.json? Baca Panduan Lengkap helipack.json.

Punya pertanyaan? Hubungi kami di support@helipod.io atau bergabung ke komunitas di hangar.helipod.io.

Blog

관련 글

전체 보기

Helipod를 사용해 보시겠어요?

지금 앱을 배포해 보세요. 무료이며 신용카드가 필요 없습니다.
WhatsApp