Skip to content

Commit 4f6e980

Browse files
authored
Merge pull request #169 from konard/issue-168-c8c1e3f993ca
fix(shell): auto-repair DNS resolution at container startup
2 parents 8ecc83a + 6709463 commit 4f6e980

7 files changed

Lines changed: 188 additions & 1 deletion

File tree

docker-compose.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ services:
99
CODEX_HOME: "/home/dev/.codex"
1010
ports:
1111
- "127.0.0.1:2222:22"
12+
dns:
13+
- 8.8.8.8
14+
- 8.8.4.4
15+
- 1.1.1.1
1216
volumes:
1317
- dev_home:/home/dev
1418
- ./authorized_keys:/authorized_keys:ro

entrypoint.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,42 @@
1111
# COMPLEXITY: O(network + repo_size)
1212
set -euo pipefail
1313

14+
# 0) Ensure DNS resolution works; repair /etc/resolv.conf if Docker DNS is broken
15+
docker_git_repair_dns() {
16+
local test_domain="github.com"
17+
local resolv="/etc/resolv.conf"
18+
local fallback_dns="8.8.8.8 8.8.4.4 1.1.1.1"
19+
20+
if getent hosts "$test_domain" >/dev/null 2>&1; then
21+
return 0
22+
fi
23+
24+
echo "[dns-repair] DNS resolution failed for $test_domain; attempting repair..."
25+
26+
local has_external=0
27+
for ns in $fallback_dns; do
28+
if grep -q "nameserver $ns" "$resolv" 2>/dev/null; then
29+
has_external=1
30+
fi
31+
done
32+
33+
if [[ "$has_external" -eq 0 ]]; then
34+
for ns in $fallback_dns; do
35+
printf "nameserver %s\n" "$ns" >> "$resolv"
36+
done
37+
echo "[dns-repair] appended fallback nameservers to $resolv"
38+
fi
39+
40+
if getent hosts "$test_domain" >/dev/null 2>&1; then
41+
echo "[dns-repair] DNS resolution restored"
42+
return 0
43+
fi
44+
45+
echo "[dns-repair] WARNING: DNS resolution still failing after repair attempt"
46+
return 1
47+
}
48+
docker_git_repair_dns || true
49+
1450
REPO_URL="${REPO_URL:-}"
1551
REPO_REF="${REPO_REF:-}"
1652
TARGET_DIR="${TARGET_DIR:-/work/app}"

packages/lib/src/core/templates-entrypoint.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
renderEntrypointZshShell,
1212
renderEntrypointZshUserRc
1313
} from "./templates-entrypoint/base.js"
14+
import { renderEntrypointDnsRepair } from "./templates-entrypoint/dns-repair.js"
1415
import { renderEntrypointClaudeConfig } from "./templates-entrypoint/claude.js"
1516
import {
1617
renderEntrypointAgentsNotice,
@@ -34,6 +35,7 @@ import {
3435
export const renderEntrypoint = (config: TemplateConfig): string =>
3536
[
3637
renderEntrypointHeader(config),
38+
renderEntrypointDnsRepair(),
3739
renderEntrypointPackageCache(config),
3840
renderEntrypointAuthorizedKeys(config),
3941
renderEntrypointCodexHome(config),
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// CHANGE: add automatic DNS repair at container startup
2+
// WHY: Docker internal DNS (127.0.0.11) intermittently loses external nameservers,
3+
// causing domain resolution to fail inside containers
4+
// QUOTE(ТЗ): "При запуске контейнера он всегда исправляет интернет соединение потому что оно время от времени ложится"
5+
// REF: issue-168
6+
// SOURCE: n/a
7+
// FORMAT THEOREM: ∀container: startup(container) → dns_healthy(container) ∨ dns_repaired(container)
8+
// PURITY: SHELL
9+
// EFFECT: Effect<void, DnsRepairError, Env>
10+
// INVARIANT: after execution, at least one nameserver in /etc/resolv.conf resolves external domains
11+
// COMPLEXITY: O(1) per probe attempt, O(max_attempts) worst case
12+
export const renderEntrypointDnsRepair = (): string =>
13+
`# 0) Ensure DNS resolution works; repair /etc/resolv.conf if Docker DNS is broken
14+
docker_git_repair_dns() {
15+
local test_domain="github.com"
16+
local resolv="/etc/resolv.conf"
17+
local fallback_dns="8.8.8.8 8.8.4.4 1.1.1.1"
18+
19+
if getent hosts "$test_domain" >/dev/null 2>&1; then
20+
return 0
21+
fi
22+
23+
echo "[dns-repair] DNS resolution failed for $test_domain; attempting repair..."
24+
25+
# Preserve Docker internal resolver but append external fallbacks
26+
local has_external=0
27+
for ns in $fallback_dns; do
28+
if grep -q "nameserver $ns" "$resolv" 2>/dev/null; then
29+
has_external=1
30+
fi
31+
done
32+
33+
if [[ "$has_external" -eq 0 ]]; then
34+
for ns in $fallback_dns; do
35+
printf "nameserver %s\\n" "$ns" >> "$resolv"
36+
done
37+
echo "[dns-repair] appended fallback nameservers to $resolv"
38+
fi
39+
40+
# Verify fix
41+
if getent hosts "$test_domain" >/dev/null 2>&1; then
42+
echo "[dns-repair] DNS resolution restored"
43+
return 0
44+
fi
45+
46+
echo "[dns-repair] WARNING: DNS resolution still failing after repair attempt"
47+
return 1
48+
}
49+
docker_git_repair_dns || true`

packages/lib/src/core/templates/docker-compose.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const buildPlaywrightFragments = (
8484
maybeBrowserService:
8585
`\n ${browserServiceName}:\n build:\n context: .\n dockerfile: ${browserDockerfile}\n container_name: ${browserContainerName}\n restart: unless-stopped\n${
8686
renderResourceLimits(resourceLimits)
87-
} environment:\n VNC_NOPW: "1"\n shm_size: "2gb"\n expose:\n - "9223"\n volumes:\n - ${browserVolumeName}:/data\n networks:\n - ${networkName}\n`,
87+
} environment:\n VNC_NOPW: "1"\n shm_size: "2gb"\n expose:\n - "9223"\n dns:\n - 8.8.8.8\n - 8.8.4.4\n - 1.1.1.1\n volumes:\n - ${browserVolumeName}:/data\n networks:\n - ${networkName}\n`,
8888
maybeBrowserVolume: ` ${browserVolumeName}:\n`
8989
}
9090
}
@@ -153,6 +153,10 @@ ${renderResourceLimits(resourceLimits)} volumes:
153153
- ${config.codexAuthPath}:${config.codexHome}
154154
- ${renderSharedCodexHostMount(config.dockerGitPath)}:${config.codexHome}-shared
155155
- /var/run/docker.sock:/var/run/docker.sock
156+
dns:
157+
- 8.8.8.8
158+
- 8.8.4.4
159+
- 1.1.1.1
156160
networks:
157161
- ${fragments.networkName}
158162
${fragments.maybeBrowserService}`
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { describe, expect, it } from "@effect/vitest"
2+
3+
import { defaultTemplateConfig, type TemplateConfig } from "../../src/core/domain.js"
4+
import { renderDockerCompose } from "../../src/core/templates/docker-compose.js"
5+
import { renderEntrypoint } from "../../src/core/templates-entrypoint.js"
6+
import { renderEntrypointDnsRepair } from "../../src/core/templates-entrypoint/dns-repair.js"
7+
8+
const makeTemplateConfig = (overrides: Partial<TemplateConfig> = {}): TemplateConfig => ({
9+
...defaultTemplateConfig,
10+
repoUrl: "https://github.com/org/repo.git",
11+
containerName: "dg-test",
12+
serviceName: "dg-test",
13+
sshUser: "dev",
14+
targetDir: "/home/dev/org/repo",
15+
volumeName: "dg-test-home",
16+
dockerGitPath: "/workspace/.docker-git",
17+
authorizedKeysPath: "/workspace/authorized_keys",
18+
envGlobalPath: "/workspace/.orch/env/global.env",
19+
envProjectPath: "/workspace/.orch/env/project.env",
20+
codexAuthPath: "/workspace/.orch/auth/codex",
21+
codexSharedAuthPath: "/workspace/.orch/auth/codex-shared",
22+
geminiAuthPath: "/workspace/.orch/auth/gemini",
23+
...overrides
24+
})
25+
26+
describe("renderEntrypointDnsRepair", () => {
27+
it("renders the fallback nameserver repair block", () => {
28+
const dnsRepair = renderEntrypointDnsRepair()
29+
30+
expect(dnsRepair).toContain('local test_domain="github.com"')
31+
expect(dnsRepair).toContain('local fallback_dns="8.8.8.8 8.8.4.4 1.1.1.1"')
32+
expect(dnsRepair).toContain('printf "nameserver %s\\n" "$ns" >> "$resolv"')
33+
expect(dnsRepair).toContain('echo "[dns-repair] WARNING: DNS resolution still failing after repair attempt"')
34+
expect(dnsRepair).toContain("docker_git_repair_dns || true")
35+
})
36+
37+
it("injects DNS repair before the package cache setup in the full entrypoint", () => {
38+
const entrypoint = renderEntrypoint(makeTemplateConfig())
39+
const dnsRepair = renderEntrypointDnsRepair()
40+
const dnsRepairIndex = entrypoint.indexOf(dnsRepair)
41+
const packageCacheIndex = entrypoint.indexOf(
42+
"# Share package manager caches across all docker-git containers"
43+
)
44+
45+
expect(dnsRepairIndex).toBeGreaterThanOrEqual(0)
46+
expect(packageCacheIndex).toBeGreaterThan(dnsRepairIndex)
47+
})
48+
})
49+
50+
describe("renderDockerCompose", () => {
51+
it("renders fallback DNS servers for the main container even without Playwright", () => {
52+
const compose = renderDockerCompose(makeTemplateConfig())
53+
54+
expect(compose).toContain("container_name: dg-test")
55+
expect(compose).toContain(" dns:\n - 8.8.8.8\n - 8.8.4.4\n - 1.1.1.1\n networks:")
56+
expect(compose).not.toContain("dg-test-browser")
57+
expect((compose.match(/\n dns:\n/g) ?? []).length).toBe(1)
58+
})
59+
60+
it("renders fallback DNS servers for the browser sidecar when Playwright is enabled", () => {
61+
const compose = renderDockerCompose(
62+
makeTemplateConfig({
63+
enableMcpPlaywright: true
64+
}),
65+
{
66+
cpuLimit: 1.5,
67+
ramLimit: "2g"
68+
}
69+
)
70+
const browserServiceIndex = compose.indexOf("\n dg-test-browser:\n")
71+
const browserDnsIndex = compose.indexOf(
72+
' dns:\n - 8.8.8.8\n - 8.8.4.4\n - 1.1.1.1\n volumes:\n - dg-test-home-browser:/data\n',
73+
browserServiceIndex
74+
)
75+
76+
expect(compose).toContain('MCP_PLAYWRIGHT_CDP_ENDPOINT: "http://dg-test-browser:9223"')
77+
expect(browserServiceIndex).toBeGreaterThanOrEqual(0)
78+
expect(browserDnsIndex).toBeGreaterThan(browserServiceIndex)
79+
expect((compose.match(/\n dns:\n/g) ?? []).length).toBe(2)
80+
})
81+
})

packages/lib/tests/usecases/prepare-files.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ const readEnableMcpPlaywrightFlag = (value: unknown): boolean | undefined => {
125125
return typeof flag === "boolean" ? flag : undefined
126126
}
127127

128+
const countOccurrences = (source: string, fragment: string): number =>
129+
source.split(fragment).length - 1
130+
128131
describe("prepareProjectFiles", () => {
129132
it.effect("force-env refresh rewrites managed templates", () =>
130133
withTempDir((root) =>
@@ -147,6 +150,7 @@ describe("prepareProjectFiles", () => {
147150
const entrypointPath = path.join(outDir, "entrypoint.sh")
148151
const entrypoint = yield* _(fs.readFileString(entrypointPath))
149152
const composeBefore = yield* _(fs.readFileString(path.join(outDir, "docker-compose.yml")))
153+
const dnsBlock = " dns:\n - 8.8.8.8\n - 8.8.4.4\n - 1.1.1.1"
150154
const entrypointSyntaxExitCode = yield* _(
151155
runCommandExitCode({
152156
cwd: outDir,
@@ -171,6 +175,11 @@ describe("prepareProjectFiles", () => {
171175
expect(entrypoint).toContain('. /etc/profile 2>/dev/null || true;')
172176
expect(entrypoint).toContain("codex exec")
173177
expect(entrypoint).not.toContain("codex --approval-mode full-auto")
178+
expect(entrypoint).toContain("docker_git_repair_dns() {")
179+
expect(entrypoint).toContain('local test_domain="github.com"')
180+
expect(entrypoint).toContain('local fallback_dns="8.8.8.8 8.8.4.4 1.1.1.1"')
181+
expect(entrypoint).toContain('printf "nameserver %s\\n" "$ns" >> "$resolv"')
182+
expect(entrypoint).toContain("docker_git_repair_dns || true")
174183
expect(entrypoint).toContain('"plugin": ["oh-my-opencode"]')
175184
expect(entrypoint).toContain("branch '$REPO_REF' missing; retrying without --branch")
176185
expect(entrypoint).not.toContain("git ls-remote --symref")
@@ -185,6 +194,7 @@ describe("prepareProjectFiles", () => {
185194
expect(composeBefore).not.toContain("dg-test-browser")
186195
expect(composeBefore).toContain("docker-git-shared")
187196
expect(composeBefore).toContain("external: true")
197+
expect(countOccurrences(composeBefore, dnsBlock)).toBe(1)
188198

189199
yield* _(
190200
prepareProjectFiles(outDir, root, globalConfig, withMcp, {
@@ -208,6 +218,7 @@ describe("prepareProjectFiles", () => {
208218
expect(composeAfter).toContain("container_name: dg-test-browser\n restart: unless-stopped")
209219
expect(composeAfter).toContain("docker-git-shared")
210220
expect(composeAfter).toContain("external: true")
221+
expect(countOccurrences(composeAfter, dnsBlock)).toBe(2)
211222
expect(readEnableMcpPlaywrightFlag(configAfter)).toBe(true)
212223
expect(configAfterText).toContain('"cpuLimit": "30%"')
213224
expect(configAfterText).toContain('"ramLimit": "30%"')

0 commit comments

Comments
 (0)