A self-hosted, single-user development environment for a UGREEN NAS. It gives you VS Code in a browser through code-server, a persistent workspace, and a practical CLI toolchain similar to the day-to-day experience of GitHub Codespaces.
This project is designed for linux/amd64 and linux/arm64. It does not install
Cloudflare Tunnel or Tailscale in the application image; both are documented as
external access options below.
Two multi-architecture variants are published for linux/amd64 and
linux/arm64:
| Tag | Best for | Contents |
|---|---|---|
latest or vX.Y.Z |
A ready-to-use workstation | Core tools, AI CLIs, uv, Docker clients, build tools, and editor extensions |
slim or vX.Y.Z-slim |
Smaller downloads and storage use | Core tools; AI CLIs install into the persistent .local volume on first use |
Both variants include:
- Browser-based VS Code via code-server
- Guided first-run setup for a blank workspace or a GitHub repository
- Git, Git LFS, GitHub CLI, key-only SSH, GnuPG, and common terminal tools
- Python 3,
pip,pipx, and virtual environments - NVM with Node.js 24, npm, pnpm, Yarn, and TypeScript
- A dark editor theme and an explicit, writable
/workspace - Claude Code (
claude), OpenAI Codex CLI (codex), and OpenCode CLI
The full image also bundles uv, Docker/Buildx/Compose clients, common
native build tools, all three AI CLIs, and Python/Ruff/ESLint/Prettier/YAML/TOML
editor extensions. The slim image omits those optional components. Its
claude, codex, or opencode command installs all three CLIs on first use;
the installation persists in /home/coder/.local.
The image intentionally does not include Docker Engine, Cloudflare Tunnel, or Tailscale.
You need:
- A UGREEN NAS with Docker/Container Manager installed
- Docker Compose v2, either through the NAS interface or an SSH shell
- A writable NAS directory for source code
- At least 2 CPU cores and 4 GB RAM available; more is useful for large builds
- A strong code-server password from which to generate the required Argon2id hash, even when using a VPN or identity-aware proxy
This is a trusted, single-user development container. The coder account has
passwordless sudo inside the container. Do not expose it directly to the
public internet.
For a point-and-click UGOS Pro deployment, follow the complete UGOS Pro setup guide. It uses the standalone, paste-ready UGOS Docker Compose example and covers storage paths, UID/GID permissions, first login, updates, backups, remote access, and troubleshooting.
For an SSH or file-based Compose deployment:
-
Clone this repository into one NAS directory, or download
docker-compose.yml,.env.example, andscripts/generate-hashed-password.shwhile preserving that script path. -
Copy the environment template and create an absolute workspace path:
cp .env.example .env mkdir -p /volume1/docker/ugreen-nas-codespace/workspace chmod 600 .env
-
Generate the password hash locally. The password is read without echoing and is never written to
.env:./scripts/generate-hashed-password.sh
Paste the complete
HASHED_PASSWORD=...output over the empty line in.env. The doubled$$characters are required by Docker Compose. -
Edit the remaining
.envvalues:- Keep
IMAGE_NAME=joweenflores/ugreen-nas-codespace:latest, or select a published semantic version for reproducible deployments. Use:slimfor the smaller variant. - Set
PUIDandPGIDto the owner of the NAS workspace. From SSH, useidto inspect your user and group IDs. - Set
WORKSPACE_PATHto the existing absolute NAS path created above. - Set
SSH_AUTHORIZED_KEYto one public key, or setENABLE_SSH=false. - Change
TZ,WEB_PORT, andBIND_ADDRESSif needed.
- Keep
-
Pull and start:
docker compose pull docker compose up -d docker compose ps
-
Open
http://NAS-LAN-IP:8443and enter the original password used to generate the hash. The original password is not stored in Compose.
UGOS Pro users who prefer the Project editor should use the dedicated example rather than pasting the environment-variable-based root file.
Clone the repository, set IMAGE_NAME=ugreen-nas-codespace:local in .env, and
use the build override. Set IMAGE_VARIANT=slim to build the smaller variant:
docker compose -f docker-compose.yml -f compose/build.yml build --pull
docker compose -f docker-compose.yml -f compose/build.yml up -dThe first build downloads the base image, language runtimes, CLI tools, and editor extensions, so it can take several minutes. Security-sensitive Go CLIs are rebuilt from pinned upstream release commits with the patched Go toolchain. The final filesystem is flattened so removed or replaced base-image binaries are not retained in image history or reported as active packages by scanners.
Pull requests build and scan both image variants with Docker Scout. CI rejects fixable critical or high-severity findings before a change can merge, except for a repository-reviewed OpenVEX assessment when the vulnerable code is not in the image's execution path.
After the first browser login, the editor asks how to prepare /workspace.
The chooser uses native VS Code controls and codicons, so it automatically
matches the active code-server theme, keyboard navigation, and accessibility:
- Blank project uses the mounted workspace as a fresh project folder. It never deletes existing NAS files; if the folder is not empty, it asks before using the files already there.
- Clone from GitHub asks for
OWNER/REPOSITORYor agithub.comURL, opens a terminal for GitHub CLI browser authentication, configures Git's credential helper, and clones into the empty workspace. The clone is refused when the folder contains files. - GitLab is shown as coming soon and does not configure or store anything.
- Ask me later shows the choices again on the next browser session.
For a private repository, follow the one-time code and URL shown in the setup terminal. GitHub CLI uses its browser-based OAuth flow. If the repository belongs to an organization that enforces SAML SSO, first establish an active SSO session and authorize GitHub CLI for that organization; organization policy may require an administrator to approve the OAuth app. See GitHub's OAuth authorization and SAML SSO guidance.
The completion marker is retained in /home/coder/.config. To choose again,
open the Command Palette and run UGREEN Codespace: Reset First-Run Setup.
You can also run UGREEN Codespace: Run First-Run Setup without resetting
the saved choice. The equivalent terminal commands are:
ugreen-onboard status
ugreen-onboard resetAfter choosing a project, configure your Git identity in the integrated terminal:
git config --global user.name "Your Name"
git config --global user.email "[email protected]"Then start each AI coding CLI once:
claude
codex
opencode- Claude Code walks through its supported login flow.
- Codex offers its available sign-in methods on first run. See the official Codex CLI guide.
- In OpenCode, use
/connectto select and authenticate an LLM provider. See the OpenCode setup guide. - On the slim image, the first of these commands downloads all three AI CLIs
into the persistent
.localvolume. Runinstall-ai-tools --upgradelater to refresh them.
Authentication state is retained in named volumes. GitHub CLI uses secure
credential storage when one is available, but may store its OAuth token in its
configuration file when the container has no credential store. Treat the
.config volume and UGOS administrator access as sensitive. No GitHub token is
accepted through Compose or baked into the image. Do not bake API keys, tokens,
SSH private keys, or .env files into a custom image. See the
GitHub CLI authentication reference.
| Path | Storage | Purpose |
|---|---|---|
/workspace |
NAS bind mount | Repositories and project files |
/home/coder/.config |
Named volume | First-run state, GitHub CLI credentials, OpenCode, and app config |
/home/coder/.local |
Named volume | VS Code data/extensions and OpenCode data |
/home/coder/.claude |
Named volume | Claude Code state |
/home/coder/.codex |
Named volume | Codex state |
/home/coder/.ssh |
Named volume | SSH configuration and keys |
/home/coder/.gnupg |
Named volume | GPG configuration and keys |
Back up the workspace directory and named volumes before migrating the NAS.
docker compose down keeps them; docker compose down --volumes deletes the
named volumes and therefore removes retained tool configuration and credentials.
Only HASHED_PASSWORD is accepted. The container rejects PASSWORD and exits
if the required Argon2i/Argon2id hash is absent or malformed. Generate a
Compose-safe Argon2id value with:
./scripts/generate-hashed-password.shThe script uses a local argon2 command when available, otherwise it uses a
one-off container. It never prints the original password. For automation only,
it also accepts a password on standard input with --stdin; avoid shell command
arguments because they may be recorded in history or process listings.
Protect .env with mode 0600. A password hash is safer to store than its
plaintext source, but it is still authentication material and can be attacked
offline. Use a long, unique password. See code-server's
official hashed-password guidance.
The image includes a non-root SSH server on container port 2222. It accepts
public keys only: SSH passwords, keyboard-interactive authentication, empty
passwords, and root login are disabled.
-
Create a dedicated key on your laptop if you do not already have one:
ssh-keygen -t ed25519 -f ~/.ssh/ugreen_codespace -C ugreen-codespace cat ~/.ssh/ugreen_codespace.pub
-
Paste that single public-key line into
SSH_AUTHORIZED_KEYin.env. Never copy the private key to the NAS or container. Redeploy withdocker compose up -d. -
Add a laptop-side
~/.ssh/configentry:Host ugreen-codespace HostName NAS-LAN-OR-TAILSCALE-IP User coder Port 2222 IdentityFile ~/.ssh/ugreen_codespace
-
Connect with
ssh ugreen-codespace, or install Microsoft's Remote - SSH extension in desktop VS Code and run Remote-SSH: Connect to Host.
The browser editor and SSH sessions both open the same /workspace. If you do
not need SSH, set ENABLE_SSH=false; when no authorized key is configured, the
SSH server does not start.
Keep code-server hash authentication enabled. A tunnel or VPN protects network access but does not replace the IDE password. Do not directly forward either the web port or SSH port from your router to the public internet.
Install Tailscale on the NAS host, on a supported UGREEN package, or as a separately managed container. Do not add it to this development image.
- Simple private access: keep
BIND_ADDRESS=0.0.0.0and browse tohttp://NAS-TAILSCALE-IP:8443. Use Tailscale ACLs to restrict the port. - For terminal or Remote-SSH access, connect to the NAS Tailscale address on
port
2222and restrict that port with Tailscale ACLs. - Host-side HTTPS: set
BIND_ADDRESS=127.0.0.1, then configure Tailscale Serve on the NAS to proxylocalhost:8443. Follow the current Tailscale Serve documentation.
Install and run cloudflared separately on the NAS or another trusted device on
the same LAN. Create a published application route whose service URL is
http://NAS-LAN-IP:8443 (or http://localhost:8443 when cloudflared runs on
the NAS). See the Cloudflare Tunnel setup guide.
Place a Cloudflare Access application and identity policy in front of the hostname. Do not create a public DNS route without an access policy and the code-server password. Tailscale is generally simpler for raw SSH; Cloudflare SSH requires a separately configured Access TCP/SSH flow.
Docker CLI, Buildx, and Compose are installed, but the NAS Docker socket is not mounted by default. To enable it:
stat -c '%g' /var/run/docker.sockPut that number in DOCKER_GID in .env, then start with the override:
docker compose -f docker-compose.yml -f compose/docker-socket.yml up -dIf building locally as well, include both overrides:
docker compose \
-f docker-compose.yml \
-f compose/build.yml \
-f compose/docker-socket.yml \
up -d --buildSecurity warning: access to /var/run/docker.sock is effectively root-level
control of the NAS. Enable it only for a trusted user, keep the IDE private, and
never mount the socket into an internet-exposed or shared instance.
For bind mounts launched from inside the codespace, Docker resolves host paths, not container-only paths. Use NAS paths that exist identically on the host when a child container needs to mount project files.
Update a published deployment:
docker compose pull
docker compose up -d
docker image prunedocker image prune removes only unused image layers; it does not remove the
workspace or named volumes.
For more OS packages, editor extensions, or globally installed npm tools, fork the repository and update the Dockerfile. Runtime changes outside the mounted paths are intentionally ephemeral and disappear when the container is replaced.
Build arguments let maintainers change the code-server, uv, NVM, Node, Claude
Code, Codex, and OpenCode versions. Pin the npm arguments to exact versions for
reproducible releases. The defaults use the upstream latest channel for the
three AI CLIs so newly built images receive current releases.
Protected merges to main automatically create a SemVer tag and GitHub Release,
then invoke the multi-architecture Docker Hub publishing workflow. Versions
start at v0.1.0 and are derived from the squash-merge commit:
type(scope)!:or aBREAKING CHANGE:footer increments the major version.feat:increments the minor version.- Other changes increment the patch version.
To enable publishing:
- Create the public Docker Hub repository
joweenflores/ugreen-nas-codespace. - In the GitHub repository, add Actions secrets:
DOCKERHUB_USERNAMEDOCKERHUB_TOKEN(use a Docker Hub access token, not your password)
- Merge a pull request into
main. Direct pushes and force pushes are blocked.
Each release builds linux/amd64 and linux/arm64. The full image receives the
exact Git tag (vX.Y.Z), latest, X.Y.Z, major/minor, major-only, and
commit-SHA tags. The slim image receives slim, vX.Y.Z-slim, matching
major/minor tags with -slim, and a -slim SHA tag. Both variants include OCI
metadata and build provenance. Manually pushed v*.*.* tags and manual
workflow dispatches are also supported.
This project provides the most useful personal workflow pieces, but it is not a drop-in implementation of GitHub Codespaces. It is one long-running container, not an isolated VM-per-workspace control plane. It has no organization policy, automatic suspend/billing, disposable workspace orchestration, or multi-user isolation. Run one instance per trusted user if several people need access.
code-server uses the Open VSX extension registry. A few Microsoft Marketplace extensions may be unavailable or licensed only for Microsoft's products.
Copyright © 2026 Joween Flores and contributors.
Licensed under the GNU Affero General Public License version 3 only
(AGPL-3.0-only). See LICENSE.
Bundled and downloaded tools remain under their respective upstream licenses; the AGPL does not relicense those separate works. UGREEN, GitHub, VS Code, Cloudflare, Tailscale, Anthropic, OpenAI, and OpenCode are trademarks of their respective owners.
This is an independent community project. The project and its repository owners and maintainers are not affiliated with, employed by, sponsored by, or endorsed by UGREEN, and have not been paid or otherwise compensated by UGREEN for developing, maintaining, or promoting this project. Use of the UGREEN name describes intended hardware compatibility only and does not imply an official relationship, certification, or endorsement. Contributors participate in their individual capacities unless they explicitly state otherwise.