Commit: 9fad089

Initial commit: codefactory OpenCode setup

codefactory committed on 2026-10-18 at 08:34
- startup.sh: OpenCode auth import + serve entrypoint
- .containerfile: Ubuntu image with OpenCode, Node 26, pnpm, .NET 10, Rust, Docker CLI
- docker-compose.yml: opencode service with real Docker socket, git repos,
  config and workspace mounts, Traefik labels on proxy network
- README.md: description of current state
- PLAN.md: future work (DinD, dev sub-domains, per-project SDKs, agent skills)
.containerfile +29 -0
diff --git a/.containerfile b/.containerfile
new file mode 100644
index 0000000..992f065
@@ -0,0 +1,29 @@
FROM ubuntu AS run
WORKDIR /code
RUN apt-get update
RUN apt-get install -y git curl ca-certificates libatomic1
# Install OpenCode
RUN curl -fsSL https://opencode.ai/v2/install | bash
# Install Node.js + pnpm
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash \
&& \. "$HOME/.nvm/nvm.sh" \
&& nvm install 26 \
&& npm install -g corepack \
&& corepack enable pnpm
# Install .NET
RUN apt-get install -y dotnet-sdk-10.0
# Install Rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
# "Install" Docker
COPY --from=docker:cli /usr/local/bin/docker /usr/local/bin/docker
COPY /startup.sh /root/startup.sh
ENTRYPOINT ["/bin/bash", "/root/startup.sh"]
PLAN.md +111 -0
diff --git a/PLAN.md b/PLAN.md
new file mode 100644
index 0000000..44a19de
@@ -0,0 +1,111 @@
# Plan — codefactory
Future work for the self-hosted OpenCode instance. The README describes what is
deployed now; this file captures what is still to be built.
## 1. Docker-in-Docker for development + real Docker for image builds
Right now the container mounts the host's real `/var/run/docker.sock`. That is
convenient for building images to deploy to Portainer + Traefik, but it is the
real Docker — one bad `docker rm -rf` or privileged container can hurt the
host.
Goal split:
- **Real Docker socket** (current mount): used *only* to build images that get
pushed to the registry and deployed via Portainer + Traefik. Read-only where
possible, no dev containers left running on it.
- **Docker-in-Docker (DinD)**: a separate, nested Docker daemon inside the
OpenCode container (or a sibling container) used by agents for development
builds, tests, and scratch containers. It dies with the dev container and
cannot touch the host Docker.
Agents should know which daemon to talk to (`DOCKER_HOST`) depending on the
task: dev work → DinD; release image → real Docker.
## 2. Expose development servers to the internet
Agents spinning up dev servers (Next.js, .NET, etc.) need them reachable from a
laptop/phone for testing.
Proposal:
- Run a second Traefik instance (or use the existing one with a dedicated
entrypoint/router) for ephemeral dev services.
- Route through sub-sub-domains like
`dev-<slug>.codefactory.xn--sberg-lra.net` or
`<project>.dev.codefactory.xn--sberg-lra.net`.
- Short-lived routers/services created on demand and torn down when the dev
session ends, so nothing stale is exposed.
- Ideally password-protected or internal-only (Tailscale/Cloudflare Tunnel as
an alternative to public exposure).
## 3. Persist all configs
Current persistence:
- `/bigger/codefactory/config` → OpenCode/agent config.
- `/bigger/codefactory/workspace` → working directory.
- `/big/repos` → git server repos (bind-mounted, not created by this repo).
Still to add:
- Git user config (`~/.gitconfig`, credentials, SSH keys) saved to the config
volume and restored on startup, so agents commit as me and can push to my git
server.
- OpenCode skills, settings, and model preferences saved to the config volume
(already mounted, just needs to be used deliberately).
- Any agent-installed per-project SDKs (see #4) restored across container
restarts.
## 4. Per-project SDKs installed by agents, surviving restarts
Currently the image has global SDKs (Node 26, .NET 10, Rust). The wish is for
agents to install SDKs *per project* (e.g. a specific Node version, a .NET SDK,
a Rust toolchain) so projects don't interfere with each other.
Requirements:
- SDK installs must live in the persisted workspace/config volume (not the
container's writable layer), so they survive `docker compose down/up`.
- Agents should be able to install on demand (nvm, rustup, dotnet-install
script, etc.) and record what each project needs (e.g. `.tool-versions`,
`runtimeconfig.json`, `rust-toolchain.toml`, or a project manifest).
- A "bootstrap" step in skills so any agent joining a project can reinstall the
project's SDKs from the manifest.
This is explicitly marked a future item — not blocking the MVP.
## 5. Agent skills for "how my setup works"
When agents work on *other* projects (not codefactory itself), they should
know how my infrastructure works so they can deploy correctly. Skills to write
and store in the config volume:
- **Portainer + Traefik skill:** how stacks are deployed, where compose files
live, how Traefik labels map to routers/services, how to add a new service to
the `proxy` network, where certs come from.
- **Git server skill:** where `/big/repos` lives, how to create/push repos,
auth method (SSH keys / HTTP), remote naming.
- **Docker skill:** real Docker vs DinD, which socket to use for what, image
naming/tagging convention, registry to push to.
- **Domain/Traefik skill:** `codefactory.xn--sberg-lra.net` is the main entry;
dev sub-sub-domains follow `dev-*.codefactory.xn--sberg-lra.net`; TLS is
handled by Traefik.
- **Deployment skill:** end-to-end flow from code in `/repos` → build image
(real Docker) → push → Portainer stack deploy → Traefik route.
Skills should be written as OpenCode skills (markdown + scripts) in
`/bigger/codefactory/config/` so they persist and load into every session.
## Suggested order
1. Persist git config + OpenCode skills to the config volume (quick win).
2. Write the Portainer/Traefik/git/Docker agent skills.
3. Add DinD for dev builds + guardrails around the real Docker socket.
4. Dev-server exposure via Traefik sub-sub-domains.
5. Per-project SDKs with manifests + bootstrap skill.
## Safety notes
- The real Docker socket is mounted `rw`. Avoid `docker system prune`,
`docker rm -f` on host containers, or privileged containers unless intended.
- `AUTH` in `docker-compose.yml` is a placeholder — the real value is injected
at runtime and never committed.
- `/big/repos` is my git server's data — treat it as read-only unless creating
a repo deliberately.
README.md +44 -0
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c10362a
@@ -0,0 +1,44 @@
# codefactory
Self-hosted OpenCode instance running on my server, reachable remotely from any
laptop or phone. It is connected to my on-server git server and has access to
the real Docker socket so it can build images for my Portainer + Traefik
homelab stack.
## What it is right now
- **OpenCode** served on `0.0.0.0:80` inside a container named `opencode`,
built from `codefactory` image (see `.containerfile`).
- **Remote access** via Traefik at `codefactory.xn--sberg-lra.net` (external
`proxy` network, Traefik labels on the service).
- **Authentication** imported at startup from the `AUTH` env var.
- **Host access mounted in:**
- `/var/run/docker.sock` — real Docker, used to build images for Portainer +
Traefik deployments (⚠️ this is the real Docker, not rootless/Dind — be
careful).
- `/big/repos` → `/repos` — my on-server git server's repos.
- `/bigger/codefactory/config` → `/root/.config/opencode/` — OpenCode/agent
config persistence.
- `/bigger/codefactory/workspace` → `/code/` — working directory persistence.
- **Pre-installed SDKs** in the image: Node.js 26 (via nvm), pnpm, .NET 10.0
SDK, Rust, git, curl.
- **Entrypoint:** `startup.sh` imports OpenCode auth, then runs `opencode serve`.
## Files
| File | Purpose |
|------|---------|
| `startup.sh` | Container entrypoint: imports OpenCode auth and starts `opencode serve`. |
| `.containerfile` | Builds the `codefactory` image (Ubuntu + OpenCode + SDKs + Docker CLI). |
| `docker-compose.yml` | Runs the `opencode` service with mounts, env, Traefik labels, `proxy` network. |
| `README.md` | This file. |
## Status
MVP deployed. OpenCode is reachable at `codefactory.xn--sberg-lra.net` and can
read/write repos under `/repos` and build Docker images via the mounted socket.
Persistent config lives under `/bigger/codefactory/`.
See `PLAN.md` for upcoming work (Docker-in-Docker for dev builds, per-project
SDKs, dev-server exposure via Traefik sub-sub-domains, agent skills for
deploying to Portainer + Traefik, etc.).
docker-compose.yml +27 -0
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..46b5dc3
@@ -0,0 +1,27 @@
services:
opencode:
container_name: opencode
image: codefactory
restart: unless-stopped
group_add:
- 1001
- 988
volumes:
- /var/run/docker.sock:/var/run/docker.sock:rw
- /big/repos:/repos:rw
- /bigger/codefactory/config:/root/.config/opencode/:rw
- /bigger/codefactory/workspace:/code/:rw
environment:
AUTH: <hidden>
PORT: 80
labels:
- "traefik.enable=true"
- "traefik.http.routers.codefactory.rule=Host(`codefactory.xn--sberg-lra.net`)"
- "traefik.http.services.codefactory.loadbalancer.server.port=80"
- "traefik.docker.network=proxy"
networks:
- "proxy"
networks:
proxy:
external: true
startup.sh +7 -0
diff --git a/startup.sh b/startup.sh
new file mode 100644
index 0000000..7bb2e0d
@@ -0,0 +1,7 @@
#!/bin/bash
set -euo pipefail
echo "$AUTH" | /root/.opencode/bin/opencode auth import
exec /root/.opencode/bin/opencode serve --hostname 0.0.0.0 --port 80