Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b99abb7c39 | ||
|
|
c2ae11a293 | ||
|
|
f1555259de | ||
|
|
50f8fbcfda | ||
|
|
73f29092ff | ||
|
|
08b24af341 | ||
|
|
b9c54f1ff5 | ||
|
|
4d76dd601c | ||
|
|
3af9936d27 | ||
|
|
dc721e511e | ||
|
|
054b3b8fd4 | ||
|
|
b3896ff642 | ||
|
|
5b2823de1e | ||
|
|
0007f76d09 | ||
|
|
f8e6d1caaa | ||
|
|
e684795aee | ||
|
|
7964273552 | ||
|
|
4ad4bce29d | ||
|
|
d670d18810 | ||
|
|
a1957a156e | ||
|
|
6266e9ec3a | ||
|
|
7c4fcde88d | ||
|
|
d36b28c55a | ||
|
|
6b41340f1d | ||
|
|
4b9ea43402 | ||
|
|
e5f23a9189 | ||
|
|
15e3e32b47 |
@@ -57,7 +57,8 @@ jobs:
|
||||
|
||||
The commands below show how to install the nightly version of uncloud.
|
||||
|
||||
> **Warning**: These are nightly development builds and may be unstable.
|
||||
> [!WARNING]
|
||||
> These are nightly development builds and may be unstable.
|
||||
|
||||
**Install uncloud CLI:**
|
||||
|
||||
|
||||
@@ -154,7 +154,6 @@ Here is a diagram of an Uncloud multi-provider cluster of 3 machines:
|
||||
```bash
|
||||
$ uc machine init --name oracle-vm ubuntu@152.67.101.197
|
||||
|
||||
Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh
|
||||
⏳ Running Uncloud install script...
|
||||
✓ Docker is already installed.
|
||||
⏳ Installing Docker...
|
||||
@@ -169,9 +168,6 @@ Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski
|
||||
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
|
||||
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
|
||||
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
|
||||
⏳ Downloading uncloud-corrosion binary: https://github.com/psviderski/corrosion/releases/latest/download/corrosion-aarch64-unknown-linux-gnu.tar.gz
|
||||
✓ uncloud-corrosion binary installed: /usr/local/bin/uncloud-corrosion
|
||||
✓ Systemd unit file created: /etc/systemd/system/uncloud-corrosion.service
|
||||
⏳ Starting Uncloud machine daemon (uncloud.service)...
|
||||
✓ Uncloud machine daemon started.
|
||||
✓ Uncloud installed on the machine successfully! 🎉
|
||||
@@ -190,21 +186,20 @@ DNS records updated to use only the internet-reachable machines running caddy se
|
||||
*.xuw3xd.cluster.uncloud.run A → 152.67.101.197
|
||||
```
|
||||
|
||||
1. The CLI SSHs into the machine and installs Docker, the `uncloudd` machine daemon and
|
||||
[corrosion](https://github.com/superfly/corrosion) service, managed by systemd.
|
||||
1. The CLI SSHs into the machine and installs Docker and the `uncloudd` machine daemon, managed by systemd.
|
||||
2. Generates a unique WireGuard key pair, allocates a dedicated subnet `10.210.0.0/24` for the machine and its
|
||||
containers, and configures `uncloudd` accordingly. All subsequent communication happens with `uncloudd`
|
||||
through its gRPC API over SSH.
|
||||
3. Configures and starts `corrosion`, a CRDT-based distributed SQLite database to share cluster state between machines.
|
||||
3. Configures and starts [corrosion](https://github.com/superfly/corrosion), a CRDT-based distributed SQLite database to
|
||||
share cluster state between machines, as a Docker container managed by `uncloudd`.
|
||||
4. Creates a Docker bridge network connected to the WireGuard interface.
|
||||
5. This machine becomes an entry point for the newly created cluster which is stored in the cluster config under
|
||||
`~/.config/uncloud` on your local machine.
|
||||
5. This machine becomes an entry point for the newly created cluster. Its address is stored as a connection info in the
|
||||
cluster config at `~/.config/uncloud/config.yaml` on your local machine.
|
||||
|
||||
**When you add another machine:**
|
||||
|
||||
```bash
|
||||
$ uc machine add --name hetzner-server root@5.223.45.199
|
||||
Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh
|
||||
⏳ Running Uncloud install script...
|
||||
✓ Docker is already installed.
|
||||
✓ Linux user and group 'uncloud' created.
|
||||
@@ -215,9 +210,6 @@ Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski
|
||||
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
|
||||
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
|
||||
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
|
||||
⏳ Downloading uncloud-corrosion binary: https://github.com/psviderski/corrosion/releases/latest/download/corrosion-x86_64-unknown-linux-gnu.tar.gz
|
||||
✓ uncloud-corrosion binary installed: /usr/local/bin/uncloud-corrosion
|
||||
✓ Systemd unit file created: /etc/systemd/system/uncloud-corrosion.service
|
||||
⏳ Starting Uncloud machine daemon (uncloud.service)...
|
||||
✓ Uncloud machine daemon started.
|
||||
✓ Uncloud installed on the machine successfully! 🎉
|
||||
@@ -246,9 +238,10 @@ hetzner-server Up 10.210.1.1/24 5.223.45.199 5.223.45.199:51820, [2
|
||||
3. Registers the second machine in the cluster state and exchanges WireGuard keys with the first machine.
|
||||
4. Both machines establish a WireGuard tunnel between each other, allowing Docker containers connected to the bridge
|
||||
network to communicate directly across machines.
|
||||
5. Configures and starts `corrosion` on the second machine to sync the cluster state.
|
||||
5. Configures and starts the `uncloud-corrosion` container on the second machine to sync the cluster state.
|
||||
6. The second machine is added as an alternative entry point in the cluster config.
|
||||
7. If one of the machines goes offline, the other machine can still serve cluster operations.
|
||||
7. If one of the machines goes offline, the other machine can still be used by `uc` to connect and run cluster
|
||||
operations.
|
||||
|
||||
If one more machine is added, the process repeats with a new subnet. The new machine needs to establish a WireGuard
|
||||
connection with only one of the existing machines. Other machines will learn about it through the shared cluster state
|
||||
|
||||
@@ -40,13 +40,19 @@ func NewAddCommand() *cobra.Command {
|
||||
Short: "Add a remote machine to a cluster.",
|
||||
Long: `Add a new machine to an existing Uncloud cluster.
|
||||
|
||||
By default, it installs Docker and the Uncloud daemon on the machine over SSH unless --no-install
|
||||
is specified, which assumes they are already installed and running.
|
||||
|
||||
Connection methods:
|
||||
[ssh://]user@host - Use system 'ssh' command with full SSH config support (default, no prefix required)
|
||||
ssh+go://user@host - Use Go's built-in SSH library`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM")
|
||||
|
||||
cli.BindEnvToFlag(cmd, "version", "UNCLOUD_DAEMON_VERSION")
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
|
||||
// Determine connection mode and strip scheme.
|
||||
@@ -79,7 +85,7 @@ Connection methods:
|
||||
)
|
||||
cmd.Flags().BoolVar(
|
||||
&opts.noInstall, "no-install", false,
|
||||
"Skip installation of Docker, Uncloud daemon, and dependencies on the machine. "+
|
||||
"Skip installation of Docker and the Uncloud daemon on the machine. "+
|
||||
"Assumes they're already installed and running.",
|
||||
)
|
||||
cmd.Flags().StringVar(
|
||||
@@ -94,7 +100,7 @@ Connection methods:
|
||||
)
|
||||
cmd.Flags().StringVar(
|
||||
&opts.version, "version", "latest",
|
||||
"Version of the Uncloud daemon to install on the machine.",
|
||||
"Version of the Uncloud daemon to install on the machine. [$UNCLOUD_DAEMON_VERSION]",
|
||||
)
|
||||
cmd.Flags().StringSliceVar(
|
||||
&opts.wgEndpoints, "wg-endpoint", nil,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package machine
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// runVersionFlagEnvTest builds the given command for each case, stubs out RunE so only flag
|
||||
// parsing and PreRunE run (no SSH calls), and asserts the resolved --version flag value.
|
||||
// Note: cannot use t.Parallel() because subtests use t.Setenv().
|
||||
func runVersionFlagEnvTest(t *testing.T, newCommand func() *cobra.Command) {
|
||||
t.Helper()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
env string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "env var sets version when flag not passed",
|
||||
args: []string{"root@localhost"},
|
||||
env: "v1.2.3",
|
||||
want: "v1.2.3",
|
||||
},
|
||||
{
|
||||
name: "explicit flag wins over env var",
|
||||
args: []string{"root@localhost", "--version", "v9.9.9"},
|
||||
env: "v1.2.3",
|
||||
want: "v9.9.9",
|
||||
},
|
||||
{
|
||||
name: "defaults to latest when neither flag nor env var set",
|
||||
args: []string{"root@localhost"},
|
||||
want: "latest",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.env != "" {
|
||||
t.Setenv("UNCLOUD_DAEMON_VERSION", tt.env)
|
||||
}
|
||||
|
||||
cmd := newCommand()
|
||||
cmd.RunE = func(*cobra.Command, []string) error {
|
||||
return nil
|
||||
}
|
||||
cmd.SetArgs(tt.args)
|
||||
require.NoError(t, cmd.Execute())
|
||||
|
||||
version, err := cmd.Flags().GetString("version")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, version)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestInitCommandVersionFlagEnvVar verifies that the UNCLOUD_DAEMON_VERSION environment variable
|
||||
// is bound to the --version flag of 'machine init', and that an explicit flag wins.
|
||||
func TestInitCommandVersionFlagEnvVar(t *testing.T) {
|
||||
runVersionFlagEnvTest(t, NewInitCommand)
|
||||
}
|
||||
|
||||
// TestAddCommandVersionFlagEnvVar verifies that the UNCLOUD_DAEMON_VERSION environment variable
|
||||
// is bound to the --version flag of 'machine add', and that an explicit flag wins.
|
||||
func TestAddCommandVersionFlagEnvVar(t *testing.T) {
|
||||
runVersionFlagEnvTest(t, NewAddCommand)
|
||||
}
|
||||
@@ -45,6 +45,9 @@ func NewInitCommand() *cobra.Command {
|
||||
Long: `Initialise a new cluster by setting up a remote machine as the first member.
|
||||
This command creates a new context in your Uncloud config to manage the cluster.
|
||||
|
||||
By default, it installs Docker and the Uncloud daemon on the machine over SSH unless --no-install
|
||||
is specified, which assumes they are already installed and running.
|
||||
|
||||
Connection methods:
|
||||
[ssh://]user@host - Use system 'ssh' command with full SSH config support (default, no prefix required)
|
||||
ssh+go://user@host - Use Go's built-in SSH library`,
|
||||
@@ -62,9 +65,12 @@ Connection methods:
|
||||
uc machine init root@<your-server-ip> --no-caddy --no-dns`,
|
||||
// TODO: support initialising a cluster on the local machine.
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM")
|
||||
|
||||
cli.BindEnvToFlag(cmd, "version", "UNCLOUD_DAEMON_VERSION")
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
|
||||
var remoteMachine *cli.RemoteMachine
|
||||
@@ -117,7 +123,7 @@ Connection methods:
|
||||
)
|
||||
cmd.Flags().BoolVar(
|
||||
&opts.noInstall, "no-install", false,
|
||||
"Skip installation of Docker, Uncloud daemon, and dependencies on the machine. "+
|
||||
"Skip installation of Docker and the Uncloud daemon on the machine. "+
|
||||
"Assumes they're already installed and running.",
|
||||
)
|
||||
cmd.Flags().StringVar(
|
||||
@@ -132,7 +138,7 @@ Connection methods:
|
||||
)
|
||||
cmd.Flags().StringVar(
|
||||
&opts.version, "version", "latest",
|
||||
"Version of the Uncloud daemon to install on the machine.",
|
||||
"Version of the Uncloud daemon to install on the machine. [$UNCLOUD_DAEMON_VERSION]",
|
||||
)
|
||||
cmd.Flags().StringSliceVar(
|
||||
&opts.wgEndpoints, "wg-endpoint", nil,
|
||||
|
||||
@@ -136,8 +136,7 @@ func remove(ctx context.Context, uncli *cli.CLI, nameOrID string, opts removeOpt
|
||||
return fmt.Errorf("confirm removal: %w", err)
|
||||
}
|
||||
if !confirmed {
|
||||
fmt.Println("Cancelled. Machine was not removed.")
|
||||
return nil
|
||||
return cli.Cancelled("Cancelled. Machine was not removed.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,9 +30,11 @@ func NewScaleCommand(groupID string) *cobra.Command {
|
||||
Short: "Scale a replicated service by changing the number of replicas.",
|
||||
Long: "Scale a replicated service by changing the number of replicas.",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
cli.BindEnvToFlag(cmd, "yes", "UNCLOUD_AUTO_CONFIRM")
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||
|
||||
opts.service = args[0]
|
||||
|
||||
@@ -92,8 +92,7 @@ func remove(ctx context.Context, uncli *cli.CLI, names []string, opts removeOpti
|
||||
return fmt.Errorf("confirm removal: %w", err)
|
||||
}
|
||||
if !confirmed {
|
||||
fmt.Println("Cancelled. No volumes were removed.")
|
||||
return nil
|
||||
return cli.Cancelled("Cancelled. No volumes were removed.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,8 +124,8 @@ require (
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||
|
||||
@@ -660,8 +660,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -694,8 +694,8 @@ golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
|
||||
@@ -31,9 +31,8 @@ const (
|
||||
//
|
||||
// The two minimums are independent: a client might require a newer daemon for new
|
||||
// features, while that same daemon could still handle requests from older clients.
|
||||
// TODO: update to 0.20.0 before releasing 0.20.0.
|
||||
MinClientVersion = "0.20.0-nightly"
|
||||
MinServerVersion = "0.20.0-nightly"
|
||||
MinClientVersion = "0.20.0"
|
||||
MinServerVersion = "0.20.0"
|
||||
|
||||
ReleaseURL = "https://github.com/psviderski/uncloud/releases/latest"
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
systemd "github.com/coreos/go-systemd/daemon"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
@@ -22,6 +23,13 @@ const (
|
||||
Image = "ghcr.io/unlabs-dev/corrosion:2026.6.15"
|
||||
// ContainerName is the name of the managed Corrosion container.
|
||||
ContainerName = "uncloud-corrosion"
|
||||
|
||||
// systemdStartTimeoutExtension is the duration of each systemd service start timeout extension that prevents
|
||||
// a slow image pull from exceeding the start timeout (TimeoutStartSec). Extensions are a no-op if Corrosion
|
||||
// is not starting during a systemd service startup.
|
||||
systemdStartTimeoutExtension = 30 * time.Second
|
||||
// systemdStartTimeoutExtendMax caps the total duration the start timeout can be extended for.
|
||||
systemdStartTimeoutExtendMax = 5 * time.Minute
|
||||
)
|
||||
|
||||
type DockerService struct {
|
||||
@@ -175,8 +183,14 @@ func (s *DockerService) createAndStart(ctx context.Context) error {
|
||||
}
|
||||
defer respBody.Close()
|
||||
|
||||
// The pull on the first daemon start may take longer than the systemd service start timeout (TimeoutStartSec)
|
||||
// on a slow connection. Keep extending the timeout while the pull is in progress so systemd doesn't kill
|
||||
// the daemon before it reports readiness. This is a no-op if not running under systemd.
|
||||
stopExtending := extendSystemdStartTimeout(ctx)
|
||||
// Wait for pull to complete.
|
||||
if _, err := io.Copy(io.Discard, respBody); err != nil {
|
||||
_, err = io.Copy(io.Discard, respBody)
|
||||
stopExtending()
|
||||
if err != nil {
|
||||
return fmt.Errorf("read pull response: %w", err)
|
||||
}
|
||||
slog.Info("Docker image pulled.", "image", s.Image, "duration", time.Since(start).String())
|
||||
@@ -192,3 +206,40 @@ func (s *DockerService) createAndStart(ctx context.Context) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// extendSystemdStartTimeout periodically asks systemd to extend the service start timeout, for up to
|
||||
// systemdStartTimeoutExtendMax. The returned function stops the extensions. It is a no-op when the service
|
||||
// is not running under systemd (NOTIFY_SOCKET is not set).
|
||||
func extendSystemdStartTimeout(ctx context.Context) (stop context.CancelFunc) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
go func() {
|
||||
// Send extensions more frequently than they expire so a single missed tick doesn't time out the start.
|
||||
ticker := time.NewTicker(systemdStartTimeoutExtension / 3)
|
||||
defer ticker.Stop()
|
||||
deadline := time.Now().Add(systemdStartTimeoutExtendMax)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if time.Now().After(deadline) {
|
||||
slog.Warn("Stopped extending the systemd service start timeout: corrosion service is taking "+
|
||||
"too long to start.", "timeout", systemdStartTimeoutExtendMax)
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf("EXTEND_TIMEOUT_USEC=%d", systemdStartTimeoutExtension.Microseconds())
|
||||
if _, err := systemd.SdNotify(false, msg); err != nil {
|
||||
slog.Warn("Failed to extend the systemd service start timeout when starting corrosion service.",
|
||||
"err", err)
|
||||
return
|
||||
}
|
||||
slog.Info("Extended systemd service start timeout while corrosion service is starting.",
|
||||
"duration", systemdStartTimeoutExtension)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return cancel
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ Uncloud stores its configuration in `~/.config/uncloud/config.yaml`. If you wish
|
||||
|
||||
Begin by initialising the first node in your cluster with `uc machine init [USER@HOST:PORT]`. If you do not have a need for Caddy reverse proxy, you may disable this feature with `--no-caddy`. If you want to avoid using uncloud's managed DNS service, add the `--no-dns` flag.
|
||||
|
||||
This command will idempotently install Docker, uncloudd, uncloud-corrosion. If Caddy is enabled, it will set up a reverse proxy. If Uncloud DNS is enabled, it will create a DNS A record for the machine's public IP address under `*.[CLUSTER ID].uncld.dev`.
|
||||
This command will idempotently install Docker and uncloudd. If Caddy is enabled, it will set up a reverse proxy. If Uncloud DNS is enabled, it will create a DNS A record for the machine's public IP address under `*.[CLUSTER ID].uncld.dev`.
|
||||
|
||||
If you wish to uninstall Uncloud and its components, run `uncloud-uninstall`.
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ install_uncloud_binaries() {
|
||||
# 0.20.0~nightly-abc < 0.20.0 < 0.21.0~nightly-def.
|
||||
# latest_version is always a clean stable tag from releases/latest, so the substitution is one-sided.
|
||||
local newest
|
||||
newest=$(printf '%s\n%s\n' "${installed_version//-/~}" "${latest_version}" | sort -V | tail -n1)
|
||||
newest=$(printf '%s\n%s\n' "${installed_version//-/\~}" "${latest_version}" | sort -V | tail -n1)
|
||||
if [ "${newest}" = "${latest_version}" ]; then
|
||||
log "⏳ Upgrading uncloudd ${installed_version} → ${latest_version}..."
|
||||
uncloudd_url="${UNCLOUD_GITHUB_URL}/releases/download/v${latest_version}/${uncloudd_archive_name}"
|
||||
@@ -287,7 +287,7 @@ Wants=network-online.target
|
||||
[Service]
|
||||
Type=notify
|
||||
ExecStart=${INSTALL_BIN_DIR}/uncloudd
|
||||
TimeoutStartSec=15
|
||||
TimeoutStartSec=20
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
|
||||
|
||||
@@ -3,10 +3,30 @@ set -e
|
||||
|
||||
GITHUB_REPO="psviderski/uncloud"
|
||||
INSTALL_DIR=${INSTALL_DIR:-/usr/local/bin}
|
||||
# Use the latest version or specify the version to install:
|
||||
# Use the latest version or specify the version to install (the 'v' prefix is optional):
|
||||
# curl ... | VERSION=v1.2.3 sh
|
||||
VERSION=${VERSION:-latest}
|
||||
|
||||
# Normalise numeric versions to a 'vX.Y.Z' release tag so VERSION can be passed with or without
|
||||
# the 'v' prefix. Non-numeric refs such as 'nightly' are tags as-is and left untouched.
|
||||
case "${VERSION#v}" in
|
||||
[0-9]*) VERSION="v${VERSION#v}" ;;
|
||||
esac
|
||||
|
||||
|
||||
# The CLI archive and binary were renamed from 'uncloud' to 'uc' in v0.20.0. Returns success (0) when VERSION
|
||||
# is a numeric release older than v0.20.0, which still uses the legacy 'uncloud_*' archive and 'uncloud' binary name.
|
||||
# Newer versions and non-numeric refs such as 'nightly' use 'uc'.
|
||||
is_legacy_version() {
|
||||
v="${VERSION#v}"
|
||||
major="${v%%.*}"
|
||||
rest="${v#*.}"
|
||||
minor="${rest%%.*}"
|
||||
case "$major" in ''|*[!0-9]*) return 1 ;; esac
|
||||
case "$minor" in ''|*[!0-9]*) return 1 ;; esac
|
||||
[ "$major" -eq 0 ] && [ "$minor" -lt 20 ]
|
||||
}
|
||||
|
||||
print_manual_install() {
|
||||
RELEASES_URL="https://github.com/${GITHUB_REPO}/releases/${VERSION}"
|
||||
echo "Failed while attempting to install uncloud CLI. You can install it manually:"
|
||||
@@ -63,7 +83,14 @@ esac
|
||||
if [ "$VERSION" = "latest" ]; then
|
||||
fetch_latest_version
|
||||
fi
|
||||
BINARY_NAME="uc_${BINARY_OS}_${BINARY_ARCH}.tar.gz"
|
||||
|
||||
# Pick the archive and binary name for the requested version. Pre-v0.20.0 releases ship the legacy 'uncloud' name.
|
||||
# v0.20.0 and newer ship 'uc'.
|
||||
CLI_NAME="uc"
|
||||
if is_legacy_version; then
|
||||
CLI_NAME="uncloud"
|
||||
fi
|
||||
BINARY_NAME="${CLI_NAME}_${BINARY_OS}_${BINARY_ARCH}.tar.gz"
|
||||
BINARY_URL="https://github.com/${GITHUB_REPO}/releases/download/${VERSION}/${BINARY_NAME}"
|
||||
CHECKSUM_URL="https://github.com/${GITHUB_REPO}/releases/download/$VERSION/checksums.txt"
|
||||
|
||||
@@ -94,7 +121,7 @@ if [ -z "${SUDO}" ]; then
|
||||
else
|
||||
echo "Installing uc binary to ${INSTALL_DIR} using sudo. You may be prompted for your password."
|
||||
fi
|
||||
if ! $SUDO install ./uc "${INSTALL_DIR}/uc"; then
|
||||
if ! $SUDO install "./${CLI_NAME}" "${INSTALL_DIR}/uc"; then
|
||||
echo "Failed to install uc binary to ${INSTALL_DIR}"
|
||||
print_manual_install
|
||||
exit 1
|
||||
|
||||
@@ -1,9 +1,32 @@
|
||||
{
|
||||
admin off
|
||||
servers {
|
||||
# Trust the Uncloud ingress Caddy (cluster network) so the X-Forwarded-For chain it forwards
|
||||
# is preserved and the real client IP is logged in the client_ip field.
|
||||
trusted_proxies static 10.210.0.0/16
|
||||
}
|
||||
}
|
||||
|
||||
:8000 {
|
||||
# Reverse proxy PostHog through our own domain so ad blockers don't drop analytics requests.
|
||||
# Static assets (e.g. array.js) are served from a separate PostHog assets host.
|
||||
handle_path /phproxy/static/* {
|
||||
rewrite * /static{uri}
|
||||
reverse_proxy https://us-assets.i.posthog.com {
|
||||
header_up Host us-assets.i.posthog.com
|
||||
}
|
||||
}
|
||||
handle_path /phproxy/* {
|
||||
reverse_proxy https://us.i.posthog.com {
|
||||
header_up Host us.i.posthog.com
|
||||
}
|
||||
}
|
||||
|
||||
root * /usr/share/caddy
|
||||
|
||||
# Serve clean URLs for landing pages, e.g. /hub -> hub.html.
|
||||
try_files {path}.html
|
||||
|
||||
file_server
|
||||
log
|
||||
}
|
||||
|
||||
@@ -28,4 +28,4 @@ COPY --from=prod /opt/docusaurus/build /usr/share/caddy
|
||||
# Copy the landing page assets.
|
||||
COPY landing/images /usr/share/caddy/images
|
||||
COPY landing/js /usr/share/caddy/js
|
||||
COPY landing/index.html landing/style.css /usr/share/caddy/
|
||||
COPY landing/index.html landing/hub.html landing/style.css /usr/share/caddy/
|
||||
|
||||
@@ -14,33 +14,41 @@ Uncloud covers all the essentials for operating apps in production without overw
|
||||
traditional container orchestrators like Kubernetes or Swarm:
|
||||
|
||||
* Initial machine and network setup
|
||||
* Building images and pushing them directly to your machines without a registry
|
||||
* Zero-downtime rolling deployments
|
||||
* Health checks and automatic restarts
|
||||
* Automatic HTTPS and reverse proxy configuration
|
||||
* Scaling services across multiple machines
|
||||
* Cross-machine service communication without exposing ports to the internet
|
||||
* DNS-based service discovery
|
||||
* Automatic HTTPS and reverse proxy configuration
|
||||
* Load balancing
|
||||
* Persistent storage
|
||||
|
||||
## Use cases
|
||||
|
||||
Some of the common use cases Uncloud is a great fit for:
|
||||
Uncloud is a great fit for anything from production workloads to a single-server homelab:
|
||||
|
||||
- **Self-hosting and Homelabs**: Run your self-hosted apps on your own hardware. Start with a single machine and add
|
||||
more as your needs grow.
|
||||
- **Production web apps and SaaS**: Run your product on VMs from any cloud provider or your own servers with
|
||||
zero-downtime rolling deployments, health checks, and automatic HTTPS. Spread replicas across multiple machines to
|
||||
keep your app available even when a machine goes down.
|
||||
- **Outgrowing Docker Compose**: Level up your Docker Compose setup with zero-downtime deployments, replicas across
|
||||
multiple machines for improved reliability, cross-machine service communication, automated reverse proxy management,
|
||||
and more using the same Compose file.
|
||||
- **Small to medium web applications**: Deploy your SaaS product, websites, or personal projects with redundancy across
|
||||
multiple machines for better reliability and your peace of mind.
|
||||
- **Hybrid setups (cloud + on-prem)**: Combine cloud VMs with on-premise for cost savings and data sovereignty — all
|
||||
managed through the same interface.
|
||||
- **Moving off a cloud PaaS or Kubernetes**: Get a Heroku-like deployment workflow on your own servers, without the high
|
||||
PaaS costs or the complexity of Kubernetes.
|
||||
- **Migrating from Docker Swarm**: Swarm has been in maintenance mode for years. Uncloud offers an actively developed
|
||||
alternative that keeps the familiar Compose format and drops the manager quorum. You also get secure WireGuard
|
||||
networking across machines, image push without a registry, and automatic reverse proxy management out of the box.
|
||||
- **Hybrid setups (cloud + on-prem)**: Combine cloud VMs with on-premise servers and distribute workloads for cost
|
||||
savings and data sovereignty. For example, keep your database on your own hardware and scale web replicas out to cloud
|
||||
VMs. Manage everything together through the same interface.
|
||||
- **Agencies and freelancers**: Host multiple client projects with proper isolation on shared infrastructure, optimising
|
||||
costs and resources.
|
||||
- **Edge computing**: Deploy applications closer to your users for lower latency and better performance.
|
||||
- **Dev/staging environments**: Spin up additional environments for development and testing that mirror production
|
||||
reusing the same Compose configuration.
|
||||
- **Self-hosting and homelabs**: Run your self-hosted apps on your own hardware. Start with a single machine and add
|
||||
more as your needs grow.
|
||||
- **Dev/staging environments**: Spin up additional environments for development and testing that mirror production,
|
||||
using the same Compose configuration.
|
||||
|
||||
## What makes Uncloud special
|
||||
|
||||
@@ -69,10 +77,10 @@ Talos [KubeSpan](https://www.talos.dev/v1.10/talos-guides/network/kubespan/).
|
||||
|
||||
### Managed DNS service (optional)
|
||||
|
||||
Uncloud can provide **managed DNS records** like `<service-name>.<cluster-id>.uncld.dev` for your public
|
||||
services through free [Uncloud DNS](https://github.com/psviderski/uncloud-dns) service. You can deploy a service and
|
||||
instantly access it from anywhere with a proper DNS name and HTTPS without any manual DNS configuration. This makes
|
||||
self-hosting much more accessible and simplifies the process of adding your own domain later.
|
||||
Uncloud can provide **managed DNS records** like `<service-name>.<cluster-id>.uncld.dev` for your public services
|
||||
through free [Uncloud DNS](https://github.com/psviderski/uncloud-dns) service. You can deploy a service and instantly
|
||||
access it from anywhere with a proper DNS name and HTTPS without any manual DNS configuration. This makes self-hosting
|
||||
much more accessible and simplifies the process of adding your own domain later.
|
||||
|
||||
### No complex orchestration
|
||||
|
||||
|
||||
@@ -99,13 +99,15 @@ Follow the same steps to upgrade to the latest version in the future.
|
||||
## Debian
|
||||
|
||||
On a Debian system, you can install Uncloud CLI from an unofficial
|
||||
[repository](https://debian.griffo.io/) maintained by
|
||||
[repository](https://deb.griffo.io/) maintained by
|
||||
[@dariogriffo](https://github.com/dariogriffo):
|
||||
|
||||
```shell
|
||||
curl -sS https://debian.griffo.io/EA0F721D231FDD3A0A17B9AC7808B4DD62C41256.asc | sudo gpg --dearmor --yes -o /etc/apt/trusted.gpg.d/debian.griffo.io.gpg
|
||||
echo "deb https://debian.griffo.io/apt $(lsb_release -sc 2>/dev/null) main" | sudo tee /etc/apt/sources.list.d/debian.griffo.io.list
|
||||
apt install -y uncloud
|
||||
sudo install -d -m 0755 /etc/apt/keyrings
|
||||
curl -fsSL https://deb.griffo.io/EA0F721D231FDD3A0A17B9AC7808B4DD62C41256.asc | sudo gpg --dearmor --yes -o /etc/apt/keyrings/deb.griffo.io.gpg
|
||||
echo "deb [signed-by=/etc/apt/keyrings/deb.griffo.io.gpg] https://deb.griffo.io/apt $(lsb_release -sc 2>/dev/null) main" | sudo tee /etc/apt/sources.list.d/deb.griffo.io.list
|
||||
sudo apt update
|
||||
sudo apt install -y uncloud
|
||||
```
|
||||
|
||||
Alternatively, you can download `.deb` packages directly from the repository
|
||||
@@ -116,7 +118,7 @@ Alternatively, you can download `.deb` packages directly from the repository
|
||||
After installation, verify that `uc` command is working:
|
||||
|
||||
```shell
|
||||
uc --version
|
||||
uc version
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
# Deploy demo app
|
||||
|
||||
In this guide, we'll deploy [Excalidraw](https://excalidraw.com) — a popular sketching and diagramming tool — to your
|
||||
In this guide, we'll deploy [Excalidraw](https://excalidraw.com), a popular sketching and diagramming tool, to your
|
||||
Linux server. You'll learn the **basics of Uncloud** and see how simple it is to **run web apps** on your own
|
||||
infrastructure with secure internet access.
|
||||
|
||||
:::info NOTE
|
||||
To give you a chance to play with Uncloud without even leaving your browser or needing your own servers, we're providing interactive tutorials and playgrounds on the [iximiuz Labs](https://labs.iximiuz.com/) platform.
|
||||
|
||||
You can follow [this tutorial](https://labs.iximiuz.com/tutorials/uncloud-create-cluster-ebebf72b) which walks you through creating a new cluster with two machines and then deploying a simple web service to it.
|
||||
To give you a chance to play with Uncloud without even leaving your browser or needing your own servers, we're providing
|
||||
interactive tutorials and playgrounds on the [iximiuz Labs](https://labs.iximiuz.com/) platform.
|
||||
|
||||
You can also launch the [Uncloud playground](https://labs.iximiuz.com/playgrounds/uncloud-cluster-64523f7c) where you can play with an already initialized Uncloud cluster.
|
||||
You can follow [this tutorial](https://labs.iximiuz.com/tutorials/uncloud-create-cluster-ebebf72b) which walks you
|
||||
through creating a new cluster with two machines and then deploying a simple web service to it.
|
||||
|
||||
You can also launch the [Uncloud playground](https://labs.iximiuz.com/playgrounds/uncloud-cluster-64523f7c) where you
|
||||
can play with an already initialised Uncloud cluster.
|
||||
:::
|
||||
|
||||
## Prerequisites
|
||||
@@ -53,8 +57,7 @@ This command will:
|
||||
- Install the Uncloud daemon on your server
|
||||
- Create a Docker network for Uncloud-managed containers
|
||||
- Deploy [Caddy](https://caddyserver.com/) as your reverse proxy listening on host ports 80 and 443
|
||||
- Reserve a free `xxxxxx.uncld.dev` subdomain via the Uncloud managed DNS service and point it to your
|
||||
server's IP
|
||||
- Reserve a free `xxxxxx.uncld.dev` subdomain via the Uncloud managed DNS service and point it to your server's IP
|
||||
|
||||
All in about a minute!
|
||||
|
||||
@@ -63,7 +66,6 @@ All in about a minute!
|
||||
|
||||
```
|
||||
$ uc machine init root@157.180.72.195
|
||||
Downloading Uncloud install script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh
|
||||
⏳ Running Uncloud install script...
|
||||
⏳ Installing Docker...
|
||||
# Executing docker install script, commit: 53a22f61c0628e58e1d6680b49e82993d304b449
|
||||
@@ -142,9 +144,6 @@ WARNING: Access to the remote API on a privileged Docker daemon is equivalent
|
||||
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
|
||||
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
|
||||
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
|
||||
⏳ Downloading uncloud-corrosion binary: https://github.com/psviderski/corrosion/releases/latest/download/corrosion-x86_64-unknown-linux-gnu.tar.gz
|
||||
✓ uncloud-corrosion binary installed: /usr/local/bin/uncloud-corrosion
|
||||
✓ Systemd unit file created: /etc/systemd/system/uncloud-corrosion.service
|
||||
⏳ Starting Uncloud machine daemon (uncloud.service)...
|
||||
✓ Uncloud machine daemon started.
|
||||
✓ Uncloud installed on the machine successfully! 🎉
|
||||
@@ -152,17 +151,17 @@ Cluster initialised with machine 'machine-dc3c' and saved as context 'default' i
|
||||
Current cluster context is now 'default'.
|
||||
Waiting for the machine to be ready...
|
||||
|
||||
Reserved cluster domain: 7za6s7.uncld.dev
|
||||
[+] Deploying service caddy 7/2
|
||||
✔ Container caddy-d7uk on machine-dc3c Started 6.1s
|
||||
✔ Image caddy:2.10.0 on machine-dc3c Pulled 3.7s
|
||||
Reserved cluster domain: sh8hsb.uncld.dev
|
||||
[+] Deploying service caddy 2/2
|
||||
✔ Container caddy-d7uk on machine-dc3c Running 11.1s
|
||||
✔ Image caddy:2.11.4 on machine-dc3c Pulled 3.7s
|
||||
|
||||
Updating cluster domain records in Uncloud DNS to point to machines running caddy service...
|
||||
[+] Verifying internet access to caddy service 1/1
|
||||
✔ Machine machine-dc3c (157.180.72.195) Reachable 0.7s
|
||||
|
||||
DNS records updated to use only the internet-reachable machines running caddy service:
|
||||
*.7za6s7.uncld.dev A → 157.180.72.195
|
||||
*.sh8hsb.uncld.dev A → 157.180.72.195
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -181,11 +180,11 @@ You'll see the progress of the deployment and the public URL where you can acces
|
||||
|
||||
```
|
||||
[+] Running service excalidraw (replicated mode) 2/2
|
||||
✔ Container excalidraw-azpc on machine-dc3c Started 8.9s
|
||||
✔ Container excalidraw-azpc on machine-dc3c Healthy 37.1s
|
||||
✔ Image excalidraw/excalidraw on machine-dc3c Pulled 4.7s
|
||||
|
||||
excalidraw endpoints:
|
||||
• https://excalidraw.7za6s7.uncld.dev → :80
|
||||
• https://excalidraw.sh8hsb.uncld.dev → :80
|
||||
```
|
||||
|
||||
## Verify your deployment
|
||||
@@ -197,12 +196,12 @@ uc inspect excalidraw
|
||||
```
|
||||
|
||||
```
|
||||
ID: 4d2de1600b6ada221a03896cd388836c
|
||||
Service ID: 4d2de1600b6ada221a03896cd388836c
|
||||
Name: excalidraw
|
||||
Mode: replicated
|
||||
|
||||
CONTAINER ID IMAGE CREATED STATUS MACHINE
|
||||
fde7ac7f11ad excalidraw/excalidraw About a minute ago Up About a minute (healthy) machine-dc3c
|
||||
CONTAINER ID IMAGE CREATED STATUS IP ADDRESS MACHINE
|
||||
fde7ac7f11ad excalidraw/excalidraw:latest About a minute ago Up About a minute (healthy) 10.210.0.3 machine-dc3c
|
||||
```
|
||||
|
||||
In this example, the service has one container running on the machine `machine-dc3c` (our server). The container is up
|
||||
@@ -215,9 +214,9 @@ uc ls
|
||||
```
|
||||
|
||||
```
|
||||
NAME MODE REPLICAS ENDPOINTS
|
||||
caddy global 1
|
||||
excalidraw replicated 1 https://excalidraw.7za6s7.uncld.dev → :80
|
||||
NAME MODE REPLICAS IMAGE ENDPOINTS
|
||||
caddy global 1 caddy:2.11.4
|
||||
excalidraw replicated 1 excalidraw/excalidraw:latest https://excalidraw.sh8hsb.uncld.dev → :80
|
||||
```
|
||||
|
||||
You can see `caddy` service listed here. That's your reverse proxy, running as a regular Uncloud service.
|
||||
@@ -227,7 +226,7 @@ You can see `caddy` service listed here. That's your reverse proxy, running as a
|
||||
Open your browser and navigate to the URL shown in the endpoints. It may take a moment for Caddy to obtain a TLS
|
||||
certificate from Let's Encrypt. If it doesn't load immediately, wait a few seconds and try again.
|
||||
|
||||

|
||||

|
||||
|
||||
You now have:
|
||||
|
||||
@@ -235,6 +234,25 @@ You now have:
|
||||
- A **public URL** with **automatic HTTPS** you can share with your team and friends
|
||||
- **Full control over your data** — no analytics or tracking
|
||||
|
||||
## View service logs
|
||||
|
||||
Want to see what's happening inside your service? Use the `uc logs` command to view logs from the service container:
|
||||
|
||||
```shell
|
||||
uc logs excalidraw
|
||||
```
|
||||
|
||||
```
|
||||
Jul 14 10:53:05.910 machine-dc3c excalidraw/fde7a ::1 - - [14/Jul/2026:00:53:05 +0000] "GET / HTTP/1.1" 200 6843 "-" "Wget" "-"
|
||||
Jul 14 10:53:31.456 machine-dc3c excalidraw/fde7a 10.210.0.2 - - [14/Jul/2026:00:53:31 +0000] "GET /sw.js HTTP/1.1" 200 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0"
|
||||
```
|
||||
|
||||
Add the `-f` flag to stream new logs in real-time. Press `Ctrl+C` to stop:
|
||||
|
||||
```shell
|
||||
uc logs excalidraw -f
|
||||
```
|
||||
|
||||
## Convert to Docker Compose format
|
||||
|
||||
Uncloud supports the [Compose file format](https://docs.docker.com/reference/compose-file/) for defining services. This
|
||||
@@ -253,9 +271,9 @@ services:
|
||||
|
||||
:::info note
|
||||
|
||||
The `x-ports` key is an Uncloud-specific extension to the Compose file format. It allows you to specify ports that
|
||||
should be published as HTTP(S) endpoints. Uncloud automatically configures the reverse proxy (Caddy) to route traffic to
|
||||
these ports.
|
||||
The [`x-ports`](../8-compose-file-reference/2-extensions.md#x-ports) key is an Uncloud-specific extension to the Compose
|
||||
file format. It allows you to specify ports that should be published as HTTP(S) endpoints. Uncloud automatically
|
||||
configures the reverse proxy (Caddy) to route traffic to these ports.
|
||||
|
||||
:::
|
||||
|
||||
@@ -274,26 +292,28 @@ We've successfully converted our deployment created with `uc run` to a Compose f
|
||||
|
||||
## Use your own domain
|
||||
|
||||
Want to use your own domain, for example, `excalidraw.example.com` instead of `excalidraw.7za6s7.uncld.dev`?
|
||||
Want to use your own domain, for example, `excalidraw.example.com` instead of `excalidraw.sh8hsb.uncld.dev`?
|
||||
|
||||
Add a CNAME record `excalidraw.example.com` in your DNS provider (Cloudflare, Namecheap, etc.) pointing to
|
||||
`excalidraw.7za6s7.uncld.dev`. Alternatively, you can add an A record pointing to your server's IP.
|
||||
`excalidraw.sh8hsb.uncld.dev`. Alternatively, you can add an A record pointing to your server's IP.
|
||||
|
||||
:::info note
|
||||
|
||||
These instructions set up your own domain **in addition to** the Uncloud managed DNS name
|
||||
`excalidraw.7za6s7.uncld.dev`.
|
||||
`excalidraw.sh8hsb.uncld.dev`.
|
||||
|
||||
If you want to avoid the managed service altogether, add `--no-dns` to your `uc machine init` command, and point an A
|
||||
DNS record to your server(s)'s IP(s).
|
||||
DNS record to your servers' IPs.
|
||||
|
||||
:::
|
||||
|
||||
Then update the published port `80/https` in `compose.yaml` to use your domain:
|
||||
|
||||
```yaml title="compose.yaml"
|
||||
...
|
||||
x-ports:
|
||||
services:
|
||||
excalidraw:
|
||||
image: excalidraw/excalidraw
|
||||
x-ports:
|
||||
- excalidraw.example.com:80/https
|
||||
```
|
||||
|
||||
@@ -304,25 +324,32 @@ uc deploy
|
||||
```
|
||||
|
||||
```
|
||||
Deployment plan:
|
||||
- Deploy service [name=excalidraw]
|
||||
- machine-dc3c: Run container [image=excalidraw/excalidraw]
|
||||
- machine-dc3c: Remove container [name=excalidraw-azpc]
|
||||
Deployment plan
|
||||
|
||||
Do you want to continue?
|
||||
context: default
|
||||
|
||||
Choose [y/N]: y
|
||||
Chose: Yes!
|
||||
~ update service excalidraw
|
||||
│ image: excalidraw/excalidraw:latest
|
||||
│
|
||||
╰── +/- replace container excalidraw/fde7ac7f11ad on machine-dc3c
|
||||
|
||||
[+] Deploying services 2/2
|
||||
✔ Container excalidraw-0z12 on machine-dc3c Started 3.5s
|
||||
✔ Container excalidraw-azpc on machine-dc3c Removed 3.4s
|
||||
──────────────────────────────────────────
|
||||
1 replace (start-first) · across 1 machine
|
||||
|
||||
Proceed with deployment to default? [y/N] y
|
||||
|
||||
[+] Deploying to default 2/2
|
||||
✔ Container excalidraw-0z12 on machine-dc3c Healthy 30.6s
|
||||
✔ Container excalidraw/fde7ac7f11ad on machine-dc3c Removed 0.4s
|
||||
```
|
||||
|
||||
Notice how Uncloud performed a **zero-downtime deployment** — it started the new container with the updated
|
||||
configuration before removing the old one. Your service stayed available throughout the update.
|
||||
Uncloud prints a deployment plan and asks for confirmation before making any changes. The plan says it will replace the
|
||||
running container with a new one using the
|
||||
[`start-first` order](../4-guides/1-deployments/4-rolling-deployments.md#update-order). This means Uncloud starts the
|
||||
new container with the updated configuration, waits for it to become healthy, and only then removes the old one. Your
|
||||
service stays available throughout the update. That's a **zero-downtime deployment**.
|
||||
|
||||
Give it a moment for Caddy to obtain a TLS certificate, then visit https://excalidraw.example.com.
|
||||
Give it a moment for Caddy to obtain a TLS certificate, then visit https://excalidraw.example.com (use your own domain).
|
||||
|
||||
## Clean up
|
||||
|
||||
@@ -355,7 +382,7 @@ This command will:
|
||||
<summary>💡 Expand to see example output</summary>
|
||||
|
||||
```
|
||||
⚠️This script will uninstall Uncloud and remove ALL Uncloud managed containers on this machine.
|
||||
⚠️ This script will uninstall Uncloud and remove ALL Uncloud managed containers on this machine.
|
||||
The following actions will be performed:
|
||||
- Remove Uncloud systemd services
|
||||
- Remove Uncloud binaries and data
|
||||
@@ -365,68 +392,73 @@ The following actions will be performed:
|
||||
- Remove Uncloud WireGuard interface
|
||||
Do you want to proceed with uninstallation? [y/N] y
|
||||
⏳ Stopping systemd services...
|
||||
Removed "/etc/systemd/system/multi-user.target.wants/uncloud.service".
|
||||
The unit files have no installation config (WantedBy=, RequiredBy=, UpheldBy=,
|
||||
Also=, or Alias= settings in the [Install] section, and DefaultInstance= for
|
||||
template units). This means they are not meant to be enabled or disabled using systemctl.
|
||||
|
||||
Possible reasons for having these kinds of units are:
|
||||
• A unit may be statically enabled by being symlinked from another unit's
|
||||
.wants/, .requires/, or .upholds/ directory.
|
||||
• A unit's purpose may be to act as a helper for some other unit which has
|
||||
a requirement dependency on it.
|
||||
• A unit may be started when needed via activation (socket, path, timer,
|
||||
D-Bus, udev, scripted systemctl call, ...).
|
||||
• In case of template units, the unit is meant to be enabled with some
|
||||
instance name specified.
|
||||
Removed /etc/systemd/system/multi-user.target.wants/uncloud.service.
|
||||
✓ Systemd services stopped.
|
||||
⏳ Removing systemd service files...
|
||||
removed '/etc/systemd/system/uncloud.service'
|
||||
removed '/etc/systemd/system/uncloud-corrosion.service'
|
||||
✓ Systemd service files removed.
|
||||
⏳ Removing binaries...
|
||||
removed '/usr/local/bin/uncloudd'
|
||||
removed '/usr/local/bin/uncloud-corrosion'
|
||||
✓ Binaries removed.
|
||||
⏳ Removing uncloudd-managed corrosion Docker container...
|
||||
uncloud-corrosion
|
||||
✓ uncloud-corrosion container removed.
|
||||
⏳ Removing data and run directories...
|
||||
removed '/var/lib/uncloud/machine.db-wal'
|
||||
removed '/var/lib/uncloud/caddy/caddy/autosave.json'
|
||||
removed directory '/var/lib/uncloud/caddy/caddy'
|
||||
removed '/var/lib/uncloud/caddy/caddy.json'
|
||||
removed directory '/var/lib/uncloud/caddy'
|
||||
removed '/var/lib/uncloud/machine.json'
|
||||
removed '/var/lib/uncloud/machine.db-shm'
|
||||
removed '/var/lib/uncloud/corrosion/admin.sock'
|
||||
removed '/var/lib/uncloud/corrosion/config.toml'
|
||||
removed '/var/lib/uncloud/corrosion/subscriptions/b4e825113f1143e5b27715b62193a9f8/sub.sqlite-wal'
|
||||
removed '/var/lib/uncloud/corrosion/subscriptions/b4e825113f1143e5b27715b62193a9f8/sub.sqlite-shm'
|
||||
removed '/var/lib/uncloud/corrosion/subscriptions/b4e825113f1143e5b27715b62193a9f8/sub.sqlite'
|
||||
removed directory '/var/lib/uncloud/corrosion/subscriptions/b4e825113f1143e5b27715b62193a9f8'
|
||||
removed '/var/lib/uncloud/corrosion/subscriptions/5e04cbb20a2743c382cfbd4949922351/sub.sqlite'
|
||||
removed directory '/var/lib/uncloud/corrosion/subscriptions/5e04cbb20a2743c382cfbd4949922351'
|
||||
removed directory '/var/lib/uncloud/corrosion/subscriptions'
|
||||
removed '/var/lib/uncloud/corrosion/schema.sql'
|
||||
removed '/var/lib/uncloud/corrosion/store.db'
|
||||
removed directory '/var/lib/uncloud/corrosion'
|
||||
removed '/var/lib/uncloud/machine.db'
|
||||
removed '/var/lib/uncloud/corrosion/store.db'
|
||||
removed '/var/lib/uncloud/corrosion/subscriptions/754e24df40f8476389cf6dbfa7b542c8/sub.sqlite'
|
||||
removed directory '/var/lib/uncloud/corrosion/subscriptions/754e24df40f8476389cf6dbfa7b542c8'
|
||||
removed '/var/lib/uncloud/corrosion/subscriptions/125e6ada8eec4f3cad192e1890db55c2/sub.sqlite'
|
||||
removed directory '/var/lib/uncloud/corrosion/subscriptions/125e6ada8eec4f3cad192e1890db55c2'
|
||||
removed directory '/var/lib/uncloud/corrosion/subscriptions'
|
||||
removed '/var/lib/uncloud/corrosion/config.toml'
|
||||
removed '/var/lib/uncloud/corrosion/schema.sql'
|
||||
removed directory '/var/lib/uncloud/corrosion'
|
||||
removed '/var/lib/uncloud/machine.json'
|
||||
removed '/var/lib/uncloud/caddy/caddy.json'
|
||||
removed '/var/lib/uncloud/caddy/caddy/autosave.json'
|
||||
removed '/var/lib/uncloud/caddy/caddy/last_clean.json'
|
||||
removed directory '/var/lib/uncloud/caddy/caddy/locks'
|
||||
removed '/var/lib/uncloud/caddy/caddy/instance.uuid'
|
||||
removed '/var/lib/uncloud/caddy/caddy/acme/acme-staging-v02.api.letsencrypt.org-directory/users/default/default.json'
|
||||
removed '/var/lib/uncloud/caddy/caddy/acme/acme-staging-v02.api.letsencrypt.org-directory/users/default/default.key'
|
||||
removed directory '/var/lib/uncloud/caddy/caddy/acme/acme-staging-v02.api.letsencrypt.org-directory/users/default'
|
||||
removed directory '/var/lib/uncloud/caddy/caddy/acme/acme-staging-v02.api.letsencrypt.org-directory/users'
|
||||
removed directory '/var/lib/uncloud/caddy/caddy/acme/acme-staging-v02.api.letsencrypt.org-directory'
|
||||
removed '/var/lib/uncloud/caddy/caddy/acme/acme-v02.api.letsencrypt.org-directory/users/default/default.json'
|
||||
removed '/var/lib/uncloud/caddy/caddy/acme/acme-v02.api.letsencrypt.org-directory/users/default/default.key'
|
||||
removed directory '/var/lib/uncloud/caddy/caddy/acme/acme-v02.api.letsencrypt.org-directory/users/default'
|
||||
removed directory '/var/lib/uncloud/caddy/caddy/acme/acme-v02.api.letsencrypt.org-directory/users'
|
||||
removed directory '/var/lib/uncloud/caddy/caddy/acme/acme-v02.api.letsencrypt.org-directory/challenge_tokens'
|
||||
removed directory '/var/lib/uncloud/caddy/caddy/acme/acme-v02.api.letsencrypt.org-directory'
|
||||
removed directory '/var/lib/uncloud/caddy/caddy/acme'
|
||||
removed '/var/lib/uncloud/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/excalidraw.sh8hsb.uncld.dev/excalidraw.sh8hsb.uncld.dev.key'
|
||||
removed '/var/lib/uncloud/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/excalidraw.sh8hsb.uncld.dev/excalidraw.sh8hsb.uncld.dev.crt'
|
||||
removed '/var/lib/uncloud/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/excalidraw.sh8hsb.uncld.dev/excalidraw.sh8hsb.uncld.dev.json'
|
||||
removed directory '/var/lib/uncloud/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/excalidraw.sh8hsb.uncld.dev'
|
||||
removed directory '/var/lib/uncloud/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory'
|
||||
removed directory '/var/lib/uncloud/caddy/caddy/certificates'
|
||||
removed directory '/var/lib/uncloud/caddy/caddy'
|
||||
removed '/var/lib/uncloud/caddy/Caddyfile'
|
||||
removed directory '/var/lib/uncloud/caddy'
|
||||
removed '/var/lib/uncloud/machine.db-wal'
|
||||
removed directory '/var/lib/uncloud'
|
||||
removed '/run/uncloud/caddy/admin.sock'
|
||||
removed directory '/run/uncloud/caddy'
|
||||
removed '/run/uncloud/corrosion/admin.sock'
|
||||
removed directory '/run/uncloud/corrosion'
|
||||
removed directory '/run/uncloud'
|
||||
✓ Data and run directories removed.
|
||||
⏳ Removing Linux user and group...
|
||||
✓ Linux user 'uncloud' removed.
|
||||
Linux group 'uncloud' does not exist or was already removed.
|
||||
⏳ Looking for Docker containers and network created by Uncloud...
|
||||
Found 4 Uncloud managed containers.
|
||||
Found 1 Uncloud managed containers.
|
||||
⏳ Stopping Uncloud managed containers...
|
||||
20613f6046d0
|
||||
1f1a65b78e93
|
||||
4300bde4a2b0
|
||||
053fdd57ec56
|
||||
b2eb9968e468
|
||||
⏳ Removing Uncloud managed containers...
|
||||
20613f6046d0
|
||||
1f1a65b78e93
|
||||
4300bde4a2b0
|
||||
053fdd57ec56
|
||||
b2eb9968e468
|
||||
✓ Uncloud managed containers stopped and removed.
|
||||
⏳ Removing Docker network uncloud...
|
||||
uncloud
|
||||
|
||||
|
Before Width: | Height: | Size: 389 KiB |
@@ -1,6 +1,6 @@
|
||||
# Connecting to a cluster
|
||||
|
||||
`uc` only needs to reach one machine to work with the entire cluster. That machine acts as an **entry point** and
|
||||
`uc` only needs to reach **one machine** to work with the entire cluster. That machine acts as an **entry point** and
|
||||
forwards requests to other machines as needed.
|
||||
|
||||
`uc` stores **cluster contexts** and **connection details** in a [configuration file](../../7-cli-config-reference.md)
|
||||
@@ -46,6 +46,27 @@ When you run a `uc` command, it determines which cluster to connect to using thi
|
||||
Once the context is resolved, `uc` tries each connection in the context's `connections` list in order until one
|
||||
succeeds.
|
||||
|
||||
## User permissions on the machine
|
||||
|
||||
When `uc` connects to a machine over SSH, it communicates with the Uncloud daemon through the Unix socket
|
||||
`/run/uncloud/uncloud.sock` on that machine. The daemon restricts access to the socket to the `root` user and members
|
||||
of the `uncloud` Linux group. This means your SSH user must be either `root` or a member of the `uncloud` group.
|
||||
|
||||
In most cases you don't need to set this up manually. When you initialise or add a machine with a non-root user,
|
||||
`uc machine init` and `uc machine add` automatically add that user to the `uncloud` group during installation.
|
||||
|
||||
If you want to connect with a different non-root user later, add them to the group on the machine:
|
||||
|
||||
```shell
|
||||
sudo usermod -aG uncloud <username>
|
||||
```
|
||||
|
||||
The group change only applies to new SSH sessions. If `uc` still fails with a permission denied error after adding the
|
||||
user, close any long-running SSH connections to the machine (for example, SSH ControlMaster sessions) and try again.
|
||||
|
||||
The same requirement applies when running `uc` locally on a cluster machine with a `unix://` connection. The local user
|
||||
must be `root` or a member of the `uncloud` group.
|
||||
|
||||
## Global flags and environment variables
|
||||
|
||||
These flags are available on every `uc` command. They can also be set with an environment variable. The flag takes
|
||||
|
||||
@@ -17,7 +17,7 @@ uc run -p app.example.com:8000/https app:latest
|
||||
|
||||
```
|
||||
[+] Running service app-mwng (replicated mode) 1/1
|
||||
✔ Container app-mwng-6lub on machine-fnr9 Started
|
||||
✔ Container app-mwng-6lub on machine-fnr9 Running
|
||||
|
||||
app-mwng endpoints:
|
||||
• https://app.example.com → :8000
|
||||
|
||||
@@ -6,6 +6,9 @@ Add a remote machine to a cluster.
|
||||
|
||||
Add a new machine to an existing Uncloud cluster.
|
||||
|
||||
By default, it installs Docker and the Uncloud daemon on the machine over SSH unless --no-install
|
||||
is specified, which assumes they are already installed and running.
|
||||
|
||||
Connection methods:
|
||||
[ssh://]user@host - Use system 'ssh' command with full SSH config support (default, no prefix required)
|
||||
ssh+go://user@host - Use Go's built-in SSH library
|
||||
@@ -20,10 +23,10 @@ uc machine add [USER@]HOST[:PORT] [flags]
|
||||
-h, --help help for add
|
||||
-n, --name string Assign a name to the machine. (default is the machine's hostname)
|
||||
--no-caddy Don't deploy Caddy reverse proxy service to the machine.
|
||||
--no-install Skip installation of Docker, Uncloud daemon, and dependencies on the machine. Assumes they're already installed and running.
|
||||
--no-install Skip installation of Docker and the Uncloud daemon on the machine. Assumes they're already installed and running.
|
||||
--public-ip string Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, blank '' or 'none' to disable ingress on this machine, or specify an IP address. (default "auto")
|
||||
-i, --ssh-key string Path to SSH private key for remote login (if not already added to SSH agent). (default "~/.ssh/id_ed25519")
|
||||
--version string Version of the Uncloud daemon to install on the machine. (default "latest")
|
||||
--version string Version of the Uncloud daemon to install on the machine. [$UNCLOUD_DAEMON_VERSION] (default "latest")
|
||||
--wg-endpoint strings WireGuard endpoint address that other machines in the cluster should use to establish WireGuard connections
|
||||
to this machine. This doesn't change the address/port WireGuard listens on the machine.
|
||||
Format: IP, IP:PORT, IPv6, or [IPv6]:PORT. Default port is the value of --wg-port if omitted.
|
||||
|
||||
@@ -7,6 +7,9 @@ Initialise a new cluster with a remote machine as the first member.
|
||||
Initialise a new cluster by setting up a remote machine as the first member.
|
||||
This command creates a new context in your Uncloud config to manage the cluster.
|
||||
|
||||
By default, it installs Docker and the Uncloud daemon on the machine over SSH unless --no-install
|
||||
is specified, which assumes they are already installed and running.
|
||||
|
||||
Connection methods:
|
||||
[ssh://]user@host - Use system 'ssh' command with full SSH config support (default, no prefix required)
|
||||
ssh+go://user@host - Use Go's built-in SSH library
|
||||
@@ -42,10 +45,10 @@ uc machine init [schema://]USER@HOST[:PORT] [flags]
|
||||
--network string IPv4 network CIDR to use for machines and services. (default "10.210.0.0/16")
|
||||
--no-caddy Don't deploy Caddy reverse proxy service to the machine. You can deploy it later with 'uc caddy deploy'.
|
||||
--no-dns Don't reserve a cluster domain in Uncloud DNS. You can reserve it later with 'uc dns reserve'.
|
||||
--no-install Skip installation of Docker, Uncloud daemon, and dependencies on the machine. Assumes they're already installed and running.
|
||||
--no-install Skip installation of Docker and the Uncloud daemon on the machine. Assumes they're already installed and running.
|
||||
--public-ip string Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, blank '' or 'none' to disable ingress on this machine, or specify an IP address. (default "auto")
|
||||
-i, --ssh-key string Path to SSH private key for remote login (if not already added to SSH agent). (default "~/.ssh/id_ed25519")
|
||||
--version string Version of the Uncloud daemon to install on the machine. (default "latest")
|
||||
--version string Version of the Uncloud daemon to install on the machine. [$UNCLOUD_DAEMON_VERSION] (default "latest")
|
||||
--wg-endpoint strings WireGuard endpoint address that other machines in the cluster should use to establish WireGuard connections
|
||||
to this machine. This doesn't change the address/port WireGuard listens on the machine.
|
||||
Format: IP, IP:PORT, IPv6, or [IPv6]:PORT. Default port is the value of --wg-port if omitted.
|
||||
|
||||
@@ -12,7 +12,19 @@ import {themes as prismThemes} from 'prism-react-renderer';
|
||||
const config = {
|
||||
title: 'Uncloud',
|
||||
tagline: 'Self-host and scale web apps without Kubernetes complexity',
|
||||
favicon: 'img/favicon.png',
|
||||
// Use the SVG logo as the primary favicon with a PNG fallback to match the landing pages.
|
||||
favicon: 'img/logo.svg',
|
||||
headTags: [
|
||||
{
|
||||
tagName: 'link',
|
||||
attributes: {
|
||||
rel: 'alternate icon',
|
||||
type: 'image/png',
|
||||
href: '/img/favicon.png',
|
||||
sizes: '96x96',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// Set the production url of your site here
|
||||
url: 'https://uncloud.run',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Inter+Tight:ital,wght@0,500;0,600;0,700;1,700&display=fallback');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Inter+Tight:ital,wght@0,500;0,600;0,700;1,700&family=Caveat:wght@600&display=fallback');
|
||||
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@@ -13,3 +13,47 @@
|
||||
[x-cloak=""] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Smooth in-page anchor scrolling, disabled for users who prefer reduced motion. */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
/* Handwritten margin notes. */
|
||||
.hw-note {
|
||||
font-family: 'Caveat', cursive;
|
||||
font-weight: 600;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* A hand-swiped stroke with uneven rounded edges. */
|
||||
.marker-red {
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
color: #fff;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.marker-red::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset: 0.08em -0.18em -0.02em -0.14em;
|
||||
background: linear-gradient(100deg,
|
||||
rgba(220, 38, 38, 0.82) 0%,
|
||||
rgba(239, 68, 68, 0.95) 28%,
|
||||
rgba(225, 29, 72, 0.9) 72%,
|
||||
rgba(220, 38, 38, 0.88) 100%);
|
||||
border-radius: 0.3em 0.5em 0.4em 0.6em;
|
||||
transform: skew(-10deg) rotate(-0.5deg);
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 875 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -3,22 +3,22 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Self-host and scale web apps without Kubernetes complexity | Uncloud</title>
|
||||
<title>Uncloud | Multi-node Docker Compose for production</title>
|
||||
<meta name="description"
|
||||
content="Take your Docker Compose apps to production with zero-downtime deployments, automatic HTTPS, and cross-machine scaling. Self-hosting made reliable without the complexity.">
|
||||
content="Deploy and scale Docker Compose apps across cloud VMs and your own servers. A simpler alternative to Kubernetes, Swarm, Nomad, and Kamal.">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link rel="canonical" href="https://uncloud.run">
|
||||
<link href="./style.css" rel="stylesheet">
|
||||
<link rel="apple-touch-icon" href="./images/apple-touch-icon.png" sizes="180x180"/>
|
||||
<link rel="icon" type="image/svg+xml" href="./images/logo.svg"/>
|
||||
<link rel="alternate icon" type="image/png" href="./images/favicon.png" sizes="96x96"/>
|
||||
|
||||
<!-- OpenGraph tags for social media -->
|
||||
<meta property="og:title"
|
||||
content="Self-host and scale web apps without Kubernetes complexity | Uncloud"/>
|
||||
<meta property="og:title" content="Multi-node Docker Compose for production"/>
|
||||
<meta property="og:description"
|
||||
content="Take your Docker Compose apps to production with zero-downtime deployments, automatic HTTPS, and cross-machine scaling. Self-hosting made reliable without the complexity."/>
|
||||
<meta property="og:image" content="https://uncloud.run/images/logo-wide.png"/>
|
||||
<meta property="og:image:alt" content="Uncloud logo - Self-host web apps with ease"/>
|
||||
content="Deploy and scale Docker Compose apps across cloud VMs and your own servers. A simpler alternative to Kubernetes, Swarm, Nomad, and Kamal."/>
|
||||
<meta property="og:image" content="https://uncloud.run/images/og.png"/>
|
||||
<meta property="og:image:alt" content="Uncloud: multi-node Docker Compose for production"/>
|
||||
<meta property="og:url" content="https://uncloud.run"/>
|
||||
<meta property="og:type" content="website"/>
|
||||
<meta property="og:site_name" content="Uncloud"/>
|
||||
@@ -26,23 +26,13 @@
|
||||
<meta property="twitter:domain" content="uncloud.run">
|
||||
<meta property="twitter:url" content="https://uncloud.run">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title"
|
||||
content="Self-host and scale web apps without Kubernetes complexity | Uncloud">
|
||||
<meta name="twitter:title" content="Multi-node Docker Compose for production">
|
||||
<meta name="twitter:description"
|
||||
content="Take your Docker Compose apps to production with zero-downtime deployments, automatic HTTPS, and cross-machine scaling. Self-hosting made reliable without the complexity.">
|
||||
<meta name="twitter:image" content="https://uncloud.run/images/logo-wide.png">
|
||||
<meta name="twitter:image:alt" content="Uncloud logo - Self-host web apps with ease">
|
||||
<style>
|
||||
/* Prevent all inline SVGs from appearing full screen */
|
||||
svg {
|
||||
max-width: 24px;
|
||||
max-height: 24px;
|
||||
}
|
||||
content="Deploy and scale Docker Compose apps across cloud VMs and your own servers. A simpler alternative to Kubernetes, Swarm, Nomad, and Kamal.">
|
||||
<meta name="twitter:image" content="https://uncloud.run/images/og.png">
|
||||
<meta name="twitter:image:alt" content="Uncloud: multi-node Docker Compose for production">
|
||||
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
</style>
|
||||
<script src="./js/posthog.js" defer></script>
|
||||
</head>
|
||||
|
||||
<body class="font-inter antialiased bg-white text-zinc-900 tracking-tight">
|
||||
@@ -67,6 +57,15 @@
|
||||
<nav class="flex grow">
|
||||
<!-- Left side navigation -->
|
||||
<ul class="flex items-center">
|
||||
<li>
|
||||
<a class="text-sm font-medium text-zinc-500 hover:text-zinc-900 px-2 sm:px-5 py-2 transition inline-flex items-center"
|
||||
href="/hub">
|
||||
Hub
|
||||
<span class="inline-block ml-1 -translate-y-1.5 text-[10px] font-semibold uppercase tracking-wide text-violet-700 bg-violet-100 px-1.5 py-0.5 rounded">
|
||||
New
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="text-sm font-medium text-zinc-500 hover:text-zinc-900 px-2 sm:px-5 py-2 transition"
|
||||
href="/docs">Docs</a>
|
||||
@@ -75,7 +74,8 @@
|
||||
<a class="text-sm font-medium text-zinc-500 hover:text-zinc-900 px-2 sm:px-5 py-2 transition"
|
||||
href="/blog">Blog</a>
|
||||
</li>
|
||||
<li>
|
||||
<!-- Hidden on small screens to keep the nav from overflowing at 390px. -->
|
||||
<li class="hidden md:block">
|
||||
<a class="text-sm font-medium text-zinc-500 hover:text-zinc-900 px-2 sm:px-5 py-2 transition"
|
||||
href="https://psviderski.substack.com/" target="_blank">Newsletter</a>
|
||||
</li>
|
||||
@@ -140,7 +140,7 @@
|
||||
<div class="max-w-xs mx-auto sm:max-w-none sm:inline-flex sm:justify-center space-y-4 sm:space-y-0 sm:space-x-4">
|
||||
<div>
|
||||
<a class="btn text-zinc-100 bg-zinc-900 hover:bg-zinc-800 w-full shadow flex items-center justify-center"
|
||||
href="https://github.com/psviderski/uncloud/?tab=readme-ov-file#-quick-start">
|
||||
href="/docs/getting-started/deploy-demo-app">
|
||||
<svg class="w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
@@ -213,7 +213,7 @@
|
||||
<a href="https://uncloud.run/discord" target="_blank" rel="noopener"
|
||||
class="group block relative text-center md:px-5 after:hidden md:after:block after:absolute after:right-0 after:top-1/2 after:-translate-y-1/2 after:w-px after:h-8 after:border-l after:border-zinc-300 after:border-dashed last:after:hidden">
|
||||
<h4 class="font-inter-tight text-2xl md:text-3xl font-bold tabular-nums mb-2"><span
|
||||
x-data="counter(380)" x-text="counterValue">380</span></h4>
|
||||
x-data="counter(390)" x-text="counterValue">390</span>+</h4>
|
||||
<p class="text-zinc-500 transition-colors group-hover:text-zinc-800">Fans on
|
||||
Discord</p>
|
||||
</a>
|
||||
@@ -714,11 +714,47 @@ prod-3 201.45.91.123:51820 1m56s ago 11ms 5.12MB 9.34MB
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Uncloud Hub -->
|
||||
<section class="relative bg-violet-50 border-y border-violet-100">
|
||||
<div class="py-12 md:py-20">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="grid lg:grid-cols-2 gap-10 lg:gap-16 items-center">
|
||||
<div class="lg:max-w-md lg:col-start-2 lg:row-start-1">
|
||||
<div class="font-mono text-xs font-medium uppercase tracking-[0.18em] text-violet-600 mb-4">
|
||||
Coming soon
|
||||
</div>
|
||||
<h2 class="font-inter-tight text-3xl md:text-4xl font-bold text-zinc-900 mb-4">
|
||||
<span class="text-violet-600">Uncloud Hub</span>: see what's happening in
|
||||
production
|
||||
</h2>
|
||||
<p class="text-lg text-zinc-500 mb-6">
|
||||
A hosted dashboard and observability stack for your Uncloud clusters. Connect a
|
||||
cluster and get metrics, logs, and alerts for every service and machine, without
|
||||
building and monitoring the monitoring yourself.
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center gap-x-6 gap-y-4">
|
||||
<a class="btn text-zinc-100 bg-zinc-900 hover:bg-zinc-800 shadow"
|
||||
href="/hub">Learn about Hub</a>
|
||||
<a class="text-sm font-medium text-violet-700 hover:text-violet-900 transition"
|
||||
href="/hub#early-access">Get early access →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-start-1 lg:row-start-1 rounded-xl overflow-hidden border border-zinc-200 shadow-2xl shadow-zinc-950/10">
|
||||
<img src="./images/hub-hero.webp" width="2560" height="1520"
|
||||
alt="Uncloud Hub cluster overview dashboard, design preview"
|
||||
class="w-full block" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FAQ -->
|
||||
<section id="faq" class="relative bg-zinc-50">
|
||||
<div class="py-12 md:py-20">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="text-center pb-8 md:pb-12">
|
||||
<h2 class="font-inter-tight text-3xl md:text-4xl font-bold text-zinc-900 mb-4">
|
||||
Frequently asked questions
|
||||
@@ -731,17 +767,17 @@ prod-3 201.45.91.123:51820 1m56s ago 11ms 5.12MB 9.34MB
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-zinc-200 border-y border-zinc-200">
|
||||
<details class="group py-5">
|
||||
<details class="group">
|
||||
<summary
|
||||
class="flex items-center justify-between gap-4 cursor-pointer list-none [&::-webkit-details-marker]:hidden font-inter-tight text-lg font-semibold text-zinc-900">
|
||||
class="flex items-center justify-between gap-4 py-5 cursor-pointer list-none [&::-webkit-details-marker]:hidden font-inter-tight font-semibold text-zinc-900">
|
||||
<span>How is this different from Docker Swarm?</span>
|
||||
<svg class="w-4 h-4 shrink-0 text-zinc-400 transition-transform duration-200 group-open:rotate-45"
|
||||
<svg class="w-5 h-5 shrink-0 text-zinc-400 transition-transform duration-200 group-open:rotate-45"
|
||||
fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"
|
||||
aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14m-7-7h14"/>
|
||||
</svg>
|
||||
</summary>
|
||||
<p class="mt-3 text-zinc-500">
|
||||
<p class="text-zinc-500 leading-relaxed pb-5">
|
||||
Swarm needs a Raft quorum of manager nodes and has been in maintenance mode for
|
||||
years. Uncloud has no control plane at all. Machines sync state peer to peer, and
|
||||
any subset keeps working. You also get WireGuard networking across clouds and even
|
||||
@@ -750,17 +786,17 @@ prod-3 201.45.91.123:51820 1m56s ago 11ms 5.12MB 9.34MB
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details class="group py-5">
|
||||
<details class="group">
|
||||
<summary
|
||||
class="flex items-center justify-between gap-4 cursor-pointer list-none [&::-webkit-details-marker]:hidden font-inter-tight text-lg font-semibold text-zinc-900">
|
||||
class="flex items-center justify-between gap-4 py-5 cursor-pointer list-none [&::-webkit-details-marker]:hidden font-inter-tight font-semibold text-zinc-900">
|
||||
<span>What happens when a machine goes down?</span>
|
||||
<svg class="w-4 h-4 shrink-0 text-zinc-400 transition-transform duration-200 group-open:rotate-45"
|
||||
<svg class="w-5 h-5 shrink-0 text-zinc-400 transition-transform duration-200 group-open:rotate-45"
|
||||
fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"
|
||||
aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14m-7-7h14"/>
|
||||
</svg>
|
||||
</summary>
|
||||
<p class="mt-3 text-zinc-500">
|
||||
<p class="text-zinc-500 leading-relaxed pb-5">
|
||||
The rest of the cluster keeps working. There's no control plane to lose, and
|
||||
healthy replicas on other machines keep serving traffic. Uncloud deliberately
|
||||
doesn't auto-reschedule containers. You decide when to replace the machine and
|
||||
@@ -768,23 +804,66 @@ prod-3 201.45.91.123:51820 1m56s ago 11ms 5.12MB 9.34MB
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details class="group py-5">
|
||||
<details class="group">
|
||||
<summary
|
||||
class="flex items-center justify-between gap-4 cursor-pointer list-none [&::-webkit-details-marker]:hidden font-inter-tight text-lg font-semibold text-zinc-900">
|
||||
class="flex items-center justify-between gap-4 py-5 cursor-pointer list-none [&::-webkit-details-marker]:hidden font-inter-tight font-semibold text-zinc-900">
|
||||
<span>Will it replace my CI/CD?</span>
|
||||
<svg class="w-4 h-4 shrink-0 text-zinc-400 transition-transform duration-200 group-open:rotate-45"
|
||||
<svg class="w-5 h-5 shrink-0 text-zinc-400 transition-transform duration-200 group-open:rotate-45"
|
||||
fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"
|
||||
aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14m-7-7h14"/>
|
||||
</svg>
|
||||
</summary>
|
||||
<p class="mt-3 text-zinc-500">
|
||||
<p class="text-zinc-500 leading-relaxed pb-5">
|
||||
No, and that's deliberate. <code>uc</code>
|
||||
is just a CLI that connects to your cluster over SSH. Run <code>uc deploy</code>
|
||||
locally or in GitHub Actions to integrate with existing workflows.
|
||||
There are no agents to install and no pipelines to migrate.
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details class="group">
|
||||
<summary
|
||||
class="flex items-center justify-between gap-4 py-5 cursor-pointer list-none [&::-webkit-details-marker]:hidden font-inter-tight font-semibold text-zinc-900">
|
||||
<span>Do I need Hub to use Uncloud?</span>
|
||||
<svg class="w-5 h-5 shrink-0 text-zinc-400 transition-transform duration-200 group-open:rotate-45"
|
||||
fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"
|
||||
aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14m-7-7h14"/>
|
||||
</svg>
|
||||
</summary>
|
||||
<p class="text-zinc-500 leading-relaxed pb-5">
|
||||
No, Uncloud is open source and fully functional on its own without Hub. You can use
|
||||
the CLI to manage your clusters and deploy your apps across servers from Compose
|
||||
files. You keep full control over your servers. Hub adds a web UI and an
|
||||
observability stack on top so you don't have to build and manage one yourself.
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Newsletter subscription -->
|
||||
<section class="relative bg-white">
|
||||
<div class="py-12 md:py-20">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="max-w-2xl mx-auto text-center">
|
||||
<h2 class="font-inter-tight text-3xl md:text-4xl font-bold text-zinc-900 mb-4">
|
||||
Follow the development journey
|
||||
</h2>
|
||||
<p class="text-lg text-zinc-500">
|
||||
Subscribe to get early insights into new features.
|
||||
See <a class="font-medium text-zinc-600 underline decoration-zinc-300 underline-offset-2 hover:text-zinc-900 hover:decoration-zinc-400 transition-colors"
|
||||
href="https://psviderski.substack.com/" target="_blank" rel="noopener">previous
|
||||
newsletters</a>.
|
||||
</p>
|
||||
<div class="mt-6 md:mt-8 flex justify-center">
|
||||
<iframe src="https://psviderski.substack.com/embed?transparent=true" width="480"
|
||||
height="160" title="Subscribe to the Uncloud newsletter" class="w-full max-w-[480px]"
|
||||
style="border:none; background:transparent;" frameborder="0"
|
||||
scrolling="no"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -829,21 +908,6 @@ prod-3 201.45.91.123:51820 1m56s ago 11ms 5.12MB 9.34MB
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Newsletter subscription -->
|
||||
<div class="mt-12 md:mt-16">
|
||||
<p class="text-zinc-400 mb-4">
|
||||
Subscribe to follow the development journey and get early insights into new features.<br>
|
||||
See <a href="https://psviderski.substack.com/" target="_blank" rel="noopener"
|
||||
class="text-zinc-200 underline decoration-zinc-500 underline-offset-2 hover:text-zinc-100 hover:decoration-zinc-300 transition-colors">previous
|
||||
newsletters</a>.
|
||||
</p>
|
||||
<div class="flex justify-center">
|
||||
<iframe src="https://psviderski.substack.com/embed?transparent=true" width="480"
|
||||
height="150" title="Subscribe to the Uncloud newsletter"
|
||||
style="border:none; background:transparent;" frameborder="0"
|
||||
scrolling="no"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -859,7 +923,7 @@ prod-3 201.45.91.123:51820 1m56s ago 11ms 5.12MB 9.34MB
|
||||
<div class="sm:col-span-6 md:col-span-6 lg:col-span-8 max-md:order-1 flex flex-col">
|
||||
<div class="mb-4">
|
||||
<!-- Logo -->
|
||||
<a class="flex items-center" href="index.html">
|
||||
<a class="flex items-center" href="/">
|
||||
<img class="w-8 h-8 rounded shadow-sm shadow-zinc-950/20" src="./images/logo.svg"
|
||||
alt="Logo">
|
||||
<span class="ml-2 font-semibold text-lg text-zinc-900">uncloud</span>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// PostHog analytics and form processing for the landing pages.
|
||||
// The project API key is public and safe to expose in client-side code.
|
||||
const POSTHOG_API_KEY = 'phc_nsuhPtAsiYAFiSYmc2KwA5Homz6miXWjf3Hy4J4H3QMV';
|
||||
|
||||
(function () {
|
||||
// Official PostHog JS snippet from Project settings - General - HTML snippet.
|
||||
!function(t,e){var o,n,p,r;e.__SV||(window.posthog && window.posthog.__loaded)||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="ki Ci init qi Hi pr Bi zi Di capture calculateEventProperties Qi register register_once register_for_session unregister unregister_for_session Ki getFeatureFlag getFeatureFlagPayload getFeatureFlagResult getAllFeatureFlags isFeatureEnabled reloadFeatureFlags updateFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey displaySurvey cancelPendingSurvey canRenderSurvey canRenderSurveyAsync Xi identify setPersonProperties unsetPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset shutdown setIdentity clearIdentity get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException addExceptionStep captureLog startExceptionAutocapture stopExceptionAutocapture loadToolbar get_property getSessionProperty Ji Gi createPersonProfile setInternalOrTestUser Yi Ai rn opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing get_explicit_consent_status is_capturing clear_opt_in_out_capturing Vi debug mr it getPageViewId captureTraceFeedback captureTraceMetric Oi".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
||||
posthog.init(POSTHOG_API_KEY, {
|
||||
// Send events through our own domain (see website/Caddyfile) so ad blockers don't drop them.
|
||||
api_host: window.location.origin + '/phproxy',
|
||||
ui_host: "https://us.posthog.com",
|
||||
defaults: '2026-05-30',
|
||||
person_profiles: 'identified_only',
|
||||
})
|
||||
})();
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 25 KiB |