mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d78d038fd1 | ||
|
|
624caf037b | ||
|
|
ff1a8fbcf9 | ||
|
|
3aa8fead76 | ||
|
|
4075f85b72 | ||
|
|
59f6fa75ca | ||
|
|
8b66a11394 | ||
|
|
fa3d992dbc | ||
|
|
a7d8afb052 | ||
|
|
0d820bc75a | ||
|
|
6a4e63dbd4 | ||
|
|
7699b530c2 | ||
|
|
f1abbbea6d | ||
|
|
683ad22358 | ||
|
|
8f00e0082e | ||
|
|
c34542a9d8 | ||
|
|
88db1cb6a9 | ||
|
|
dfa6e001c5 | ||
|
|
f49ec465d0 | ||
|
|
1ffe9acdd7 | ||
|
|
e35e69b4be | ||
|
|
11c2e641f1 | ||
|
|
bb97537df7 | ||
|
|
9f4b8179b5 | ||
|
|
f7b079c4f2 | ||
|
|
3648187219 | ||
|
|
93346301fb | ||
|
|
cc86c8660c | ||
|
|
2963436538 | ||
|
|
6e326277bb | ||
|
|
469d6fadcd | ||
|
|
5f214247ca |
@@ -2,19 +2,46 @@
|
||||
<img src="./website/images/logo.svg" height="100" alt="Uncloud logo"/>
|
||||
<h1>Uncloud</h1>
|
||||
<p><strong>Docker simplicity. Multi-machine power.</strong></p>
|
||||
|
||||
<p>
|
||||
<a href="https://discord.gg/eR35KQJhPu"><img src="https://img.shields.io/badge/discord-5865F2.svg?style=for-the-badge&logo=discord&logoColor=white" alt="Join Discord"></a>
|
||||
<a href="https://x.com/psviderski"><img src="https://img.shields.io/badge/follow-black?style=for-the-badge&logo=X&logoColor=while" alt="Follow on X"></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Uncloud is a lightweight clustering and container orchestration tool that lets you deploy and manage web apps across
|
||||
cloud VMs and bare metal with minimized cluster management overhead. It creates a secure WireGuard mesh network between
|
||||
cloud VMs and bare metal with minimised cluster management overhead. It creates a secure WireGuard mesh network between
|
||||
your Docker hosts and provides automatic service discovery, load balancing, ingress with HTTPS, and simple CLI commands
|
||||
to manage your apps.
|
||||
|
||||
Unlike traditional orchestrators, there's no central control plane and quorum to maintain. Each machine maintains a
|
||||
synchronized copy of the cluster state through peer-to-peer communication, keeping cluster operations functional even if
|
||||
synchronised copy of the cluster state through peer-to-peer communication, keeping cluster operations functional even if
|
||||
some machines go offline.
|
||||
|
||||
Uncloud aims to be the solution for developers who want the flexibility of self-hosted infrastructure without the
|
||||
operational complexity of Kubernetes.
|
||||
Uncloud is the solution for developers who want the flexibility of self-hosted infrastructure without the operational
|
||||
complexity of Kubernetes.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* **Deploy anywhere**: Combine cloud VMs, dedicated servers, and bare metal into a unified computing environment —
|
||||
regardless of location or provider.
|
||||
* **Docker Compose**: Familiar [Docker Compose](https://compose-spec.io/) format for defining services and volumes. No
|
||||
need to learn a new bespoke DSL.
|
||||
* **Zero-downtime deployments**: Rolling updates without service interruption. Automatic rollback on failure is coming
|
||||
soon.
|
||||
* **Service discovery**: Built-in DNS server resolves service names to container IPs.
|
||||
* **Persistent storage**: Run stateful services with Docker volumes managed across machines.
|
||||
* **Zero-config private network**: Automatic WireGuard mesh with peer discovery and NAT traversal. Containers get unique
|
||||
IPs for direct cross-machine communication.
|
||||
* **No control plane**: Fully decentralised design eliminates single points of failure and reduces operational overhead.
|
||||
* **Imperative over declarative**: Favoring imperative operations over state reconciliation simplifies both the mental
|
||||
model and troubleshooting.
|
||||
* **Managed DNS**: Automatic DNS records `*.<id>.cluster.uncloud.run` for services with public access via managed
|
||||
[Uncloud DNS](https://github.com/psviderski/uncloud-dns) service.
|
||||
* **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's
|
||||
Encrypt.
|
||||
* **Docker-like CLI**: Familiar commands for managing both infrastructure and applications.
|
||||
* **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster.
|
||||
|
||||
## 🎬 Quick demo
|
||||
|
||||
@@ -24,36 +51,6 @@ Deploy a highly available web app with automatic HTTPS across multiple regions a
|
||||
<img src="https://embed-ssl.wistia.com/deliveries/3cf7014a48b93afc556444bed3e39a8c.jpg?image_crop_resized=900x526&image_play_button_rounded=true&image_play_button_size=2x&image_play_button_color=18181Be0" alt="Uncloud demo" width="450" height="263" />
|
||||
</a>
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* **Deploy anywhere**: Combine cloud VMs, dedicated servers, and bare metal into a unified computing environment —
|
||||
regardless of location or provider.
|
||||
* **Zero-config private network**: Automatic WireGuard mesh with peer discovery and NAT traversal. Containers get unique
|
||||
IPs for direct cross-machine communication.
|
||||
* **No control plane**: Fully decentralized design eliminates single points of failure and reduces operational overhead.
|
||||
* **Imperative over declarative**: Favoring imperative operations over state reconciliation simplifies both the mental
|
||||
model and troubleshooting.
|
||||
* **Zero-downtime deployments**: Rolling updates with health checks and automatic rollback on failure.
|
||||
* **Service discovery**: Built-in DNS server resolves service names to container IPs.
|
||||
* **Managed DNS**: Automatic DNS records `*.<id>.cluster.uncloud.run` for services with public access via managed
|
||||
[Uncloud DNS](https://github.com/psviderski/uncloud-dns) service.
|
||||
* **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's
|
||||
Encrypt.
|
||||
* **Docker-like CLI**: Familiar commands for managing both infrastructure and applications.
|
||||
* **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster.
|
||||
|
||||
Coming soon:
|
||||
|
||||
* Infrastructure as Code using Docker Compose format
|
||||
* Project isolation through environments/namespaces
|
||||
* Persistent volumes and secrets management
|
||||
* Monitoring and log aggregation
|
||||
* Database deployment and management
|
||||
|
||||
Here is a diagram of an Uncloud multi-provider cluster of 3 machines:
|
||||
|
||||

|
||||
|
||||
## 💫 Why Uncloud?
|
||||
|
||||
Modern cloud platforms like Heroku and Render offer amazing developer experiences but at a premium price. Traditional
|
||||
@@ -62,14 +59,15 @@ believe there's a sweet spot in between — a pragmatic solution for the majorit
|
||||
scale. You should be able to:
|
||||
|
||||
* **Own your infrastructure and data**: Whether driven by costs, compliance, or flexibility, run applications on any
|
||||
combination of cloud VMs and personal hardware while maintaining the cloud-like experience you love.
|
||||
* **Stay simple**: Don't worry about control planes, highly-available clusters, or complex YAML configurations for
|
||||
common use cases.
|
||||
combination of cloud VMs and personal hardware while controlling your data and maintaining the cloud-like experience
|
||||
you love.
|
||||
* **Stay simple as you grow**: Start with a single machine and add more whenever you need without changing your
|
||||
workflow. No worrying about highly-available control planes or complex YAML configurations.
|
||||
* **Build with proven primitives**: Get production-grade networking, deployment primitives, service discovery, load
|
||||
balancing, and ingress with HTTPS out of the box without becoming a distributed systems expert.
|
||||
* **Support sustainable computing** 🌿: Minimize system overhead to maximize resources available for your applications.
|
||||
* **Support sustainable computing** 🌿: Minimise system overhead to maximise resources available for your applications.
|
||||
|
||||
Uncloud's goal is to make deployment and management of containerized applications feel as seamless as using a cloud
|
||||
Uncloud's goal is to make deployment and management of containerised applications feel as seamless as using a cloud
|
||||
platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bare metal servers.
|
||||
|
||||
## 🚀 Quick start
|
||||
@@ -83,7 +81,7 @@ platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bar
|
||||
curl -fsS https://get.uncloud.run/install.sh | sh
|
||||
```
|
||||
|
||||
2. Initialize your first machine:
|
||||
2. Initialise your first machine:
|
||||
|
||||
```bash
|
||||
uc machine init root@your-server-ip
|
||||
@@ -118,10 +116,16 @@ View the [user guide](docs/user_guide.md) for more information.
|
||||
|
||||
## ⚙️ How it works
|
||||
|
||||
Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals. Here, let's peek
|
||||
under the hood to see what happens when you run certain commands.
|
||||
Check out the [design document](docs/design.md) to understand Uncloud's design philosophy and goals.
|
||||
|
||||
**When you initialize a new cluster on a machine:**
|
||||
Here is a diagram of an Uncloud multi-provider cluster of 3 machines:
|
||||
|
||||

|
||||
|
||||
<details>
|
||||
<summary>Peek under the hood to see what happens when you run certain commands.</summary>
|
||||
|
||||
**When you initialise a new cluster on a machine:**
|
||||
|
||||
```bash
|
||||
$ uc machine init --name oracle-vm ubuntu@152.67.101.197
|
||||
@@ -254,23 +258,26 @@ Look ma, no control plane or master nodes to maintain! Just a simple overlay net
|
||||
sync that lets machines work together. Want to check on things or make changes? Connect to any machine either implicitly
|
||||
using the CLI or directly over SSH. They all have the complete cluster state and can control everything. It's like each
|
||||
machine is a full backup of your control plane.
|
||||
</details>
|
||||
|
||||
## 🏗 Project status
|
||||
|
||||
Uncloud is currently in active development and is **not ready for production use**. Features may change significantly
|
||||
and there may be breaking changes between releases.
|
||||
|
||||
I'd love your input! Here's how you can contribute:
|
||||
We'd love your input! Here's how you can contribute:
|
||||
|
||||
* 🐛 Found a bug? [Open an issue](https://github.com/psviderski/uncloud/issues)
|
||||
* 💡 Have ideas? [Join the discussion](https://github.com/psviderski/uncloud/discussions)
|
||||
* 💡 Have ideas or need help? [Join our Discord community](https://discord.gg/eR35KQJhPu) where we discuss features,
|
||||
roadmap, implementation details, and help each other out.
|
||||
|
||||
## 📫 Stay updated
|
||||
|
||||
* [Subscribe](https://uncloud.run/#subscribe) to my newsletter to follow the progress, get early insights into new
|
||||
* Join our [Discord server](https://discord.gg/eR35KQJhPu) for real-time discussions, support, and updates.
|
||||
* Follow [@psviderski](https://x.com/psviderski) on X/Twitter.
|
||||
* Subscribe to [my newsletter](https://uncloud.run/#subscribe) to follow the progress, get early insights into new
|
||||
features, and be the first to know when it's ready for production use.
|
||||
* Watch this repository for releases.
|
||||
* Follow [@psviderski](https://github.com/psviderski) on GitHub.
|
||||
|
||||
## ❤️ Contributors
|
||||
|
||||
|
||||
@@ -152,7 +152,8 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||
}
|
||||
|
||||
func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, progressOut *streams.Out) error {
|
||||
if _, err := clusterClient.GetDomain(ctx); err != nil {
|
||||
domain, err := clusterClient.GetDomain(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, api.ErrNotFound) {
|
||||
fmt.Println("Skipping DNS records update as no cluster domain is reserved (see 'uc dns').")
|
||||
return nil
|
||||
@@ -164,7 +165,7 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
|
||||
// TODO: split the method into two: one to get the records and one to update them to ask for update confirmation.
|
||||
|
||||
var records []*pb.DNSRecord
|
||||
err := progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
records, err = clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName)
|
||||
return err
|
||||
@@ -172,18 +173,19 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
|
||||
if err != nil {
|
||||
if errors.Is(err, client.ErrNoReachableMachines) {
|
||||
fmt.Println()
|
||||
fmt.Println("DNS records could not be updated as there are no internet-reachable machines running " +
|
||||
"caddy containers.")
|
||||
fmt.Printf("DNS records for domain '%s' could not be updated as there are no internet-reachable "+
|
||||
"machines running caddy containers.\n", domain)
|
||||
fmt.Println()
|
||||
fmt.Println("Possible solutions:")
|
||||
fmt.Println("- Ensure your machines have public IP addresses")
|
||||
fmt.Println("- Use --public-ip flag when adding machines to override the automatically detected IPs")
|
||||
fmt.Println("- Check firewall settings on your machines")
|
||||
fmt.Println("- Configure port forwarding if behind NAT")
|
||||
fmt.Println("- Retry Caddy deployment after resolving connectivity issues with 'uc caddy deploy'")
|
||||
fmt.Println("- Retry Caddy deployment with 'uc caddy deploy' after resolving connectivity issues")
|
||||
fmt.Println()
|
||||
fmt.Println("Your services will not be accessible from the internet until at least one machine " +
|
||||
"becomes reachable.")
|
||||
fmt.Println("Your services won't be accessible from the internet until at least one machine " +
|
||||
"becomes reachable. If you aren't planning to expose any services publicly, you can release " +
|
||||
"the domain by running 'uc dns release'.")
|
||||
}
|
||||
return fmt.Errorf("failed to update DNS records pointing to caddy service: %w", err)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ type inspectOptions struct {
|
||||
func NewInspectCommand() *cobra.Command {
|
||||
opts := inspectOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect",
|
||||
Use: "inspect SERVICE",
|
||||
Short: "Display detailed information on a service.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -26,7 +26,7 @@ func NewCreateCommand() *cobra.Command {
|
||||
opts := createOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create [FLAGS] VOLUME_NAME",
|
||||
Use: "create VOLUME_NAME",
|
||||
Short: "Create a volume on a specific machine.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -20,7 +20,7 @@ func NewInspectCommand() *cobra.Command {
|
||||
opts := inspectOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [FLAGS] VOLUME_NAME",
|
||||
Use: "inspect VOLUME_NAME",
|
||||
Short: "Display detailed information on a volume.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -23,7 +23,7 @@ func NewListCommand() *cobra.Command {
|
||||
opts := listOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls [FLAGS]",
|
||||
Use: "ls",
|
||||
Aliases: []string{"list"},
|
||||
Short: "List volumes across all machines in the cluster.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -21,7 +21,7 @@ func NewRemoveCommand() *cobra.Command {
|
||||
opts := removeOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "rm [FLAGS] VOLUME_NAME [VOLUME_NAME...]",
|
||||
Use: "rm VOLUME_NAME [VOLUME_NAME...]",
|
||||
Aliases: []string{"remove", "delete"},
|
||||
Short: "Remove one or more volumes.",
|
||||
Long: "Remove one or more volumes. You cannot remove a volume that is in use by a container.",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -0,0 +1,41 @@
|
||||
# Website
|
||||
|
||||
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
|
||||
|
||||
### Installation
|
||||
|
||||
```
|
||||
$ yarn
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
```
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||
|
||||
### Deployment
|
||||
|
||||
Using SSH:
|
||||
|
||||
```
|
||||
$ USE_SSH=true yarn deploy
|
||||
```
|
||||
|
||||
Not using SSH:
|
||||
|
||||
```
|
||||
$ GIT_USER=<Your GitHub username> yarn deploy
|
||||
```
|
||||
|
||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
slug: /
|
||||
---
|
||||
|
||||
# Overview
|
||||
|
||||
Uncloud is lightweight tool for creating a cluster of machines running Docker and deploying web applications across
|
||||
them. It is designed to be simple and easy to use, while still providing the essential features to manage and scale
|
||||
your apps in production.
|
||||
@@ -0,0 +1,153 @@
|
||||
// @ts-check
|
||||
// `@type` JSDoc annotations allow editor autocompletion and type checking
|
||||
// (when paired with `@ts-check`).
|
||||
// There are various equivalent ways to declare your Docusaurus config.
|
||||
// See: https://docusaurus.io/docs/api/docusaurus-config
|
||||
|
||||
import {themes as prismThemes} from 'prism-react-renderer';
|
||||
|
||||
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
|
||||
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
title: 'Uncloud Docs',
|
||||
tagline: 'Dinosaurs are cool',
|
||||
favicon: 'img/favicon.png',
|
||||
|
||||
// Set the production url of your site here
|
||||
url: 'https://docs.uncloud.run',
|
||||
// Set the /<baseUrl>/ pathname under which your site is served
|
||||
// For GitHub pages deployment, it is often '/<projectName>/'
|
||||
baseUrl: '/',
|
||||
|
||||
// GitHub pages deployment config.
|
||||
// If you aren't using GitHub pages, you don't need these.
|
||||
organizationName: '', // Usually your GitHub org/user name.
|
||||
projectName: '', // Usually your repo name.
|
||||
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
|
||||
// Even if you don't use internationalization, you can use this field to set
|
||||
// useful metadata like html lang. For example, if your site is Chinese, you
|
||||
// may want to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en'],
|
||||
},
|
||||
|
||||
themes: [
|
||||
[
|
||||
'@easyops-cn/docusaurus-search-local',
|
||||
/** @type {import("@easyops-cn/docusaurus-search-local").PluginOptions} */
|
||||
({
|
||||
docsRouteBasePath: '/',
|
||||
hashed: true,
|
||||
highlightSearchTermsOnTargetPage: true,
|
||||
}),
|
||||
],
|
||||
],
|
||||
presets: [
|
||||
[
|
||||
'classic',
|
||||
/** @type {import('@docusaurus/preset-classic').Options} */
|
||||
({
|
||||
docs: {
|
||||
// Remove this to remove the "edit this page" links.
|
||||
editUrl: 'https://github.com/psviderski/uncloud/edit/main/docs/',
|
||||
// Serve the docs at the site's root.
|
||||
routeBasePath: '/',
|
||||
showLastUpdateTime: true,
|
||||
sidebarPath: './sidebars.js',
|
||||
},
|
||||
// blog: {
|
||||
// showReadingTime: true,
|
||||
// feedOptions: {
|
||||
// type: ['rss', 'atom'],
|
||||
// xslt: true,
|
||||
// },
|
||||
// // Please change this to your repo.
|
||||
// // Remove this to remove the "edit this page" links.
|
||||
// editUrl:
|
||||
// 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
|
||||
// // Useful options to enforce blogging best practices
|
||||
// onInlineTags: 'warn',
|
||||
// onInlineAuthors: 'warn',
|
||||
// onUntruncatedBlogPosts: 'warn',
|
||||
// },
|
||||
theme: {
|
||||
customCss: './src/css/custom.css',
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig:
|
||||
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
|
||||
({
|
||||
colorMode: {
|
||||
// The color mode when user first visits the site.
|
||||
defaultMode: "light",
|
||||
// Whether to use the prefers-color-scheme media-query, using user system preferences,
|
||||
// instead of the hardcoded defaultMode.
|
||||
respectPrefersColorScheme: true,
|
||||
},
|
||||
// The meta image URL for the site (og:image and twitter:image meta tags).
|
||||
// Relative to your site's "static" directory. Cannot be SVGs. Can be external URLs too.
|
||||
image: 'img/social-card.png',
|
||||
navbar: {
|
||||
title: 'Uncloud Docs',
|
||||
logo: {
|
||||
alt: 'Uncloud Logo',
|
||||
src: 'img/logo.svg',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'search',
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/psviderski/uncloud',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: 'light',
|
||||
links: [
|
||||
{
|
||||
title: 'Uncloud',
|
||||
items: [
|
||||
{
|
||||
html: 'Made with ❤️ by <a href="https://github.com/psviderski">@psviderski</a> in Australia'
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Community',
|
||||
items: [
|
||||
{
|
||||
label: 'Discord',
|
||||
href: 'https://discord.gg/eR35KQJhPu',
|
||||
},
|
||||
{
|
||||
label: 'X',
|
||||
href: 'https://x.com/psviderski',
|
||||
},
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/psviderski/uncloud',
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
prism: {
|
||||
theme: prismThemes.github,
|
||||
darkTheme: prismThemes.dracula,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
+18434
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.7.0",
|
||||
"@docusaurus/preset-classic": "3.7.0",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.49.2",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "3.7.0",
|
||||
"@docusaurus/types": "3.7.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 3 chrome version",
|
||||
"last 3 firefox version",
|
||||
"last 5 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// @ts-check
|
||||
|
||||
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
|
||||
|
||||
/**
|
||||
* Creating a sidebar enables you to:
|
||||
- create an ordered group of docs
|
||||
- render a sidebar for each doc of that group
|
||||
- provide next/previous navigation
|
||||
|
||||
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||
|
||||
Create as many sidebars as you want.
|
||||
|
||||
@type {import('@docusaurus/plugin-content-docs').SidebarsConfig}
|
||||
*/
|
||||
const sidebars = {
|
||||
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
|
||||
|
||||
// But you can create a sidebar manually
|
||||
/*
|
||||
tutorialSidebar: [
|
||||
'intro',
|
||||
'hello',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Tutorial',
|
||||
items: ['tutorial-basics/create-a-document'],
|
||||
},
|
||||
],
|
||||
*/
|
||||
};
|
||||
|
||||
export default sidebars;
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Any CSS included here will be global. The classic template
|
||||
* bundles Infima by default. Infima is a CSS framework designed to
|
||||
* work well for content-centric websites.
|
||||
*/
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap');
|
||||
|
||||
/* You can override the default Infima variables here. */
|
||||
:root {
|
||||
--ifm-font-family-base: 'Inter', sans-serif;
|
||||
--ifm-heading-font-family: 'Inter', sans-serif;
|
||||
|
||||
--ifm-color-primary: #2e8555;
|
||||
--ifm-color-primary-dark: #29784c;
|
||||
--ifm-color-primary-darker: #277148;
|
||||
--ifm-color-primary-darkest: #205d3b;
|
||||
--ifm-color-primary-light: #33925d;
|
||||
--ifm-color-primary-lighter: #359962;
|
||||
--ifm-color-primary-lightest: #3cad6e;
|
||||
--ifm-code-font-size: 95%;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||
[data-theme='dark'] {
|
||||
--ifm-color-primary: #25c2a0;
|
||||
--ifm-color-primary-dark: #21af90;
|
||||
--ifm-color-primary-darker: #1fa588;
|
||||
--ifm-color-primary-darkest: #1a8870;
|
||||
--ifm-color-primary-light: #29d5b0;
|
||||
--ifm-color-primary-lighter: #32d8b4;
|
||||
--ifm-color-primary-lightest: #4fddbf;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.menu {
|
||||
font-size: 0.875rem;
|
||||
font-weight: var(--ifm-font-weight-normal);
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">
|
||||
<g>
|
||||
<rect x="0" y="0" width="80" height="80" rx="10" fill="#18181B"/>
|
||||
<path d="M20 32 L20 48 L36 48 L36 32 M60 32 L45 32 L45 48 L60 48" stroke="white" stroke-width="6" fill="none"
|
||||
stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 341 B |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
@@ -94,6 +94,11 @@ func NewServer(listenAddr netip.Addr, resolver Resolver, upstreams []netip.AddrP
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListenAddr returns the address the DNS server is listening on.
|
||||
func (s *Server) ListenAddr() netip.Addr {
|
||||
return s.listenAddr
|
||||
}
|
||||
|
||||
// Run starts the DNS server listening on both UDP and TCP ports. The server on TCP is not critical so it won't return
|
||||
// an error if it fails to start. The server will run until the context is canceled or an error occurs.
|
||||
func (s *Server) Run(ctx context.Context) error {
|
||||
|
||||
@@ -4,19 +4,19 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
dockercontainer "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/psviderski/uncloud/internal/machine/store"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"log/slog"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
NetworkName = "uncloud"
|
||||
UserChain = "DOCKER-USER"
|
||||
// EventsDebounceInterval defines how long to wait before processing the next Docker event. Multiple events
|
||||
// occurring within this window will be processed together as a single event to prevent system overload.
|
||||
EventsDebounceInterval = 100 * time.Millisecond
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
// EnsureUncloudNetwork is a stub for darwin.
|
||||
func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
|
||||
return fmt.Errorf("not supported on darwin")
|
||||
// EnsureUncloudNetwork is a stub for Darwin.
|
||||
func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix, dnsServer netip.Addr) error {
|
||||
return fmt.Errorf("not supported on Darwin")
|
||||
}
|
||||
|
||||
@@ -3,24 +3,28 @@ package docker
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
|
||||
dnetwork "github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/libnetwork/iptables"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"github.com/psviderski/uncloud/internal/machine/dns"
|
||||
"github.com/psviderski/uncloud/internal/machine/firewall"
|
||||
"github.com/psviderski/uncloud/internal/machine/network"
|
||||
)
|
||||
|
||||
// EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet
|
||||
// if it doesn't exist. If the network exists but has a different subnet, it removes and recreates the network.
|
||||
// It also configures iptables to allow container access from the WireGuard network.
|
||||
func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
|
||||
func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix, dnsServer netip.Addr) error {
|
||||
// Ensure the Docker network 'uncloud' is created with the correct subnet.
|
||||
needsCreation := false
|
||||
nw, err := m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{})
|
||||
if err != nil {
|
||||
if !client.IsErrNotFound(err) {
|
||||
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
|
||||
return fmt.Errorf("inspect Docker network '%s': %w", NetworkName, err)
|
||||
}
|
||||
needsCreation = true
|
||||
} else if nw.IPAM.Config[0].Subnet != subnet.String() {
|
||||
@@ -31,7 +35,7 @@ func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
|
||||
)
|
||||
if err = m.client.NetworkRemove(ctx, NetworkName); err != nil {
|
||||
// It can still fail if the network is in use by a container. Leave it to the user to resolve the issue.
|
||||
return fmt.Errorf("remove Docker network %q: %w", NetworkName, err)
|
||||
return fmt.Errorf("remove Docker network '%s': %w", NetworkName, err)
|
||||
}
|
||||
needsCreation = true
|
||||
}
|
||||
@@ -50,12 +54,12 @@ func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
|
||||
},
|
||||
},
|
||||
); err != nil {
|
||||
return fmt.Errorf("create Docker network %q: %w", NetworkName, err)
|
||||
return fmt.Errorf("create Docker network '%s': %w", NetworkName, err)
|
||||
}
|
||||
slog.Info("Docker network created.", "name", NetworkName, "subnet", subnet.String())
|
||||
|
||||
if nw, err = m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{}); err != nil {
|
||||
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
|
||||
return fmt.Errorf("inspect Docker network '%s': %w", NetworkName, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,10 +71,36 @@ func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
|
||||
// Bridge name doesn't seem to be documented but this is the source code where it is generated:
|
||||
// https://github.com/moby/moby/blob/v27.2.1/libnetwork/drivers/bridge/bridge_linux.go#L664
|
||||
bridgeName := "br-" + nw.ID[:12]
|
||||
|
||||
if err = configureIptables(bridgeName, dnsServer); err != nil {
|
||||
return fmt.Errorf("configure iptables for Docker network '%s': %w", NetworkName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// configureIptables configures iptables rules for the uncloud Docker network.
|
||||
func configureIptables(bridgeName string, dnsServer netip.Addr) error {
|
||||
ipt := iptables.GetIptable(iptables.IPv4)
|
||||
rule := []string{"--in-interface", network.WireGuardInterfaceName, "--out-interface", bridgeName, "-j", "ACCEPT"}
|
||||
if err = ipt.ProgramRule(iptables.Filter, UserChain, iptables.Insert, rule); err != nil {
|
||||
// Allow traffic from other machines and their containers through the WG mesh to the Uncloud containers
|
||||
// on the machine.
|
||||
wgRule := []string{"--in-interface", network.WireGuardInterfaceName, "--out-interface", bridgeName, "-j", "ACCEPT"}
|
||||
if err := ipt.ProgramRule(iptables.Filter, firewall.DockerUserChain, iptables.Insert, wgRule); err != nil {
|
||||
return fmt.Errorf("insert iptables rule: %w", err)
|
||||
}
|
||||
|
||||
// Allow DNS queries from Uncloud containers to the embedded DNS server.
|
||||
for _, proto := range []string{"udp", "tcp"} {
|
||||
dnsRule := []string{
|
||||
"--in-interface", bridgeName,
|
||||
"--dst", dnsServer.String(),
|
||||
"--protocol", proto,
|
||||
"--dport", strconv.Itoa(dns.Port),
|
||||
"-j", "ACCEPT",
|
||||
}
|
||||
if err := ipt.ProgramRule(iptables.Filter, firewall.UncloudInputChain, iptables.Insert, dnsRule); err != nil {
|
||||
return fmt.Errorf("insert iptables rule: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package firewall
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ConfigureIptablesChains is a stub for Darwin.
|
||||
func ConfigureIptablesChains() error {
|
||||
return fmt.Errorf("not supported on Darwin")
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/libnetwork/iptables"
|
||||
"github.com/psviderski/uncloud/internal/machine/network"
|
||||
)
|
||||
|
||||
const (
|
||||
DockerUserChain = "DOCKER-USER"
|
||||
UncloudInputChain = "UNCLOUD-INPUT"
|
||||
)
|
||||
|
||||
// ConfigureIptablesChains sets up custom iptables chains and initial firewall rules for Uncloud networking.
|
||||
func ConfigureIptablesChains() error {
|
||||
// Ensure iptables UNCLOUD-INPUT chain with a RETURN rule exists. All existing rules are flushed.
|
||||
ipt := iptables.GetIptable(iptables.IPv4)
|
||||
if _, err := ipt.NewChain(UncloudInputChain, iptables.Filter); err != nil {
|
||||
return fmt.Errorf("create iptables chain '%s': %w", UncloudInputChain, err)
|
||||
}
|
||||
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-F", UncloudInputChain); err != nil {
|
||||
return fmt.Errorf("flush iptables chain '%s': %w", UncloudInputChain, err)
|
||||
}
|
||||
if err := ipt.AddReturnRule(UncloudInputChain); err != nil {
|
||||
return fmt.Errorf("add the RETURN rule for iptables chain '%s': %w", UncloudInputChain, err)
|
||||
}
|
||||
|
||||
// Ensure the main iptables INPUT chain has a jump rule to the UNCLOUD-INPUT chain before any DROP/REJECT rules.
|
||||
jumpRule := []string{"-m", "comment", "--comment", "Uncloud-managed", "-j", UncloudInputChain}
|
||||
if !ipt.Exists(iptables.Filter, "INPUT", jumpRule...) {
|
||||
// Look for the first DROP/REJECT rule in the INPUT chain.
|
||||
out, err := ipt.Raw("-t", string(iptables.Filter), "-L", "INPUT", "--line-numbers")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get iptables rules for chain '%s': %w", UncloudInputChain, err)
|
||||
}
|
||||
|
||||
firstRejectRuleNum := 0
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
if fields[1] == "DROP" || fields[1] == "REJECT" {
|
||||
if ruleNum, err := strconv.Atoi(fields[0]); err == nil {
|
||||
firstRejectRuleNum = ruleNum
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var addJumpRule []string
|
||||
if firstRejectRuleNum > 0 {
|
||||
addJumpRule = append([]string{"-t", string(iptables.Filter), "-I", "INPUT", strconv.Itoa(firstRejectRuleNum)},
|
||||
jumpRule...)
|
||||
} else {
|
||||
addJumpRule = append([]string{"-t", string(iptables.Filter), "-A", "INPUT"}, jumpRule...)
|
||||
}
|
||||
if err = ipt.RawCombinedOutput(addJumpRule...); err != nil {
|
||||
return fmt.Errorf("add iptables rule '%s': %w", strings.Join(addJumpRule, " "), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Allow WireGuard traffic to the machine.
|
||||
acceptWireGuardRule := []string{"-p", "udp", "--dport", strconv.Itoa(network.WireGuardPort), "-j", "ACCEPT"}
|
||||
err := ipt.ProgramRule(iptables.Filter, UncloudInputChain, iptables.Insert, acceptWireGuardRule)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert iptables rule '%s': %w", strings.Join(acceptWireGuardRule, " "), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -209,7 +209,7 @@ func NewMachine(config *Config) (*Machine, error) {
|
||||
}
|
||||
c := cluster.NewCluster(corroStore, corroAdmin)
|
||||
|
||||
// Init a gRPC Docker server that proxies requests to the local Docker daemon.
|
||||
// Init dependencies for a gRPC Docker server that proxies requests to the local Docker daemon.
|
||||
dockerCli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create Docker client: %w", err)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/psviderski/uncloud/internal/machine/corroservice"
|
||||
"github.com/psviderski/uncloud/internal/machine/dns"
|
||||
"github.com/psviderski/uncloud/internal/machine/docker"
|
||||
"github.com/psviderski/uncloud/internal/machine/firewall"
|
||||
"github.com/psviderski/uncloud/internal/machine/network"
|
||||
"github.com/psviderski/uncloud/internal/machine/store"
|
||||
"golang.org/x/sync/errgroup"
|
||||
@@ -79,6 +80,10 @@ func newNetworkController(
|
||||
}
|
||||
|
||||
func (nc *networkController) Run(ctx context.Context) error {
|
||||
if err := firewall.ConfigureIptablesChains(); err != nil {
|
||||
return fmt.Errorf("configure iptables chains: %w", err)
|
||||
}
|
||||
|
||||
if err := nc.wgnet.Configure(*nc.state.Network); err != nil {
|
||||
return fmt.Errorf("configure WireGuard network: %w", err)
|
||||
}
|
||||
@@ -215,7 +220,7 @@ func (nc *networkController) prepareAndWatchDocker(ctx context.Context) error {
|
||||
return fmt.Errorf("wait for Docker daemon: %w", err)
|
||||
}
|
||||
|
||||
if err := manager.EnsureUncloudNetwork(ctx, nc.state.Network.Subnet); err != nil {
|
||||
if err := manager.EnsureUncloudNetwork(ctx, nc.state.Network.Subnet, nc.dnsServer.ListenAddr()); err != nil {
|
||||
return fmt.Errorf("ensure Docker network: %w", err)
|
||||
}
|
||||
slog.Info("Docker network configured.")
|
||||
|
||||
@@ -160,10 +160,7 @@ func (p *Provisioner) waitPortPublished(ctx context.Context, containerID string,
|
||||
}
|
||||
|
||||
binding, ok := c.NetworkSettings.Ports[port]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("port '%s' not published", port)
|
||||
}
|
||||
if len(binding) > 0 {
|
||||
if ok && len(binding) > 0 {
|
||||
return binding, nil
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -31,6 +31,7 @@ const (
|
||||
)
|
||||
|
||||
var serviceIDRegexp = regexp.MustCompile("^[0-9a-f]{32}$")
|
||||
var dnsLabelRegexp = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)
|
||||
|
||||
func ValidateServiceID(id string) bool {
|
||||
return serviceIDRegexp.MatchString(id)
|
||||
@@ -104,7 +105,14 @@ func (s *ServiceSpec) Validate() error {
|
||||
return fmt.Errorf("invalid mode: %q", s.Mode)
|
||||
}
|
||||
|
||||
// TODO: validate the service name is a valid DNS label.
|
||||
if s.Name != "" {
|
||||
if len(s.Name) > 63 {
|
||||
return fmt.Errorf("service name too long (max 63 characters): %q", s.Name)
|
||||
}
|
||||
if !dnsLabelRegexp.MatchString(s.Name) {
|
||||
return fmt.Errorf("invalid service name: %q. must be 1-63 characters, lowercase letters, numbers, and dashes only; must start and end with a letter or number", s.Name)
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range s.Ports {
|
||||
if (p.Mode == "" || p.Mode == PortModeIngress) &&
|
||||
@@ -138,6 +146,7 @@ func (s *ServiceSpec) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func (s *ServiceSpec) Clone() ServiceSpec {
|
||||
spec := *s
|
||||
|
||||
|
||||
+1
-1
@@ -24,12 +24,12 @@ var caddyImageTagRegex = regexp.MustCompile(`^2\.\d+\.\d+$`)
|
||||
// The service is deployed in global mode to all machines in the cluster. If the image is not provided, the latest
|
||||
// version of the official Caddy Docker image is used.
|
||||
func (cli *Client) NewCaddyDeployment(image string, placement api.Placement) (*deploy.Deployment, error) {
|
||||
if image == "" {
|
||||
latest, err := LatestCaddyImage()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("look up latest Caddy image: %w", err)
|
||||
}
|
||||
|
||||
if image == "" {
|
||||
image = reference.FamiliarString(latest)
|
||||
}
|
||||
|
||||
|
||||
+135
-38
@@ -3,7 +3,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Deploy Docker Apps on Your Infrastructure | Uncloud</title>
|
||||
<title>Deploy and scale Docker apps on your infrastructure without Kubernetes | Uncloud</title>
|
||||
<meta name="description"
|
||||
content="Uncloud helps you deploy container applications on your infrastructure — from cloud VMs to bare metal. Replace Heroku and Render, no Kubernetes required.">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
@@ -13,7 +13,8 @@
|
||||
<link rel="alternate icon" type="image/png" href="./images/favicon.png" sizes="96x96"/>
|
||||
|
||||
<!-- OpenGraph tags for social media -->
|
||||
<meta property="og:title" content="Deploy Docker Apps on Your Infrastructure | Uncloud"/>
|
||||
<meta property="og:title"
|
||||
content="Deploy and scale Docker apps on your infrastructure without Kubernetes | Uncloud"/>
|
||||
<meta property="og:description"
|
||||
content="Uncloud helps you deploy container applications on your infrastructure — from cloud VMs to bare metal. Replace Heroku and Render, no Kubernetes required."/>
|
||||
<meta property="og:image" content="https://uncloud.run/images/logo-wide.png"/>
|
||||
@@ -25,7 +26,8 @@
|
||||
<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="Deploy Docker Apps on Your Infrastructure | Uncloud">
|
||||
<meta name="twitter:title"
|
||||
content="Deploy and scale Docker apps on your infrastructure without Kubernetes | Uncloud">
|
||||
<meta name="twitter:description"
|
||||
content="Uncloud helps you deploy container applications on your infrastructure — from cloud VMs to bare metal. Replace Heroku and Render, no Kubernetes required.">
|
||||
<meta name="twitter:image" content="https://uncloud.run/images/logo-wide.png">
|
||||
@@ -54,19 +56,29 @@
|
||||
<ul class="flex grow justify-end flex-wrap items-center">
|
||||
<li>
|
||||
<a class="text-sm font-medium text-zinc-500 hover:text-zinc-900 px-3 lg:px-5 py-2 flex items-center transition"
|
||||
href="https://github.com/psviderski/uncloud">
|
||||
<svg class="w-4 h-4 mr-1" 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">
|
||||
<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/>
|
||||
<path d="M9 18c-4.51 2-5-2-7-2"/>
|
||||
href="https://discord.gg/eR35KQJhPu">
|
||||
<svg class="w-4 h-4 mr-1 fill-current" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/>
|
||||
</svg>
|
||||
<span class="ml-1 hidden sm:inline">View Source</span>
|
||||
<span class="ml-1 hidden sm:inline">Discord</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="text-sm font-medium text-zinc-500 hover:text-zinc-900 px-3 lg:px-5 py-2 flex items-center transition"
|
||||
href="https://github.com/psviderski/uncloud">
|
||||
<svg class="w-4 h-4 mr-1 fill-current" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
|
||||
</svg>
|
||||
<span class="ml-1 hidden sm:inline">GitHub</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="ml-1">
|
||||
<a class="btn-sm text-zinc-100 bg-zinc-900 hover:bg-zinc-800 w-full shadow"
|
||||
href="https://github.com/psviderski/uncloud/?tab=readme-ov-file#-quick-start">Get Started</a>
|
||||
href="https://github.com/psviderski/uncloud/?tab=readme-ov-file#-quick-start">
|
||||
Get Started
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -86,18 +98,19 @@
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="text-center pb-12 md:pb-16">
|
||||
<h1 class="font-inter-tight text-4xl md:text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-zinc-500 via-zinc-900 to-zinc-900 pb-4">
|
||||
Run on your servers<br>
|
||||
Cut cloud costs
|
||||
Keep your infrastructure simple<br>
|
||||
Deploy and manage containers with joy
|
||||
</h1>
|
||||
<p class="text-lg text-zinc-500 mb-8">
|
||||
Uncloud is a lightweight Docker clustering tool for running web apps on your own servers
|
||||
— from cloud VMs to bare metal.
|
||||
Replace Heroku and Render, no Kubernetes required.
|
||||
Uncloud is a lightweight Docker clustering tool for running and scaling web apps on your
|
||||
servers — from cloud VMs to bare metal. No Kubernetes required.
|
||||
</p>
|
||||
<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"
|
||||
href="https://github.com/psviderski/uncloud/?tab=readme-ov-file#-quick-start">Quick Start</a>
|
||||
href="https://github.com/psviderski/uncloud/?tab=readme-ov-file#-quick-start">
|
||||
Quick Start
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn text-zinc-600 bg-white hover:text-zinc-900 w-full shadow flex items-center justify-center"
|
||||
@@ -117,16 +130,31 @@
|
||||
</div>
|
||||
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 flex justify-center pb-12 relative before:absolute before:-top-12 before:w-96 before:h-96 before:bg-zinc-900 before:opacity-[.15] before:rounded-full before:blur-3xl before:-z-10">
|
||||
<div class="w-full md:min-h-[420px]">
|
||||
<script src="https://fast.wistia.com/player.js" async></script>
|
||||
<script src="https://fast.wistia.com/embed/k47uwt9uau.js" async type="module"></script>
|
||||
<style>wistia-player[media-id='k47uwt9uau']:not(:defined) {
|
||||
background: center / contain no-repeat url('https://fast.wistia.com/embed/medias/k47uwt9uau/swatch');
|
||||
display: block;
|
||||
filter: blur(5px);
|
||||
padding-top: 58.33%;
|
||||
}</style>
|
||||
<wistia-player media-id="k47uwt9uau" aspect="1.7142857142857142"></wistia-player>
|
||||
<div class="coding w-full bg-zinc-800 rounded-lg border border-zinc-700/50 shadow-2xl">
|
||||
<!-- Title bar -->
|
||||
<div class="pl-4 pt-4 flex items-center">
|
||||
<div class="flex gap-2">
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-red-500"></div>
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-yellow-500"></div>
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-green-500"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Terminal content -->
|
||||
<div class="p-4 text-sm sm:p-6 sm:text-[0.9375rem] text-white overflow-x-auto">
|
||||
<pre class="text-zinc-500 select-none"># Start with any cloud VM or your own server</pre>
|
||||
<pre><span class="text-emerald-500 select-none">$ </span>uc machine init root@137.123.45.67</pre>
|
||||
|
||||
<pre> </pre>
|
||||
<pre class="text-zinc-500 select-none"># Deploy your app with automatic HTTPS</pre>
|
||||
<pre><span class="text-emerald-500 select-none">$ </span>uc run --name my-app -p app.example.com:8000/https app-image:latest</pre>
|
||||
<pre class="text-zinc-200">✨ Your app is available at <span class="underline">https://app.example.com</span></pre>
|
||||
|
||||
<pre> </pre>
|
||||
<pre class="text-zinc-500 select-none"># Achieve high availability by adding more machines and scaling the app</pre>
|
||||
<pre><span
|
||||
class="text-emerald-500 select-none">$ </span>uc machine add ubuntu@65.21.45.67</pre>
|
||||
<pre><span class="text-emerald-500 select-none">$ </span>uc scale my-app 2</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,7 +169,7 @@
|
||||
Cloud-like experience you control
|
||||
</h2>
|
||||
<p class="text-lg text-zinc-500">
|
||||
Keep the Docker workflows you love while optimizing infrastructure costs
|
||||
Keep the Docker Compose workflows you love while deploying anywhere with ease.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -362,7 +390,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fully decentralized -->
|
||||
<!-- Fully decentralised -->
|
||||
<div class="text-left flex items-center px-6 py-4 rounded border border-transparent">
|
||||
<svg class="shrink-0 fill-zinc-400 stroke-zinc-400 mr-3"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -371,11 +399,11 @@
|
||||
</svg>
|
||||
<div>
|
||||
<div class="font-inter-tight text-lg font-semibold text-zinc-200 mb-1">
|
||||
Fully decentralized
|
||||
Fully decentralised
|
||||
</div>
|
||||
<div>
|
||||
Unlike traditional orchestrators, there's no central control plane to
|
||||
maintain. Each machine maintains a synchronized copy of the cluster
|
||||
maintain. Each machine maintains a synchronised copy of the cluster
|
||||
state
|
||||
through peer-to-peer communication, keeping cluster operations
|
||||
functional
|
||||
@@ -502,7 +530,7 @@
|
||||
<h3 class="font-inter-tight font-semibold text-zinc-200">No vendor lock-in</h3>
|
||||
</div>
|
||||
<p class="text-sm">Mix cloud providers and your own hardware freely to
|
||||
optimize costs and performance, without changing how you deploy or manage apps.</p>
|
||||
optimise costs and performance, without changing how you deploy or manage apps.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -510,9 +538,39 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Demo Section -->
|
||||
<section class="relative bg-white text-zinc-800">
|
||||
<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 text-center pb-12 md:pb-16">
|
||||
<h2 class="font-inter-tight text-3xl md:text-4xl font-bold text-zinc-900 mb-4">
|
||||
See in action
|
||||
</h2>
|
||||
<p class="text-lg text-zinc-500">
|
||||
Deploy a highly available web app with automatic HTTPS across multiple regions and
|
||||
on-premises in just a couple minutes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 flex justify-center pb-12 relative before:absolute before:-top-12 before:w-96 before:h-96 before:bg-zinc-900 before:opacity-[.15] before:rounded-full before:blur-3xl before:-z-10">
|
||||
<script src="https://fast.wistia.com/player.js" async></script>
|
||||
<script src="https://fast.wistia.com/embed/k47uwt9uau.js" async type="module"></script>
|
||||
<style>wistia-player[media-id='k47uwt9uau']:not(:defined) {
|
||||
background: center / contain no-repeat url('https://fast.wistia.com/embed/medias/k47uwt9uau/swatch');
|
||||
display: block;
|
||||
filter: blur(5px);
|
||||
padding-top: 58.33%;
|
||||
}</style>
|
||||
<wistia-player media-id="k47uwt9uau" aspect="1.7142857142857142"
|
||||
class="w-full rounded-xl shadow-lg"></wistia-player>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section id="subscribe">
|
||||
<div class="py-12 md:py-20">
|
||||
<div class="py-12 md:py-20 border-t border-zinc-200">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="relative max-w-3xl mx-auto text-center">
|
||||
<h2 class="font-inter-tight text-3xl md:text-4xl font-bold text-zinc-900 mb-4">
|
||||
@@ -529,7 +587,7 @@
|
||||
<p class="text-lg text-zinc-500 mb-8">
|
||||
Uncloud is an open source project I'm actively developing. I'd love to share this journey
|
||||
with you. Subscribe to follow the progress, get early insights into new features, and be the
|
||||
first to know when it's ready for production use.
|
||||
first to know when it's ready for production use. No marketing, ads or spam.
|
||||
<p>
|
||||
|
||||
<!-- Subscribe form -->
|
||||
@@ -561,9 +619,9 @@
|
||||
Want to contribute? Check out the
|
||||
<a href="https://github.com/psviderski/uncloud/issues"
|
||||
class="text-zinc-900 hover:underline">GitHub issues</a>
|
||||
or join the
|
||||
<a href="https://github.com/psviderski/uncloud/discussions"
|
||||
class="text-zinc-900 hover:underline">GitHub Discussions</a>.
|
||||
and join the
|
||||
<a href="https://discord.gg/eR35KQJhPu"
|
||||
class="text-zinc-900 hover:underline">Discord server</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -576,11 +634,50 @@
|
||||
<!-- Site footer -->
|
||||
<footer>
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="flex justify-center py-8 md:py-12 border-t border-zinc-200">
|
||||
<div class="flex flex-col items-center py-8 md:py-12 border-t border-zinc-200">
|
||||
<div class="text-sm text-zinc-500">
|
||||
Made with ❤️ by
|
||||
<a class="underline hover:text-zinc-900" href="https://github.com/psviderski">Pasha Sviderski</a>
|
||||
in Australia
|
||||
</div>
|
||||
|
||||
<!-- Social links -->
|
||||
<ul class="flex space-x-4 mt-4 mb-1">
|
||||
<li>
|
||||
<a class="flex justify-center items-center text-zinc-700 hover:text-zinc-900 transition"
|
||||
href="https://discord.gg/eR35KQJhPu" aria-label="Discord">
|
||||
<svg class="fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20"
|
||||
height="20">
|
||||
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="flex justify-center items-center text-zinc-700 hover:text-zinc-900 transition"
|
||||
href="https://x.com/psviderski" aria-label="Twitter">
|
||||
<svg class="fill-current" xmlns="http://www.w3.org/2000/svg" width="20" height="20">
|
||||
<path d="m7.063 3 3.495 4.475L14.601 3h2.454l-5.359 5.931L18 17h-4.938l-3.866-4.893L4.771 17H2.316l5.735-6.342L2 3h5.063Zm-.74 1.347H4.866l8.875 11.232h1.36L6.323 4.347Z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="flex justify-center items-center text-zinc-700 hover:text-zinc-900 transition"
|
||||
href="https://t.me/spyyy" aria-label="Telegram">
|
||||
<svg class="fill-current" xmlns="http://www.w3.org/2000/svg" width="20" height="20">
|
||||
<path d="M17.968 3.276a.338.338 0 0 0-.232-.253 1.192 1.192 0 0 0-.63.045S3.087 8.106 2.286 8.664c-.172.121-.23.19-.259.272-.138.4.293.573.293.573l3.613 1.177a.388.388 0 0 0 .183-.011c.822-.519 8.27-5.222 8.7-5.38.068-.02.118 0 .1.049-.172.6-6.606 6.319-6.64 6.354a.138.138 0 0 0-.05.118l-.337 3.528s-.142 1.1.956 0a30.66 30.66 0 0 1 1.9-1.738c1.242.858 2.58 1.806 3.156 2.3a1 1 0 0 0 .732.283.825.825 0 0 0 .7-.622S17.894 5.292 17.98 3.909c.008-.135.021-.217.021-.317a1.177 1.177 0 0 0-.032-.316Z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="flex justify-center items-center text-zinc-700 hover:text-zinc-900 transition"
|
||||
href="https://github.com/psviderski/uncloud" aria-label="Telegram">
|
||||
<svg class="fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20"
|
||||
height="20">
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
+94
-4
@@ -1314,6 +1314,14 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
height: 3.5rem;
|
||||
}
|
||||
|
||||
.h-2{
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
.h-2\.5{
|
||||
height: 0.625rem;
|
||||
}
|
||||
|
||||
.h-24{
|
||||
height: 6rem;
|
||||
}
|
||||
@@ -1334,6 +1342,14 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.w-2{
|
||||
width: 0.5rem;
|
||||
}
|
||||
|
||||
.w-2\.5{
|
||||
width: 0.625rem;
|
||||
}
|
||||
|
||||
.w-4{
|
||||
width: 1rem;
|
||||
}
|
||||
@@ -1402,6 +1418,12 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.select-none{
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.grid-cols-1{
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
@@ -1446,6 +1468,10 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gap-2{
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.gap-3{
|
||||
gap: 0.75rem;
|
||||
}
|
||||
@@ -1508,6 +1534,10 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.overflow-x-auto{
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.rounded{
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
@@ -1586,6 +1616,10 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
border-color: rgb(228 228 231 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-zinc-700\/50{
|
||||
border-color: rgb(63 63 70 / 0.5);
|
||||
}
|
||||
|
||||
.bg-gray-100{
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
||||
@@ -1596,11 +1630,26 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-green-500{
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(34 197 94 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-red-500{
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-white{
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-yellow-500{
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(234 179 8 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-zinc-50{
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(250 250 250 / var(--tw-bg-opacity));
|
||||
@@ -1640,6 +1689,10 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.fill-current{
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.fill-zinc-300{
|
||||
fill: #d4d4d8;
|
||||
}
|
||||
@@ -1652,6 +1705,10 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
stroke: #a1a1aa;
|
||||
}
|
||||
|
||||
.p-4{
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.p-5{
|
||||
padding: 1.25rem;
|
||||
}
|
||||
@@ -1719,10 +1776,18 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.pl-4{
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.pt-32{
|
||||
padding-top: 8rem;
|
||||
}
|
||||
|
||||
.pt-4{
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.pt-6{
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
@@ -1836,6 +1901,11 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.text-white{
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-zinc-100{
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(244 244 245 / var(--tw-text-opacity));
|
||||
@@ -1861,6 +1931,16 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
color: rgb(82 82 91 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-zinc-700{
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(63 63 70 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-zinc-800{
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(39 39 42 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-zinc-900{
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(24 24 27 / var(--tw-text-opacity));
|
||||
@@ -1887,6 +1967,12 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.shadow-lg{
|
||||
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.shadow-sm{
|
||||
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
|
||||
@@ -2197,10 +2283,18 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
margin-bottom: calc(0px * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.sm\:p-6{
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.sm\:px-6{
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.sm\:text-\[0\.9375rem\]{
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px){
|
||||
@@ -2212,10 +2306,6 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.md\:min-h-\[420px\]{
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
.md\:max-w-4xl{
|
||||
max-width: 56rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user