@opencode-cloud/core 15.2.0 → 17.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +1 -1
- package/README.md +35 -5
- package/package.json +1 -1
- package/src/docker/Dockerfile +26 -138
- package/src/docker/README.dockerhub.md +10 -2
- package/src/docker/files/README.md +4 -0
- package/src/docker/files/bashrc.extra +25 -0
- package/src/docker/files/entrypoint.sh +199 -0
- package/src/docker/files/healthcheck.sh +13 -0
- package/src/docker/files/opencode-broker.service +30 -0
- package/src/docker/files/opencode.jsonc +5 -0
- package/src/docker/files/opencode.service +15 -0
- package/src/docker/files/pam/opencode +9 -0
- package/src/docker/files/starship.toml +21 -0
- package/src/docker/image.rs +102 -7
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
[](https://github.com/pRizz/opencode-cloud/actions/workflows/ci.yml)
|
|
4
4
|
[](https://gitea.com/pRizz/opencode-cloud)
|
|
5
5
|
[](https://crates.io/crates/opencode-cloud)
|
|
6
|
-
[](https://github.com/pRizz/opencode-cloud/pkgs/container/opencode-cloud-sandbox)
|
|
7
6
|
[](https://hub.docker.com/r/prizz/opencode-cloud-sandbox)
|
|
8
7
|
[](https://hub.docker.com/r/prizz/opencode-cloud-sandbox)
|
|
8
|
+
[](https://github.com/pRizz/opencode-cloud/pkgs/container/opencode-cloud-sandbox)
|
|
9
9
|
[](https://docs.rs/opencode-cloud)
|
|
10
10
|
[](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0.html)
|
|
11
11
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -51,6 +51,16 @@ Quick deploy provisions a private EC2 instance behind a public ALB with HTTPS.
|
|
|
51
51
|
Docs: `docs/deploy/aws.md` (includes teardown steps and S3 hosting setup for forks)
|
|
52
52
|
Credentials: `docs/deploy/aws.md#retrieving-credentials`
|
|
53
53
|
|
|
54
|
+
## Deploy to DigitalOcean
|
|
55
|
+
|
|
56
|
+
[](https://marketplace.digitalocean.com/apps/opencode-cloud)
|
|
57
|
+
|
|
58
|
+
Marketplace one-click deploy provisions a Droplet that bootstraps opencode-cloud
|
|
59
|
+
on first boot (listing pending).
|
|
60
|
+
|
|
61
|
+
Manual Droplet setup: `docs/deploy/digitalocean-droplet.md`
|
|
62
|
+
Marketplace docs: `docs/deploy/digitalocean-marketplace.md`
|
|
63
|
+
|
|
54
64
|
## Features
|
|
55
65
|
|
|
56
66
|
- **Sandboxed execution** - opencode runs inside a Docker container, isolated from your host system
|
|
@@ -77,12 +87,12 @@ The sandbox container image is named **`opencode-cloud-sandbox`** (not `opencode
|
|
|
77
87
|
|
|
78
88
|
**Why use the CLI?** It configures volumes, ports, and upgrades safely, so you don’t have to manage `docker run` flags or image updates yourself.
|
|
79
89
|
|
|
80
|
-
The image is published to both registries:
|
|
90
|
+
The image is published to both registries (Docker Hub is the primary distribution):
|
|
81
91
|
|
|
82
92
|
| Registry | Image |
|
|
83
93
|
|----------|-------|
|
|
84
|
-
| GitHub Container Registry | [`ghcr.io/prizz/opencode-cloud-sandbox`](https://github.com/pRizz/opencode-cloud/pkgs/container/opencode-cloud-sandbox) |
|
|
85
94
|
| Docker Hub | [`prizz/opencode-cloud-sandbox`](https://hub.docker.com/r/prizz/opencode-cloud-sandbox) |
|
|
95
|
+
| GitHub Container Registry | [`ghcr.io/prizz/opencode-cloud-sandbox`](https://github.com/pRizz/opencode-cloud/pkgs/container/opencode-cloud-sandbox) |
|
|
86
96
|
|
|
87
97
|
Pull commands:
|
|
88
98
|
|
|
@@ -182,7 +192,7 @@ occ start --port 8080
|
|
|
182
192
|
# Start and open browser
|
|
183
193
|
occ start --open
|
|
184
194
|
|
|
185
|
-
# Check service status
|
|
195
|
+
# Check service status (includes broker health: Healthy/Degraded/Unhealthy)
|
|
186
196
|
occ status
|
|
187
197
|
|
|
188
198
|
# View logs
|
|
@@ -194,7 +204,7 @@ occ logs -f
|
|
|
194
204
|
# View opencode-broker logs (systemd/journald required)
|
|
195
205
|
occ logs --broker
|
|
196
206
|
|
|
197
|
-
#
|
|
207
|
+
# Troubleshoot broker health issues reported by `occ status`
|
|
198
208
|
occ logs --broker --no-follow
|
|
199
209
|
|
|
200
210
|
# Note: Broker logs require systemd/journald. This is enabled by default on supported Linux
|
|
@@ -238,6 +248,26 @@ occ mount clean --purge --force
|
|
|
238
248
|
# Factory reset host (container, volumes, mounts, config/data)
|
|
239
249
|
occ reset host --force
|
|
240
250
|
|
|
251
|
+
### Container Mode
|
|
252
|
+
|
|
253
|
+
When `occ` runs inside the opencode container, it will auto-detect this and switch to **container runtime**.
|
|
254
|
+
Override if needed:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
occ --runtime host <command>
|
|
258
|
+
OPENCODE_RUNTIME=host occ <command>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Supported commands in container runtime:
|
|
262
|
+
- `occ status`
|
|
263
|
+
- `occ logs`
|
|
264
|
+
- `occ user`
|
|
265
|
+
- `occ update opencode`
|
|
266
|
+
|
|
267
|
+
Notes:
|
|
268
|
+
- Host/Docker lifecycle commands are disabled in container runtime.
|
|
269
|
+
- `occ logs` and `occ update opencode` require systemd inside the container. If systemd is not available, run those commands from the host instead.
|
|
270
|
+
|
|
241
271
|
### Webapp-triggered update (command file)
|
|
242
272
|
|
|
243
273
|
When running in foreground mode (for example via `occ install`, which uses `occ start --no-daemon`),
|
package/package.json
CHANGED
package/src/docker/Dockerfile
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# =============================================================================
|
|
2
2
|
# opencode-cloud Container Image
|
|
3
3
|
# =============================================================================
|
|
4
|
+
# IMPORTANT:
|
|
5
|
+
# Keep scripts/service/config assets in `packages/core/src/docker/files/`
|
|
6
|
+
# and COPY them into the image instead of embedding large inline shell blocks.
|
|
7
|
+
# When adding files, also update `packages/core/src/docker/image.rs`
|
|
8
|
+
# (`create_build_context`) so CLI-driven Docker builds include them.
|
|
9
|
+
#
|
|
4
10
|
# A comprehensive development environment for AI-assisted coding with opencode.
|
|
5
11
|
#
|
|
6
12
|
# Features:
|
|
@@ -452,61 +458,12 @@ RUN git config --global init.defaultBranch main \
|
|
|
452
458
|
&& git config --global diff.colorMoved default
|
|
453
459
|
|
|
454
460
|
# Starship configuration (minimal, fast prompt)
|
|
455
|
-
|
|
456
|
-
&& printf '%s\n' \
|
|
457
|
-
'# Minimal starship config for fast prompt' \
|
|
458
|
-
'format = """' \
|
|
459
|
-
'$directory\' \
|
|
460
|
-
'$git_branch\' \
|
|
461
|
-
'$git_status\' \
|
|
462
|
-
'$character"""' \
|
|
463
|
-
'' \
|
|
464
|
-
'[directory]' \
|
|
465
|
-
'truncation_length = 3' \
|
|
466
|
-
'truncate_to_repo = true' \
|
|
467
|
-
'' \
|
|
468
|
-
'[git_branch]' \
|
|
469
|
-
'format = "[$branch]($style) "' \
|
|
470
|
-
'style = "bold purple"' \
|
|
471
|
-
'' \
|
|
472
|
-
'[git_status]' \
|
|
473
|
-
'format = '"'"'([$all_status$ahead_behind]($style) )'"'"'' \
|
|
474
|
-
'' \
|
|
475
|
-
'[character]' \
|
|
476
|
-
'success_symbol = "[>](bold green)"' \
|
|
477
|
-
'error_symbol = "[>](bold red)"' \
|
|
478
|
-
> /home/opencode/.config/starship.toml
|
|
461
|
+
COPY --chown=opencode:opencode packages/core/src/docker/files/starship.toml /home/opencode/.config/starship.toml
|
|
479
462
|
|
|
480
463
|
# Shell aliases
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
'alias ls="eza --icons"' \
|
|
485
|
-
'alias ll="eza -l --icons"' \
|
|
486
|
-
'alias la="eza -la --icons"' \
|
|
487
|
-
'alias lt="eza --tree --icons"' \
|
|
488
|
-
'alias grep="rg"' \
|
|
489
|
-
'alias top="btop"' \
|
|
490
|
-
'' \
|
|
491
|
-
'# Git aliases' \
|
|
492
|
-
'alias g="git"' \
|
|
493
|
-
'alias gs="git status"' \
|
|
494
|
-
'alias gd="git diff"' \
|
|
495
|
-
'alias gc="git commit"' \
|
|
496
|
-
'alias gp="git push"' \
|
|
497
|
-
'alias gl="git pull"' \
|
|
498
|
-
'alias gco="git checkout"' \
|
|
499
|
-
'alias gb="git branch"' \
|
|
500
|
-
'alias lg="lazygit"' \
|
|
501
|
-
'' \
|
|
502
|
-
'# Docker aliases (for Docker-in-Docker)' \
|
|
503
|
-
'alias d="docker"' \
|
|
504
|
-
'alias dc="docker compose"' \
|
|
505
|
-
'' \
|
|
506
|
-
>> /home/opencode/.bashrc
|
|
507
|
-
|
|
508
|
-
# Set up pipx path
|
|
509
|
-
RUN echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.bashrc
|
|
464
|
+
COPY --chown=opencode:opencode packages/core/src/docker/files/bashrc.extra /home/opencode/.bashrc.extra
|
|
465
|
+
RUN cat /home/opencode/.bashrc.extra >> /home/opencode/.bashrc \
|
|
466
|
+
&& rm /home/opencode/.bashrc.extra
|
|
510
467
|
|
|
511
468
|
# -----------------------------------------------------------------------------
|
|
512
469
|
# Stage 2: opencode build
|
|
@@ -535,7 +492,7 @@ USER opencode
|
|
|
535
492
|
# commit on the main branch of https://github.com/pRizz/opencode.
|
|
536
493
|
# Update it by running: ./scripts/update-opencode-commit.sh
|
|
537
494
|
# Build opencode from source (BuildKit cache mounts disabled for now)
|
|
538
|
-
RUN OPENCODE_COMMIT="
|
|
495
|
+
RUN OPENCODE_COMMIT="41731edead75a52aceac0b48c22474bdeafc746c" \
|
|
539
496
|
&& rm -rf /tmp/opencode-repo \
|
|
540
497
|
&& git clone --depth 1 https://github.com/pRizz/opencode.git /tmp/opencode-repo \
|
|
541
498
|
&& cd /tmp/opencode-repo \
|
|
@@ -575,18 +532,8 @@ ENV PATH="/opt/opencode/bin:${PATH}"
|
|
|
575
532
|
# This allows opencode to authenticate users via PAM (same users as Cockpit)
|
|
576
533
|
# NOTE: Requires root privileges to write to /etc/pam.d/
|
|
577
534
|
USER root
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
'# Install to /etc/pam.d/opencode' \
|
|
581
|
-
'' \
|
|
582
|
-
'# Standard UNIX authentication' \
|
|
583
|
-
'auth required pam_unix.so' \
|
|
584
|
-
'account required pam_unix.so' \
|
|
585
|
-
'' \
|
|
586
|
-
'# Optional: Enable TOTP 2FA (uncomment when pam_google_authenticator is installed)' \
|
|
587
|
-
'# auth required pam_google_authenticator.so' \
|
|
588
|
-
> /etc/pam.d/opencode \
|
|
589
|
-
&& chmod 644 /etc/pam.d/opencode
|
|
535
|
+
COPY packages/core/src/docker/files/pam/opencode /etc/pam.d/opencode
|
|
536
|
+
RUN chmod 644 /etc/pam.d/opencode
|
|
590
537
|
|
|
591
538
|
# Verify PAM config file exists
|
|
592
539
|
RUN ls -la /etc/pam.d/opencode && cat /etc/pam.d/opencode
|
|
@@ -596,38 +543,7 @@ RUN ls -la /etc/pam.d/opencode && cat /etc/pam.d/opencode
|
|
|
596
543
|
# -----------------------------------------------------------------------------
|
|
597
544
|
# Create opencode-broker service for PAM authentication
|
|
598
545
|
# NOTE: Requires root privileges to write to /etc/systemd/system/
|
|
599
|
-
|
|
600
|
-
'[Unit]' \
|
|
601
|
-
'Description=OpenCode Authentication Broker' \
|
|
602
|
-
'Documentation=https://github.com/pRizz/opencode' \
|
|
603
|
-
'After=network.target' \
|
|
604
|
-
'' \
|
|
605
|
-
'[Service]' \
|
|
606
|
-
'Type=notify' \
|
|
607
|
-
'ExecStart=/usr/local/bin/opencode-broker' \
|
|
608
|
-
'ExecReload=/bin/kill -HUP $MAINPID' \
|
|
609
|
-
'Restart=always' \
|
|
610
|
-
'RestartSec=5' \
|
|
611
|
-
'' \
|
|
612
|
-
'# Security hardening' \
|
|
613
|
-
'NoNewPrivileges=false' \
|
|
614
|
-
'ProtectSystem=strict' \
|
|
615
|
-
'ProtectHome=read-only' \
|
|
616
|
-
'PrivateTmp=true' \
|
|
617
|
-
'ReadWritePaths=/run/opencode' \
|
|
618
|
-
'' \
|
|
619
|
-
'# Socket directory' \
|
|
620
|
-
'RuntimeDirectory=opencode' \
|
|
621
|
-
'RuntimeDirectoryMode=0755' \
|
|
622
|
-
'' \
|
|
623
|
-
'# Logging' \
|
|
624
|
-
'StandardOutput=journal' \
|
|
625
|
-
'StandardError=journal' \
|
|
626
|
-
'SyslogIdentifier=opencode-broker' \
|
|
627
|
-
'' \
|
|
628
|
-
'[Install]' \
|
|
629
|
-
'WantedBy=multi-user.target' \
|
|
630
|
-
> /etc/systemd/system/opencode-broker.service
|
|
546
|
+
COPY packages/core/src/docker/files/opencode-broker.service /etc/systemd/system/opencode-broker.service
|
|
631
547
|
|
|
632
548
|
# Enable opencode-broker service
|
|
633
549
|
RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
|
|
@@ -638,23 +554,7 @@ RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
|
|
|
638
554
|
# -----------------------------------------------------------------------------
|
|
639
555
|
# Create opencode as a systemd service for Cockpit integration
|
|
640
556
|
# NOTE: Requires root privileges to write to /etc/systemd/system/
|
|
641
|
-
|
|
642
|
-
'[Unit]' \
|
|
643
|
-
'Description=opencode Web Interface' \
|
|
644
|
-
'After=network.target opencode-broker.service' \
|
|
645
|
-
'' \
|
|
646
|
-
'[Service]' \
|
|
647
|
-
'Type=simple' \
|
|
648
|
-
'User=opencode' \
|
|
649
|
-
'WorkingDirectory=/home/opencode/workspace' \
|
|
650
|
-
'ExecStart=/opt/opencode/bin/opencode web --port 3000 --hostname 0.0.0.0' \
|
|
651
|
-
'Restart=always' \
|
|
652
|
-
'RestartSec=5' \
|
|
653
|
-
'Environment=PATH=/opt/opencode/bin:/home/opencode/.local/bin:/home/opencode/.cargo/bin:/home/opencode/.local/share/mise/shims:/home/opencode/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \
|
|
654
|
-
'' \
|
|
655
|
-
'[Install]' \
|
|
656
|
-
'WantedBy=multi-user.target' \
|
|
657
|
-
> /etc/systemd/system/opencode.service
|
|
557
|
+
COPY packages/core/src/docker/files/opencode.service /etc/systemd/system/opencode.service
|
|
658
558
|
|
|
659
559
|
# Enable opencode service to start at boot (manual symlink since systemctl doesn't work during build)
|
|
660
560
|
RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
|
|
@@ -664,15 +564,9 @@ RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
|
|
|
664
564
|
# opencode Configuration
|
|
665
565
|
# -----------------------------------------------------------------------------
|
|
666
566
|
# Create opencode.jsonc config file with PAM authentication enabled
|
|
667
|
-
RUN mkdir -p /home/opencode/.config/opencode
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
' "auth": {' \
|
|
671
|
-
' "enabled": true' \
|
|
672
|
-
' }' \
|
|
673
|
-
'}' \
|
|
674
|
-
> /home/opencode/.config/opencode/opencode.jsonc \
|
|
675
|
-
&& chown -R opencode:opencode /home/opencode/.config/opencode \
|
|
567
|
+
RUN mkdir -p /home/opencode/.config/opencode
|
|
568
|
+
COPY --chown=opencode:opencode packages/core/src/docker/files/opencode.jsonc /home/opencode/.config/opencode/opencode.jsonc
|
|
569
|
+
RUN chown -R opencode:opencode /home/opencode/.config/opencode \
|
|
676
570
|
&& chmod 644 /home/opencode/.config/opencode/opencode.jsonc
|
|
677
571
|
|
|
678
572
|
# Verify config file exists
|
|
@@ -684,29 +578,23 @@ RUN ls -la /home/opencode/.config/opencode/opencode.jsonc && cat /home/opencode/
|
|
|
684
578
|
# Supports both tini (default, works everywhere) and systemd (for Cockpit on Linux)
|
|
685
579
|
# Set USE_SYSTEMD=1 environment variable to use systemd init
|
|
686
580
|
# Note: Entrypoint runs as root to support both modes; tini mode drops to opencode user
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
'if [ "${USE_SYSTEMD}" = "1" ]; then' \
|
|
690
|
-
' exec /sbin/init' \
|
|
691
|
-
'else' \
|
|
692
|
-
' # Ensure broker socket directory exists' \
|
|
693
|
-
' install -d -m 0755 /run/opencode' \
|
|
694
|
-
' /usr/local/bin/opencode-broker &' \
|
|
695
|
-
' # Use runuser to switch to opencode user without password prompt' \
|
|
696
|
-
' exec runuser -u opencode -- sh -lc "cd /home/opencode/workspace && /opt/opencode/bin/opencode web --port 3000 --hostname 0.0.0.0"' \
|
|
697
|
-
'fi' \
|
|
698
|
-
> /usr/local/bin/entrypoint.sh && chmod +x /usr/local/bin/entrypoint.sh
|
|
581
|
+
COPY packages/core/src/docker/files/entrypoint.sh /usr/local/bin/entrypoint.sh
|
|
582
|
+
RUN chmod +x /usr/local/bin/entrypoint.sh
|
|
699
583
|
|
|
700
584
|
# Note: Don't set USER here - entrypoint needs root to use runuser
|
|
701
585
|
# The tini mode drops privileges to opencode user via runuser
|
|
702
586
|
|
|
587
|
+
# Healthcheck script asset
|
|
588
|
+
COPY packages/core/src/docker/files/healthcheck.sh /usr/local/bin/healthcheck.sh
|
|
589
|
+
RUN chmod +x /usr/local/bin/healthcheck.sh
|
|
590
|
+
|
|
703
591
|
# -----------------------------------------------------------------------------
|
|
704
592
|
# Health Check
|
|
705
593
|
# -----------------------------------------------------------------------------
|
|
706
|
-
# Check
|
|
594
|
+
# Check broker readiness (process + socket) and opencode web response
|
|
707
595
|
# Works for both tini and systemd modes
|
|
708
596
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
|
709
|
-
CMD
|
|
597
|
+
CMD ["/usr/local/bin/healthcheck.sh"]
|
|
710
598
|
|
|
711
599
|
# -----------------------------------------------------------------------------
|
|
712
600
|
# opencode Artifacts
|
|
@@ -24,17 +24,25 @@ https://github.com/pRizz/opencode-cloud (mirror: https://gitea.com/pRizz/opencod
|
|
|
24
24
|
Pull the image:
|
|
25
25
|
|
|
26
26
|
```
|
|
27
|
-
docker pull
|
|
27
|
+
docker pull prizz/opencode-cloud-sandbox:latest
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
Run the container:
|
|
31
31
|
|
|
32
32
|
```
|
|
33
|
-
docker run --rm -it -p 3000:3000
|
|
33
|
+
docker run --rm -it -p 3000:3000 prizz/opencode-cloud-sandbox:latest
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
The opencode web UI is available at `http://localhost:3000`.
|
|
37
37
|
|
|
38
|
+
## App Platform
|
|
39
|
+
|
|
40
|
+
- Set `http_port` to `3000` or provide `PORT`/`OPENCODE_PORT` so the health check hits the right port.
|
|
41
|
+
- App Platform storage is ephemeral. Workspace, config, and PAM users reset on redeploy unless you add external storage.
|
|
42
|
+
- Logs are visible in the App Platform UI without extra setup.
|
|
43
|
+
- Provide `OPENCODE_BOOTSTRAP_USER` with either `OPENCODE_BOOTSTRAP_PASSWORD` or `OPENCODE_BOOTSTRAP_PASSWORD_HASH` for first-boot access.
|
|
44
|
+
- App Platform supports Linux/AMD64 images and favors smaller image sizes.
|
|
45
|
+
|
|
38
46
|
## Install the opencode-cloud CLI
|
|
39
47
|
|
|
40
48
|
Cargo:
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
This directory contains files copied into the Docker image via `Dockerfile` `COPY` lines.
|
|
2
|
+
When adding new files here, update the minimal build context in
|
|
3
|
+
`packages/core/src/docker/image.rs` (see `create_build_context` and its helper)
|
|
4
|
+
so `occ`/CLI builds include the new assets.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
# Modern CLI aliases
|
|
3
|
+
alias ls="eza --icons"
|
|
4
|
+
alias ll="eza -l --icons"
|
|
5
|
+
alias la="eza -la --icons"
|
|
6
|
+
alias lt="eza --tree --icons"
|
|
7
|
+
alias grep="rg"
|
|
8
|
+
alias top="btop"
|
|
9
|
+
|
|
10
|
+
# Git aliases
|
|
11
|
+
alias g="git"
|
|
12
|
+
alias gs="git status"
|
|
13
|
+
alias gd="git diff"
|
|
14
|
+
alias gc="git commit"
|
|
15
|
+
alias gp="git push"
|
|
16
|
+
alias gl="git pull"
|
|
17
|
+
alias gco="git checkout"
|
|
18
|
+
alias gb="git branch"
|
|
19
|
+
alias lg="lazygit"
|
|
20
|
+
|
|
21
|
+
# Docker aliases (for Docker-in-Docker)
|
|
22
|
+
alias d="docker"
|
|
23
|
+
alias dc="docker compose"
|
|
24
|
+
|
|
25
|
+
export PATH="/home/opencode/.local/bin:$PATH"
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
log() {
|
|
5
|
+
echo "[opencode-cloud] $*"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
read_opencode_cloud_version() {
|
|
9
|
+
local version_file="/etc/opencode-cloud-version"
|
|
10
|
+
local version
|
|
11
|
+
|
|
12
|
+
if [ -r "${version_file}" ]; then
|
|
13
|
+
version="$(head -n 1 "${version_file}" 2>/dev/null | tr -d "\r\n")"
|
|
14
|
+
else
|
|
15
|
+
version=""
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
if [ -z "${version}" ]; then
|
|
19
|
+
printf "dev"
|
|
20
|
+
else
|
|
21
|
+
printf "%s" "${version}"
|
|
22
|
+
fi
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
print_welcome_banner() {
|
|
26
|
+
local version
|
|
27
|
+
version="$(read_opencode_cloud_version)"
|
|
28
|
+
|
|
29
|
+
log "----------------------------------------------------------------------"
|
|
30
|
+
log "Welcome to opencode-cloud-sandbox"
|
|
31
|
+
log "You are running opencode-cloud v${version}"
|
|
32
|
+
log "For questions, problems, and feature requests, file an issue:"
|
|
33
|
+
log " https://github.com/pRizz/opencode-cloud/issues"
|
|
34
|
+
log "opencode-cloud runs opencode in a Docker sandbox; use occ/opencode-cloud CLI to manage users, mounts, and updates."
|
|
35
|
+
log "Quick start: occ user add <username> | occ status | occ logs -f"
|
|
36
|
+
log "Docs: https://github.com/pRizz/opencode-cloud#readme"
|
|
37
|
+
log "----------------------------------------------------------------------"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
OPENCODE_PORT="${OPENCODE_PORT:-${PORT:-3000}}"
|
|
41
|
+
OPENCODE_HOST="${OPENCODE_HOST:-0.0.0.0}"
|
|
42
|
+
export OPENCODE_PORT OPENCODE_HOST
|
|
43
|
+
|
|
44
|
+
print_welcome_banner
|
|
45
|
+
|
|
46
|
+
detect_droplet() {
|
|
47
|
+
local hint="${OPENCODE_CLOUD_ENV:-}"
|
|
48
|
+
if [ -n "${hint}" ]; then
|
|
49
|
+
hint="$(printf "%s" "${hint}" | tr "[:upper:]" "[:lower:]")"
|
|
50
|
+
if [[ "${hint}" == *digitalocean* || "${hint}" == *droplet* ]]; then
|
|
51
|
+
return 0
|
|
52
|
+
fi
|
|
53
|
+
fi
|
|
54
|
+
curl -fsS --connect-timeout 1 --max-time 1 http://169.254.169.254/metadata/v1/id >/dev/null 2>&1
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
collect_non_persistent_paths() {
|
|
58
|
+
local -a paths=(
|
|
59
|
+
"/home/opencode/workspace"
|
|
60
|
+
"/home/opencode/.local/share/opencode"
|
|
61
|
+
"/home/opencode/.local/state/opencode"
|
|
62
|
+
"/home/opencode/.config/opencode"
|
|
63
|
+
"/var/lib/opencode-users"
|
|
64
|
+
)
|
|
65
|
+
local -a non_persistent=()
|
|
66
|
+
local fs_type
|
|
67
|
+
for path in "${paths[@]}"; do
|
|
68
|
+
fs_type="$(stat -f -c %T "${path}" 2>/dev/null || true)"
|
|
69
|
+
case "${fs_type}" in
|
|
70
|
+
""|overlay|overlayfs|tmpfs|ramfs|squashfs)
|
|
71
|
+
non_persistent+=("${path}")
|
|
72
|
+
;;
|
|
73
|
+
esac
|
|
74
|
+
done
|
|
75
|
+
if [ ${#non_persistent[@]} -eq 0 ]; then
|
|
76
|
+
return 1
|
|
77
|
+
fi
|
|
78
|
+
printf "%s\n" "${non_persistent[@]}"
|
|
79
|
+
return 0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
non_persistent_paths="$(collect_non_persistent_paths || true)"
|
|
83
|
+
if [ -n "${non_persistent_paths}" ]; then
|
|
84
|
+
log "================================================================="
|
|
85
|
+
log "WARNING: Persistence is not configured for one or more paths."
|
|
86
|
+
log "Data loss is likely if the container is recreated or updated."
|
|
87
|
+
log "Non-persistent paths:"
|
|
88
|
+
while IFS= read -r path; do
|
|
89
|
+
log " - ${path}"
|
|
90
|
+
done <<< "${non_persistent_paths}"
|
|
91
|
+
if detect_droplet; then
|
|
92
|
+
log "Detected DigitalOcean Docker Droplet environment."
|
|
93
|
+
log "By default, Docker Droplets do not configure volumes or persistence."
|
|
94
|
+
log "You will almost certainly lose data if you are not careful."
|
|
95
|
+
fi
|
|
96
|
+
log "Configure persistence: https://github.com/pRizz/opencode-cloud#readme"
|
|
97
|
+
log "================================================================="
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
if [ "${USE_SYSTEMD:-}" = "1" ]; then
|
|
101
|
+
exec /sbin/init
|
|
102
|
+
else
|
|
103
|
+
# Ensure broker socket directory exists
|
|
104
|
+
install -d -m 0755 /run/opencode
|
|
105
|
+
|
|
106
|
+
# Ensure user records directory exists (ephemeral unless mounted)
|
|
107
|
+
install -d -m 0700 /var/lib/opencode-users
|
|
108
|
+
|
|
109
|
+
restore_users() {
|
|
110
|
+
shopt -s nullglob
|
|
111
|
+
local records=(/var/lib/opencode-users/*.json)
|
|
112
|
+
if [ ${#records[@]} -eq 0 ]; then
|
|
113
|
+
return 1
|
|
114
|
+
fi
|
|
115
|
+
for record in "${records[@]}"; do
|
|
116
|
+
local username password_hash locked
|
|
117
|
+
username="$(jq -r ".username // empty" "${record}")"
|
|
118
|
+
password_hash="$(jq -r ".password_hash // empty" "${record}")"
|
|
119
|
+
locked="$(jq -r ".locked // false" "${record}")"
|
|
120
|
+
if [ -z "${username}" ]; then
|
|
121
|
+
log "Skipping invalid user record: ${record}"
|
|
122
|
+
continue
|
|
123
|
+
fi
|
|
124
|
+
if ! id -u "${username}" >/dev/null 2>&1; then
|
|
125
|
+
log "Creating user: ${username}"
|
|
126
|
+
useradd -m -s /bin/bash "${username}"
|
|
127
|
+
fi
|
|
128
|
+
if [ -n "${password_hash}" ]; then
|
|
129
|
+
usermod -p "${password_hash}" "${username}"
|
|
130
|
+
fi
|
|
131
|
+
if [ "${locked}" = "true" ]; then
|
|
132
|
+
passwd -l "${username}" >/dev/null
|
|
133
|
+
else
|
|
134
|
+
passwd -u "${username}" >/dev/null || true
|
|
135
|
+
fi
|
|
136
|
+
log "Restored user: ${username}"
|
|
137
|
+
done
|
|
138
|
+
return 0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
persist_user_record() {
|
|
142
|
+
local username="$1"
|
|
143
|
+
local shadow_hash
|
|
144
|
+
shadow_hash="$(getent shadow "${username}" | cut -d: -f2)"
|
|
145
|
+
if [ -z "${shadow_hash}" ]; then
|
|
146
|
+
log "Failed to read shadow hash for ${username}"
|
|
147
|
+
return 1
|
|
148
|
+
fi
|
|
149
|
+
local status locked
|
|
150
|
+
status="$(passwd -S "${username}" | tr -s " " | cut -d" " -f2)"
|
|
151
|
+
locked="false"
|
|
152
|
+
if [ "${status}" = "L" ]; then
|
|
153
|
+
locked="true"
|
|
154
|
+
fi
|
|
155
|
+
local record_path="/var/lib/opencode-users/${username}.json"
|
|
156
|
+
umask 077
|
|
157
|
+
jq -n --arg username "${username}" --arg hash "${shadow_hash}" --argjson locked "${locked}" '{username:$username,password_hash:$hash,locked:$locked}' > "${record_path}"
|
|
158
|
+
chmod 600 "${record_path}"
|
|
159
|
+
log "Persisted user record: ${username}"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
bootstrap_user() {
|
|
163
|
+
local username="${OPENCODE_BOOTSTRAP_USER:-}"
|
|
164
|
+
local password="${OPENCODE_BOOTSTRAP_PASSWORD:-}"
|
|
165
|
+
local password_hash="${OPENCODE_BOOTSTRAP_PASSWORD_HASH:-}"
|
|
166
|
+
if [ -z "${username}" ]; then
|
|
167
|
+
return 1
|
|
168
|
+
fi
|
|
169
|
+
if [ -z "${password_hash}" ] && [ -z "${password}" ]; then
|
|
170
|
+
log "OPENCODE_BOOTSTRAP_USER is set but no password or hash provided"
|
|
171
|
+
exit 1
|
|
172
|
+
fi
|
|
173
|
+
if ! id -u "${username}" >/dev/null 2>&1; then
|
|
174
|
+
log "Creating bootstrap user: ${username}"
|
|
175
|
+
useradd -m -s /bin/bash "${username}"
|
|
176
|
+
fi
|
|
177
|
+
if [ -n "${password_hash}" ]; then
|
|
178
|
+
usermod -p "${password_hash}" "${username}"
|
|
179
|
+
else
|
|
180
|
+
echo "${username}:${password}" | chpasswd
|
|
181
|
+
fi
|
|
182
|
+
persist_user_record "${username}"
|
|
183
|
+
log "Bootstrap user ready: ${username}"
|
|
184
|
+
return 0
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if restore_users; then
|
|
188
|
+
log "User records restored"
|
|
189
|
+
else
|
|
190
|
+
if ! bootstrap_user; then
|
|
191
|
+
log "No persisted users and no bootstrap user configured"
|
|
192
|
+
fi
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
log "Starting opencode on ${OPENCODE_HOST}:${OPENCODE_PORT}"
|
|
196
|
+
/usr/local/bin/opencode-broker &
|
|
197
|
+
# Use runuser to switch to opencode user without password prompt
|
|
198
|
+
exec runuser -u opencode -- sh -lc "cd /home/opencode/workspace && /opt/opencode/bin/opencode web --port ${OPENCODE_PORT} --hostname ${OPENCODE_HOST}"
|
|
199
|
+
fi
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
opencode_port="${OPENCODE_PORT:-${PORT:-3000}}"
|
|
5
|
+
|
|
6
|
+
if [ -d /run/systemd/system ]; then
|
|
7
|
+
systemctl is-active --quiet opencode-broker.service
|
|
8
|
+
else
|
|
9
|
+
pgrep -x opencode-broker >/dev/null
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
test -S /run/opencode/auth.sock
|
|
13
|
+
curl -fsS -H "Accept: text/html" "http://localhost:${opencode_port}/" >/dev/null
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=OpenCode Authentication Broker
|
|
3
|
+
Documentation=https://github.com/pRizz/opencode
|
|
4
|
+
After=network.target
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
Type=notify
|
|
8
|
+
ExecStart=/usr/local/bin/opencode-broker
|
|
9
|
+
ExecReload=/bin/kill -HUP $MAINPID
|
|
10
|
+
Restart=always
|
|
11
|
+
RestartSec=5
|
|
12
|
+
|
|
13
|
+
# Security hardening
|
|
14
|
+
NoNewPrivileges=false
|
|
15
|
+
ProtectSystem=strict
|
|
16
|
+
ProtectHome=read-only
|
|
17
|
+
PrivateTmp=true
|
|
18
|
+
ReadWritePaths=/run/opencode
|
|
19
|
+
|
|
20
|
+
# Socket directory
|
|
21
|
+
RuntimeDirectory=opencode
|
|
22
|
+
RuntimeDirectoryMode=0755
|
|
23
|
+
|
|
24
|
+
# Logging
|
|
25
|
+
StandardOutput=journal
|
|
26
|
+
StandardError=journal
|
|
27
|
+
SyslogIdentifier=opencode-broker
|
|
28
|
+
|
|
29
|
+
[Install]
|
|
30
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=opencode Web Interface
|
|
3
|
+
After=network.target opencode-broker.service
|
|
4
|
+
|
|
5
|
+
[Service]
|
|
6
|
+
Type=simple
|
|
7
|
+
User=opencode
|
|
8
|
+
WorkingDirectory=/home/opencode/workspace
|
|
9
|
+
ExecStart=/bin/bash -lc "OPENCODE_PORT=${OPENCODE_PORT:-${PORT:-3000}}; OPENCODE_HOST=${OPENCODE_HOST:-0.0.0.0}; exec /opt/opencode/bin/opencode web --port ${OPENCODE_PORT} --hostname ${OPENCODE_HOST}"
|
|
10
|
+
Restart=always
|
|
11
|
+
RestartSec=5
|
|
12
|
+
Environment=PATH=/opt/opencode/bin:/home/opencode/.local/bin:/home/opencode/.cargo/bin:/home/opencode/.local/share/mise/shims:/home/opencode/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
13
|
+
|
|
14
|
+
[Install]
|
|
15
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# PAM configuration for OpenCode authentication
|
|
2
|
+
# Install to /etc/pam.d/opencode
|
|
3
|
+
|
|
4
|
+
# Standard UNIX authentication
|
|
5
|
+
auth required pam_unix.so
|
|
6
|
+
account required pam_unix.so
|
|
7
|
+
|
|
8
|
+
# Optional: Enable TOTP 2FA (uncomment when pam_google_authenticator is installed)
|
|
9
|
+
# auth required pam_google_authenticator.so
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Minimal starship config for fast prompt
|
|
2
|
+
format = """
|
|
3
|
+
$directory\
|
|
4
|
+
$git_branch\
|
|
5
|
+
$git_status\
|
|
6
|
+
$character"""
|
|
7
|
+
|
|
8
|
+
[directory]
|
|
9
|
+
truncation_length = 3
|
|
10
|
+
truncate_to_repo = true
|
|
11
|
+
|
|
12
|
+
[git_branch]
|
|
13
|
+
format = "[$branch]($style) "
|
|
14
|
+
style = "bold purple"
|
|
15
|
+
|
|
16
|
+
[git_status]
|
|
17
|
+
format = '([$all_status$ahead_behind]($style) )'
|
|
18
|
+
|
|
19
|
+
[character]
|
|
20
|
+
success_symbol = "[>](bold green)"
|
|
21
|
+
error_symbol = "[>](bold red)"
|
package/src/docker/image.rs
CHANGED
|
@@ -20,6 +20,7 @@ use futures_util::StreamExt;
|
|
|
20
20
|
use http_body_util::{Either, Full};
|
|
21
21
|
use std::collections::{HashMap, HashSet, VecDeque};
|
|
22
22
|
use std::env;
|
|
23
|
+
use std::io::Write;
|
|
23
24
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
24
25
|
use tar::Builder as TarBuilder;
|
|
25
26
|
use tracing::{debug, warn};
|
|
@@ -887,13 +888,55 @@ fn create_build_context() -> Result<Vec<u8>, std::io::Error> {
|
|
|
887
888
|
|
|
888
889
|
// Add Dockerfile to archive
|
|
889
890
|
let dockerfile_bytes = DOCKERFILE.as_bytes();
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
891
|
+
append_bytes(&mut tar, "Dockerfile", dockerfile_bytes, 0o644)?;
|
|
892
|
+
append_bytes(
|
|
893
|
+
&mut tar,
|
|
894
|
+
"packages/core/src/docker/files/entrypoint.sh",
|
|
895
|
+
include_bytes!("files/entrypoint.sh"),
|
|
896
|
+
0o644,
|
|
897
|
+
)?;
|
|
898
|
+
append_bytes(
|
|
899
|
+
&mut tar,
|
|
900
|
+
"packages/core/src/docker/files/healthcheck.sh",
|
|
901
|
+
include_bytes!("files/healthcheck.sh"),
|
|
902
|
+
0o644,
|
|
903
|
+
)?;
|
|
904
|
+
append_bytes(
|
|
905
|
+
&mut tar,
|
|
906
|
+
"packages/core/src/docker/files/opencode-broker.service",
|
|
907
|
+
include_bytes!("files/opencode-broker.service"),
|
|
908
|
+
0o644,
|
|
909
|
+
)?;
|
|
910
|
+
append_bytes(
|
|
911
|
+
&mut tar,
|
|
912
|
+
"packages/core/src/docker/files/opencode.service",
|
|
913
|
+
include_bytes!("files/opencode.service"),
|
|
914
|
+
0o644,
|
|
915
|
+
)?;
|
|
916
|
+
append_bytes(
|
|
917
|
+
&mut tar,
|
|
918
|
+
"packages/core/src/docker/files/pam/opencode",
|
|
919
|
+
include_bytes!("files/pam/opencode"),
|
|
920
|
+
0o644,
|
|
921
|
+
)?;
|
|
922
|
+
append_bytes(
|
|
923
|
+
&mut tar,
|
|
924
|
+
"packages/core/src/docker/files/opencode.jsonc",
|
|
925
|
+
include_bytes!("files/opencode.jsonc"),
|
|
926
|
+
0o644,
|
|
927
|
+
)?;
|
|
928
|
+
append_bytes(
|
|
929
|
+
&mut tar,
|
|
930
|
+
"packages/core/src/docker/files/starship.toml",
|
|
931
|
+
include_bytes!("files/starship.toml"),
|
|
932
|
+
0o644,
|
|
933
|
+
)?;
|
|
934
|
+
append_bytes(
|
|
935
|
+
&mut tar,
|
|
936
|
+
"packages/core/src/docker/files/bashrc.extra",
|
|
937
|
+
include_bytes!("files/bashrc.extra"),
|
|
938
|
+
0o644,
|
|
939
|
+
)?;
|
|
897
940
|
tar.finish()?;
|
|
898
941
|
|
|
899
942
|
// Finish gzip encoding
|
|
@@ -904,11 +947,30 @@ fn create_build_context() -> Result<Vec<u8>, std::io::Error> {
|
|
|
904
947
|
Ok(archive_buffer)
|
|
905
948
|
}
|
|
906
949
|
|
|
950
|
+
fn append_bytes<W: Write>(
|
|
951
|
+
tar: &mut TarBuilder<W>,
|
|
952
|
+
path: &str,
|
|
953
|
+
contents: &[u8],
|
|
954
|
+
mode: u32,
|
|
955
|
+
) -> Result<(), std::io::Error> {
|
|
956
|
+
let mut header = tar::Header::new_gnu();
|
|
957
|
+
header.set_path(path)?;
|
|
958
|
+
header.set_size(contents.len() as u64);
|
|
959
|
+
header.set_mode(mode);
|
|
960
|
+
header.set_cksum();
|
|
961
|
+
|
|
962
|
+
tar.append(&header, contents)?;
|
|
963
|
+
Ok(())
|
|
964
|
+
}
|
|
965
|
+
|
|
907
966
|
#[cfg(test)]
|
|
908
967
|
mod tests {
|
|
909
968
|
use super::*;
|
|
910
969
|
use bollard::models::ImageSummary;
|
|
970
|
+
use flate2::read::GzDecoder;
|
|
911
971
|
use std::collections::HashMap;
|
|
972
|
+
use std::io::Cursor;
|
|
973
|
+
use tar::Archive;
|
|
912
974
|
|
|
913
975
|
fn make_image_summary(
|
|
914
976
|
id: &str,
|
|
@@ -944,6 +1006,39 @@ mod tests {
|
|
|
944
1006
|
assert_eq!(context[1], 0x8b, "should be gzip compressed");
|
|
945
1007
|
}
|
|
946
1008
|
|
|
1009
|
+
#[test]
|
|
1010
|
+
fn build_context_includes_docker_assets() {
|
|
1011
|
+
let context = create_build_context().expect("should create context");
|
|
1012
|
+
let cursor = Cursor::new(context);
|
|
1013
|
+
let decoder = GzDecoder::new(cursor);
|
|
1014
|
+
let mut archive = Archive::new(decoder);
|
|
1015
|
+
let mut found_entrypoint = false;
|
|
1016
|
+
let mut found_healthcheck = false;
|
|
1017
|
+
|
|
1018
|
+
for entry in archive.entries().expect("should read archive entries") {
|
|
1019
|
+
let entry = entry.expect("should read entry");
|
|
1020
|
+
let path = entry.path().expect("should read entry path");
|
|
1021
|
+
if path == std::path::Path::new("packages/core/src/docker/files/entrypoint.sh") {
|
|
1022
|
+
found_entrypoint = true;
|
|
1023
|
+
}
|
|
1024
|
+
if path == std::path::Path::new("packages/core/src/docker/files/healthcheck.sh") {
|
|
1025
|
+
found_healthcheck = true;
|
|
1026
|
+
}
|
|
1027
|
+
if found_entrypoint && found_healthcheck {
|
|
1028
|
+
break;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
assert!(
|
|
1033
|
+
found_entrypoint,
|
|
1034
|
+
"entrypoint asset should be in the build context"
|
|
1035
|
+
);
|
|
1036
|
+
assert!(
|
|
1037
|
+
found_healthcheck,
|
|
1038
|
+
"healthcheck asset should be in the build context"
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
947
1042
|
#[test]
|
|
948
1043
|
fn default_tag_is_latest() {
|
|
949
1044
|
assert_eq!(IMAGE_TAG_DEFAULT, "latest");
|