diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aff1d124869..7ffbe502a0ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,10 @@ Code v1.127.0 - Update to Code 1.127.0 +### Fixed + +- Prevent non-stop "parent process died" output by clearing the parent-check interval after first detection + ## [4.126.0](https://github.com/coder/code-server/releases/tag/v4.126.0) - 2026-06-24 Code v1.126.0 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000000..0d2a3c44ba86 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM codercom/code-server:latest + +USER root + +# Create directories for persistent volumes and set ownership +RUN mkdir -p /home/coder/.local/share/code-server/extensions \ + /home/coder/.config \ + /home/coder/.local/share/code-server/logs \ + /home/coder/.local/share/code-server/User && \ + chown -R coder:coder /home/coder/.local /home/coder/.config + +# Install Node.js and npm (code-server bundles its own Node.js, but we need a global npm for opencode) +RUN apt-get update && apt-get install -y --no-install-recommends \ + nodejs \ + npm \ + && rm -rf /var/lib/apt/lists/* + +# Install opencode CLI globally +RUN npm install -g opencode-ai@latest + +USER coder diff --git a/dokploy/.env.example b/dokploy/.env.example new file mode 100644 index 000000000000..909f3ba6455c --- /dev/null +++ b/dokploy/.env.example @@ -0,0 +1,46 @@ +# Code-Server Environment Variables +# Update these values before deployment + +# Authentication +PASSWORD: "your-secure-password-here" +PASSWORD_HINT: "set-a-password" + +# User Configuration +USER: admin +GROUP: admin + +# Timezone +TZ: America/New_York + +# Domain (if using reverse proxy) +PROXY_DOMAIN: "" + +# Password Policy +PASSWORD_EXPIRY_DAYS: 30 +PASSWORD_MINIMUM_LENGTH: 8 +PASSWORD_MINIMUM_DIFFERENT_CHARS: 3 + +# Feature Flags +DISABLE_GUEST_ACCESS: "true" +DISABLE_USER_SWITCHING: "true" +DISABLE_TERMINAL: "false" +DISABLE_SSH: "true" +DISABLE_UPDATE_CHECK: "true" +DISABLE_UPDATE_NOTIFICATION: "true" +DISABLE_UPDATE_POPUP: "true" +DISABLE_DASHBOARD: "false" +DISABLE_FILE_BROWSER: "false" +DISABLE_STARTUP_PAGE: "true" +DISABLE_EXTENSIONS: "false" + +# File Browser Permissions +DISABLE_FILE_BROWSER_UPLOAD: "true" +DISABLE_FILE_BROWSER_DOWNLOAD: "true" +DISABLE_FILE_BROWSER_DELETE: "true" +DISABLE_FILE_BROWSER_CREATE: "true" +DISABLE_FILE_BROWSER_RENAME: "true" +DISABLE_FILE_BROWSER_MOVE: "true" +DISABLE_FILE_BROWSER_COPY: "true" +DISABLE_FILE_BROWSER_SEARCH: "true" +DISABLE_FILE_BROWSER_ZIP: "true" +DISABLE_FILE_BROWSER_UNZIP: "true" diff --git a/dokploy/README.md b/dokploy/README.md new file mode 100644 index 000000000000..68a4a89553a3 --- /dev/null +++ b/dokploy/README.md @@ -0,0 +1,156 @@ +# Code-Server Deployment Guide for Dokploy + +## Quick Start + +1. **Set Environment Variables** + - Edit `.env` file with your desired configuration + - Set a strong `PASSWORD` + - Configure feature flags as needed + +2. **Deploy in Dokploy** + - Create a new Dokploy project + - Upload the entire `dokploy/` directory (or configure the git source to point to this repo) + - Set the environment variables from `.env` + - Deploy the service — the `Dockerfile` will build a custom image with opencode pre-installed + +3. **Access Code-Server** + - URL: `https://your-domain.com:8080` + - Username: `admin` + - Password: (from .env) + +4. **Use OpenCode** + - Open the integrated terminal in code-server + - Run `opencode` to start the AI coding agent + +## Directory Structure + +``` +code-server/ +├── Dockerfile # Custom image with opencode pre-installed +├── dokploy/ +│ ├── docker-compose.yml # Dokploy deployment config +│ ├── .env # Environment variables +│ ├── code-server-data/ # Persisted data (created automatically) +│ └── fix-permissions.sh # Permission fix helper (run once on host) +└── README.md # This file +``` + +## Environment Variables Reference + +### Authentication +- `PASSWORD` - Admin password (required) +- `PASSWORD_HINT` - Password hint for users + +### User Management +- `USER` - Username (default: admin) +- `GROUP` - User group (default: admin) +- `DISABLE_GUEST_ACCESS` - Block guest access (true/false) + +### Features +- `DISABLE_TERMINAL` - Disable terminal (true/false) +- `DISABLE_SSH` - Disable SSH access (true/false) +- `DISABLE_DASHBOARD` - Hide dashboard (true/false) +- `DISABLE_FILE_BROWSER` - Disable file browser (true/false) +- `DISABLE_STARTUP_PAGE` - Skip the startup page (true/false) +- `DISABLE_EXTENSIONS` - Disable all extensions including opencode (true/false) + +### File Browser +- `DISABLE_FILE_BROWSER_UPLOAD` - Disable uploads +- `DISABLE_FILE_BROWSER_DOWNLOAD` - Disable downloads +- `DISABLE_FILE_BROWSER_DELETE` - Disable deletes +- `DISABLE_FILE_BROWSER_CREATE` - Disable file creation +- `DISABLE_FILE_BROWSER_RENAME` - Disable renaming +- `DISABLE_FILE_BROWSER_MOVE` - Disable moving files +- `DISABLE_FILE_BROWSER_COPY` - Disable copying +- `DISABLE_FILE_BROWSER_SEARCH` - Disable search +- `DISABLE_FILE_BROWSER_ZIP` - Disable zipping +- `DISABLE_FILE_BROWSER_UNZIP` - Disable unzipping + +### Updates +- `DISABLE_UPDATE_CHECK` - Disable update checks (true/false) +- `DISABLE_UPDATE_NOTIFICATION` - Disable update notifications +- `DISABLE_UPDATE_POPUP` - Disable update popups + +## OpenCode + +[OpenCode](https://opencode.ai) is an open-source AI coding agent pre-installed in this image. + +- Run `opencode` in the code-server integrated terminal +- Use `Tab` to switch between **build** (full-access) and **plan** (read-only) agents +- Configure via `~/.opencode/config.json` or the `.opencode/` directory in your project + +The fork is maintained at [kt-production-repo/opencode](https://github.com/kt-production-repo/opencode). + +## Volume Mounts + +To ensure persistence of code-server data, extensions, settings, logs, and user-specific data, mount the following directories: + +| Host Path | Container Path | Description | +|---|---|---| +| `./code-server-data` | `/home/coder/.local/share/code-server` | Main code-server data (storage, etc.) | +| `./code-server-extensions` | `/home/coder/.local/share/code-server/extensions` | Installed VS Code extensions | +| `./code-server-settings` | `/home/coder/.config` | User configuration (includes opencode config) | +| `./code-server-logs` | `/home/coder/.local/share/code-server/logs` | Application logs | +| `./code-server-user` | `/home/coder/.local/share/code-server/User` | User-specific data (globalStorage, Machine, history) | + +**Why separate mounts?** Separating these directories makes it easier to back up specific parts (e.g., just extensions or settings) and avoids potential permission conflicts when code-server creates files in parent directories on startup. + +If you prefer a single volume mount, you can still use: +- `./code-server-data:/home/coder/.local/share/code-server` + +However, the multi-mount approach above is recommended for better organization and backup flexibility. + +**If you previously used the old single-volume layout, migrate existing data:** +```bash +# Stop the container first, then: +mkdir -p code-server-data code-server-extensions code-server-settings code-server-logs code-server-user +cp -r code-server-data/* code-server-data/ 2>/dev/null || true # if you had the old single volume named code-server-data +# Adjust paths as needed based on your previous setup +chown -R 1001:1001 code-server-data code-server-extensions code-server-settings code-server-logs code-server-user +``` + +## Resource Limits + +- CPU: 2 cores (limit), 0.5 cores (reservation) +- Memory: 2GB (limit), 512MB (reservation) + +## Troubleshooting + +### Cannot Access Code-Server +- Check if port 8080 is not in use +- Verify the service is running in Dokploy +- Check firewall settings + +### Password Reset +- Edit the `.env` file with a new password +- Redeploy the service in Dokploy +- Or access via SSH and reset: `coder --password ` + +### Performance Issues +- Reduce resource limits in `docker-compose.yml` +- Increase available memory on host +- Check disk I/O on mounted volumes + +## Security Recommendations + +1. Use a strong, unique password +2. Enable HTTPS with your reverse proxy +3. Configure firewall rules +4. Regularly update the image +5. Monitor resource usage +6. Backup the `code-server-data` directory + +## Updating + +To update to the latest version: + +1. Pull the latest image in Dokploy +2. Redeploy with the same configuration +3. Your data and settings are preserved in volumes + +## Customization + +Additional settings can be configured via: +- Command line flags in the service definition +- Environment variables +- Code-server configuration files in the `settings` volume diff --git a/dokploy/docker-compose.yml b/dokploy/docker-compose.yml new file mode 100644 index 000000000000..19a684c0716a --- /dev/null +++ b/dokploy/docker-compose.yml @@ -0,0 +1,64 @@ +version: '3.8' + +services: + code-server: + build: + context: .. + dockerfile: Dockerfile + container_name: code-server + restart: unless-stopped + ports: + - "8080:8080" + environment: + - PASSWORD=${PASSWORD} + - PASSWORD_HINT=${PASSWORD_HINT:-set-a-password} + - USER=${USER:-admin} + - GROUP=${GROUP:-admin} + - TZ=${TZ:-America/New_York} + - PROXY_DOMAIN=${PROXY_DOMAIN} + - PASSWORD_EXPIRY_DAYS=${PASSWORD_EXPIRY_DAYS:-30} + - PASSWORD_MINIMUM_LENGTH=${PASSWORD_MINIMUM_LENGTH:-8} + - PASSWORD_MINIMUM_DIFFERENT_CHARS=${PASSWORD_MINIMUM_DIFFERENT_CHARS:-3} + - DISABLE_GUEST_ACCESS=${DISABLE_GUEST_ACCESS:-true} + - DISABLE_USER_SWITCHING=${DISABLE_USER_SWITCHING:-true} + - DISABLE_TERMINAL=${DISABLE_TERMINAL:-false} + - DISABLE_SSH=${DISABLE_SSH:-true} + - DISABLE_UPDATE_CHECK=${DISABLE_UPDATE_CHECK:-true} + - DISABLE_UPDATE_NOTIFICATION=${DISABLE_UPDATE_NOTIFICATION:-true} + - DISABLE_UPDATE_POPUP=${DISABLE_UPDATE_POPUP:-true} + - DISABLE_DASHBOARD=${DISABLE_DASHBOARD:-false} + - DISABLE_FILE_BROWSER=${DISABLE_FILE_BROWSER:-false} + - DISABLE_FILE_BROWSER_UPLOAD=${DISABLE_FILE_BROWSER_UPLOAD:-true} + - DISABLE_FILE_BROWSER_DOWNLOAD=${DISABLE_FILE_BROWSER_DOWNLOAD:-true} + - DISABLE_FILE_BROWSER_DELETE=${DISABLE_FILE_BROWSER_DELETE:-true} + - DISABLE_FILE_BROWSER_CREATE=${DISABLE_FILE_BROWSER_CREATE:-true} + - DISABLE_FILE_BROWSER_RENAME=${DISABLE_FILE_BROWSER_RENAME:-true} + - DISABLE_FILE_BROWSER_MOVE=${DISABLE_FILE_BROWSER_MOVE:-true} + - DISABLE_FILE_BROWSER_COPY=${DISABLE_FILE_BROWSER_COPY:-true} + - DISABLE_FILE_BROWSER_SEARCH=${DISABLE_FILE_BROWSER_SEARCH:-true} + - DISABLE_FILE_BROWSER_ZIP=${DISABLE_FILE_BROWSER_ZIP:-true} + - DISABLE_FILE_BROWSER_UNZIP=${DISABLE_FILE_BROWSER_UNZIP:-true} + - DISABLE_EXTENSIONS=${DISABLE_EXTENSIONS:-false} + - DISABLE_STARTUP_PAGE=${DISABLE_STARTUP_PAGE:-true} +volumes: + - ./code-server-data:/home/coder/.local/share/code-server + - ./code-server-extensions:/home/coder/.local/share/code-server/extensions + - ./code-server-settings:/home/coder/.config + - ./code-server-logs:/home/coder/.local/share/code-server/logs + - ./code-server-user:/home/coder/.local/share/code-server/User + networks: + - code-server-network + security_opt: + - seccomp=unconfined + deploy: + resources: + limits: + cpus: '2' + memory: 2G + reservations: + cpus: '0.5' + memory: 512M + +networks: + code-server-network: + driver: bridge diff --git a/dokploy/fix-permissions.sh b/dokploy/fix-permissions.sh new file mode 100755 index 000000000000..bef2cbfe3e68 --- /dev/null +++ b/dokploy/fix-permissions.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Code-Server Dokploy Permission Fix Script +# Run this in the dokploy directory: /etc/dokploy/compose//code/dokploy + +set -e + +echo "Fixing code-server permissions..." + +# Create the single volume directory +echo "Creating directory..." +mkdir -p code-server-data + +# Set correct ownership (coder user is typically UID 1001) +echo "Setting ownership to coder user (UID 1001)..." +chown -R 1001:1001 code-server-data + +# Set read/write/execute for coder user +chmod -R 755 code-server-data + +echo "Permissions fixed!" +echo "" +echo "Directory: code-server-data" +echo "" +echo "Next step: Restart the code-server container in Dokploy UI" diff --git a/src/browser/media/favicon.svg b/src/browser/media/favicon.svg index 01a01541ec75..478b6e1858f6 100644 --- a/src/browser/media/favicon.svg +++ b/src/browser/media/favicon.svg @@ -1,3 +1,3 @@ - + diff --git a/src/browser/pages/error.css b/src/browser/pages/error.css index d100ee5c4666..98bc50510e76 100644 --- a/src/browser/pages/error.css +++ b/src/browser/pages/error.css @@ -2,6 +2,14 @@ box-sizing: border-box; padding: 20px; text-align: center; + max-width: 600px; + margin: 0 auto; +} + +@media (max-width: 480px) { + .error-display { + padding: 15px; + } } .error-display > .header { @@ -9,12 +17,24 @@ margin: 0; } +@media (max-width: 480px) { + .error-display > .header { + font-size: 4rem; + } +} + .error-display > .body { color: #444; color: light-dark(#444, #ccc); font-size: 1.2rem; } +@media (max-width: 480px) { + .error-display > .body { + font-size: 1rem; + } +} + .error-display > .links { margin-top: 16px; } diff --git a/src/browser/pages/error.html b/src/browser/pages/error.html index f44df0539ccb..676837407dfd 100644 --- a/src/browser/pages/error.html +++ b/src/browser/pages/error.html @@ -12,6 +12,8 @@ /> {{ERROR_TITLE}} - {{APP_NAME}} + + @@ -19,6 +21,7 @@ + @@ -31,5 +34,12 @@

{{ERROR_HEADER}}

+ + diff --git a/src/browser/pages/global.css b/src/browser/pages/global.css index ba79ea6134c6..628360cc00b8 100644 --- a/src/browser/pages/global.css +++ b/src/browser/pages/global.css @@ -31,7 +31,7 @@ button { .-button { background-color: rgb(87, 114, 245); background-color: light-dark(rgb(87, 114, 245), rgb(26, 86, 219)); - border-radius: 5px; + border-radius: 6px; border: none; box-sizing: border-box; color: white; @@ -40,6 +40,17 @@ button { padding: 18px 20px; font-weight: 500; text-decoration: none; + min-height: 44px; + min-width: 44px; +} + +@media (max-width: 480px) { + .-button { + border-radius: 8px; + padding: 20px 18px; + min-height: 48px; + font-size: 1.05rem; + } } .center-container { @@ -53,6 +64,13 @@ button { width: 100%; } +@media (max-width: 480px) { + .center-container { + padding: 15px; + min-height: 100vh; + } +} + .card-box { background-color: rgb(250, 253, 258); background-color: light-dark(rgb(250, 253, 258), #1f2937); @@ -62,6 +80,15 @@ button { rgba(0, 0, 0, 0.117647) 0px 3px 6px 0px; max-width: 650px; width: 100%; + margin: 0 auto; +} + +@media (max-width: 480px) { + .card-box { + border-radius: 8px; + box-shadow: light-dark(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.4)) 0px 4px 8px 0px; + max-width: 100%; + } } .card-box > .header { @@ -72,21 +99,45 @@ button { padding: 30px; } +@media (max-width: 480px) { + .card-box > .header { + padding: 20px; + } +} + .card-box > .header > .main { margin: 0; font-size: 1.5rem; } +@media (max-width: 480px) { + .card-box > .header > .main { + font-size: 1.3rem; + } +} + .card-box > .header > .sub { color: #555; color: light-dark(#555, #9ca3af); margin-top: 10px; } +@media (max-width: 480px) { + .card-box > .header > .sub { + font-size: 0.9rem; + } +} + .card-box > .content { padding: 40px; } +@media (max-width: 480px) { + .card-box > .content { + padding: 20px; + } +} + .card-box > .content > .none { margin: 2px 0; } @@ -99,3 +150,5 @@ canvas { top: 0; left: 0; } + +/* Icons handled by centralized icon-adapter.ts */ diff --git a/src/browser/pages/login.css b/src/browser/pages/login.css index 33b541c9e844..1e787d1de7a9 100644 --- a/src/browser/pages/login.css +++ b/src/browser/pages/login.css @@ -4,6 +4,13 @@ body { overflow: auto; } +@media (max-width: 480px) { + body { + min-height: 100vh; + overflow-x: hidden; + } +} + .login-form { display: flex; flex-direction: column; @@ -47,6 +54,14 @@ body { outline: 2px solid rgb(63, 131, 248); } +@media (max-width: 480px) { + .login-form > .field > .password { + border-radius: 8px; + padding: 18px; + min-height: 48px; + } +} + .login-form > .user { display: none; } @@ -62,6 +77,18 @@ body { } } +@media (max-width: 480px) { + .login-form { + padding: 20px; + } + + .login-form > .field > .submit { + min-height: 48px; + padding: 16px 20px; + font-size: 1.05rem; + } +} + input { -webkit-appearance: none; } diff --git a/src/browser/pages/login.html b/src/browser/pages/login.html index c7fb2f2ac67e..0d17805fed0c 100644 --- a/src/browser/pages/login.html +++ b/src/browser/pages/login.html @@ -12,6 +12,8 @@ /> {{I18N_LOGIN_TITLE}} + + @@ -19,6 +21,7 @@ + @@ -50,6 +53,7 @@

{{WELCOME_TEXT}}

+ diff --git a/src/node/wrapper.ts b/src/node/wrapper.ts index 607512e2c9fb..f6aaaafd9c96 100644 --- a/src/node/wrapper.ts +++ b/src/node/wrapper.ts @@ -154,22 +154,39 @@ abstract class Process { export class ChildProcess extends Process { public logger = logger.named(`child:${process.pid}`) + private parentCheckInterval: NodeJS.Timeout | undefined + public constructor(private readonly parentPid: number) { super() // Kill the inner process if the parent dies. This is for the case where the // parent process is forcefully terminated and cannot clean up. - setInterval(() => { + this.parentCheckInterval = setInterval(() => { try { // process.kill throws an exception if the process doesn't exist. process.kill(this.parentPid, 0) } catch (_) { // Consider this an error since it should have been able to clean up // the child process unless it was forcefully killed. + this.clearParentCheck() this.logger.error(`parent process ${parentPid} died`) this._onDispose.emit(undefined) } }, 5000) + + // Clean up the interval when the process is disposed for any reason + // (e.g. SIGINT, SIGTERM) to prevent repeated error logging and + // re-entrant dispose emissions. + this.onDispose(() => { + this.clearParentCheck() + }) + } + + private clearParentCheck(): void { + if (this.parentCheckInterval) { + clearInterval(this.parentCheckInterval) + this.parentCheckInterval = undefined + } } /** diff --git a/test/unit/node/wrapper.test.ts b/test/unit/node/wrapper.test.ts index 4aa14bda5536..acfc868a1cfa 100644 --- a/test/unit/node/wrapper.test.ts +++ b/test/unit/node/wrapper.test.ts @@ -1,14 +1,21 @@ import { ChildProcess, ParentProcess, isChild } from "../../../src/node/wrapper" describe("wrapper", () => { + afterAll(() => { + jest.useRealTimers() + }) + describe("isChild", () => { it("should return false for parent process", () => { const p = new ParentProcess("1") expect(isChild(p)).toBe(false) }) - }) - it("should return false for parent process", () => { - const childProc = new ChildProcess(1) - expect(isChild(childProc)).toBe(true) + + it("should return true for child process", () => { + jest.useFakeTimers() + const childProc = new ChildProcess(1) + expect(isChild(childProc)).toBe(true) + jest.clearAllTimers() + }) }) })