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: 0untuk 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.id atau bergabung ke komunitas di hangar.helipod.io.