Skip to content

Commit cc21c11

Browse files
Mossakaclaude
andauthored
fix: eliminate 10s container shutdown delay (#1371) (#1373)
Three root causes of the 10-second shutdown delay: 1. api-proxy Dockerfile used shell-form CMD (node runs under /bin/sh, which doesn't forward SIGTERM to the child process). Switched to exec form so node is PID 1 and handles signals directly. 2. Squid's default shutdown_lifetime (30s) causes it to wait for active connections to drain. Added shutdown_lifetime 0 since this is an ephemeral proxy with no need for connection draining. 3. Docker Compose default stop timeout is 10s. Added stop_grace_period of 2s to squid and api-proxy services since they now shut down promptly on SIGTERM. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 999f9a4 commit cc21c11

File tree

5 files changed

+30
-3
lines changed

5 files changed

+30
-3
lines changed

containers/api-proxy/Dockerfile

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,5 @@ USER apiproxy
3030
# 10004 - OpenCode API proxy (routes to Anthropic)
3131
EXPOSE 10000 10001 10002 10004
3232

33-
# Redirect stdout/stderr to log file for persistence
34-
# Use shell form to enable redirection and tee for both file and console
35-
CMD node server.js 2>&1 | tee -a /var/log/api-proxy/api-proxy.log
33+
# Use exec form so node is PID 1 and receives SIGTERM directly
34+
CMD ["node", "server.js"]

src/docker-manager.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,12 @@ describe('docker-manager', () => {
538538
expect(squid.ports).toContain('3128:3128');
539539
});
540540

541+
it('should set stop_grace_period on squid service', () => {
542+
const result = generateDockerCompose(mockConfig, mockNetworkConfig);
543+
const squid = result.services['squid-proxy'] as any;
544+
expect(squid.stop_grace_period).toBe('2s');
545+
});
546+
541547
it('should inject squid config via base64 env var when content is provided', () => {
542548
const squidConfig = 'http_port 3128\nacl allowed_domains dstdomain .github.com\n';
543549
const result = generateDockerCompose(mockConfig, mockNetworkConfig, undefined, squidConfig);
@@ -1789,6 +1795,13 @@ describe('docker-manager', () => {
17891795
expect(proxy.security_opt).toContain('no-new-privileges:true');
17901796
});
17911797

1798+
it('should set stop_grace_period on api-proxy service', () => {
1799+
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' };
1800+
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
1801+
const proxy = result.services['api-proxy'] as any;
1802+
expect(proxy.stop_grace_period).toBe('2s');
1803+
});
1804+
17921805
it('should set resource limits', () => {
17931806
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' };
17941807
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);

src/docker-manager.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ export function generateDockerCompose(
325325
'AUDIT_WRITE', // No audit log writing
326326
'SETFCAP', // No setting file capabilities
327327
],
328+
stop_grace_period: '2s',
328329
};
329330

330331
// Inject squid.conf via environment variable instead of bind mount.
@@ -1238,6 +1239,7 @@ export function generateDockerCompose(
12381239
memswap_limit: '512m',
12391240
pids_limit: 100,
12401241
cpu_shares: 512,
1242+
stop_grace_period: '2s',
12411243
};
12421244

12431245
// Use GHCR image or build locally

src/squid-config.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,15 @@ describe('generateSquidConfig', () => {
568568
const result = generateSquidConfig(config);
569569
expect(result).toContain('pconn_timeout 2 minutes');
570570
});
571+
572+
it('should include shutdown_lifetime 0 for fast shutdown', () => {
573+
const config: SquidConfig = {
574+
domains: ['example.com'],
575+
port: defaultPort,
576+
};
577+
const result = generateSquidConfig(config);
578+
expect(result).toContain('shutdown_lifetime 0 seconds');
579+
});
571580
});
572581

573582
describe('Real-world Domain Patterns', () => {

src/squid-config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,10 @@ client_lifetime 8 hours
606606
# Critical for SSE where server sends but client doesn't respond
607607
half_closed_clients on
608608
609+
# shutdown_lifetime: Time to wait for active connections during shutdown
610+
# Set to 0 because this is an ephemeral proxy — no connection draining needed
611+
shutdown_lifetime 0 seconds
612+
609613
# Debugging (can be enabled for troubleshooting)
610614
# debug_options ALL,1 33,2
611615
`;

0 commit comments

Comments
 (0)