Your computer
- Best for
- A short local test before choosing a permanent home.
- Main advantage
- No new hardware or monthly bill.
- What gets harder
- Cotton stops when the computer sleeps or leaves the network.
Learn what a server is, compare a home server, NAS, and VPS, understand why Docker and a reverse proxy exist, then follow one complete Ubuntu VPS path with persistent PostgreSQL data and automatic HTTPS.
This guide assumes you have never managed a server. It explains the words first, shows how the pieces connect, and then follows one concrete path: an Ubuntu 24.04 VPS, Docker Compose, and automatic HTTPS through Caddy.
It does not have to be a rack in a data center. Cotton can live on a computer at home or on a rented virtual machine. The important differences are where the files live, how the machine reaches the internet, and who maintains it.
The browser never talks to the database or file directory directly. DNS finds the server, Caddy accepts the encrypted web connection, and Cotton decides what data to read or write.
Updating Docker normally replaces a container. That is safe only because the real state lives outside those replaceable containers.
Without Docker you would install the correct .NET runtime, PostgreSQL tools, media utilities, users, permissions, and upgrades by hand. Docker packages the application; Compose records how all services start together.
A packaged application template. bvdcode/cotton contains Cotton and the runtime tools it needs.
A running instance of an image. It can be stopped or replaced without becoming your data store.
One YAML file that starts PostgreSQL, Cotton, and Caddy with the same network and settings.
Storage outside a replaceable container. This is where the database, file chunks, and certificates persist.
Cotton listens inside Docker on port 8080. A reverse proxy owns public ports 80 and 443, obtains the TLS certificate, and forwards each HTTPS request to Cotton. What people often call SSL is now TLS; HTTPS is ordinary web traffic protected by TLS. Users see only the normal https:// address.
The shortest first install: one domain in a Caddyfile and certificates are requested and renewed automatically.
Useful when one Docker host already routes many services through labels and shared middleware. It adds concepts a first Cotton install does not need.
The commands below target Ubuntu Server 24.04 LTS on a VPS. This is not the only valid deployment, but it is a clear baseline a first-time operator can reproduce.
A fresh server and a user that can run sudo. Size the disk for the files you plan to store.
Use a subdomain such as cloud.example.com. You will point it at the server IP before starting Caddy.
The provider gives you a server IP plus a password or SSH key. SSH is the remote terminal used for the commands below.
22 is SSH administration; 80 and 443 let Caddy issue certificates and serve the website. Do not expose PostgreSQL port 5432.
Create the VPS first. Then open the DNS panel at the company where your domain is managed. The terminal commands cannot do these two account-specific steps for you.
Choose Ubuntu 24.04 LTS, add your SSH key or receive a temporary password, and copy the public IPv4 address. Keep the server region and disk size appropriate for your users and files.
Create an A record such as cloud.example.com with the VPS IPv4 address. DNS may take time to update. Caddy cannot issue HTTPS until the name resolves to this server.
Open Terminal on macOS/Linux or Windows Terminal on Windows and run ssh <user>@<server-ip>. Accept the host fingerprint only when the IP matches the server you just created.
You see a prompt on the remote Ubuntu server. From this point every command below runs there, not on your personal computer.
PostgreSQL keeps its database in the named postgres_data volume. Cotton keeps encrypted file chunks and its key sentinel under /data/cotton. Caddy is the only public service and stores its certificates in durable volumes.
name: cotton
services:
postgres:
image: postgres:18
restart: unless-stopped
environment:
POSTGRES_DB: cotton
POSTGRES_USER: cotton
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
volumes:
- postgres_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U cotton -d cotton"]
interval: 5s
timeout: 5s
retries: 20
cotton:
image: bvdcode/cotton:latest
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
expose:
- "8080"
volumes:
- /data/cotton:/app/files
environment:
COTTON_PG_HOST: postgres
COTTON_PG_PORT: "5432"
COTTON_PG_DATABASE: cotton
COTTON_PG_USERNAME: cotton
COTTON_PG_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
security_opt:
- no-new-privileges:true
caddy:
image: caddy:2
restart: unless-stopped
depends_on:
- cotton
ports:
- "80:80"
- "443:443"
- "443:443/udp"
environment:
COTTON_DOMAIN: ${COTTON_DOMAIN:?Set COTTON_DOMAIN in .env}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
volumes:
postgres_data:
caddy_data:
caddy_config:Caddy reads the domain from .env, requests a trusted TLS certificate, renews it automatically, and forwards traffic to the Cotton container on the private Docker network.
{$COTTON_DOMAIN} {
reverse_proxy cotton:8080
}The setup command generates the database password straight into this protected file. You only replace the example domain. The recommended browser-unlock path deliberately keeps the Cotton master key out of .env.
POSTGRES_PASSWORD=<generated automatically> COTTON_DOMAIN=cloud.example.com
These commands install Ubuntu's Docker Engine and Compose package, start Docker now, and enable it after reboot.
sudo apt update sudo apt install -y docker.io docker-compose-v2 curl nano openssl ufw sudo systemctl enable --now docker sudo docker --version sudo docker compose version
This creates /opt/cotton for configuration and /data/cotton for file data, downloads the two files shown above, writes a generated database password directly to .env, and opens the domain for editing.
sudo install -d -m 0750 /opt/cotton /data/cotton cd /opt/cotton sudo curl -fsSLo compose.yml https://cottoncloud.dev/install/docker-compose.yml sudo curl -fsSLo Caddyfile https://cottoncloud.dev/install/Caddyfile DB_PASSWORD="$(openssl rand -hex 24)" printf 'POSTGRES_PASSWORD=%s\nCOTTON_DOMAIN=cloud.example.com\n' "$DB_PASSWORD" | sudo tee .env >/dev/null sudo chmod 600 .env sudo nano .env
Keep SSH open before enabling the firewall so you do not lock yourself out. If the VPS provider has a separate cloud firewall, allow TCP 80 and 443 there too.
sudo ufw allow OpenSSH sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw --force enable
Compose first checks the file, then downloads the images, starts the three services, and prints their state.
cd /opt/cotton sudo docker compose config --quiet sudo docker compose pull sudo docker compose up -d sudo docker compose ps
A new Cotton instance starts locked because the master key is intentionally not stored in Docker. The server log prints a short-lived token for the first unlock.
sudo docker compose logs cotton | grep -i "bootstrap token"
Open https://cloud.example.com/unlock with your real domain. The unlock page appears before the normal application because Cotton cannot read encrypted state without its master key.
Use Generate on the unlock page, copy the 32-character key into a password manager or offline recovery record, enter the bootstrap token, and unlock. Losing the key can make encrypted data unrecoverable.
After startup, the login form explains that no users exist. The email and password you submit create the first administrator. Use a unique password.
For this basic path choose local file storage. Answer the usage, privacy, email, and timezone questions; the UI explains each choice and most can be changed later.
Set the public base URL to the same https:// domain people will open. This keeps links, passkeys, password resets, and OIDC callbacks on the correct origin.
In the trusted-proxy setting, use Cotton's observed-proxy check and save the suggested Docker bridge boundary. Do not trust every address on the internet.
The login page loading is not the finish line.
Enable passkeys or TOTP for the admin account before the instance is reachable from outside.
The admin checkup flags public signup, missing 2FA, writable rootfs, Docker socket exposure, and more.
Restart the stack, unlock Cotton again, and verify the administrator and a test upload still exist before adding important files.
Confirm how you pull a new image and restart without losing the database or chunk storage.
An install is trustworthy only when you know how to update it and how to take recovery data off the server. A backup stored only on the same VPS is not a backup against losing that VPS.
Read Cotton release notes first. These commands pull current images and recreate services while keeping the named volumes and /data/cotton.
cd /opt/cotton sudo docker compose pull sudo docker compose up -d sudo docker compose ps
This writes a dated database dump under /var/backups/cotton. Copy that dump, /data/cotton, the master key, and /opt/cotton to separate storage as one recovery set.
sudo install -d -m 0700 /var/backups/cotton sudo sh -c 'docker compose -f /opt/cotton/compose.yml exec -T postgres pg_dump -U cotton -d cotton -Fc > /var/backups/cotton/postgres-$(date +%F).dump'
Do not delete volumes and do not regenerate the master key to make an error disappear. Start with the first failing layer in the path shown above.
Check that the A record contains the VPS public IPv4 address. Until DNS resolves correctly, Caddy cannot obtain a public certificate.
Check provider firewall rules, then sudo ufw status, then sudo docker compose ps. Ports 80 and 443 must reach Caddy.
Cotton is not ready. Read sudo docker compose logs --tail=200 cotton and fix the first startup error, commonly the database password or storage permissions.
That is expected in the recommended browser-unlock mode. Use the same saved master key. Never generate a replacement key for existing encrypted data.
You do not need to be a Docker expert, but you should understand three things: containers run the app, volumes keep data across restarts, and deleting the wrong volume can delete real data.
Yes. A home server, mini PC, or NAS-like machine can work well when it stays online, has enough storage, and has a backup plan. Public access still needs HTTPS and a reverse proxy.
For local-only testing, no. For a public instance, use a domain and HTTPS so browsers, mobile clients, and desktop clients have one stable secure address.
PostgreSQL stores structured metadata: accounts, folders, file records, shares, settings, and operational state. The large file content is stored separately as Cotton chunks.
Cotton stores file content as chunks in the configured storage backend. That backend can be a persistent filesystem path or S3-compatible storage.
Both are reverse proxies that can terminate HTTPS and forward requests to Cotton. Caddy is the shorter first-install path; Traefik is useful when an existing Docker host already routes many services through labels and shared middleware.
Yes. It provides one reproducible Ubuntu 24.04 VPS path, downloadable Compose and Caddy files, DNS and firewall steps, the first browser setup, update commands, a PostgreSQL backup command, and troubleshooting checks.