Your container build finishes successfully, the deployment shows "RUNNING", but every HTTP request times out. The logs show your app listening on port 3000, yet the load balancer returns 503. This failure mode is frustratingly common and almost always caused by one of three issues: binding to localhost instead of 0.0.0.0, reading the PORT environment variable incorrectly, or failing the health check before the app is ready.
This post breaks down how PandaStack routes traffic to your container, what the health check actually does, and how to fix the three most common port binding mistakes.
How traffic reaches your app
When you deploy a containerized app, PandaStack:
- 1Builds your Docker image (or uses a buildpack to generate one)
- 2Deploys it to Kubernetes with a
Serviceresource that routes traffic to the pod - 3Configures a load balancer (Kong ingress) to forward requests to the
Service - 4Sends periodic health checks to verify the app is ready
If the health check fails for 30 seconds, Kubernetes kills the pod and restarts it. If the health check succeeds but the app doesn't respond to real requests, the problem is port binding.
The localhost vs 0.0.0.0 trap
Most local development servers bind to localhost (127.0.0.1) by default:
// Express example
app.listen(3000, 'localhost', () => {
console.log('Server running on http://localhost:3000');
});This works locally because your browser and the server are on the same machine—localhost resolves to the loopback interface. In a container, traffic arrives via the pod's external IP, not the loopback. The load balancer sends requests to 10.244.x.x:3000, but the app is listening on 127.0.0.1:3000, so the connection is refused.
Fix: bind to 0.0.0.0, which listens on all network interfaces:
app.listen(3000, '0.0.0.0', () => {
console.log('Server running on http://0.0.0.0:3000');
});This is the single most common reason container apps deploy successfully but don't respond. The build passes because the Dockerfile is valid, the pod starts because the process doesn't crash, but requests time out because the port isn't reachable.
Reading the PORT environment variable
Kubernetes injects a PORT environment variable into the container, typically 3000 or 8080. If your app hardcodes a different port or reads PORT incorrectly, the health check probes the wrong port and fails.
Wrong:
const PORT = 8080; // Hardcoded, ignores environment
app.listen(PORT, '0.0.0.0');Right:
const PORT = parseInt(process.env.PORT || '3000', 10);
app.listen(PORT, '0.0.0.0');The parseInt is important because environment variables are strings. If you pass "3000" directly to listen(), some servers accept it, but others fail silently.
Python (FastAPI/Uvicorn):
import os
import uvicorn
if __name__ == "__main__":
port = int(os.getenv("PORT", "8000"))
uvicorn.run("app:app", host="0.0.0.0", port=port)Go (Gin):
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
r.Run("0.0.0.0:" + port)Health check configuration
PandaStack sends an HTTP GET to the health check path (/ by default, or the value you set in pandastack.json). If the endpoint returns a non-2xx status or doesn't respond within the timeout, the pod is killed.
Set the health check path explicitly:
{
"type": "container",
"healthCheckPath": "/health"
}Then add a lightweight handler:
app.get('/health', (req, res) => {
res.status(200).send('OK');
});Critical: the health check starts immediately after the pod starts. If your app takes 10 seconds to initialize (connect to a database, warm up caches), the health check fails before the app is ready. The pod is killed, restarts, and the cycle repeats—your app never reaches a stable "RUNNING" state.
Fix: either speed up initialization or make the health check tolerant of slow startups. For example, return 200 from /health even if the database isn't connected yet:
app.get('/health', (req, res) => {
// Always return 200, even if DB is still connecting
res.status(200).send('OK');
});Then add a separate /readiness endpoint that checks the database:
app.get('/readiness', async (req, res) => {
try {
await pool.query('SELECT 1');
res.status(200).send('Ready');
} catch (err) {
res.status(503).send('Not ready');
}
});Use /health for the Kubernetes liveness probe and /readiness for monitoring. PandaStack currently uses a single health check path, so set it to /health and keep it fast.
Debugging with logs
The dashboard Logs tab shows both build and runtime logs. If your app starts but doesn't respond, check the logs for:
- 1"Server running on http://localhost:3000" → Change
localhostto0.0.0.0 - 2"EADDRINUSE: address already in use" → Another process is using the port, or the app is starting multiple servers
- 3"listen EACCES: permission denied" → Trying to bind to a port < 1024 without root (use
PORT3000+) - 4No logs after "Starting server..." → The process is hanging, likely waiting for a database connection that never succeeds
If the logs show "Server running" but requests still time out, the problem is port binding or health check configuration, not a crash.
Dockerfile best practices
If you're using a custom Dockerfile, ensure it exposes the correct port and runs the server:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]The EXPOSE 3000 line documents the port but doesn't actually open it—the app must listen on that port. If your Dockerfile uses a different port (e.g., EXPOSE 8080), update pandastack.json:
{
"type": "container",
"dockerfilePath": "Dockerfile",
"healthCheckPath": "/"
}PandaStack injects PORT=3000 by default. If your Dockerfile hardcodes 8080, override it:
ENV PORT=8080Or read PORT from the environment as shown earlier.
The SIGTERM trap
When Kubernetes scales down or redeploys your app, it sends a SIGTERM signal to the process, waits 30 seconds, then sends SIGKILL. If your app doesn't handle SIGTERM, it's forcibly killed mid-request, and users see 502 errors.
Fix: listen for SIGTERM and close the server gracefully:
const server = app.listen(PORT, '0.0.0.0');
process.on('SIGTERM', () => {
console.log('SIGTERM received, closing server');
server.close(() => {
console.log('Server closed');
process.exit(0);
});
});This drains existing connections before shutting down. The load balancer stops sending new requests once it sees the pod terminating.
Testing locally with Docker
To reproduce the production environment locally, build and run your container:
docker build -t myapp .
docker run -p 3000:3000 -e PORT=3000 myappThen test:
curl http://localhost:3000If this fails locally, it will fail in production. If it works locally but fails in production, the problem is likely the health check path or a missing environment variable.
The three-step checklist
When your app builds but doesn't respond:
- 1Bind to
0.0.0.0, notlocalhost - 2Read
PORTfrom the environment, don't hardcode it - 3Set a fast health check path that returns 200 within 30 seconds of pod start
These three changes fix 90% of port binding issues.
Why this matters
Platforms like Heroku abstracted away port binding—you just deployed, and it worked. Modern container platforms give you more control but expose more failure modes. Binding to localhost is safe in local dev, but deadly in production. Reading PORT incorrectly is invisible until the health check times out.
PandaStack's logs and dashboard make it easy to debug these issues once you know what to look for. The build log shows whether the container was created successfully. The runtime log shows whether the process started. If both pass but requests time out, the issue is port binding or health checks, not the build.
Full example: Express app with correct port binding
// server.js
const express = require('express');
const app = express();
const PORT = parseInt(process.env.PORT || '3000', 10);
app.get('/', (req, res) => {
res.json({ message: 'Hello from Express' });
});
app.get('/health', (req, res) => {
res.status(200).send('OK');
});
const server = app.listen(PORT, '0.0.0.0', () => {
console.log(`Server running on http://0.0.0.0:${PORT}`);
});
process.on('SIGTERM', () => {
console.log('SIGTERM received, closing server');
server.close(() => {
process.exit(0);
});
});pandastack.json:
{
"type": "container",
"language": "nodejs",
"startCommand": "node server.js",
"healthCheckPath": "/health"
}Deploy this, and requests succeed immediately.
References
- [Express Production Best Practices](https://expressjs.com/en/advanced/best-practice-performance.html)
- [Kubernetes Health Checks](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)
- [Docker Networking](https://docs.docker.com/network/)
- [Graceful Shutdown in Node.js](https://expressjs.com/en/advanced/healthcheck-graceful-shutdown.html)