# Deploy demo app

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.

## Prerequisites

Before you begin, you'll need:

- **Uncloud CLI** [installed](1-install-cli.md) on your local machine
- A **Ubuntu or Debian server** with **public IP address** and **SSH access** using a **private key** (as `root` or a
  user with **passwordless** `sudo` privileges).

:::tip Need a server?

A small Virtual Private Server (VPS) or dedicated server from providers like [Hetzner](https://www.hetzner.com) or
[DigitalOcean](https://www.digitalocean.com) is a great choice for learning Uncloud and running lightweight services. We
recommend using a freshly installed server as existing services on ports 80 and 443 can cause conflicts.

**Minimum requirements:** 1 vCPU, 512 MB RAM, Ubuntu 22.04 or Debian 11, AMD64 (recommended) or ARM64 architecture.
Other Linux distributions may work, but haven't been tested yet.

:::

## Set up your server

First, let's turn your server into an Uncloud **machine**. This simply means setting it up so you can deploy and manage
services on it using `uc`.

```shell
uc machine init root@<your-server-ip>
```

If the SSH key to access your server isn't added to your [SSH agent](https://www.ssh.com/academy/ssh/agent), specify it
with the `-i` flag:

```shell
uc machine init root@<your-server-ip> -i ~/.ssh/id_xxx
```

This command will:

- Install the latest stable Docker version on your server if it's not already installed
- 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.cluster.uncloud.run` subdomain via the Uncloud managed DNS service and point it to your
  server's IP

All in about a minute!

<details>
<summary>💡 Expand to see example output</summary>

```
$ 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
+ sh -c apt-get -qq update >/dev/null
+ sh -c DEBIAN_FRONTEND=noninteractive apt-get -y -qq install ca-certificates curl >/dev/null
+ sh -c install -m 0755 -d /etc/apt/keyrings
+ sh -c curl -fsSL "https://download.docker.com/linux/ubuntu/gpg" -o /etc/apt/keyrings/docker.asc
+ sh -c chmod a+r /etc/apt/keyrings/docker.asc
+ sh -c echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable" > /etc/apt/sources.list.d/docker.list
+ sh -c apt-get -qq update >/dev/null
+ sh -c DEBIAN_FRONTEND=noninteractive apt-get -y -qq install docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-ce-rootless-extras docker-buildx-plugin >/dev/null

Running kernel seems to be up-to-date.

No services need to be restarted.

No containers need to be restarted.

No user sessions are running outdated binaries.

No VM guests are running outdated hypervisor (qemu) binaries on this host.
+ sh -c docker version
Client: Docker Engine - Community
 Version:           28.2.1
 API version:       1.50
 Go version:        go1.24.3
 Git commit:        879ac3f
 Built:             Wed May 28 19:25:01 2025
 OS/Arch:           linux/amd64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          28.2.1
  API version:      1.50 (minimum version 1.24)
  Go version:       go1.24.3
  Git commit:       0e2cc22
  Built:            Wed May 28 19:25:01 2025
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.7.27
  GitCommit:        05044ec0a9a75232cad458027ca83437aae3f4da
 runc:
  Version:          1.2.5
  GitCommit:        v1.2.5-0-g59923ef
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

================================================================================

To run Docker as a non-privileged user, consider setting up the
Docker daemon in rootless mode for your user:

    dockerd-rootless-setuptool.sh install

Visit https://docs.docker.com/go/rootless/ to learn about rootless mode.


To run the Docker daemon as a fully privileged service, but granting non-root
users access, refer to https://docs.docker.com/go/daemon-access/

WARNING: Access to the remote API on a privileged Docker daemon is equivalent
         to root access on the host. Refer to the 'Docker daemon attack surface'
         documentation for details: https://docs.docker.com/go/attack-surface/

================================================================================

✓ Docker installed successfully.
✓ Linux user and group 'uncloud' created.
⏳ Installing Uncloud binaries...
⏳ Downloading uncloudd binary: https://github.com/psviderski/uncloud/releases/latest/download/uncloudd_linux_amd64.tar.gz
✓ uncloudd binary installed: /usr/local/bin/uncloudd
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
✓ 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! 🎉
Cluster initialised with machine 'machine-dc3c' and saved as context 'default' in your local config (/Users/spy/.config/uncloud/config.yaml)
Current cluster context is now 'default'.
Waiting for the machine to be ready...

Reserved cluster domain: 7za6s7.cluster.uncloud.run
[+] 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

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.cluster.uncloud.run  A → 157.180.72.195
```

</details>

## Deploy Excalidraw

Now that your machine is set up, let's deploy `excalidraw` service from the
[official Docker image](https://hub.docker.com/r/excalidraw/excalidraw). The service will publish the container port 80
as HTTPS endpoint on the previously reserved domain via Caddy.

```shell
uc run --name excalidraw --publish 80/https excalidraw/excalidraw
```

You'll see the progress of the deployment and the public URL where you can access the service:

```
[+] Running service excalidraw (replicated mode) 2/2
 ✔ Container excalidraw-azpc on machine-dc3c  Started                       8.9s
   ✔ Image excalidraw/excalidraw on machine-dc3c  Pulled                    4.7s

excalidraw endpoints:
 • https://excalidraw.7za6s7.cluster.uncloud.run → :80
```

## Verify your deployment

After the service is deployed, use the `uc inspect` command to check its status and details:

```shell
uc inspect excalidraw
```

```
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
```

In this example, the service has one container running on the machine `machine-dc3c` (our server). The container is up
and healthy.

You can also list all deployed services and their public endpoints using the `uc ls` command:

```shell
uc ls
```

```
NAME         MODE         REPLICAS   ENDPOINTS
caddy        global       1
excalidraw   replicated   1          https://excalidraw.7za6s7.cluster.uncloud.run → :80
```

You can see `caddy` service listed here. That's your reverse proxy, running as a regular Uncloud service.

## It's live! Start drawing! ✨

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.

![Excalidraw running on Uncloud](./img/excalidraw-browser.png)

You now have:

- Your **own Excalidraw instance** running on your server
- A **public URL** with **automatic HTTPS** you can share with your team and friends
- **Full control over your data** — no analytics or tracking

## Convert to Docker Compose format

Uncloud supports the [Compose file format](https://docs.docker.com/reference/compose-file/) for defining services. This
allows you to version control your deployments, share configurations with your team, and deploy complex multi-service
applications with a single command.

Let's create a `compose.yaml` file in your current directory for the `excalidraw` service we just deployed.

```yaml title="compose.yaml"
services:
  excalidraw:
    image: excalidraw/excalidraw
    x-ports:
      - 80/https
```

:::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.

:::

Now deploy it:

```shell
uc deploy
```

```
Services are up to date.
```

Since `excalidraw` service is already running with the same configuration, Uncloud recognises there's nothing to change.
We've successfully converted our deployment created with `uc run` to a Compose file.

## Use your own domain

Want to use your own domain, for example, `excalidraw.example.com` instead of `excalidraw.7za6s7.cluster.uncloud.run`?

Add a CNAME record `excalidraw.example.com` in your DNS provider (Cloudflare, Namecheap, etc.) pointing to
`excalidraw.7za6s7.cluster.uncloud.run`. 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.cluster.uncloud.run`.

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).

:::

Then update the published port `80/https` in `compose.yaml` to use your domain:

```yaml title="compose.yaml"
...
x-ports:
  - excalidraw.example.com:80/https
```

Finally, deploy the changes:

```shell
uc deploy
```

```
Deployment plan:
- Deploy service [name=excalidraw]
  - machine-dc3c: Run container [image=excalidraw/excalidraw]
  - machine-dc3c: Remove container [name=excalidraw-azpc]

Do you want to continue?

Choose [y/N]: y
Chose: Yes!

[+] Deploying services 2/2
 ✔ Container excalidraw-0z12 on machine-dc3c  Started                       3.5s
 ✔ Container excalidraw-azpc on machine-dc3c  Removed                       3.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.

Give it a moment for Caddy to obtain a TLS certificate, then visit https://excalidraw.example.com.

## Clean up

TBD

## Next steps

TBD
