diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8e66913..e611b20 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1 @@ -.github/CODEOWNERS @djchen -.github/workflows/* @djchen -.github/dependabot.yml @djchen +* @djchen diff --git a/Dockerfile b/Dockerfile index 3e83eab..4755255 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,11 +54,28 @@ WORKDIR /opt/opencode-web COPY --from=build /opt/opencode-web/release/ ./ COPY --from=build /opt/opencode-web/release/runtime/generate-nginx-config.sh /docker-entrypoint.d/40-opencode-web.sh -RUN chmod +x /docker-entrypoint.d/40-opencode-web.sh +RUN sed -i '/^pid /d' /etc/nginx/nginx.conf \ + && rm -f /etc/nginx/conf.d/default.conf \ + && mkdir -p \ + /etc/nginx/conf.d \ + /opt/opencode-web/runtime-configs \ + /var/cache/nginx/client_temp \ + /var/cache/nginx/proxy_temp \ + /var/cache/nginx/fastcgi_temp \ + /var/cache/nginx/uwsgi_temp \ + /var/cache/nginx/scgi_temp \ + && chown -R nginx:nginx \ + /etc/nginx/conf.d \ + /opt/opencode-web/runtime-configs \ + /var/cache/nginx \ + && chmod +x /docker-entrypoint.d/40-opencode-web.sh \ + && chmod -R a-w /opt/opencode-web/public -EXPOSE 80 +USER nginx:nginx + +EXPOSE 8080 HEALTHCHECK --interval=1m --timeout=5s --start-period=15s --retries=3 \ - CMD wget -q --spider http://127.0.0.1/health || exit 1 + CMD wget -q --spider http://127.0.0.1:8080/health || exit 1 -CMD ["nginx", "-g", "daemon off;"] +CMD ["nginx", "-g", "pid /tmp/nginx.pid; daemon off;"] diff --git a/README.md b/README.md index a2f1424..4e2db68 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Self-host the [OpenCode](https://opencode.ai) web frontend as a static site with The container serves the same built app on multiple hostnames, injects the matching backend and title for each hostname, and applies CSS customizations. +The image runs nginx as the non-root `nginx` user and listens on container port `8080`. To expose it on host port 80, map `80:8080`. + ## Quick Start ### OpenCode Server Run `opencode serve` to expose an endpoint that OpenCode clients can use. @@ -29,7 +31,7 @@ docker compose up -d ```sh docker run -d \ --name opencode-web \ - -p 8080:80 \ + -p 8080:8080 \ -e SERVER_1_HOST=web1.opencode.example.com \ -e SERVER_1_BACKEND=https://api1.opencode.example.com \ -e SERVER_1_NAME='Server 1' \ diff --git a/config/nginx.conf.template b/config/nginx.conf.template index 124dd0e..fa9bb12 100644 --- a/config/nginx.conf.template +++ b/config/nginx.conf.template @@ -1,6 +1,12 @@ +client_body_temp_path /var/cache/nginx/client_temp; +proxy_temp_path /var/cache/nginx/proxy_temp; +fastcgi_temp_path /var/cache/nginx/fastcgi_temp; +uwsgi_temp_path /var/cache/nginx/uwsgi_temp; +scgi_temp_path /var/cache/nginx/scgi_temp; + server { - listen 80 default_server; - listen [::]:80 default_server; + listen 8080 default_server; + listen [::]:8080 default_server; server_name _; add_header Cache-Control "no-store, no-cache, must-revalidate" always; add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; connect-src * data:; font-src 'self' data:; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' data:; object-src 'none'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'" always; diff --git a/docker-compose.yaml b/docker-compose.yaml index 7788dff..b93d6ed 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,7 +2,7 @@ services: web: image: ghcr.io/djchen/opencode-web-docker ports: - - 8080:80 + - 8080:8080 environment: SERVER_1_HOST: web1.opencode.example.com SERVER_1_BACKEND: https://api1.opencode.example.com diff --git a/runtime/generate-nginx-config.sh b/runtime/generate-nginx-config.sh index 6d25a7d..a3f5a76 100755 --- a/runtime/generate-nginx-config.sh +++ b/runtime/generate-nginx-config.sh @@ -105,6 +105,7 @@ runtime_bundle_path="$runtime_root/runtime/runtime-bundle.js" nginx_template_path="$runtime_root/config/nginx.conf.template" nginx_config_path="/etc/nginx/conf.d/default.conf" nginx_servers_marker="# OPENCODE_WEB_GENERATED_SERVERS" +nginx_listen_port="8080" if [ ! -r "$runtime_bundle_path" ]; then die "Missing runtime bundle at $runtime_bundle_path" @@ -216,8 +217,8 @@ write_server_block() { cat <&1)"; printf "%s\n" "$headers" | grep -qi "cache-control.*no-store" && printf "%s\n" "$headers" | grep -qi "content-security-policy"', + 'headers="$(wget -qS --header="Host: web1.opencode.example.com" -O /dev/null http://127.0.0.1:8080/ 2>&1)"; printf "%s\n" "$headers" | grep -qi "cache-control.*no-store" && printf "%s\n" "$headers" | grep -qi "content-security-policy"', ], ); await expectSuccess("hashed assets have long-lived cache headers", [ @@ -742,7 +761,7 @@ try { [ "set -- /opt/opencode-web/public/assets/*", `asset="\${1##*/}"`, - 'headers="$(wget -qS --header="Host: web1.opencode.example.com" -O /dev/null "http://127.0.0.1/assets/$asset" 2>&1)"', + 'headers="$(wget -qS --header="Host: web1.opencode.example.com" -O /dev/null "http://127.0.0.1:8080/assets/$asset" 2>&1)"', 'printf "%s\n" "$headers" | grep -qi "cache-control.*immutable"', 'printf "%s\n" "$headers" | grep -qi "content-security-policy"', ].join(" && "), diff --git a/tests/static-csp.contracts.ts b/tests/static-csp.contracts.ts index 224a70c..39a8414 100644 --- a/tests/static-csp.contracts.ts +++ b/tests/static-csp.contracts.ts @@ -198,13 +198,38 @@ export const staticCspContracts: Contract[] = [ checks: [ match( "nginxConfigTemplate", - /listen 80 default_server;/, - "expected nginx default server to listen on IPv4 port 80", + /listen 8080 default_server;/, + "expected nginx default server to listen on IPv4 port 8080", ), match( "nginxConfigTemplate", - /listen \[::\]:80 default_server;/, - "expected nginx default server to listen on IPv6 port 80", + /listen \[::\]:8080 default_server;/, + "expected nginx default server to listen on IPv6 port 8080", + ), + match( + "nginxConfigTemplate", + /client_body_temp_path \/var\/cache\/nginx\/client_temp;/, + "expected nginx client temp path to use a writable non-root directory", + ), + match( + "nginxConfigTemplate", + /proxy_temp_path \/var\/cache\/nginx\/proxy_temp;/, + "expected nginx proxy temp path to use a writable non-root directory", + ), + match( + "nginxConfigTemplate", + /fastcgi_temp_path \/var\/cache\/nginx\/fastcgi_temp;/, + "expected nginx fastcgi temp path to use a writable non-root directory", + ), + match( + "nginxConfigTemplate", + /uwsgi_temp_path \/var\/cache\/nginx\/uwsgi_temp;/, + "expected nginx uwsgi temp path to use a writable non-root directory", + ), + match( + "nginxConfigTemplate", + /scgi_temp_path \/var\/cache\/nginx\/scgi_temp;/, + "expected nginx scgi temp path to use a writable non-root directory", ), match( "nginxConfigTemplate", @@ -221,6 +246,21 @@ export const staticCspContracts: Contract[] = [ /^# OPENCODE_WEB_GENERATED_SERVERS$/m, "expected nginx template to contain generated server marker", ), + match( + "runtimeGenerator", + /nginx_listen_port="8080"/, + "expected generated nginx server blocks to use container port 8080", + ), + match( + "runtimeGenerator", + /listen \$nginx_listen_port;/, + "expected generated nginx IPv4 listeners to use the configured listen port", + ), + match( + "runtimeGenerator", + /listen \[::\]:\$nginx_listen_port;/, + "expected generated nginx IPv6 listeners to use the configured listen port", + ), match( "runtimeGenerator", /server_name \$host;/,